push-state-tree.js
//! push-state-tree - v0.16.0 - 2024-03-30
//* https://github.com/gartz/pushStateTree/
//* Copyright (c) 2024 Gabriel Reitz Giannattasio <g@rtz.sh>; Licensed
var PushStateTree = {options: {VERSION: '0.16.0'}};
(function (root) {
"use strict";
var document = root.document;
var window = root.window;
var location = root.location;
var isIE = (function () {
var trident = window.navigator.userAgent.indexOf("Trident");
return trident >= 0;
})();
// Shim, to work with older browsers
(function () {
// Opera and IE doesn't implement location.origin
if (!root.location.origin) {
root.location.origin = root.location.protocol + "//" + root.location.host;
}
})();
(function () {
/* global HTMLDocument */
if (Function.prototype.bind) {
return;
}
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5 internal IsCallable function
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
FNOP = function () {},
fBound = function () {
var context = oThis;
if (this instanceof FNOP && oThis) {
context = this;
}
return fToBind.apply(context, aArgs.concat(Array.prototype.slice.call(arguments)));
};
FNOP.prototype = this.prototype;
fBound.prototype = new FNOP();
return fBound;
};
})();
// IE9 shims
var HashChangeEvent = root.HashChangeEvent;
var Event = root.Event;
(function () {
if (!Element.prototype.addEventListener) {
return;
}
function CustomEvent(event, params) {
params = params || {
bubbles: false,
cancelable: false,
detail: undefined,
};
var evt = document.createEvent("CustomEvent");
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
return evt;
}
CustomEvent.prototype = Event.prototype;
if (!root.CustomEvent || !!isIE) {
root.CustomEvent = CustomEvent;
}
// Opera before 15 has HashChangeEvent but throw a DOM Implement error
if (!HashChangeEvent || (root.opera && root.opera.version() < 15) || !!isIE) {
HashChangeEvent = root.CustomEvent;
}
if (!!isIE) {
Event = CustomEvent;
}
// fix for Safari
try {
new HashChangeEvent("hashchange");
} catch (e) {
HashChangeEvent = CustomEvent;
}
try {
new Event("popstate");
} catch (e) {
Event = CustomEvent;
}
})();
// IE 8 shims
(function () {
if (Element.prototype.addEventListener || !Object.defineProperty) {
return;
}
// create an MS event object and get prototype
var proto = document.createEventObject().constructor.prototype;
Object.defineProperty(proto, "target", {
get: function () {
return this.srcElement;
},
});
// IE8 addEventLister shim
var addEventListenerFunc = function (type, handler) {
if (!this.__bindedFunctions) {
this.__bindedFunctions = [];
}
var fn = handler;
if (!("on" + type in this) || type === "hashchange") {
this.__elemetIEid = this.__elemetIEid || "__ie__" + Math.random();
var customEventId = type + this.__elemetIEid;
//TODO: Bug???
//document.documentElement[customEventId];
var element = this;
var propHandler = function (event) {
// if the property changed is the custom jqmReady property
if (event.propertyName === customEventId) {
fn.call(element, document.documentElement[customEventId]);
}
};
this.__bindedFunctions.push({
original: fn,
binded: propHandler,
});
document.documentElement.attachEvent("onpropertychange", propHandler);
if (type !== "hashchange") {
return;
}
}
var bindedFn = fn.bind(this);
this.__bindedFunctions.push({
original: fn,
binded: bindedFn,
});
this.attachEvent("on" + type, bindedFn);
};
// setup the DOM and window objects
HTMLDocument.prototype.addEventListener = addEventListenerFunc;
Element.prototype.addEventListener = addEventListenerFunc;
window.addEventListener = addEventListenerFunc;
// IE8 removeEventLister shim
var removeEventListenerFunc = function (type, handler) {
if (!this.__bindedFunctions) {
this.__bindedFunctions = [];
}
var fn = handler;
var bindedFn;
if (!("on" + type in this) || type === "hashchange") {
for (var i = 0; i < this.__bindedFunctions.length; i++) {
if (this.__bindedFunctions[i].original === fn) {
bindedFn = this.__bindedFunctions[i].binded;
this.__bindedFunctions = this.__bindedFunctions.splice(i, 1);
i = this.__bindedFunctions.length;
}
}
if (bindedFn) {
document.documentElement.detachEvent("onpropertychange", bindedFn);
}
if (type !== "hashchange") {
return;
}
}
for (var j = 0; j < this.__bindedFunctions.length; j++) {
if (this.__bindedFunctions[j].original === fn) {
bindedFn = this.__bindedFunctions[j].binded;
this.__bindedFunctions = this.__bindedFunctions.splice(j, 1);
j = this.__bindedFunctions.length;
}
}
if (!bindedFn) {
return;
}
this.detachEvent("on" + type, bindedFn);
};
// setup the DOM and window objects
HTMLDocument.prototype.removeEventListener = removeEventListenerFunc;
Element.prototype.removeEventListener = removeEventListenerFunc;
window.removeEventListener = removeEventListenerFunc;
Event = function (type, obj) {
var evt = document.createEventObject();
obj = obj || {};
evt.type = type;
evt.detail = obj.detail;
if (!("on" + type in root) || type === "hashchange") {
evt.name = type;
evt.customEvent = true;
}
return evt;
};
/*jshint -W020 */
CustomEvent = Event;
HashChangeEvent = CustomEvent;
var dispatchEventFunc = function (e) {
if (!e.customEvent) {
this.fireEvent(e.type, e);
return;
}
// no event registred
if (!this.__elemetIEid) {
return;
}
var customEventId = e.name + this.__elemetIEid;
document.documentElement[customEventId] = e;
};
// setup the Element dispatchEvent used to trigger events on the board
HTMLDocument.prototype.dispatchEvent = dispatchEventFunc;
Element.prototype.dispatchEvent = dispatchEventFunc;
window.dispatchEvent = dispatchEventFunc;
})();
(function () {
// modern browser support forEach, probably will be IE8
var modernBrowser = "forEach" in Array.prototype;
// IE8 pollyfills:
// IE8 slice doesn't work with NodeList
if (!modernBrowser) {
var builtinSlice = Array.prototype.slice;
Array.prototype.slice = function () {
var arr = [];
for (var i = 0, n = this.length; i < n; i++) {
if (i in this) {
arr.push(this[i]);
}
}
return builtinSlice.apply(arr, arguments);
};
}
if (!("forEach" in Array.prototype)) {
Array.prototype.forEach = function (action, that) {
for (var i = 0; i < this.length; i++) {
if (i in this) {
action.call(that, this[i], i);
}
}
};
}
if (typeof String.prototype.trim !== "function") {
String.prototype.trim = function () {
return this.replace(/^\s+|\s+$/g, "");
};
}
if (!Array.prototype.filter) {
Array.prototype.filter = function (fun /*, thisArg */) {
if (this === void 0 || this === null) {
throw new TypeError();
}
var t = Object(this);
var len = t.length >>> 0;
if (typeof fun !== "function") {
throw new TypeError();
}
var res = [];
var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
for (var i = 0; i < len; i++) {
if (i in t) {
var val = t[i];
// NOTE: Technically this should Object.defineProperty at
// the next index, as push can be affected by
// properties on Object.prototype and Array.prototype.
// But that method's new, and collisions should be
// rare, so use the more-compatible alternative.
if (fun.call(thisArg, val, i, t)) {
res.push(val);
}
}
}
return res;
};
}
})();
var options = (root.PushStateTree && root.PushStateTree.options) || {};
// Constants for uglifiers
// PushStateTree options
var USE_PUSH_STATE = "usePushState";
var HAS_PUSH_STATE = "hasPushState";
var IGNORE_HASH = "ignoreHash";
var DEBUG = root.DEBUG || options.DEBUG;
var VERSION = options.VERSION || "development";
// PushStateTree events
var HASHCHANGE = "hashchange";
var POPSTATE = "popstate";
var LEAVE = "leave";
var UPDATE = "update";
var ENTER = "enter";
var CHANGE = "change";
var MATCH = "match";
var OLD_MATCH = "oldMatch";
// Helpers
function isInt(n) {
return !isNaN(parseFloat(n)) && n % 1 === 0 && isFinite(n);
}
function wrapProperty(scope, prop, target) {
Object.defineProperty(scope, prop, {
get: function () {
return target;
},
set: function () {},
});
}
function isExternal(url) {
// Check if a URL is external
return /^[a-z0-9]+:\/\//i.test(url);
}
function isRelative(uri) {
// Check if a URI is relative path, when begin with # or / isn't relative uri
return /^[^#/]/.test(uri);
}
function resolveRelativePath(path) {
// Resolve relative paths manually for browsers using hash navigation
var parts = path.split("/");
var i = 1;
while (i < parts.length) {
// if current part is `..` and previous part is different, remove both of them
if (parts[i] === ".." && i > 0 && parts[i - 1] !== "..") {
parts.splice(i - 1, 2);
i -= 2;
}
i++;
}
return parts
.join("/")
.replace(/\/\.\/|\.\/|\.\.\//g, "/")
.replace(/^\/$/, "");
}
// Add compatibility with old IE browsers
var elementPrototype = typeof HTMLElement !== "undefined" ? HTMLElement : Element;
function PushStateTree(options) {
options = options || {};
// Enforce the usage of PushState API, available on all modern browsers.
// True by default
options[USE_PUSH_STATE] = options[USE_PUSH_STATE] !== false;
// Ignore the hash symbol to enforce PWA navigation, those websites can still use
// hash as anchor kind of browser native navigation inside the rendered page, but
// the data of the hash ain't used as URI on the PushStateTree when this option is
// enabled (true).
//
// Example:
// 'foo/bar#zaz'
// URI when enabled: "foo/bar"
// URI when disabled: "zaz"
//
// Keep it disabled for servers that doesn't support PWA, and require hash navigation.
//
// False by default due to legacy compatibility.
options[IGNORE_HASH] = options[IGNORE_HASH] === true;
// Force the instance to always return a HTMLElement
if (!(this instanceof elementPrototype)) {
return PushStateTree.apply(document.createElement("pushstatetree-route"), arguments);
}
var rootElement = this;
this.VERSION = VERSION;
// Setup options
for (var prop in options) {
if (options.hasOwnProperty(prop)) {
rootElement[prop] = options[prop];
}
}
// Allow switch between pushState or hash navigation modes, in browser that doesn't support
// pushState it will always be false. and use hash navigation enforced.
// use backend non permanent redirect when old browsers are detected in the request.
if (!PushStateTree.prototype[HAS_PUSH_STATE]) {
wrapProperty(rootElement, USE_PUSH_STATE, false);
} else {
var usePushState = options[USE_PUSH_STATE];
Object.defineProperty(rootElement, USE_PUSH_STATE, {
get: function () {
return usePushState;
},
set: function (val) {
usePushState = val !== false;
},
});
}
// When enabled beautifyLocation will auto switch between hash to pushState when enabled
Object.defineProperty(rootElement, "beautifyLocation", {
get: function () {
return PushStateTree.prototype.beautifyLocation && usePushState;
},
set: function (value) {
PushStateTree.prototype.beautifyLocation = value === true;
},
});
rootElement.beautifyLocation = options.beautifyLocation && rootElement.usePushState;
var basePath;
Object.defineProperty(rootElement, "basePath", {
get: function () {
return basePath;
},
set: function (value) {
basePath = value || "";
if (basePath[0] !== "/") {
basePath = "/" + basePath;
}
},
});
rootElement.basePath = options.basePath;
function wrappMethodsAndPropertiesToPrototype(prop) {
if (typeof PushStateTree.prototype[prop] === "function") {
// function wrapper
rootElement[prop] = function () {
return PushStateTree.prototype[prop].apply(this, arguments);
};
} else {
if (typeof rootElement[prop] !== "undefined") return;
// property wrapper
Object.defineProperty(rootElement, prop, {
get: function () {
return PushStateTree.prototype[prop];
},
set: function (val) {
PushStateTree.prototype[prop] = val;
},
});
}
}
//TODO: emcapsulate this
for (var protoProperty in PushStateTree.prototype) {
if (PushStateTree.prototype.hasOwnProperty(protoProperty)) {
wrappMethodsAndPropertiesToPrototype(protoProperty);
}
}
wrapProperty(rootElement, "length", root.history.length);
wrapProperty(rootElement, "state", root.history.state);
var cachedUri = {
url: "",
uri: "",
};
Object.defineProperty(rootElement, "uri", {
get: function () {
if (cachedUri.url === root.location.href) return cachedUri.uri;
var ignoreHash = options[IGNORE_HASH];
var uri;
if (
!ignoreHash &&
(root.location.hash.length || root.location.href[location.href.length - 1] === "#")
) {
// Remove all begin # chars from the location when using hash
uri = root.location.hash.match(/^(#*)?(.*\/?)/)[2];
var usePushState = rootElement[USE_PUSH_STATE];
if (rootElement.beautifyLocation && rootElement.isPathValid && usePushState) {
// when using pushState, replace the browser location to avoid ugly URLs
rootElement.replaceState(
rootElement.state,
rootElement.title,
uri[0] === "/" ? uri : "/" + uri,
);
}
} else {
uri = root.location.pathname + root.location.search;
if (this.isPathValid) {
uri = uri.slice(this.basePath.length);
}
}
// Remove the very first slash, do don't match it as URI
uri = uri.replace(/^[\/]+/, "");
if (rootElement.getAttribute("uri") !== uri) {
rootElement.setAttribute("uri", uri);
}
cachedUri.url = root.location.href;
cachedUri.uri = uri;
return uri;
},
configurable: true,
});
Object.defineProperty(rootElement, "isPathValid", {
get: function () {
var uri = root.location.pathname + root.location.search;
return !this.basePath || uri.indexOf(this.basePath) === 0;
},
});
rootElement.eventStack = {
leave: [],
change: [],
enter: [],
match: [],
};
root.addEventListener(
POPSTATE,
function () {
var eventURI = rootElement.uri;
var eventState = rootElement.state;
rootElement.rulesDispatcher();
oldURI = eventURI;
oldState = eventState;
// If there is holding dispatch in the event, do it now
if (holdingDispatch) {
this.dispatch();
}
}.bind(rootElement),
);
var readOnhashchange = false;
var onhashchange = function () {
// Workaround IE8
if (readOnhashchange) return;
// Don't dispatch, because already have dispatched in popstate event
if (oldURI === rootElement.uri) return;
var eventURI = rootElement.uri;
var eventState = rootElement.state;
rootElement.rulesDispatcher();
oldURI = eventURI;
oldState = eventState;
// If there is holding dispatch in the event, do it now
if (holdingDispatch) {
this.dispatch();
}
}.bind(rootElement);
rootElement.avoidHashchangeHandler = function () {
// Avoid triggering hashchange event
root.removeEventListener(HASHCHANGE, onhashchange);
readOnhashchange = true;
};
root.addEventListener(HASHCHANGE, onhashchange);
// Uglify propourses
var dispatchHashChange = function () {
root.dispatchEvent(new HashChangeEvent(HASHCHANGE));
};
// Modern browsers
document.addEventListener("DOMContentLoaded", dispatchHashChange);
// Some IE browsers
root.addEventListener("readystatechange", dispatchHashChange);
// Almost all browsers
root.addEventListener(
"load",
function () {
dispatchHashChange();
if (isIE) {
root.setInterval(
function () {
if (rootElement.uri !== oldURI) {
dispatchHashChange();
return;
}
if (readOnhashchange) {
readOnhashchange = false;
oldURI = rootElement.uri;
root.addEventListener(HASHCHANGE, onhashchange);
}
}.bind(rootElement),
50,
);
}
}.bind(rootElement),
);
return this;
}
var oldState = null;
var oldURI = null;
var eventsQueue = [];
var holdingDispatch = false;
var holdDispatch = false;
PushStateTree.prototype = {
// Version ~0.11 beatifyLocation is enabled by default
beautifyLocation: true,
createRule: function (options) {
// Create a pushstreamtree-rule element from a literal object
var rule = document.createElement("pushstatetree-rule");
var ruleRegex = new RegExp("");
// Bind rule property with element attribute
Object.defineProperty(rule, "rule", {
get: function () {
return ruleRegex;
},
set: function (val) {
if (val instanceof RegExp) {
ruleRegex = val;
} else {
// IE8 trigger set from the property when update the attribute, avoid recursive loop
if (val === ruleRegex.toString()) return;
// Slice the pattern from the attribute
var slicedPattern = (val + "").match(/^\/(.+)\/([gmi]*)|(.*)/);
ruleRegex = new RegExp(slicedPattern[1] || slicedPattern[3], slicedPattern[2]);
}
rule.setAttribute("rule", ruleRegex.toString());
},
});
// Bind rule property with element attribute
Object.defineProperty(rule, "parentGroup", {
get: function () {
var attr = rule.getAttribute("parent-group");
if (attr && isInt(attr)) {
return +attr;
}
return null;
},
set: function (val) {
if (isInt(val)) {
rule.setAttribute("parent-group", val);
} else {
rule.removeAttribute("parent-group");
}
},
});
for (var prop in options)
if (options.hasOwnProperty(prop)) {
rule[prop] = options[prop];
}
// Match is always a array, so you can test for match[n] anytime
var match = [];
Object.defineProperty(rule, MATCH, {
get: function () {
return match;
},
set: function (val) {
match = val instanceof Array ? val : [];
},
});
var oldMatch = [];
Object.defineProperty(rule, OLD_MATCH, {
get: function () {
return oldMatch;
},
set: function (val) {
oldMatch = val instanceof Array ? val : [];
},
});
rule[MATCH] = [];
rule[OLD_MATCH] = [];
// Replicate the methods from `route` to the rule, by transversing until find and execute
// the router method, not a fast operation, but ensure the right route to be triggered
["assign", "navigate", "replace", "dispatch", "pushState", "replaceState"].forEach(
function (methodName) {
rule[methodName] = function () {
this.parentElement[methodName].apply(this.parentElement, arguments);
};
},
);
return rule;
},
add: function (options) {
// Transform any literal object in a pushstatetree-rule and append it
this.appendChild(this.createRule(options));
return this;
},
remove: function (queryOrElement) {
// Remove a pushstateree-rule, pass a element or it query
var element = queryOrElement;
if (typeof queryOrElement === "string") {
element = this.querySelector(queryOrElement);
}
if (element && element.parentElement) {
element.parentElement.removeChild(element);
return element;
}
},
dispatch: function () {
// Deferred trigger the actual browser location
if (holdDispatch) {
holdingDispatch = true;
return this;
}
holdingDispatch = false;
root.dispatchEvent(new Event(POPSTATE));
return this;
},
assign: function (url) {
// Shortcut for pushState and dispatch methods
return this.pushState(null, null, url).dispatch();
},
replace: function (url) {
// Shortcut for pushState and dispatch methods
return this.replaceState(null, null, url).dispatch();
},
navigate: function () {
this.assign.apply(this, arguments);
},
rulesDispatcher: function () {
// Will dispatch the right events in each rule
/*jshint validthis:true */
// Cache the URI, in case of an event try to change it
var debug = this.debug === true || DEBUG;
// Abort if the basePath isn't valid for this router
if (!this.isPathValid) return;
function runner(uri, oldURI) {
Array.prototype.slice
.call(this.children || this.childNodes)
.forEach(recursiveDispatcher.bind(this, uri, oldURI));
return uri;
}
eventsQueue.push(runner.bind(this, this.uri));
// Is there already a queue been executed, so just add the runner
// and let the main queue resolve it
if (eventsQueue.length > 1) {
return;
}
// Chain execute the evetsQueue
var last = oldURI;
while (eventsQueue.length > 0) {
last = eventsQueue[0].call(null, last);
eventsQueue.shift();
}
// If a dispatch is triggered inside a event callback, it need to hold
holdDispatch = true;
// A stack of all events to be dispatched, to ensure the priority order
var eventStack = this.eventStack;
// Order of events stack execution, leave event isn't here because it executes in the
// recursiveDispatcher, for one loop less
[CHANGE, ENTER, MATCH].forEach(function (type) {
// Execute the leave stack of events
while (eventStack[type].length > 0) {
var events = eventStack[type][0].events;
var element = eventStack[type][0].element;
//TODO: Ignore if there isn't same in the enter stack and remove it
while (events.length > 0) {
element.dispatchEvent(events[0]);
events.shift();
}
eventStack[type].shift();
}
});
// If there is holding dispatchs in the event, do it now
holdDispatch = false;
function recursiveDispatcher(uri, oldURI, ruleElement) {
if (!ruleElement.rule) return;
var useURI = uri;
var useOldURI = oldURI;
var parentElement;
if (typeof ruleElement.parentGroup === "number") {
useURI = "";
parentElement = ruleElement.parentElement;
if (parentElement[MATCH].length > ruleElement.parentGroup)
useURI = parentElement[MATCH][ruleElement.parentGroup] || "";
useOldURI = "";
if (parentElement[OLD_MATCH].length > ruleElement.parentGroup)
useOldURI = parentElement[OLD_MATCH][ruleElement.parentGroup] || "";
}
ruleElement[MATCH] = useURI[MATCH](ruleElement.rule);
if (typeof useOldURI === "string") {
ruleElement[OLD_MATCH] = useOldURI[MATCH](ruleElement.rule);
} else {
ruleElement[OLD_MATCH] = [];
}
var match = ruleElement[MATCH];
var oldMatch = ruleElement[OLD_MATCH];
var children = Array.prototype.slice.call(ruleElement.children);
function PushStateTreeEvent(name, params) {
params = params || {};
params.detail = params.detail || {};
params.detail[MATCH] = match || [];
params.detail[OLD_MATCH] = oldMatch || [];
params.cancelable = true;
if (debug && typeof console === "object") {
console.log({
name: name,
ruleElement: ruleElement,
params: params,
useURI: useURI,
useOldURI: useOldURI,
});
if (console.trace) console.trace();
}
var event = new root.CustomEvent(name, params);
return event;
}
// Not match or leave?
if (match.length === 0) {
if (oldMatch.length === 0 || ruleElement.routerURI !== oldURI) {
// just not match...
return;
}
ruleElement.uri = null;
ruleElement.removeAttribute("uri");
children.forEach(recursiveDispatcher.bind(this, uri, oldURI));
// Don't use stack for LEAVE event, dispatch in this loop
ruleElement.dispatchEvent(
new PushStateTreeEvent(UPDATE, {
detail: { type: LEAVE },
}),
);
ruleElement.dispatchEvent(new PushStateTreeEvent(LEAVE));
return;
}
// dispatch the match event
this.eventStack[MATCH].push({
element: ruleElement,
events: [new PushStateTreeEvent(MATCH)],
});
var isNewURI = ruleElement.routerURI !== oldURI;
ruleElement.routerURI = this.uri;
ruleElement.uri = match[0];
ruleElement.setAttribute("uri", match[0]);
if (oldMatch.length === 0 || isNewURI) {
// stack dispatch enter event
this.eventStack[ENTER].push({
element: ruleElement,
events: [
new PushStateTreeEvent(UPDATE, {
detail: { type: ENTER },
}),
new PushStateTreeEvent(ENTER),
],
});
children.forEach(recursiveDispatcher.bind(this, uri, oldURI));
return;
}
// if has something changed, dispatch the change event
if (match[0] !== oldMatch[0]) {
// stack dispatch enter event
this.eventStack[CHANGE].push({
element: ruleElement,
events: [
new PushStateTreeEvent(UPDATE, {
detail: { type: CHANGE },
}),
new PushStateTreeEvent(CHANGE),
],
});
}
children.forEach(recursiveDispatcher.bind(this, uri, oldURI));
}
},
};
function preProcessUriBeforeExecuteNativeHistoryMethods(method) {
/*jshint validthis:true */
var scopeMethod = method;
this[method] = function () {
// Wrap method
// remove the method from arguments
var args = Array.prototype.slice.call(arguments);
// if has a basePath translate the not relative paths to use the basePath
if (scopeMethod === "pushState" || scopeMethod === "replaceState") {
if (!isExternal(args[2])) {
// When not external link, need to normalize the URI
if (isRelative(args[2])) {
// Relative to the uri
var basePath = this.uri.match(/^([^?#]*)\//);
basePath = basePath ? basePath[1] + "/" : "";
args[2] = basePath + args[2];
} else {
// This isn't relative, will cleanup / and # from the begin and use the remain path
args[2] = args[2].match(/^([#/]*)?(.*)/)[2];
}
if (!this[USE_PUSH_STATE]) {
// Ignore basePath when using location.hash and resolve relative path and keep
// the current location.pathname, some browsers history API might apply the new pathname
// with the hash content if not explicit
args[2] = location.pathname + "#" + resolveRelativePath(args[2]);
} else {
// Add the basePath to your uri, not allowing to go by pushState outside the basePath
args[2] = this.basePath + args[2];
}
}
}
root.history[scopeMethod].apply(root.history, args);
// Chainnable
return this;
};
}
// Wrap history methods
for (var method in root.history) {
if (typeof root.history[method] === "function") {
preProcessUriBeforeExecuteNativeHistoryMethods.call(PushStateTree.prototype, method);
}
}
PushStateTree.prototype[HAS_PUSH_STATE] = root.history && !!root.history.pushState;
if (!PushStateTree.prototype[HAS_PUSH_STATE]) {
PushStateTree.prototype[USE_PUSH_STATE] = false;
}
var lastTitle = null;
if (!PushStateTree.prototype.pushState) {
PushStateTree.prototype.pushState = function (state, title, uri) {
var t = document.title || "";
uri = uri || "";
if (lastTitle !== null) {
document.title = lastTitle;
}
this.avoidHashchangeHandler();
// Replace hash url
if (isExternal(uri)) {
// this will redirect the browser, so doesn't matters the rest...
root.location.href = uri;
}
// Remove the has if is it present
if (uri[0] === "#") {
uri = uri.slice(1);
}
if (isRelative(uri)) {
uri = root.location.hash.slice(1, root.location.hash.lastIndexOf("/") + 1) + uri;
uri = resolveRelativePath(uri);
}
root.location.hash = uri;
document.title = t;
lastTitle = title;
return this;
};
}
if (!PushStateTree.prototype.replaceState) {
PushStateTree.prototype.replaceState = function (state, title, uri) {
var t = document.title || "";
uri = uri || "";
if (lastTitle !== null) {
document.title = lastTitle;
}
this.avoidHashchangeHandler();
// Replace the url
if (isExternal(uri)) {
throw new Error("Invalid url replace.");
}
if (uri[0] === "#") {
uri = uri.slice(1);
}
if (isRelative(uri)) {
var relativePos = root.location.hash.lastIndexOf("/") + 1;
uri = root.location.hash.slice(1, relativePos) + uri;
uri = resolveRelativePath(uri);
}
// Always use hash navigation
uri = "#" + uri;
root.location.replace(uri);
document.title = t;
lastTitle = title;
return this;
};
}
root.PushStateTree = PushStateTree;
// Node import support
if (typeof module !== "undefined") module.exports = PushStateTree;
})(
(function () {
"use strict";
if (typeof globalThis !== "undefined") {
return globalThis;
} else if (typeof window !== "undefined") {
return window;
} else if (typeof global !== "undefined") {
return global;
} else if (typeof self !== "undefined") {
return self;
}
})(),
);