white-label-router 1.1.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,351 +1,267 @@
1
- (function (global, factory) {
2
- if (typeof define === "function" && define.amd) {
3
- define(['module', 'gator'], factory);
4
- } else if (typeof exports !== "undefined") {
5
- factory(module, require('gator'));
6
- } else {
7
- var mod = {
8
- exports: {}
1
+ "use strict";
2
+ /** History API router with ordered path-boundary matching. */
3
+ class Router {
4
+ routes;
5
+ url;
6
+ route;
7
+ scope;
8
+ previousRoute;
9
+ pageTitle;
10
+ mediator;
11
+ boundPushStateClick;
12
+ boundPopState;
13
+ boundMediatorNavigate;
14
+ listenersInitialized;
15
+ locationData = { url: '', data: { url: [], mediator: undefined, query: {} } };
16
+ /**
17
+ * Create an instance with its own state and listener references.
18
+ */
19
+ constructor() {
20
+ // placeholder for routes defined when extended
21
+ this.routes = {
22
+ defaultRoute: {
23
+ view: {
24
+ initialize: () => {
25
+ return true;
26
+ },
27
+ destroy: () => {
28
+ return true;
29
+ }
30
+ }
31
+ }
9
32
  };
10
- factory(mod, global.gator);
11
- global.index = mod.exports;
12
- }
13
- })(this, function (module, _gator) {
14
- 'use strict';
15
-
16
- var _gator2 = _interopRequireDefault(_gator);
17
-
18
- function _interopRequireDefault(obj) {
19
- return obj && obj.__esModule ? obj : {
20
- default: obj
33
+ // this will hold the current url
34
+ this.url = '';
35
+ // this holds the current route
36
+ this.route = null;
37
+ // can house a container for the router to act upon
38
+ this.scope = null;
39
+ // this holds the previous route for destorying later
40
+ this.previousRoute = null;
41
+ // this will hold the page title (not yet supported by browsers)
42
+ this.pageTitle = null;
43
+ // optional mediator placeholder
44
+ this.mediator = false;
45
+ // Stable listener references allow destroy() to release browser resources.
46
+ this.boundPushStateClick = this.eventPushStateClick.bind(this);
47
+ this.boundPopState = this.eventPopState.bind(this);
48
+ this.boundMediatorNavigate = (data = {}) => {
49
+ this.navigate(data.url, data, false);
21
50
  };
51
+ this.listenersInitialized = false;
22
52
  }
23
-
24
- var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
25
- return typeof obj;
26
- } : function (obj) {
27
- return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj;
28
- };
29
-
30
- function _classCallCheck(instance, Constructor) {
31
- if (!(instance instanceof Constructor)) {
32
- throw new TypeError("Cannot call a class as a function");
53
+ /**
54
+ * Start this instance and return it for lifecycle chaining.
55
+ * @returns This instance for chaining.
56
+ */
57
+ initialize() {
58
+ //get the url fragment w/query string
59
+ this.url = window.location.pathname + window.location.search;
60
+ // set up the events
61
+ this.addListeners();
62
+ // navigate to correct view
63
+ this.navigate(false, {}, false);
64
+ return this;
65
+ }
66
+ /**
67
+ * Register stable document, history, and optional mediator handlers once.
68
+ * @returns This instance for chaining.
69
+ */
70
+ addListeners() {
71
+ if (this.listenersInitialized) {
72
+ return this;
73
+ }
74
+ // Delegate push-state links from one document listener.
75
+ document.addEventListener('click', this.boundPushStateClick);
76
+ //bind window popstates
77
+ window.addEventListener('popstate', this.boundPopState);
78
+ //listen to a mediator if present
79
+ if (this.mediator) {
80
+ this.mediator.on('router:navigate', this.boundMediatorNavigate);
33
81
  }
82
+ this.listenersInitialized = true;
83
+ return this;
34
84
  }
35
-
36
- var _createClass = function () {
37
- function defineProperties(target, props) {
38
- for (var i = 0; i < props.length; i++) {
39
- var descriptor = props[i];
40
- descriptor.enumerable = descriptor.enumerable || false;
41
- descriptor.configurable = true;
42
- if ("value" in descriptor) descriptor.writable = true;
43
- Object.defineProperty(target, descriptor.key, descriptor);
44
- }
85
+ /**
86
+ * Release owned state and listeners so the instance can leave the application lifecycle.
87
+ * @returns This instance after cleanup.
88
+ */
89
+ destroy() {
90
+ this.removeListeners();
91
+ return this;
92
+ }
93
+ /**
94
+ * Release listeners owned by this instance; subclasses may extend the lifecycle hook.
95
+ * @returns This instance for chaining.
96
+ */
97
+ removeListeners() {
98
+ if (!this.listenersInitialized) {
99
+ return this;
100
+ }
101
+ // Unbind the same stable listener objects registered by addListeners().
102
+ document.removeEventListener('click', this.boundPushStateClick);
103
+ //bind window popstates
104
+ window.removeEventListener('popstate', this.boundPopState);
105
+ if (this.mediator && typeof this.mediator.removeListener === 'function') {
106
+ this.mediator.removeListener('router:navigate', this.boundMediatorNavigate);
107
+ }
108
+ this.listenersInitialized = false;
109
+ return this;
110
+ }
111
+ /**
112
+ * Intercept eligible same-origin push-state links while preserving normal browser actions.
113
+ * @param e - Browser event being handled.
114
+ * @returns True when the browser should handle the event, or this router after interception.
115
+ */
116
+ eventPushStateClick(e) {
117
+ if (e.defaultPrevented ||
118
+ (e.button !== undefined && e.button !== 0) ||
119
+ e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
120
+ return true;
121
+ }
122
+ const eventTarget = e.target;
123
+ const source = eventTarget && (typeof eventTarget.closest === 'function' ? eventTarget : eventTarget.parentElement);
124
+ const anchor = source && typeof source.closest === 'function'
125
+ ? source.closest('a[href][data-pushstate]')
126
+ : null;
127
+ if (!anchor) {
128
+ return true;
129
+ }
130
+ const target = anchor.getAttribute('target');
131
+ if (anchor.hasAttribute('download') ||
132
+ (target && target.toLowerCase() !== '_self')) {
133
+ return true;
134
+ }
135
+ const location = new URL(anchor.getAttribute('href') || '', window.location.href || `${window.location.origin}/`);
136
+ if (location.origin !== window.location.origin) {
137
+ return true;
45
138
  }
46
-
47
- return function (Constructor, protoProps, staticProps) {
48
- if (protoProps) defineProperties(Constructor.prototype, protoProps);
49
- if (staticProps) defineProperties(Constructor, staticProps);
50
- return Constructor;
139
+ e.preventDefault();
140
+ // closest() supports nested elements at any depth inside the selected anchor.
141
+ this.url = `${location.pathname}${location.search}${location.hash}`;
142
+ this.navigate(false, {}, false);
143
+ return this;
144
+ }
145
+ /**
146
+ * Restore browser history state without adding a new history entry.
147
+ * @param e - Browser event being handled.
148
+ * @returns This router after restoring the history state.
149
+ */
150
+ eventPopState(e) {
151
+ this.url = (e.state || '');
152
+ this.navigate(false, {}, true);
153
+ return this;
154
+ }
155
+ /**
156
+ * Decode URL query parameters, keeping the last value for duplicate keys.
157
+ * @param queryString - Encoded URL query portion.
158
+ * @returns A record of decoded query names and their final values.
159
+ */
160
+ parseQueryString(queryString) {
161
+ return Object.fromEntries(new URLSearchParams(queryString));
162
+ }
163
+ /**
164
+ * Build decoded path, query, and mediator data for the selected route.
165
+ * @param mediatorData - Optional caller metadata passed to the route.
166
+ * @returns No value; locationData is updated in place.
167
+ */
168
+ setLocationData(mediatorData) {
169
+ const parsedUrl = new URL(this.url, window.location.origin);
170
+ const matchedRoute = this.route && this.route !== 'defaultRoute' ? this.route : '';
171
+ // this is the object passed to the matching view
172
+ this.locationData = {
173
+ url: this.url,
174
+ data: {
175
+ url: [],
176
+ mediator: mediatorData,
177
+ query: {}
178
+ }
51
179
  };
52
- }();
53
-
54
- (function () {
55
-
56
- 'use strict';
57
-
58
- module.exports = function () {
59
- function _class() {
60
- _classCallCheck(this, _class);
61
-
62
- // placeholder for routes defined when extended
63
- this.routes = {
64
- defaultRoute: {
65
- view: {
66
- initialize: function initialize() {
67
- return true;
68
- },
69
- destroy: function destroy() {
70
- return true;
71
- }
72
- }
73
- }
74
- };
75
-
76
- // this will hold the current url
77
- this.url = '';
78
-
79
- // this holds the current route
80
- this.route = null;
81
-
82
- // can house a container for the router to act upon
83
- this.scope = null;
84
-
85
- // this holds the previous route for destorying later
86
- this.previousRoute = null;
87
-
88
- // this will hold the page title (not yet supported by browsers)
89
- this.pageTitle = null;
90
-
91
- // optional mediator placeholder
92
- this.mediator = false;
180
+ // parse out the url data
181
+ this.locationData.data.url = parsedUrl.pathname
182
+ .slice(matchedRoute.length)
183
+ .split('/')
184
+ .filter(Boolean)
185
+ .map((fragment) => {
186
+ try {
187
+ return decodeURIComponent(fragment);
93
188
  }
94
-
95
- _createClass(_class, [{
96
- key: 'initialize',
97
- value: function initialize() {
98
-
99
- // set orgin if not supported by the browser
100
- if (!window.location.origin) {
101
- window.location.origin = window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : '');
102
- }
103
-
104
- //get the url fragment w/query string
105
- this.url = window.location.pathname + (window.location.search || '') || '';
106
-
107
- // set up the events
108
- this.addListeners();
109
-
110
- // navigate to correct view
111
- this.navigate(false, {}, false);
112
-
113
- return this;
114
- }
115
- }, {
116
- key: 'addListeners',
117
- value: function addListeners() {
118
- var _this = this;
119
-
120
- //bind all pushstate links
121
- (0, _gator2.default)(document).on('click', 'a[href][data-pushstate]', this.eventPushStateClick.bind(this));
122
-
123
- //bind window popstates
124
- window.addEventListener('popstate', this.eventPopState.bind(this));
125
-
126
- //listen to a mediator if present
127
- if (this.mediator) {
128
-
129
- this.mediator.on('router:navigate', function (data) {
130
- _this.navigate(data.url, data || {}, false);
131
- });
132
- }
133
-
134
- return this;
135
- }
136
- }, {
137
- key: 'destroy',
138
- value: function destroy() {
139
-
140
- this.removeListeners();
141
-
142
- return this;
143
- }
144
- }, {
145
- key: 'removeListeners',
146
- value: function removeListeners() {
147
-
148
- //unbind all pushstate links
149
- (0, _gator2.default)(document).off('click', 'a[href][data-pushstate]');
150
-
151
- //bind window popstates
152
- window.removeEventListener('popstate', this.eventPopState.bind(this));
153
-
154
- return this;
155
- }
156
- }, {
157
- key: 'eventPushStateClick',
158
- value: function eventPushStateClick(e) {
159
-
160
- e.preventDefault();
161
-
162
- //for example an image tag can initiate a click event in side an anchor
163
- if (e.target.tagName === 'A') {
164
- this.url = e.target.getAttribute('href') || '';
165
- } else {
166
- this.url = e.target.parentNode.getAttribute('href') || '';
167
- }
168
-
169
- this.navigate(false, {}, false);
170
-
171
- return this;
172
- }
173
- }, {
174
- key: 'eventPopState',
175
- value: function eventPopState(e) {
176
-
177
- this.url = e.state || '';
178
-
179
- this.navigate(false, {}, true);
180
-
181
- return this;
182
- }
183
- }, {
184
- key: 'parseQueryString',
185
- value: function parseQueryString(queryString) {
186
-
187
- var params = {};
188
- var queries = void 0;
189
- var temp = void 0;
190
- var i = void 0;
191
- var l = void 0;
192
-
193
- // Split into key/value pairs
194
- queries = queryString.split('&amp;');
195
-
196
- // Convert the array of strings into an object
197
- for (i = 0, l = queries.length; i < l; i++) {
198
- temp = queries[i].split('=');
199
- params[temp[0]] = temp[1];
200
- }
201
-
202
- return params;
203
- }
204
- }, {
205
- key: 'setLocationData',
206
- value: function setLocationData(mediatorData) {
207
-
208
- //split the url here to seperate query strings from url path
209
- var urlFragments = this.url.split('?');
210
-
211
- // this is the object passed to the matching view
212
- this.locationData = {
213
- url: this.url,
214
- data: {
215
- url: [],
216
- mediator: mediatorData,
217
- query: {}
218
- }
219
- };
220
-
221
- // parse out the url data
222
- this.locationData.data.url = urlFragments[0].replace(this.route, '').split('/');
223
- this.locationData.data.url.splice(0, 1);
224
-
225
- // remove any trailing empty items from the array
226
- if (this.locationData.data.url.length && this.locationData.data.url[this.locationData.data.url.length - 1] === '') {
227
- this.locationData.data.url.splice(this.locationData.data.url.length - 1, 1);
228
- }
229
-
230
- //add query string data
231
- if (urlFragments[1]) {
232
- this.locationData.data.query = this.parseQueryString(urlFragments[1].replace('?', ''));
233
- }
234
- }
235
- }, {
236
- key: 'navigate',
237
- value: function navigate(url, mediatorData, isPopState) {
238
-
239
- // allow navigate to use a specified url
240
- if (url) {
241
- this.url = url;
242
- }
243
-
244
- //clean up url for matching
245
- this.url = this.url.replace(window.location.origin, '');
246
-
247
- // reset this to null for new location
248
- this.route = null;
249
-
250
- //find the first matcher that starts with the same string as a defined route
251
- for (var route in this.routes) {
252
-
253
- // we have a match
254
- if (this.url.lastIndexOf(route, 0) === 0) {
255
-
256
- this.route = route;
257
-
258
- //stop looping we are done
259
- break;
260
- }
261
- }
262
-
263
- this.setLocationData(mediatorData);
264
-
265
- // if a catch all default route is defined execute that when not match is found
266
- if (!this.route && this.routes.defaultRoute) {
267
-
268
- this.route = 'defaultRoute';
269
- }
270
-
271
- if (this.route) {
272
-
273
- // check if the page is secure and if the user passes the specified secure checking function
274
- if (typeof this.routes[this.route].secure === 'function' && this.routes[this.route].secure(this.scope, this.locationData) !== true) {
275
- return false;
276
- }
277
-
278
- //if previous route then destroy it
279
- if (this.previousRoute) {
280
-
281
- // else if it is an object check if there is a view class
282
- if (_typeof(this.routes[this.previousRoute]) === 'object' && _typeof(this.routes[this.previousRoute].view) === 'object' && typeof this.routes[this.previousRoute].view.destroy === 'function') {
283
-
284
- //destroy the view
285
- this.routes[this.previousRoute].view.destroy(this.scope, this.locationData);
286
- }
287
- }
288
-
289
- //execute the route if it is a simple function
290
- if (typeof this.routes[this.route] === 'function') {
291
-
292
- //execute the view
293
- this.routes[this.route](this.scope, this.locationData);
294
-
295
- //cache the old route
296
- this.previousRoute = this.route;
297
- } else if (_typeof(this.routes[this.route]) === 'object' && typeof this.routes[this.route].view === 'function') {
298
-
299
- //execute the view
300
- this.routes[this.route].view(this.scope, this.locationData);
301
-
302
- // if a title is defined for this route we'll set it
303
- if (typeof this.routes[this.route].title === 'string') {
304
- this.pageTitle = this.routes[this.route].title;
305
- } else {
306
- this.pageTitle = null;
307
- }
308
-
309
- //cache the old route
310
- this.previousRoute = this.route;
311
-
312
- // else if it is an object check if there is a view class
313
- } else if (_typeof(this.routes[this.route]) === 'object' && _typeof(this.routes[this.route].view) === 'object' && typeof this.routes[this.route].view.initialize === 'function') {
314
-
315
- // if a title is defined for this route we'll set it
316
- if (typeof this.routes[this.route].title === 'string') {
317
- this.pageTitle = this.routes[this.route].title;
318
- } else {
319
- this.pageTitle = null;
320
- }
321
-
322
- //execute the view
323
- this.routes[this.route].view.initialize(this.scope, this.locationData);
324
-
325
- //cache the old route
326
- this.previousRoute = this.route;
327
- } else {
328
-
329
- return false;
330
- }
331
- }
332
-
333
- // make sure to not set pushstate on back button click
334
- if (!isPopState) {
335
- //set in history the new url
336
- window.history.pushState(this.url, this.pageTitle, this.url);
337
- }
338
-
339
- // since browsers don't support setting the title with pushstate yet
340
- if (this.pageTitle) {
341
- document.title = this.pageTitle;
342
- }
343
-
344
- return this;
345
- }
346
- }]);
347
-
348
- return _class;
349
- }();
350
- })();
351
- });
189
+ catch (error) {
190
+ return fragment;
191
+ }
192
+ });
193
+ //add query string data
194
+ this.locationData.data.query = this.parseQueryString(parsedUrl.search);
195
+ }
196
+ /**
197
+ * Select a route, enforce its guard, transition view lifecycles, and update history and title.
198
+ * @param url - URL or optional adapter argument.
199
+ * @param mediatorData - Optional caller metadata passed to the route.
200
+ * @param isPopState - Whether navigation came from browser history.
201
+ * @returns False when a guard or route rejects navigation; otherwise this router.
202
+ */
203
+ navigate(url, mediatorData, isPopState = false) {
204
+ // allow navigate to use a specified url
205
+ if (url) {
206
+ this.url = url;
207
+ }
208
+ //clean up url for matching
209
+ this.url = this.url.replace(window.location.origin, '');
210
+ // reset this to null for new location
211
+ this.route = null;
212
+ const pathname = new URL(this.url, window.location.origin).pathname;
213
+ // Match complete path segments so /page2 does not also match /page23.
214
+ for (let route in this.routes) {
215
+ if (route !== 'defaultRoute' &&
216
+ (pathname === route || pathname.startsWith(`${route}/`))) {
217
+ this.route = route;
218
+ //stop looping we are done
219
+ break;
220
+ }
221
+ }
222
+ this.setLocationData(mediatorData);
223
+ // if a catch all default route is defined execute that when not match is found
224
+ if (!this.route && this.routes.defaultRoute) {
225
+ this.route = 'defaultRoute';
226
+ }
227
+ if (this.route) {
228
+ const selected = this.routes[this.route];
229
+ if (typeof selected !== 'function' && selected.secure &&
230
+ selected.secure(this.scope, this.locationData) !== true) {
231
+ return false;
232
+ }
233
+ const previous = this.previousRoute ? this.routes[this.previousRoute] : undefined;
234
+ if (previous && typeof previous !== 'function' && typeof previous.view === 'object' && previous.view.destroy) {
235
+ previous.view.destroy(this.scope, this.locationData);
236
+ }
237
+ if (typeof selected === 'function') {
238
+ selected(this.scope, this.locationData);
239
+ }
240
+ else if (typeof selected.view === 'function') {
241
+ selected.view(this.scope, this.locationData);
242
+ this.pageTitle = typeof selected.title === 'string' ? selected.title : null;
243
+ }
244
+ else if (selected.view && typeof selected.view.initialize === 'function') {
245
+ this.pageTitle = typeof selected.title === 'string' ? selected.title : null;
246
+ selected.view.initialize(this.scope, this.locationData);
247
+ }
248
+ else {
249
+ return false;
250
+ }
251
+ this.previousRoute = this.route;
252
+ }
253
+ // make sure to not set pushstate on back button click
254
+ if (!isPopState) {
255
+ //set in history the new url
256
+ window.history.pushState(this.url, this.pageTitle || '', this.url);
257
+ }
258
+ // since browsers don't support setting the title with pushstate yet
259
+ if (this.pageTitle) {
260
+ document.title = this.pageTitle;
261
+ }
262
+ return this;
263
+ }
264
+ }
265
+ ;
266
+ module.exports = Router;
267
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/** Navigation payload, including caller-defined metadata. */\ninterface NavigationData {url?: string; [key: string]: unknown}\n/** URL-derived values supplied to route handlers. */\ninterface LocationData {url: string; data: {url: string[]; mediator: NavigationData | undefined; query: Record<string, string>}}\ntype RouteHandler = (scope: Element | null, location: LocationData) => unknown;\n/** An object route can own a function or a view lifecycle. */\ninterface RouteObject {title?: string; secure?: RouteHandler; view?: RouteHandler | {initialize?: RouteHandler; destroy?: RouteHandler}}\ntype Route = RouteHandler | RouteObject;\n/** Minimal event-bus contract required by the router. */\ninterface NavigationMediator {\n on(event: string, callback: (data?: NavigationData) => unknown): unknown;\n removeListener?(event: string, callback: (data?: NavigationData) => unknown): unknown;\n}\n/** History API router with ordered path-boundary matching. */\nclass Router {\n routes: Record<string, Route>;\n url: string;\n route: string | null;\n scope: Element | null;\n previousRoute: string | null;\n pageTitle: string | null;\n mediator: NavigationMediator | false;\n boundPushStateClick: (event: MouseEvent) => unknown;\n boundPopState: (event: PopStateEvent) => unknown;\n boundMediatorNavigate: (data?: NavigationData) => unknown;\n listenersInitialized: boolean;\n locationData: LocationData = {url: '', data: {url: [], mediator: undefined, query: {}}};\n\n /**\n * Create an instance with its own state and listener references.\n */\n constructor() {\n\n // placeholder for routes defined when extended\n this.routes = {\n defaultRoute: {\n view: {\n initialize: () => {\n return true;\n },\n destroy: () => {\n return true;\n }\n }\n }\n }\n\n // this will hold the current url\n this.url = '';\n\n // this holds the current route\n this.route = null;\n\n // can house a container for the router to act upon\n this.scope = null;\n\n // this holds the previous route for destorying later\n this.previousRoute = null;\n\n // this will hold the page title (not yet supported by browsers)\n this.pageTitle = null;\n\n // optional mediator placeholder\n this.mediator = false;\n\n // Stable listener references allow destroy() to release browser resources.\n this.boundPushStateClick = this.eventPushStateClick.bind(this);\n this.boundPopState = this.eventPopState.bind(this);\n this.boundMediatorNavigate = (data = {}) => {\n this.navigate(data.url, data, false);\n };\n this.listenersInitialized = false;\n\n }\n\n /**\n * Start this instance and return it for lifecycle chaining.\n * @returns This instance for chaining.\n */\n initialize() {\n\n //get the url fragment w/query string\n this.url = window.location.pathname + window.location.search;\n\n // set up the events\n this.addListeners();\n\n // navigate to correct view\n this.navigate(false, {}, false);\n\n return this;\n\n }\n\n /**\n * Register stable document, history, and optional mediator handlers once.\n * @returns This instance for chaining.\n */\n addListeners() {\n\n if (this.listenersInitialized) {\n return this;\n }\n\n // Delegate push-state links from one document listener.\n document.addEventListener('click', this.boundPushStateClick);\n\n //bind window popstates\n window.addEventListener('popstate', this.boundPopState);\n\n //listen to a mediator if present\n if (this.mediator) {\n this.mediator.on('router:navigate', this.boundMediatorNavigate);\n }\n\n this.listenersInitialized = true;\n return this;\n\n }\n\n /**\n * Release owned state and listeners so the instance can leave the application lifecycle.\n * @returns This instance after cleanup.\n */\n destroy() {\n\n this.removeListeners();\n\n return this;\n\n }\n\n /**\n * Release listeners owned by this instance; subclasses may extend the lifecycle hook.\n * @returns This instance for chaining.\n */\n removeListeners() {\n\n if (!this.listenersInitialized) {\n return this;\n }\n\n // Unbind the same stable listener objects registered by addListeners().\n document.removeEventListener('click', this.boundPushStateClick);\n\n //bind window popstates\n window.removeEventListener('popstate', this.boundPopState);\n\n if (this.mediator && typeof this.mediator.removeListener === 'function') {\n this.mediator.removeListener('router:navigate', this.boundMediatorNavigate);\n }\n\n this.listenersInitialized = false;\n return this;\n\n }\n\n /**\n * Intercept eligible same-origin push-state links while preserving normal browser actions.\n * @param e - Browser event being handled.\n * @returns True when the browser should handle the event, or this router after interception.\n */\n eventPushStateClick(e: MouseEvent) {\n\n if (\n e.defaultPrevented ||\n (e.button !== undefined && e.button !== 0) ||\n e.metaKey || e.ctrlKey || e.shiftKey || e.altKey\n ) {\n return true;\n }\n\n const eventTarget = e.target as Element | null;\n const source = eventTarget && (\n typeof eventTarget.closest === 'function' ? eventTarget : eventTarget.parentElement\n );\n const anchor = source && typeof source.closest === 'function'\n ? source.closest('a[href][data-pushstate]')\n : null;\n if (!anchor) {\n return true;\n }\n\n const target = anchor.getAttribute('target');\n if (\n anchor.hasAttribute('download') ||\n (target && target.toLowerCase() !== '_self')\n ) {\n return true;\n }\n\n const location = new URL(\n anchor.getAttribute('href') || '',\n window.location.href || `${window.location.origin}/`\n );\n if (location.origin !== window.location.origin) {\n return true;\n }\n\n e.preventDefault();\n\n // closest() supports nested elements at any depth inside the selected anchor.\n this.url = `${location.pathname}${location.search}${location.hash}`;\n\n this.navigate(false, {}, false);\n\n return this;\n\n }\n\n /**\n * Restore browser history state without adding a new history entry.\n * @param e - Browser event being handled.\n * @returns This router after restoring the history state.\n */\n eventPopState(e: PopStateEvent) {\n\n this.url = (e.state || '');\n\n this.navigate(false, {}, true);\n\n return this;\n\n }\n\n /**\n * Decode URL query parameters, keeping the last value for duplicate keys.\n * @param queryString - Encoded URL query portion.\n * @returns A record of decoded query names and their final values.\n */\n parseQueryString(queryString: string) {\n return Object.fromEntries(new URLSearchParams(queryString));\n\n }\n\n /**\n * Build decoded path, query, and mediator data for the selected route.\n * @param mediatorData - Optional caller metadata passed to the route.\n * @returns No value; locationData is updated in place.\n */\n setLocationData(mediatorData?: NavigationData) {\n\n const parsedUrl = new URL(this.url, window.location.origin);\n const matchedRoute = this.route && this.route !== 'defaultRoute' ? this.route : '';\n\n // this is the object passed to the matching view\n this.locationData = {\n url: this.url,\n data: {\n url: [],\n mediator: mediatorData,\n query: {}\n }\n };\n\n // parse out the url data\n this.locationData.data.url = parsedUrl.pathname\n .slice(matchedRoute.length)\n .split('/')\n .filter(Boolean)\n .map((fragment) => {\n try {\n return decodeURIComponent(fragment);\n } catch (error) {\n return fragment;\n }\n });\n\n //add query string data\n this.locationData.data.query = this.parseQueryString(parsedUrl.search);\n\n }\n\n /**\n * Select a route, enforce its guard, transition view lifecycles, and update history and title.\n * @param url - URL or optional adapter argument.\n * @param mediatorData - Optional caller metadata passed to the route.\n * @param isPopState - Whether navigation came from browser history.\n * @returns False when a guard or route rejects navigation; otherwise this router.\n */\n navigate(url?: string | false, mediatorData?: NavigationData, isPopState = false) {\n\n // allow navigate to use a specified url\n if (url) {\n this.url = url;\n }\n\n //clean up url for matching\n this.url = this.url.replace(window.location.origin, '');\n\n // reset this to null for new location\n this.route = null;\n\n const pathname = new URL(this.url, window.location.origin).pathname;\n\n // Match complete path segments so /page2 does not also match /page23.\n for (let route in this.routes) {\n\n if (\n route !== 'defaultRoute' &&\n (pathname === route || pathname.startsWith(`${route}/`))\n ) {\n\n this.route = route;\n\n //stop looping we are done\n break;\n\n }\n\n }\n\n this.setLocationData(mediatorData);\n\n // if a catch all default route is defined execute that when not match is found\n if (!this.route && this.routes.defaultRoute) {\n\n this.route = 'defaultRoute';\n\n }\n\n if (this.route) {\n const selected = this.routes[this.route];\n if (typeof selected !== 'function' && selected.secure &&\n selected.secure(this.scope, this.locationData) !== true) {\n return false;\n }\n const previous = this.previousRoute ? this.routes[this.previousRoute] : undefined;\n if (previous && typeof previous !== 'function' && typeof previous.view === 'object' && previous.view.destroy) {\n previous.view.destroy(this.scope, this.locationData);\n }\n if (typeof selected === 'function') {\n selected(this.scope, this.locationData);\n } else if (typeof selected.view === 'function') {\n selected.view(this.scope, this.locationData);\n this.pageTitle = typeof selected.title === 'string' ? selected.title : null;\n } else if (selected.view && typeof selected.view.initialize === 'function') {\n this.pageTitle = typeof selected.title === 'string' ? selected.title : null;\n selected.view.initialize(this.scope, this.locationData);\n } else {\n return false;\n }\n this.previousRoute = this.route;\n }\n\n // make sure to not set pushstate on back button click\n if (!isPopState) {\n //set in history the new url\n window.history.pushState(this.url, this.pageTitle || '', this.url);\n }\n\n // since browsers don't support setting the title with pushstate yet\n if (this.pageTitle) {\n document.title = this.pageTitle;\n }\n\n return this;\n\n }\n\n};\n\n\n/** Public route configuration and navigation payload types. */\nnamespace Router {\n export type Handler = RouteHandler;\n export type Route = RouteHandler | RouteObject;\n export type Location = LocationData;\n export type Navigation = NavigationData;\n}\nexport = Router;\n"]}