603 lines
15 KiB
JavaScript
603 lines
15 KiB
JavaScript
|
/* global window, module */
|
||
|
|
||
|
;(function (global, factory) {
|
||
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||
|
typeof define === 'function' && define.amd ? define(factory) :
|
||
|
global.Router = factory()
|
||
|
}(this, (function () {
|
||
|
|
||
|
/**
|
||
|
* Router
|
||
|
*
|
||
|
* @version: 1.2.7
|
||
|
* @author Graidenix
|
||
|
*
|
||
|
* @constructor
|
||
|
*
|
||
|
* @param {object} options
|
||
|
* @returns {Router}
|
||
|
*/
|
||
|
function Router(options) {
|
||
|
var settings = this._getSettings(options);
|
||
|
|
||
|
|
||
|
this.notFoundHandler = settings.page404;
|
||
|
this.mode = (!window.history || !window.history.pushState) ? "hash" : settings.mode;
|
||
|
this.root = settings.root === "/" ? "/" : "/" + this._trimSlashes(settings.root) + "/";
|
||
|
this.beforeHook = settings.hooks.before;
|
||
|
this.securityHook = settings.hooks.secure;
|
||
|
|
||
|
this.routes = [];
|
||
|
if (settings.routes && settings.routes.length > 0) {
|
||
|
var _this = this;
|
||
|
settings.routes.forEach(function (route) {
|
||
|
_this.add(route.rule, route.handler, route.options);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
this._pageState = null;
|
||
|
this._currentPage = null;
|
||
|
this._skipCheck = false;
|
||
|
this._action = null;
|
||
|
|
||
|
if (this.mode === "hash") {
|
||
|
this._historyStack = [];
|
||
|
this._historyIdx = 0;
|
||
|
this._historyState = "add"
|
||
|
}
|
||
|
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Define Router Page
|
||
|
*
|
||
|
* @param {string} uri
|
||
|
* @param {object} query
|
||
|
* @param {Array} params
|
||
|
* @param {object} state
|
||
|
* @param {object} options
|
||
|
*
|
||
|
* @constructor
|
||
|
*/
|
||
|
Router.Page = function (uri, query, params, state, options) {
|
||
|
this.uri = uri || "";
|
||
|
this.query = query || {};
|
||
|
this.params = params || [];
|
||
|
this.state = state || null;
|
||
|
this.options = options || {};
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Sanitize options and add default values
|
||
|
*
|
||
|
* @param {object} options
|
||
|
* @returns {object}
|
||
|
* @private
|
||
|
*/
|
||
|
Router.prototype._getSettings = function (options) {
|
||
|
var settings = {};
|
||
|
var defaults = {
|
||
|
routes: [],
|
||
|
mode: "history",
|
||
|
root: "/",
|
||
|
hooks: {
|
||
|
"before": function () {
|
||
|
},
|
||
|
"secure": function () {
|
||
|
return true;
|
||
|
}
|
||
|
},
|
||
|
page404: function (page) {
|
||
|
console.error({
|
||
|
page: page,
|
||
|
message: "404. Page not found"
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
|
||
|
options = options || {};
|
||
|
["routes", "mode", "root", "page404"].forEach(function (key) {
|
||
|
settings[key] = options[key] || defaults[key];
|
||
|
});
|
||
|
|
||
|
settings.hooks = Object.assign({}, defaults.hooks, options.hooks || {});
|
||
|
|
||
|
return settings;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Get URI for Router "history" mode
|
||
|
*
|
||
|
* @private
|
||
|
* @returns {string}
|
||
|
*/
|
||
|
Router.prototype._getHistoryFragment = function () {
|
||
|
var fragment = decodeURI(window.location.pathname);
|
||
|
if (this.root !== "/") {
|
||
|
fragment = fragment.replace(this.root, "");
|
||
|
}
|
||
|
return this._trimSlashes(fragment);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Get URI for router "hash" mode
|
||
|
*
|
||
|
* @private
|
||
|
* @returns {string}
|
||
|
*/
|
||
|
Router.prototype._getHashFragment = function () {
|
||
|
var hash = window.location.hash.substr(1).replace(/(\?.*)$/, "");
|
||
|
return this._trimSlashes(hash);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Get current URI
|
||
|
*
|
||
|
* @private
|
||
|
* @returns {string}
|
||
|
*/
|
||
|
Router.prototype._getFragment = function () {
|
||
|
if (this.mode === "history") {
|
||
|
return this._getHistoryFragment();
|
||
|
} else {
|
||
|
return this._getHashFragment();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Trim slashes for path
|
||
|
*
|
||
|
* @private
|
||
|
* @param {string} path
|
||
|
* @returns {string}
|
||
|
*/
|
||
|
Router.prototype._trimSlashes = function (path) {
|
||
|
if (typeof path !== "string") {
|
||
|
return "";
|
||
|
}
|
||
|
return path.toString().replace(/\/$/, "").replace(/^\//, "");
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* 404 Page Handler
|
||
|
*
|
||
|
* @private
|
||
|
*/
|
||
|
Router.prototype._page404 = function (path) {
|
||
|
this._currentPage = new Router.Page(path);
|
||
|
this.notFoundHandler(path);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Convert the string route rule to RegExp rule
|
||
|
*
|
||
|
* @param {string} route
|
||
|
* @returns {RegExp}
|
||
|
* @private
|
||
|
*/
|
||
|
Router.prototype._parseRouteRule = function (route) {
|
||
|
if (typeof route !== "string") {
|
||
|
return route;
|
||
|
}
|
||
|
var uri = this._trimSlashes(route);
|
||
|
var rule = uri
|
||
|
.replace(/([\\\/\-\_\.])/g, "\\$1")
|
||
|
.replace(/\{[a-zA-Z]+\}/g, "(:any)")
|
||
|
.replace(/\:any/g, "[\\w\\-\\_\\.]+")
|
||
|
.replace(/\:word/g, "[a-zA-Z]+")
|
||
|
.replace(/\:num/g, "\\d+");
|
||
|
|
||
|
return new RegExp("^" + rule + "$", "i");
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Parse query string and return object for it
|
||
|
*
|
||
|
* @param {string} query
|
||
|
* @returns {object}
|
||
|
* @private
|
||
|
*/
|
||
|
Router.prototype._parseQuery = function (query) {
|
||
|
var _query = {};
|
||
|
if (typeof query !== "string") {
|
||
|
return _query;
|
||
|
}
|
||
|
|
||
|
if (query[0] === "?") {
|
||
|
query = query.substr(1);
|
||
|
}
|
||
|
|
||
|
this._queryString = query;
|
||
|
query.split("&").forEach(function (row) {
|
||
|
var parts = row.split("=");
|
||
|
if (parts[0] !== "") {
|
||
|
if (parts[1] === undefined) {
|
||
|
parts[1] = true;
|
||
|
}
|
||
|
_query[decodeURIComponent(parts[0])] = parts[1];
|
||
|
}
|
||
|
});
|
||
|
return _query;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Get query for `history` mode
|
||
|
*
|
||
|
* @returns {Object}
|
||
|
* @private
|
||
|
*/
|
||
|
Router.prototype._getHistoryQuery = function () {
|
||
|
return this._parseQuery(window.location.search);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Get query for `hash` mode
|
||
|
*
|
||
|
* @returns {Object}
|
||
|
* @private
|
||
|
*/
|
||
|
Router.prototype._getHashQuery = function () {
|
||
|
var index = window.location.hash.indexOf("?");
|
||
|
var query = (index !== -1) ? window.location.hash.substr(index) : "";
|
||
|
return this._parseQuery(query);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Get query as object
|
||
|
*
|
||
|
* @private
|
||
|
* @returns {Object}
|
||
|
*/
|
||
|
Router.prototype._getQuery = function () {
|
||
|
if (this.mode === "history") {
|
||
|
return this._getHistoryQuery();
|
||
|
} else {
|
||
|
return this._getHashQuery();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Add route to routes list
|
||
|
*
|
||
|
* @param {string|RegExp} rule
|
||
|
* @param {function} handler
|
||
|
* @param {{}} options
|
||
|
* @returns {Router}
|
||
|
*/
|
||
|
Router.prototype.add = function (rule, handler, options) {
|
||
|
this.routes.push({
|
||
|
rule: this._parseRouteRule(rule),
|
||
|
handler: handler,
|
||
|
options: options
|
||
|
});
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Remove a route from routes list
|
||
|
*
|
||
|
* @param param
|
||
|
* @returns {Router}
|
||
|
*/
|
||
|
Router.prototype.remove = function (param) {
|
||
|
var _this = this;
|
||
|
if (typeof param === "string") {
|
||
|
param = this._parseRouteRule(param).toString();
|
||
|
}
|
||
|
this.routes.some(function (route, i) {
|
||
|
if (route.handler === param || route.rule.toString() === param) {
|
||
|
_this.routes.splice(i, 1);
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
});
|
||
|
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Reset the state of Router
|
||
|
*
|
||
|
* @returns {Router}
|
||
|
*/
|
||
|
Router.prototype.reset = function () {
|
||
|
this.routes = [];
|
||
|
this.mode = null;
|
||
|
this.root = "/";
|
||
|
this._pageState = {};
|
||
|
this.removeUriListener();
|
||
|
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Add current page in history stack
|
||
|
* @private
|
||
|
*/
|
||
|
Router.prototype._pushHistory = function () {
|
||
|
var _this = this,
|
||
|
fragment = this._getFragment();
|
||
|
|
||
|
if (this.mode === "hash") {
|
||
|
if (this._historyState === "add") {
|
||
|
if (this._historyIdx !== this._historyStack.length - 1) {
|
||
|
this._historyStack.splice(this._historyIdx + 1);
|
||
|
}
|
||
|
|
||
|
this._historyStack.push({
|
||
|
path: fragment,
|
||
|
state: _this._pageState
|
||
|
});
|
||
|
|
||
|
this._historyIdx = this._historyStack.length - 1;
|
||
|
}
|
||
|
this._historyState = "add";
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
* @param asyncRequest boolean
|
||
|
* @returns {PromiseResult<boolean> | boolean}
|
||
|
* @private
|
||
|
*/
|
||
|
Router.prototype._unloadCallback = function (asyncRequest) {
|
||
|
var result;
|
||
|
|
||
|
if (this._skipCheck) {
|
||
|
return asyncRequest ? Promise.resolve(true) : true;
|
||
|
}
|
||
|
|
||
|
if (this._currentPage && this._currentPage.options && this._currentPage.options.unloadCb) {
|
||
|
result = this._currentPage.options.unloadCb(this._currentPage, asyncRequest);
|
||
|
if (!asyncRequest || result instanceof Promise) {
|
||
|
return result;
|
||
|
}
|
||
|
return result ? Promise.resolve(result) : Promise.reject(result);
|
||
|
} else {
|
||
|
return asyncRequest ? Promise.resolve(true) : true;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Check if router has the action for current path
|
||
|
*
|
||
|
* @returns {boolean}
|
||
|
* @private
|
||
|
*/
|
||
|
Router.prototype._findRoute = function () {
|
||
|
var _this = this,
|
||
|
fragment = this._getFragment();
|
||
|
|
||
|
return this.routes.some(function (route) {
|
||
|
var match = fragment.match(route.rule);
|
||
|
if (match) {
|
||
|
match.shift();
|
||
|
var query = _this._getQuery();
|
||
|
var page = new Router.Page(fragment, query, match, _this._pageState, route.options);
|
||
|
|
||
|
if (!_this.securityHook(page)) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
_this._currentPage = page;
|
||
|
if (_this._skipCheck) {
|
||
|
_this._skipCheck = false;
|
||
|
return true;
|
||
|
}
|
||
|
_this.beforeHook(page);
|
||
|
route.handler.apply(page, match);
|
||
|
_this._pageState = null;
|
||
|
|
||
|
window.onbeforeunload = function (ev) {
|
||
|
if (_this._unloadCallback(false)) {
|
||
|
return;
|
||
|
}
|
||
|
ev.returnValue = true;
|
||
|
return true;
|
||
|
};
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
});
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
*/
|
||
|
Router.prototype._treatAsync = function () {
|
||
|
var result;
|
||
|
|
||
|
result = this._currentPage.options.unloadCb(this._currentPage, true);
|
||
|
if (!(result instanceof Promise)) {
|
||
|
result = result ? Promise.resolve(result) : Promise.reject(result);
|
||
|
}
|
||
|
|
||
|
result
|
||
|
.then(this._processUri.bind(this))
|
||
|
.catch(this._resetState.bind(this));
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
* @private
|
||
|
*/
|
||
|
Router.prototype._resetState = function () {
|
||
|
this._skipCheck = true;
|
||
|
this.navigateTo(this._current, this._currentPage.state, true);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Replace current page with new one
|
||
|
*/
|
||
|
Router.prototype._processUri = function () {
|
||
|
var fragment = this._getFragment(),
|
||
|
found;
|
||
|
|
||
|
this._current = fragment;
|
||
|
this._pushHistory();
|
||
|
|
||
|
found = this._findRoute.call(this);
|
||
|
if (!found) {
|
||
|
this._page404(fragment);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Check the URL and execute handler for its route
|
||
|
*
|
||
|
* @returns {Router}
|
||
|
*/
|
||
|
Router.prototype.check = function () {
|
||
|
if (this._skipCheck) return this;
|
||
|
|
||
|
// if page has unload cb treat as promise
|
||
|
if (this._currentPage && this._currentPage.options && this._currentPage.options.unloadCb) {
|
||
|
this._treatAsync();
|
||
|
} else {
|
||
|
this._processUri();
|
||
|
}
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Add the URI listener
|
||
|
*
|
||
|
* @returns {Router}
|
||
|
*/
|
||
|
Router.prototype.addUriListener = function () {
|
||
|
if (this.mode === "history") {
|
||
|
window.onpopstate = this.check.bind(this);
|
||
|
} else {
|
||
|
window.onhashchange = this.check.bind(this);
|
||
|
}
|
||
|
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Remove the URI listener
|
||
|
*
|
||
|
* @returns {Router}
|
||
|
*/
|
||
|
Router.prototype.removeUriListener = function () {
|
||
|
window.onpopstate = null;
|
||
|
window.onhashchange = null;
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Redirect to a page with replace state
|
||
|
*
|
||
|
* @param {string} path
|
||
|
* @param {object} state
|
||
|
* @param {boolean} silent
|
||
|
*
|
||
|
* @returns {Router}
|
||
|
*/
|
||
|
Router.prototype.redirectTo = function (path, state, silent) {
|
||
|
path = this._trimSlashes(path) || "";
|
||
|
this._pageState = state || null;
|
||
|
this._skipCheck = !!silent;
|
||
|
|
||
|
if (this.mode === "history") {
|
||
|
history.replaceState(state, null, this.root + this._trimSlashes(path));
|
||
|
return this.check();
|
||
|
} else {
|
||
|
this._historyIdx--;
|
||
|
window.location.hash = path;
|
||
|
}
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Navigate to a page
|
||
|
*
|
||
|
* @param {string} path
|
||
|
* @param {object} state
|
||
|
* @param {boolean} silent
|
||
|
*
|
||
|
* @returns {Router}
|
||
|
*/
|
||
|
Router.prototype.navigateTo = function (path, state, silent) {
|
||
|
path = this._trimSlashes(path) || "";
|
||
|
this._pageState = state || null;
|
||
|
this._skipCheck = !!silent;
|
||
|
|
||
|
if (this.mode === "history") {
|
||
|
history.pushState(state, null, this.root + this._trimSlashes(path));
|
||
|
return this.check();
|
||
|
} else {
|
||
|
window.location.hash = path;
|
||
|
}
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Refresh page with recall route handler
|
||
|
* @returns {Router}
|
||
|
*/
|
||
|
Router.prototype.refresh = function () {
|
||
|
if (!this._currentPage) {
|
||
|
return this;
|
||
|
}
|
||
|
var path = this._currentPage.uri + "?" + this._queryString;
|
||
|
return this.navigateTo(path, this._currentPage.state);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Go Back in browser history
|
||
|
* Simulate "Back" button
|
||
|
*
|
||
|
* @returns {Router}
|
||
|
*/
|
||
|
Router.prototype.back = function () {
|
||
|
if (this.mode === "history") {
|
||
|
window.history.back();
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
return this.go(this._historyIdx - 1);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Go Forward in browser history
|
||
|
* Simulate "Forward" button
|
||
|
*
|
||
|
* @returns {Router}
|
||
|
*/
|
||
|
Router.prototype.forward = function () {
|
||
|
if (this.mode === "history") {
|
||
|
window.history.forward();
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
return this.go(this._historyIdx + 1);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Go to a specific history page
|
||
|
*
|
||
|
* @param {number} count
|
||
|
* @returns {Router}
|
||
|
*/
|
||
|
Router.prototype.go = function (count) {
|
||
|
if (this.mode === "history") {
|
||
|
window.history.go(count);
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
var page = this._historyStack[count];
|
||
|
if (!page) {
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
this._historyIdx = count;
|
||
|
this._historyState = "hold";
|
||
|
return this.navigateTo(page.path, page.state);
|
||
|
};
|
||
|
|
||
|
return Router;
|
||
|
})));
|