TTS Framework - JavaScript assets.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
tts_js/core/addons/grapnel.js

361 lines
14 KiB

/*
* Grapnel Router
* @author Greg Sabia Tucker <greg@narrowlabs.com>
* @copyright 2019 Greg Sabia Tucker
* @link https://github.com/baseprime/grapnel
* Released under MIT License. See http://opensource.org/licenses/MIT
*/
class MyRouter {
constructor(opts) {
var self = this; /* Scope reference */
this.events = {}; /* Event Listeners */
this.state = null; /* Router state object */
this.options = opts || {}; /* Options */
this.options.env = this.options.env || (!!(Object.keys(root).length === 0 && process && process.browser !== true) ? 'server' : 'client');
this.options.mode = this.options.mode || (!!(this.options.env !== 'server' && this.options.pushState && root.history && root.history.pushState) ? 'pushState' : 'hashchange');
if ('function' === typeof root.addEventListener) {
root.addEventListener('hashchange', function () {
self.trigger('hashchange');
});
root.addEventListener('popstate', function (e) {
/* Make sure popstate doesn't run on init -- this is a common issue with Safari and old versions of Chrome */
if (self.state && self.state.previousState === null) {
return false;
}
self.trigger('navigate');
});
}
return this;
}
}
/**
* Create a RegExp Route from a string
* This is the heart of the router and I've made it as small as possible!
* @param {String} path - Path of route
* @param {Array} keys - Array of keys to fill
* @param {Bool} sensitive - Case sensitive comparison
* @param {Bool} strict - Strict mode
*/
MyRouter.regexRoute = function (path, keys, sensitive, strict) {
if (path instanceof RegExp) {
return path;
}
if (path instanceof Array) {
path = '(' + path.join('|') + ')';
}
/* Build route RegExp */
path = path.concat(strict ? '' : '/?')
.replace(/\/\(/g, '(?:/')
.replace(/\+/g, '__plus__')
.replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function (_, slash, format, key, capture, optional) {
keys.push({name: key, optional: !!optional});
slash = slash || '';
return '' + (optional ? '' : slash) + '(?:' + (optional ? slash : '') + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' + (optional || '');
})
.replace(/([\/.])/g, '\\$1')
.replace(/__plus__/g, '(.+)')
.replace(/\*/g, '(.*)');
return new RegExp('^' + path + '$', sensitive ? '' : 'i');
};
/**
* ForEach workaround utility
* @param {Array} a - to iterate
* @param {Function} callback
*/
MyRouter._forEach = function (a, callback) {
if (typeof Array.prototype.forEach === 'function') {
return Array.prototype.forEach.call(a, callback);
}
/* Replicate forEach() */
return function (c, next) {
for (var i = 0, n = this.length; i < n; ++i) {
c.call(next, this[i], i, this);
}
}.call(a, callback);
};
/**
* Add an route and handler
* @param {String|RegExp} route name
* @return {self} Router
*/
MyRouter.prototype.get = MyRouter.prototype.add = function (route) {
var self = this, middleware = Array.prototype.slice.call(arguments, 1, -1),
handler = Array.prototype.slice.call(arguments, -1)[0], request = new Request(route);
var invoke = function RouteHandler() {
/* Build request parameters */
var req = request.parse(self.path());
/* Check if matches are found */
if (req.match) {
/* Match found */
var extra = {route: route, params: req.params, req: req, regex: req.match};
/* Create call stack -- add middleware first, then handler */
var stack = new CallStack(self, extra).enqueue(middleware.concat(handler));
/* Trigger main event */
self.trigger('match', stack, req);
/* Continue */
if (!stack.runCallback) {
return self;
}
/* Previous state becomes current state */
stack.previousState = self.state;
/* Save new state */
self.state = stack;
/* Prevent this handler from being called if parent handler in stack has instructed not to propagate any more events */
if (stack.parent() && stack.parent().propagateEvent === false) {
stack.propagateEvent = false;
return self;
}
/* Call handler */
stack.callback();
}
/* Returns self */
return self;
};
/* Event name */
var eventName = (self.options.mode !== 'pushState' && self.options.env !== 'server') ? 'hashchange' : 'navigate';
/* Invoke when route is defined, and once again when app navigates */
return invoke().on(eventName, invoke);
};
/**
* Fire an event listener
* @param {String} event name
* @param {Mixed} [attributes] Parameters that will be applied to event handler
* @return {self} Router
*/
MyRouter.prototype.trigger = function (event) {
var self = this, params = Array.prototype.slice.call(arguments, 1);
/* Call matching events */
if (this.events[event]) {
MyRouter._forEach(this.events[event], function (fn) {
fn.apply(self, params);
});
}
return this;
};
/**
* Add an event listener
* @param {String} event name (multiple events can be called when separated by a space " ")
* @param {Function} handler - callback
* @return {self} Router
*/
MyRouter.prototype.on = MyRouter.prototype.bind = function (event, handler) {
var self = this, events = event.split(' ');
MyRouter._forEach(events, function (event) {
if (self.events[event]) {
self.events[event].push(handler);
} else {
self.events[event] = [handler];
}
});
return this;
};
/**
* Allow event to be called only once
* @param {String} event name(s)
* @param {Function} handler - callback
* @return {self} Router
*/
MyRouter.prototype.once = function (event, handler) {
var ran = false;
return this.on(event, function () {
if (ran) {
return false;
}
ran = true;
handler.apply(this, arguments);
handler = null;
return true;
});
};
/**
* @param {String} context - Route context (without trailing slash)
* param {[Function]} Middleware (optional)
* @return {Function} Adds route to context
*/
MyRouter.prototype.context = function (context) {
var self = this, middleware = Array.prototype.slice.call(arguments, 1);
return function () {
var value = arguments[0],
submiddleware = (arguments.length > 2) ? Array.prototype.slice.call(arguments, 1, -1) : [],
handler = Array.prototype.slice.call(arguments, -1)[0],
prefix = (context.slice(-1) !== '/' && value !== '/' && value !== '') ? context + '/' : context,
path = (value.substr(0, 1) !== '/') ? value : value.substr(1),
pattern = prefix + path;
return self.add.apply(self, [pattern].concat(middleware).concat(submiddleware).concat([handler]));
};
};
/**
* Navigate through history API
* @param {String} path - Pathname
* @return {self} Router
*/
MyRouter.prototype.navigate = function (path) {
return this.path(path).trigger('navigate');
};
MyRouter.prototype.path = function (pathname) {
var self = this, frag;
if ('string' === typeof pathname) {
/* Set path */
if (self.options.mode === 'pushState') {
frag = (self.options.root) ? (self.options.root + pathname) : pathname;
root.history.pushState({}, null, frag);
} else if (root.location) {
root.location.hash = (self.options.hashBang ? '!' : '') + pathname;
} else {
root._pathname = pathname || '';
}
return this;
} else if ('undefined' === typeof pathname) {
/* Get path */
if (self.options.mode === 'pushState') {
frag = root.location.pathname.replace(self.options.root, '');
} else if (self.options.mode !== 'pushState' && root.location) {
frag = (root.location.hash) ? root.location.hash.split((self.options.hashBang ? '#!' : '#'))[1] : '';
} else {
frag = root._pathname || '';
}
return frag;
} else if (pathname === false) {
/* Clear path */
if (self.options.mode === 'pushState') {
root.history.pushState({}, null, self.options.root || '/');
} else if (root.location) {
root.location.hash = (self.options.hashBang) ? '!' : '';
}
return self;
}
};
/**
* Create routes based on an object
* @param {Object} [Options, Routes]
* @param {Object Routes}
* @return {self} Router
*/
MyRouter.listen = function () {
var opts, routes;
if (arguments[0] && arguments[1]) {
opts = arguments[0];
routes = arguments[1];
} else {
routes = arguments[0];
}
/* Return a new MyRouter instance */
return (function () {
/* TODO: Accept multi-level routes */
for (var key in routes) {
this.add.call(this, key, routes[key]);
}
return this;
}).call(new MyRouter(opts || {}));
};
/**
* Create a call stack that can be enqueued by handlers and middleware
* @param {Object} router - Router
* @param {Object} extendObj - Extend
* @return {self} CallStack
*/
class CallStack {
constructor(router, extendObj) {
this.stack = CallStack.global.slice(0);
this.router = router;
this.runCallback = true;
this.callbackRan = false;
this.propagateEvent = true;
this.value = router.path();
for (var key in extendObj) {
this[key] = extendObj[key];
}
return this;
}
}
/**
* Build request parameters and allow them to be checked against a string (usually the current path)
* @param {String} route - Route
* @return {self} Request
*/
function Request(route) {
this.route = route;
this.keys = [];
this.regex = MyRouter.regexRoute(route, this.keys);
}
;
/* This allows global middleware */
CallStack.global = [];
/**
* Prevent a callback from being called
* @return {self} CallStack
*/
CallStack.prototype.preventDefault = function () {
this.runCallback = false;
};
/**
* Prevent any future callbacks from being called
* @return {self} CallStack
*/
CallStack.prototype.stopPropagation = function () {
this.propagateEvent = false;
};
/**
* Get parent state
* @return {Object} Previous state
*/
CallStack.prototype.parent = function () {
var hasParentEvents = !!(this.previousState && this.previousState.value && this.previousState.value == this.value);
return (hasParentEvents) ? this.previousState : false;
};
/**
* Run a callback (calls to next)
* @return {self} CallStack
*/
CallStack.prototype.callback = function () {
this.callbackRan = true;
this.timeStamp = Date.now();
this.next();
};
/**
* Add handler or middleware to the stack
* @param {Function|Array} handler - Handler or a array of handlers
* @param {Int} atIndex - Index to start inserting
* @return {self} CallStack
*/
CallStack.prototype.enqueue = function (handler, atIndex) {
var handlers = (!Array.isArray(handler)) ? [handler] : ((atIndex < handler.length) ? handler.reverse() : handler);
while (handlers.length) {
this.stack.splice(atIndex || this.stack.length + 1, 0, handlers.shift());
}
return this;
};
/**
* Call to next item in stack -- this adds the `req`, `event`, and `next()` arguments to all middleware
* @return {self} CallStack
*/
CallStack.prototype.next = function () {
var self = this;
return this.stack.shift().call(this.router, this.req, this, function next() {
self.next.call(self);
});
};
/**
* Match a path string -- returns a request object if there is a match -- returns false otherwise
* @param {String} path
* @return {Object} req
*/
Request.prototype.parse = function (path) {
var match = path.match(this.regex), self = this;
var req = {params: {}, keys: this.keys, matches: (match || []).slice(1), match: match};
/* Build parameters */
MyRouter._forEach(req.matches, function (value, i) {
var key = (self.keys[i] && self.keys[i].name) ? self.keys[i].name : i;
/* Parameter key will be its key or the iteration index. This is useful if a wildcard (*) is matched */
req.params[key] = (value) ? decodeURIComponent(value) : undefined;
});
return req;
};
MyRouter.CallStack = CallStack;
MyRouter.Request = Request;
tts.Router = new MyRouter();
/*
* End of MyRouter Router
*/