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/README.md +152 -267
- package/dist/index.d.ts +111 -0
- package/dist/index.js +259 -343
- package/dist/index.js.map +1 -0
- package/package.json +42 -8
- package/.babelrc +0 -4
- package/.editorconfig +0 -13
- package/.npmignore +0 -2
- package/.travis.yml +0 -5
- package/bin/compile.sh +0 -2
- package/bower.json +0 -26
- package/src/index.js +0 -325
package/dist/index.js
CHANGED
|
@@ -1,351 +1,267 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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('&');
|
|
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":";AAaA,8DAA8D;AAC9D,MAAM,MAAM;IACR,MAAM,CAAwB;IAC9B,GAAG,CAAS;IACZ,KAAK,CAAgB;IACrB,KAAK,CAAiB;IACtB,aAAa,CAAgB;IAC7B,SAAS,CAAgB;IACzB,QAAQ,CAA6B;IACrC,mBAAmB,CAAiC;IACpD,aAAa,CAAoC;IACjD,qBAAqB,CAAqC;IAC1D,oBAAoB,CAAU;IAC9B,YAAY,GAAiB,EAAC,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,EAAC,GAAG,EAAE,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAC,EAAC,CAAC;IAExF;;OAEG;IACH;QAEI,+CAA+C;QAC/C,IAAI,CAAC,MAAM,GAAG;YACV,YAAY,EAAE;gBACV,IAAI,EAAE;oBACF,UAAU,EAAE,GAAG,EAAE;wBACb,OAAO,IAAI,CAAC;oBAChB,CAAC;oBACD,OAAO,EAAE,GAAG,EAAE;wBACV,OAAO,IAAI,CAAC;oBAChB,CAAC;iBACJ;aACJ;SACJ,CAAA;QAED,iCAAiC;QACjC,IAAI,CAAC,GAAG,GAAG,EAAE,CAAC;QAEd,+BAA+B;QAC/B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAElB,mDAAmD;QACnD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAElB,qDAAqD;QACrD,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAE1B,gEAAgE;QAChE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QAEtB,gCAAgC;QAChC,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QAEtB,2EAA2E;QAC3E,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/D,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnD,IAAI,CAAC,qBAAqB,GAAG,CAAC,IAAI,GAAG,EAAE,EAAE,EAAE;YACvC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QACzC,CAAC,CAAC;QACF,IAAI,CAAC,oBAAoB,GAAG,KAAK,CAAC;IAEtC,CAAC;IAED;;;OAGG;IACH,UAAU;QAEN,qCAAqC;QACrC,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;QAE7D,oBAAoB;QACpB,IAAI,CAAC,YAAY,EAAE,CAAC;QAEpB,2BAA2B;QAC3B,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC;QAEhC,OAAO,IAAI,CAAC;IAEhB,CAAC;IAED;;;OAGG;IACH,YAAY;QAER,IAAI,IAAI,CAAC,oBAAoB,EAAE,CAAC;YAC5B,OAAO,IAAI,CAAC;QAChB,CAAC;QAED,wDAAwD;QACxD,QAAQ,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,mBAAmB,CAAC,CAAC;QAE7D,uBAAuB;QACvB,MAAM,CAAC,gBAAgB,CAAC,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;QAExD,iCAAiC;QACjC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAChB,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,iBAAiB,EAAE,IAAI,CAAC,qBAAqB,CAAC,CAAC;QACpE,CAAC;QAED,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;QACjC,OAAO,IAAI,CAAC;IAEhB,CAAC;IAED;;;OAGG;IACH,OAAO;QAEH,IAAI,CAAC,eAAe,EAAE,CAAC;QAEvB,OAAO,IAAI,CAAC;IAEhB,CAAC;IAED;;;OAGG;IACH,eAAe;QAEX,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,CAAC;YAC7B,OAAO,IAAI,CAAC;QAChB,CAAC;QAED,wEAAwE;QACxE,QAAQ,CAAC,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,mBAAmB,CAAC,CAAC;QAEhE,uBAAuB;QACvB,MAAM,CAAC,mBAAmB,CAAC,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;QAE3D,IAAI,IAAI,CAAC,QAAQ,IAAI,OAAO,IAAI,CAAC,QAAQ,CAAC,cAAc,KAAK,UAAU,EAAE,CAAC;YACtE,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,iBAAiB,EAAE,IAAI,CAAC,qBAAqB,CAAC,CAAC;QAChF,CAAC;QAED,IAAI,CAAC,oBAAoB,GAAG,KAAK,CAAC;QAClC,OAAO,IAAI,CAAC;IAEhB,CAAC;IAED;;;;OAIG;IACH,mBAAmB,CAAC,CAAa;QAE7B,IACI,CAAC,CAAC,gBAAgB;YAClB,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC;YAC1C,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,MAAM,EAClD,CAAC;YACC,OAAO,IAAI,CAAC;QAChB,CAAC;QAED,MAAM,WAAW,GAAG,CAAC,CAAC,MAAwB,CAAC;QAC/C,MAAM,MAAM,GAAG,WAAW,IAAI,CAC1B,OAAO,WAAW,CAAC,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,aAAa,CACtF,CAAC;QACF,MAAM,MAAM,GAAG,MAAM,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,UAAU;YACzD,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,yBAAyB,CAAC;YAC3C,CAAC,CAAC,IAAI,CAAC;QACX,IAAI,CAAC,MAAM,EAAE,CAAC;YACV,OAAO,IAAI,CAAC;QAChB,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QAC7C,IACI,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC;YAC/B,CAAC,MAAM,IAAI,MAAM,CAAC,WAAW,EAAE,KAAK,OAAO,CAAC,EAC9C,CAAC;YACC,OAAO,IAAI,CAAC;QAChB,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,GAAG,CACpB,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,EACjC,MAAM,CAAC,QAAQ,CAAC,IAAI,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CACvD,CAAC;QACF,IAAI,QAAQ,CAAC,MAAM,KAAK,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YAC7C,OAAO,IAAI,CAAC;QAChB,CAAC;QAED,CAAC,CAAC,cAAc,EAAE,CAAC;QAEnB,8EAA8E;QAC9E,IAAI,CAAC,GAAG,GAAG,GAAG,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC;QAEpE,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC;QAEhC,OAAO,IAAI,CAAC;IAEhB,CAAC;IAED;;;;OAIG;IACH,aAAa,CAAC,CAAgB;QAE1B,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;QAE3B,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;QAE/B,OAAO,IAAI,CAAC;IAEhB,CAAC;IAED;;;;OAIG;IACH,gBAAgB,CAAC,WAAmB;QAChC,OAAO,MAAM,CAAC,WAAW,CAAC,IAAI,eAAe,CAAC,WAAW,CAAC,CAAC,CAAC;IAEhE,CAAC;IAED;;;;OAIG;IACH,eAAe,CAAC,YAA6B;QAEzC,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC5D,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,KAAK,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAEnF,iDAAiD;QACjD,IAAI,CAAC,YAAY,GAAG;YAChB,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,IAAI,EAAE;gBACF,GAAG,EAAE,EAAE;gBACP,QAAQ,EAAE,YAAY;gBACtB,KAAK,EAAE,EAAE;aACZ;SACJ,CAAC;QAEF,yBAAyB;QACzB,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,GAAG,SAAS,CAAC,QAAQ;aAC1C,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC;aAC1B,KAAK,CAAC,GAAG,CAAC;aACV,MAAM,CAAC,OAAO,CAAC;aACf,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE;YACd,IAAI,CAAC;gBACD,OAAO,kBAAkB,CAAC,QAAQ,CAAC,CAAC;YACxC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACb,OAAO,QAAQ,CAAC;YACpB,CAAC;QACL,CAAC,CAAC,CAAC;QAEP,uBAAuB;QACvB,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAE3E,CAAC;IAED;;;;;;OAMG;IACH,QAAQ,CAAC,GAAoB,EAAE,YAA6B,EAAE,UAAU,GAAG,KAAK;QAE5E,wCAAwC;QACxC,IAAI,GAAG,EAAE,CAAC;YACN,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACnB,CAAC;QAED,2BAA2B;QAC3B,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAExD,sCAAsC;QACtC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAElB,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC;QAEpE,sEAAsE;QACtE,KAAK,IAAI,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAE5B,IACI,KAAK,KAAK,cAAc;gBACxB,CAAC,QAAQ,KAAK,KAAK,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,EAC1D,CAAC;gBAEC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;gBAEnB,0BAA0B;gBAC1B,MAAM;YAEV,CAAC;QAEL,CAAC;QAED,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC;QAEnC,+EAA+E;QAC/E,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;YAE1C,IAAI,CAAC,KAAK,GAAG,cAAc,CAAC;QAEhC,CAAC;QAED,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACb,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACzC,IAAI,OAAO,QAAQ,KAAK,UAAU,IAAI,QAAQ,CAAC,MAAM;gBACjD,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,KAAK,IAAI,EAAE,CAAC;gBAC1D,OAAO,KAAK,CAAC;YACjB,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAClF,IAAI,QAAQ,IAAI,OAAO,QAAQ,KAAK,UAAU,IAAI,OAAO,QAAQ,CAAC,IAAI,KAAK,QAAQ,IAAI,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;gBAC3G,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;YACzD,CAAC;YACD,IAAI,OAAO,QAAQ,KAAK,UAAU,EAAE,CAAC;gBACjC,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;YAC5C,CAAC;iBAAM,IAAI,OAAO,QAAQ,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;gBAC7C,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;gBAC7C,IAAI,CAAC,SAAS,GAAG,OAAO,QAAQ,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;YAChF,CAAC;iBAAM,IAAI,QAAQ,CAAC,IAAI,IAAI,OAAO,QAAQ,CAAC,IAAI,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;gBACzE,IAAI,CAAC,SAAS,GAAG,OAAO,QAAQ,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;gBAC5E,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;YAC5D,CAAC;iBAAM,CAAC;gBACJ,OAAO,KAAK,CAAC;YACjB,CAAC;YACD,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC;QACpC,CAAC;QAED,sDAAsD;QACtD,IAAI,CAAC,UAAU,EAAE,CAAC;YACd,4BAA4B;YAC5B,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,IAAI,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QACvE,CAAC;QAED,oEAAoE;QACpE,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACjB,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC;QACpC,CAAC;QAED,OAAO,IAAI,CAAC;IAEhB,CAAC;CAEJ;AAAA,CAAC;AAUF,iBAAS,MAAM,CAAC","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"]}
|