import { RouteParams } from "../base/RouteParams.js";
import { Helper } from "../util/Helper.js";
import { States } from "./States.js";
import { Events } from "./Events.js";
const UrlPattern = new UP();
/**
* Router for handling routing events (ie. changes of the URL) and resolving and triggering corresponding states.
*/
class Router
{
/**
* Constructor
* @param {App} appInstance - Instance of app
*/
constructor( appInstance )
{
this.app = appInstance;
this.mode = this.app.settings.get( 'router.mode', 'url' );
this.basePath = this.app.settings.get( 'router.basePath', null );
this._routeActions = [];
this.isEnabled = false;
this.previousRoute = null;
this.currentRoute = null;
// Remove any index.html, index.php etc from url
// Note: the query string (ie. window.location.search) gets also elimated here.
const lastPathPart = window.location.href.split( "/" ).pop();
if ( lastPathPart && lastPathPart.split( "." ).length > 1 )
{
let cleanPath = window.location.href.replace( lastPathPart, '' );
cleanPath = cleanPath.replace( window.location.origin, '' );
cleanPath = Helper.trim( cleanPath, '/' );
if ( cleanPath.length > 0 )
{
window.history.replaceState( null, null, `/${cleanPath}/` );
}
}
if ( null === this.basePath )
{
// Try "best guess"
this.basePath = "";
}
this.basePath = Helper.trim( this.basePath, '/' );
}
/**
* Adds route and action
*
* @param {string} route - Route pattern
* @param {State} stateClass - State class which belongs to route pattern
*/
addRoute( route, stateClass )
{
let sRoute = Helper.trim( route, '/' );
sRoute = '/' + sRoute;
if ( true === Helper.isClass( stateClass ) )
{
if ( false === this.app.states.exists( stateClass.ID ) )
{
this.app.states.add( stateClass );
}
this._routeActions.push(
{
"action" : stateClass.ID,
"route" : new UrlPattern( sRoute )
}
);
}
// else: check if object and if object has an enter and exit method
else
{
throw new Error( 'Invalid action.' );
}
}
resolveActionDataByRoute( route )
{
let routeData = null,
params = {},
query = {},
routeSplits = route.split( "?" );
route = routeSplits[ 0 ];
if ( routeSplits.length > 1 )
{
let sp = new URLSearchParams( routeSplits[ 1 ] );
query = Object.fromEntries( sp.entries() );
}
for (let si = 0; si < this._routeActions.length; si++ )
{
params = this._routeActions[ si ].route.match( route );
if ( params )
{
routeData = {
"routeAction" : this._routeActions[ si ].action,
"routeParams" : new RouteParams( params, query )
};
break;
}
}
// If it is default route
if ( null === routeData )
{
// @todo For later - check setting if default scene should be shown
if ( route === '/' )
{
routeData = {
"routeAction" : States.DEFAULT_INDEX_STATE_ID,
"routeParams" : null
};
}
else if ( this.app.states.isNotFoundStateEnabled() )
{
routeData = {
"routeAction" : States.DEFAULT_NOT_FOUND_STATE_ID,
"routeParams" : null
}
}
}
return routeData;
}
createUrl( str ) {
// @todo Check whether its hash based routing or not
if ( this.mode === 'hash' )
{
return '#/' + Helper.trim( str, '/' );
}
else if ( 'url' === this.mode )
{
return window.location.origin + '/' + this.basePath + '/' + Helper.trim( str, '/' );
}
}
startsWithHash(string)
{
const regEx = /^#/;
const startsWithHash = regEx.test(string);
return Boolean(startsWithHash);
}
/**
* Enables router logic
*/
enable()
{
if ( true === this.isEnabled )
{
return;
}
this.isEnabled = true;
if ( this.mode === 'url' )
{
this.app.container.addEventListener( 'click', this.processUrl.bind( this ), false);
// Fix to properly handle backbutton
window.addEventListener( 'popstate', ( e ) =>
{
this.app.emit(
Events.EVENT.POPSTATE,
{
originalEvent : e
}
);
this.processUrl();
});
}
else if ( this.mode === 'hash' )
{
window.addEventListener( 'hashchange', this.processHash.bind( this ) );
}
else
{
console.erorr( `Invalid mode: ${mode} detected` );
}
}
/**
* Disables router logic
*/
disable()
{
this.isEnabled = false;
if ( this.mode === 'url' )
{
document.removeEventListener( 'click', this.processUrl.bind( this ) );
}
else if ( this.mode === 'hash' )
{
window.removeEventListener( 'hashchange', this.processHash.bind( this ) );
}
}
/**
* @private
*/
process()
{
if ( this.mode === 'url' )
{
this.processUrl();
}
else if ( this.mode === 'hash' )
{
this.processHash();
}
}
processHash()
{
const hash = location.hash || '#';
let route = hash.slice(1);
// always start with a leading slash
route = '/' + Helper.trim( route, '/' );
this.previousRoute = this.currentRoute;
this.currentRoute = route;
this.execute( this.resolveActionDataByRoute( route ) );
}
processUrl( event = null )
{
let url = window.location.href;
if ( event )
{
const target = event.target.closest('a');
// we are interested only in anchor tag clicks
if ( !target || target.hostname !== location.hostname ) {
return;
}
event.preventDefault();
url = target.getAttribute('href');
}
// we don't care about example.com#hash or
// example.com/#hash links
if (this.startsWithHash(url)) {
return;
}
let route = url.replace( window.location.origin + '/' + Helper.trim( this.basePath, '/' ), '' );
route = '/' + Helper.trim( route, '/' );
this.previousRoute = this.currentRoute;
this.currentRoute = route;
const actionData = this.resolveActionDataByRoute( route );
if ( actionData )
{
window.history.pushState( null, null, url );
}
this.execute( actionData );
}
redirect( url, forceReload = false )
{
if ( 'hash' === this.mode )
{
location.hash = '/' + Helper.trim( url, '/' );
if ( true === forceReload )
{
this.processHash();
}
}
else if ( 'url' === this.mode )
{
if ( true === forceReload )
{
window.history.pushState( null, null, url );
}
else
{
window.history.replaceState( null, null, url );
this.processUrl();
}
}
}
/**
* Update browser URL without triggering the processing
*
* @param {String} url - Sets the url part
*/
setUrl( url )
{
if ( 'hash' === this.mode )
{
location.hash = '/' + Helper.trim( url, '/' );
}
else if ( 'url' === this.mode )
{
window.history.replaceState( null, null, url );
}
}
resolveRoute( route )
{
let r = Helper.trim( route, '/#' );
r = Helper.trim( r, '#' );
r = Helper.trim( r, '/' );
return '/' + r;
}
async execute( actionData )
{
// Get view call
try
{
if ( actionData && actionData.hasOwnProperty( 'routeAction' ) && actionData.hasOwnProperty( 'routeParams' ) )
{
let stateInstance = this.app.states.create(
actionData.routeAction,
actionData.routeParams
);
await this.app.states.switchTo( stateInstance );
}
else
{
console.error( 'No state found.' );
}
}
catch( e )
{
console && console.error( e );
// Uncatched error
}
}
}
// Generated by CoffeeScript 1.10.0
function UP() {
var slice = [].slice;
var P, UrlPattern, astNodeContainsSegmentsForProvidedParams, astNodeToNames, astNodeToRegexString, baseAstNodeToRegexString, concatMap, defaultOptions, escapeForRegex, getParam, keysAndValuesToObject, newParser, regexGroupCount, stringConcatMap, stringify;
escapeForRegex = function(string) {
return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
};
concatMap = function(array, f) {
var i, length, results;
results = [];
i = -1;
length = array.length;
while (++i < length) {
results = results.concat(f(array[i]));
}
return results;
};
stringConcatMap = function(array, f) {
var i, length, result;
result = '';
i = -1;
length = array.length;
while (++i < length) {
result += f(array[i]);
}
return result;
};
regexGroupCount = function(regex) {
return (new RegExp(regex.toString() + '|')).exec('').length - 1;
};
keysAndValuesToObject = function(keys, values) {
var i, key, length, object, value;
object = {};
i = -1;
length = keys.length;
while (++i < length) {
key = keys[i];
value = values[i];
if (value == null) {
continue;
}
if (object[key] != null) {
if (!Array.isArray(object[key])) {
object[key] = [object[key]];
}
object[key].push(value);
} else {
object[key] = value;
}
}
return object;
};
P = {};
P.Result = function(value, rest) {
this.value = value;
this.rest = rest;
};
P.Tagged = function(tag, value) {
this.tag = tag;
this.value = value;
};
P.tag = function(tag, parser) {
return function(input) {
var result, tagged;
result = parser(input);
if (result == null) {
return;
}
tagged = new P.Tagged(tag, result.value);
return new P.Result(tagged, result.rest);
};
};
P.regex = function(regex) {
return function(input) {
var matches, result;
matches = regex.exec(input);
if (matches == null) {
return;
}
result = matches[0];
return new P.Result(result, input.slice(result.length));
};
};
P.sequence = function() {
var parsers;
parsers = 1 <= arguments.length ? slice.call(arguments, 0) : [];
return function(input) {
var i, length, parser, rest, result, values;
i = -1;
length = parsers.length;
values = [];
rest = input;
while (++i < length) {
parser = parsers[i];
result = parser(rest);
if (result == null) {
return;
}
values.push(result.value);
rest = result.rest;
}
return new P.Result(values, rest);
};
};
P.pick = function() {
var indexes, parsers;
indexes = arguments[0], parsers = 2 <= arguments.length ? slice.call(arguments, 1) : [];
return function(input) {
var array, result;
result = P.sequence.apply(P, parsers)(input);
if (result == null) {
return;
}
array = result.value;
result.value = array[indexes];
return result;
};
};
P.string = function(string) {
var length;
length = string.length;
return function(input) {
if (input.slice(0, length) === string) {
return new P.Result(string, input.slice(length));
}
};
};
P.lazy = function(fn) {
var cached;
cached = null;
return function(input) {
if (cached == null) {
cached = fn();
}
return cached(input);
};
};
P.baseMany = function(parser, end, stringResult, atLeastOneResultRequired, input) {
var endResult, parserResult, rest, results;
rest = input;
results = stringResult ? '' : [];
while (true) {
if (end != null) {
endResult = end(rest);
if (endResult != null) {
break;
}
}
parserResult = parser(rest);
if (parserResult == null) {
break;
}
if (stringResult) {
results += parserResult.value;
} else {
results.push(parserResult.value);
}
rest = parserResult.rest;
}
if (atLeastOneResultRequired && results.length === 0) {
return;
}
return new P.Result(results, rest);
};
P.many1 = function(parser) {
return function(input) {
return P.baseMany(parser, null, false, true, input);
};
};
P.concatMany1Till = function(parser, end) {
return function(input) {
return P.baseMany(parser, end, true, true, input);
};
};
P.firstChoice = function() {
var parsers;
parsers = 1 <= arguments.length ? slice.call(arguments, 0) : [];
return function(input) {
var i, length, parser, result;
i = -1;
length = parsers.length;
while (++i < length) {
parser = parsers[i];
result = parser(input);
if (result != null) {
return result;
}
}
};
};
newParser = function(options) {
var U;
U = {};
U.wildcard = P.tag('wildcard', P.string(options.wildcardChar));
U.optional = P.tag('optional', P.pick(1, P.string(options.optionalSegmentStartChar), P.lazy(function() {
return U.pattern;
}), P.string(options.optionalSegmentEndChar)));
U.name = P.regex(new RegExp("^[" + options.segmentNameCharset + "]+"));
U.named = P.tag('named', P.pick(1, P.string(options.segmentNameStartChar), P.lazy(function() {
return U.name;
})));
U.escapedChar = P.pick(1, P.string(options.escapeChar), P.regex(/^./));
U["static"] = P.tag('static', P.concatMany1Till(P.firstChoice(P.lazy(function() {
return U.escapedChar;
}), P.regex(/^./)), P.firstChoice(P.string(options.segmentNameStartChar), P.string(options.optionalSegmentStartChar), P.string(options.optionalSegmentEndChar), U.wildcard)));
U.token = P.lazy(function() {
return P.firstChoice(U.wildcard, U.optional, U.named, U["static"]);
});
U.pattern = P.many1(P.lazy(function() {
return U.token;
}));
return U;
};
defaultOptions = {
escapeChar: '\\',
segmentNameStartChar: ':',
segmentValueCharset: 'a-zA-Z0-9-_~ %',
segmentNameCharset: 'a-zA-Z0-9',
optionalSegmentStartChar: '(',
optionalSegmentEndChar: ')',
wildcardChar: '*'
};
baseAstNodeToRegexString = function(astNode, segmentValueCharset) {
if (Array.isArray(astNode)) {
return stringConcatMap(astNode, function(node) {
return baseAstNodeToRegexString(node, segmentValueCharset);
});
}
switch (astNode.tag) {
case 'wildcard':
return '(.*?)';
case 'named':
return "([" + segmentValueCharset + "]+)";
case 'static':
return escapeForRegex(astNode.value);
case 'optional':
return '(?:' + baseAstNodeToRegexString(astNode.value, segmentValueCharset) + ')?';
}
};
astNodeToRegexString = function(astNode, segmentValueCharset) {
if (segmentValueCharset == null) {
segmentValueCharset = defaultOptions.segmentValueCharset;
}
return '^' + baseAstNodeToRegexString(astNode, segmentValueCharset) + '$';
};
astNodeToNames = function(astNode) {
if (Array.isArray(astNode)) {
return concatMap(astNode, astNodeToNames);
}
switch (astNode.tag) {
case 'wildcard':
return ['_'];
case 'named':
return [astNode.value];
case 'static':
return [];
case 'optional':
return astNodeToNames(astNode.value);
}
};
getParam = function(params, key, nextIndexes, sideEffects) {
var index, maxIndex, result, value;
if (sideEffects == null) {
sideEffects = false;
}
value = params[key];
if (value == null) {
if (sideEffects) {
throw new Error("no values provided for key `" + key + "`");
} else {
return;
}
}
index = nextIndexes[key] || 0;
maxIndex = Array.isArray(value) ? value.length - 1 : 0;
if (index > maxIndex) {
if (sideEffects) {
throw new Error("too few values provided for key `" + key + "`");
} else {
return;
}
}
result = Array.isArray(value) ? value[index] : value;
if (sideEffects) {
nextIndexes[key] = index + 1;
}
return result;
};
astNodeContainsSegmentsForProvidedParams = function(astNode, params, nextIndexes) {
var i, length;
if (Array.isArray(astNode)) {
i = -1;
length = astNode.length;
while (++i < length) {
if (astNodeContainsSegmentsForProvidedParams(astNode[i], params, nextIndexes)) {
return true;
}
}
return false;
}
switch (astNode.tag) {
case 'wildcard':
return getParam(params, '_', nextIndexes, false) != null;
case 'named':
return getParam(params, astNode.value, nextIndexes, false) != null;
case 'static':
return false;
case 'optional':
return astNodeContainsSegmentsForProvidedParams(astNode.value, params, nextIndexes);
}
};
stringify = function(astNode, params, nextIndexes) {
if (Array.isArray(astNode)) {
return stringConcatMap(astNode, function(node) {
return stringify(node, params, nextIndexes);
});
}
switch (astNode.tag) {
case 'wildcard':
return getParam(params, '_', nextIndexes, true);
case 'named':
return getParam(params, astNode.value, nextIndexes, true);
case 'static':
return astNode.value;
case 'optional':
if (astNodeContainsSegmentsForProvidedParams(astNode.value, params, nextIndexes)) {
return stringify(astNode.value, params, nextIndexes);
} else {
return '';
}
}
};
UrlPattern = function(arg1, arg2) {
var groupCount, options, parsed, parser, withoutWhitespace;
if (arg1 instanceof UrlPattern) {
this.isRegex = arg1.isRegex;
this.regex = arg1.regex;
this.ast = arg1.ast;
this.names = arg1.names;
return;
}
this.isRegex = arg1 instanceof RegExp;
if (!(('string' === typeof arg1) || this.isRegex)) {
throw new TypeError('argument must be a regex or a string');
}
if (this.isRegex) {
this.regex = arg1;
if (arg2 != null) {
if (!Array.isArray(arg2)) {
throw new Error('if first argument is a regex the second argument may be an array of group names but you provided something else');
}
groupCount = regexGroupCount(this.regex);
if (arg2.length !== groupCount) {
throw new Error("regex contains " + groupCount + " groups but array of group names contains " + arg2.length);
}
this.names = arg2;
}
return;
}
if (arg1 === '') {
throw new Error('argument must not be the empty string');
}
withoutWhitespace = arg1.replace(/\s+/g, '');
if (withoutWhitespace !== arg1) {
throw new Error('argument must not contain whitespace');
}
options = {
escapeChar: (arg2 != null ? arg2.escapeChar : void 0) || defaultOptions.escapeChar,
segmentNameStartChar: (arg2 != null ? arg2.segmentNameStartChar : void 0) || defaultOptions.segmentNameStartChar,
segmentNameCharset: (arg2 != null ? arg2.segmentNameCharset : void 0) || defaultOptions.segmentNameCharset,
segmentValueCharset: (arg2 != null ? arg2.segmentValueCharset : void 0) || defaultOptions.segmentValueCharset,
optionalSegmentStartChar: (arg2 != null ? arg2.optionalSegmentStartChar : void 0) || defaultOptions.optionalSegmentStartChar,
optionalSegmentEndChar: (arg2 != null ? arg2.optionalSegmentEndChar : void 0) || defaultOptions.optionalSegmentEndChar,
wildcardChar: (arg2 != null ? arg2.wildcardChar : void 0) || defaultOptions.wildcardChar
};
parser = newParser(options);
parsed = parser.pattern(arg1);
if (parsed == null) {
throw new Error("couldn't parse pattern");
}
if (parsed.rest !== '') {
throw new Error("could only partially parse pattern");
}
this.ast = parsed.value;
this.regex = new RegExp(astNodeToRegexString(this.ast, options.segmentValueCharset));
this.names = astNodeToNames(this.ast);
};
UrlPattern.prototype.match = function(url) {
var groups, match;
match = this.regex.exec(url);
if (match == null) {
return null;
}
groups = match.slice(1);
if (this.names) {
return keysAndValuesToObject(this.names, groups);
} else {
return groups;
}
};
UrlPattern.prototype.stringify = function(params) {
if (params == null) {
params = {};
}
if (this.isRegex) {
throw new Error("can't stringify patterns generated from a regex");
}
if (params !== Object(params)) {
throw new Error("argument must be an object or undefined");
}
return stringify(this.ast, params, {});
};
UrlPattern.escapeForRegex = escapeForRegex;
UrlPattern.concatMap = concatMap;
UrlPattern.stringConcatMap = stringConcatMap;
UrlPattern.regexGroupCount = regexGroupCount;
UrlPattern.keysAndValuesToObject = keysAndValuesToObject;
UrlPattern.P = P;
UrlPattern.newParser = newParser;
UrlPattern.defaultOptions = defaultOptions;
UrlPattern.astNodeToRegexString = astNodeToRegexString;
UrlPattern.astNodeToNames = astNodeToNames;
UrlPattern.getParam = getParam;
UrlPattern.astNodeContainsSegmentsForProvidedParams = astNodeContainsSegmentsForProvidedParams;
UrlPattern.stringify = stringify;
return UrlPattern;
};
export { Router };