OLD | NEW |
| (Empty) |
1 (function(window, document) { | |
2 // <app-route path="/path" [import="/page/cust-el.html"] [element="cust-el"] [
template]></app-route> | |
3 document.registerElement('app-route', { | |
4 prototype: Object.create(HTMLElement.prototype) | |
5 }); | |
6 | |
7 // <active-route></active-route> holds the active route's content when `shadow
` is not enabled | |
8 document.registerElement('active-route', { | |
9 prototype: Object.create(HTMLElement.prototype) | |
10 }); | |
11 | |
12 // <app-router [shadow] [trailingSlash="strict|ignore"] [init="auto|manual"]><
/app-router> | |
13 var router = Object.create(HTMLElement.prototype); | |
14 | |
15 var importedURIs = {}; | |
16 var isIE = 'ActiveXObject' in window; | |
17 | |
18 // fire(type, detail, node) - Fire a new CustomEvent(type, detail) on the node | |
19 // | |
20 // listen with document.querySelector('app-router').addEventListener(type, fun
ction(event) { | |
21 // event.detail, event.preventDefault() | |
22 // }) | |
23 function fire(type, detail, node) { | |
24 // create a CustomEvent the old way for IE9/10 support | |
25 var event = document.createEvent('CustomEvent'); | |
26 | |
27 // initCustomEvent(type, bubbles, cancelable, detail) | |
28 event.initCustomEvent(type, false, true, detail); | |
29 | |
30 // returns false when event.preventDefault() is called, true otherwise | |
31 return node.dispatchEvent(event); | |
32 } | |
33 | |
34 // Initial set up when attached | |
35 router.attachedCallback = function() { | |
36 if(this.getAttribute('init') !== 'manual') { | |
37 this.init(); | |
38 } | |
39 }; | |
40 | |
41 // Initialize the router | |
42 router.init = function() { | |
43 if (this.isInitialized) { | |
44 return; | |
45 } | |
46 this.isInitialized = true; | |
47 this.activeRoute = document.createElement('app-route'); | |
48 | |
49 // Listen for URL change events. | |
50 this.stateChangeHandler = this.go.bind(this); | |
51 window.addEventListener('popstate', this.stateChangeHandler, false); | |
52 if (isIE) { | |
53 // IE is truly special! A hashchange is supposed to trigger a popstate, ma
king popstate the only | |
54 // even you should need to listen to. Not the case in IE! Make another eve
nt listener for it! | |
55 window.addEventListener('hashchange', this.stateChangeHandler, false); | |
56 } | |
57 | |
58 // set up an <active-route> element for the active route's content | |
59 this.activeRouteContent = document.createElement('active-route'); | |
60 this.appendChild(this.activeRouteContent); | |
61 if (this.hasAttribute('shadow')) { | |
62 this.activeRouteContent = this.activeRouteContent.createShadowRoot(); | |
63 } | |
64 | |
65 // load the web component for the active route | |
66 this.go(); | |
67 }; | |
68 | |
69 // clean up global event listeners | |
70 router.detachedCallback = function() { | |
71 window.removeEventListener('popstate', this.stateChangeHandler, false); | |
72 if (isIE) { | |
73 window.removeEventListener('hashchange', this.stateChangeHandler, false); | |
74 } | |
75 }; | |
76 | |
77 // go() - Find the first <app-route> that matches the current URL and change t
he active route | |
78 router.go = function() { | |
79 var urlPath = this.parseUrlPath(window.location.href); | |
80 var eventDetail = { | |
81 path: urlPath | |
82 }; | |
83 if (!fire('state-change', eventDetail, this)) { | |
84 return; | |
85 } | |
86 var routes = this.querySelectorAll('app-route'); | |
87 for (var i = 0; i < routes.length; i++) { | |
88 if (this.testRoute(routes[i].getAttribute('path'), urlPath, this.getAttrib
ute('trailingSlash'), routes[i].hasAttribute('regex'))) { | |
89 this.activateRoute(routes[i], urlPath); | |
90 return; | |
91 } | |
92 } | |
93 fire('not-found', eventDetail, this); | |
94 }; | |
95 | |
96 // activateRoute(route, urlPath) - Activate the route | |
97 router.activateRoute = function(route, urlPath) { | |
98 var eventDetail = { | |
99 path: urlPath, | |
100 route: route, | |
101 oldRoute: this.activeRoute | |
102 }; | |
103 if (!fire('activate-route-start', eventDetail, this)) { | |
104 return; | |
105 } | |
106 if (!fire('activate-route-start', eventDetail, route)) { | |
107 return; | |
108 } | |
109 | |
110 this.activeRoute.removeAttribute('active'); | |
111 route.setAttribute('active', 'active'); | |
112 this.activeRoute = route; | |
113 | |
114 var importUri = route.getAttribute('import'); | |
115 var routePath = route.getAttribute('path'); | |
116 var isRegExp = route.hasAttribute('regex'); | |
117 var elementName = route.getAttribute('element'); | |
118 var isTemplate = route.hasAttribute('template'); | |
119 var isElement = !isTemplate; | |
120 | |
121 // import custom element | |
122 if (isElement && importUri) { | |
123 this.importAndActivateCustomElement(importUri, elementName, routePath, url
Path, isRegExp, eventDetail); | |
124 } | |
125 // pre-loaded custom element | |
126 else if (isElement && !importUri && elementName) { | |
127 this.activateCustomElement(elementName, routePath, urlPath, isRegExp, even
tDetail); | |
128 } | |
129 // import template | |
130 else if (isTemplate && importUri) { | |
131 this.importAndActivateTemplate(importUri, route, eventDetail); | |
132 } | |
133 // pre-loaded template | |
134 else if (isTemplate && !importUri) { | |
135 this.activateTemplate(route, eventDetail); | |
136 } | |
137 }; | |
138 | |
139 // importAndActivateCustomElement(importUri, elementName, routePath, urlPath,
isRegExp, eventDetail) - Import the custom element then replace the active route | |
140 // with a new instance of the custom element | |
141 router.importAndActivateCustomElement = function(importUri, elementName, route
Path, urlPath, isRegExp, eventDetail) { | |
142 if (!importedURIs.hasOwnProperty(importUri)) { | |
143 importedURIs[importUri] = true; | |
144 var elementLink = document.createElement('link'); | |
145 elementLink.setAttribute('rel', 'import'); | |
146 elementLink.setAttribute('href', importUri); | |
147 document.head.appendChild(elementLink); | |
148 } | |
149 this.activateCustomElement(elementName || importUri.split('/').slice(-1)[0].
replace('.html', ''), routePath, urlPath, isRegExp, eventDetail); | |
150 }; | |
151 | |
152 // activateCustomElement(elementName, routePath, urlPath, isRegExp, eventDetai
l) - Replace the active route with a new instance of the custom element | |
153 router.activateCustomElement = function(elementName, routePath, urlPath, isReg
Exp, eventDetail) { | |
154 var resourceEl = document.createElement(elementName); | |
155 var routeArgs = this.routeArguments(routePath, urlPath, window.location.href
, isRegExp); | |
156 for (var arg in routeArgs) { | |
157 if (routeArgs.hasOwnProperty(arg)) { | |
158 resourceEl[arg] = routeArgs[arg]; | |
159 } | |
160 } | |
161 this.activeElement(resourceEl, eventDetail); | |
162 }; | |
163 | |
164 // importAndActivateTemplate(importUri, route, eventDetail) - Import the templ
ate then replace the active route with a clone of the template's content | |
165 router.importAndActivateTemplate = function(importUri, route, eventDetail) { | |
166 if (importedURIs.hasOwnProperty(importUri)) { | |
167 // previously imported. this is an async operation and may not be complete
yet. | |
168 var previousLink = document.querySelector('link[href="' + importUri + '"]'
); | |
169 if (previousLink.import) { | |
170 // the import is complete | |
171 this.activeElement(document.importNode(previousLink.import.querySelector
('template').content, true), eventDetail); | |
172 } else { | |
173 // wait for `onload` | |
174 previousLink.onload = function() { | |
175 if (route.hasAttribute('active')) { | |
176 this.activeElement(document.importNode(previousLink.import.querySele
ctor('template').content, true), eventDetail); | |
177 } | |
178 }.bind(this); | |
179 } | |
180 } else { | |
181 // template hasn't been loaded yet | |
182 importedURIs[importUri] = true; | |
183 var templateLink = document.createElement('link'); | |
184 templateLink.setAttribute('rel', 'import'); | |
185 templateLink.setAttribute('href', importUri); | |
186 templateLink.onload = function() { | |
187 if (route.hasAttribute('active')) { | |
188 this.activeElement(document.importNode(templateLink.import.querySelect
or('template').content, true), eventDetail); | |
189 } | |
190 }.bind(this); | |
191 document.head.appendChild(templateLink); | |
192 } | |
193 }; | |
194 | |
195 // activateTemplate(route, eventDetail) - Replace the active route with a clon
e of the template's content | |
196 router.activateTemplate = function(route, eventDetail) { | |
197 var clone = document.importNode(route.querySelector('template').content, tru
e); | |
198 this.activeElement(clone, eventDetail); | |
199 }; | |
200 | |
201 // activeElement(element, eventDetail) - Replace the active route's content wi
th the new element | |
202 router.activeElement = function(element, eventDetail) { | |
203 while (this.activeRouteContent.firstChild) { | |
204 this.activeRouteContent.removeChild(this.activeRouteContent.firstChild); | |
205 } | |
206 this.activeRouteContent.appendChild(element); | |
207 fire('activate-route-end', eventDetail, this); | |
208 fire('activate-route-end', eventDetail, eventDetail.route); | |
209 }; | |
210 | |
211 // urlPath(url) - Parses the url to get the path | |
212 // | |
213 // This will return the hash path if it exists or return the real path if no h
ash path exists. | |
214 // | |
215 // Example URL = 'http://domain.com/other/path?queryParam3=false#/example/path
?queryParam1=true&queryParam2=example%20string' | |
216 // path = '/example/path' | |
217 // | |
218 // Note: The URL must contain the protocol like 'http(s)://' | |
219 router.parseUrlPath = function(url) { | |
220 // The relative URI is everything after the third slash including the third
slash | |
221 // Example relativeUri = '/other/path?queryParam3=false#/example/path?queryP
aram1=true&queryParam2=example%20string' | |
222 var splitUrl = url.split('/'); | |
223 var relativeUri = '/' + splitUrl.splice(3, splitUrl.length - 3).join('/'); | |
224 | |
225 // The path is everything in the relative URI up to the first ? or # | |
226 // Example path = '/other/path' | |
227 var path = relativeUri.split(/[\?#]/)[0]; | |
228 | |
229 // The hash is everything from the first # up to the the search starting wit
h ? if it exists | |
230 // Example hash = '#/example/path' | |
231 var hashIndex = relativeUri.indexOf('#'); | |
232 if (hashIndex !== -1) { | |
233 var hash = relativeUri.substring(hashIndex).split('?')[0]; | |
234 if (hash.substring(0, 2) === '#/') { | |
235 // Hash path | |
236 path = hash.substring(1); | |
237 } else if (hash.substring(0, 3) === '#!/') { | |
238 // Hashbang path | |
239 path = hash.substring(2); | |
240 } | |
241 } | |
242 | |
243 return path; | |
244 }; | |
245 | |
246 // router.testRoute(routePath, urlPath, trailingSlashOption, isRegExp) - Test
if the route's path matches the URL's path | |
247 // | |
248 // Example routePath: '/example/*' | |
249 // Example urlPath = '/example/path' | |
250 router.testRoute = function(routePath, urlPath, trailingSlashOption, isRegExp)
{ | |
251 // This algorithm tries to fail or succeed as quickly as possible for the mo
st common cases. | |
252 | |
253 // handle trailing slashes (options: strict (default), ignore) | |
254 if (trailingSlashOption === 'ignore') { | |
255 // remove trailing / from the route path and URL path | |
256 if(urlPath.slice(-1) === '/') { | |
257 urlPath = urlPath.slice(0, -1); | |
258 } | |
259 if(routePath.slice(-1) === '/' && !isRegExp) { | |
260 routePath = routePath.slice(0, -1); | |
261 } | |
262 } | |
263 | |
264 if (isRegExp) { | |
265 // parse HTML attribute path="/^\/\w+\/\d+$/i" to a regular expression `ne
w RegExp('^\/\w+\/\d+$', 'i')` | |
266 // note that 'i' is the only valid option. global 'g', multiline 'm', and
sticky 'y' won't be valid matchers for a path. | |
267 if (routePath.charAt(0) !== '/') { | |
268 // must start with a slash | |
269 return false; | |
270 } | |
271 routePath = routePath.slice(1); | |
272 var options = ''; | |
273 if (routePath.slice(-1) === '/') { | |
274 routePath = routePath.slice(0, -1); | |
275 } | |
276 else if (routePath.slice(-2) === '/i') { | |
277 routePath = routePath.slice(0, -2); | |
278 options = 'i'; | |
279 } | |
280 else { | |
281 // must end with a slash followed by zero or more options | |
282 return false; | |
283 } | |
284 return new RegExp(routePath, options).test(urlPath); | |
285 } | |
286 | |
287 // If the urlPath is an exact match or '*' then the route is a match | |
288 if (routePath === urlPath || routePath === '*') { | |
289 return true; | |
290 } | |
291 | |
292 // Look for wildcards | |
293 if (routePath.indexOf('*') === -1 && routePath.indexOf(':') === -1) { | |
294 // No wildcards and we already made sure it wasn't an exact match so the t
est fails | |
295 return false; | |
296 } | |
297 | |
298 // Example urlPathSegments = ['', example', 'path'] | |
299 var urlPathSegments = urlPath.split('/'); | |
300 | |
301 // Example routePathSegments = ['', 'example', '*'] | |
302 var routePathSegments = routePath.split('/'); | |
303 | |
304 // There must be the same number of path segments or it isn't a match | |
305 if (urlPathSegments.length !== routePathSegments.length) { | |
306 return false; | |
307 } | |
308 | |
309 // Check equality of each path segment | |
310 for (var i = 0; i < routePathSegments.length; i++) { | |
311 // The path segments must be equal, be a wildcard segment '*', or be a pat
h parameter like ':id' | |
312 var routeSegment = routePathSegments[i]; | |
313 if (routeSegment !== urlPathSegments[i] && routeSegment !== '*' && routeSe
gment.charAt(0) !== ':') { | |
314 // The path segment wasn't the same string and it wasn't a wildcard or p
arameter | |
315 return false; | |
316 } | |
317 } | |
318 | |
319 // Nothing failed. The route matches the URL. | |
320 return true; | |
321 }; | |
322 | |
323 // router.routeArguments(routePath, urlPath, url, isRegExp) - Gets the path va
riables and query parameter values from the URL | |
324 router.routeArguments = function routeArguments(routePath, urlPath, url, isReg
Exp) { | |
325 var args = {}; | |
326 | |
327 // Example urlPathSegments = ['', example', 'path'] | |
328 var urlPathSegments = urlPath.split('/'); | |
329 | |
330 if (!isRegExp) { | |
331 // Example routePathSegments = ['', 'example', '*'] | |
332 var routePathSegments = routePath.split('/'); | |
333 | |
334 // Get path variables | |
335 // urlPath '/customer/123' | |
336 // routePath '/customer/:id' | |
337 // parses id = '123' | |
338 for (var index = 0; index < routePathSegments.length; index++) { | |
339 var routeSegment = routePathSegments[index]; | |
340 if (routeSegment.charAt(0) === ':') { | |
341 args[routeSegment.substring(1)] = urlPathSegments[index]; | |
342 } | |
343 } | |
344 } | |
345 | |
346 // Get the query parameter values | |
347 // The search is the query parameters including the leading '?' | |
348 var searchIndex = url.indexOf('?'); | |
349 var search = ''; | |
350 if (searchIndex !== -1) { | |
351 search = url.substring(searchIndex); | |
352 var hashIndex = search.indexOf('#'); | |
353 if (hashIndex !== -1) { | |
354 search = search.substring(0, hashIndex); | |
355 } | |
356 } | |
357 // If it's a hash URL we need to get the search from the hash | |
358 var hashPathIndex = url.indexOf('#/'); | |
359 var hashBangPathIndex = url.indexOf('#!/'); | |
360 if (hashPathIndex !== -1 || hashBangPathIndex !== -1) { | |
361 var hash = ''; | |
362 if (hashPathIndex !== -1) { | |
363 hash = url.substring(hashPathIndex); | |
364 } else { | |
365 hash = url.substring(hashBangPathIndex); | |
366 } | |
367 searchIndex = hash.indexOf('?'); | |
368 if (searchIndex !== -1) { | |
369 search = hash.substring(searchIndex); | |
370 } | |
371 } | |
372 | |
373 var queryParameters = search.substring(1).split('&'); | |
374 // split() on an empty string has a strange behavior of returning [''] inste
ad of [] | |
375 if (queryParameters.length === 1 && queryParameters[0] === '') { | |
376 queryParameters = []; | |
377 } | |
378 for (var i = 0; i < queryParameters.length; i++) { | |
379 var queryParameter = queryParameters[i]; | |
380 var queryParameterParts = queryParameter.split('='); | |
381 args[queryParameterParts[0]] = queryParameterParts.splice(1, queryParamete
rParts.length - 1).join('='); | |
382 } | |
383 | |
384 // Parse the arguments into unescaped strings, numbers, or booleans | |
385 for (var arg in args) { | |
386 var value = args[arg]; | |
387 if (value === 'true') { | |
388 args[arg] = true; | |
389 } else if (value === 'false') { | |
390 args[arg] = false; | |
391 } else if (!isNaN(value) && value !== '') { | |
392 // numeric | |
393 args[arg] = +value; | |
394 } else { | |
395 // string | |
396 args[arg] = decodeURIComponent(value); | |
397 } | |
398 } | |
399 | |
400 return args; | |
401 }; | |
402 | |
403 document.registerElement('app-router', { | |
404 prototype: router | |
405 }); | |
406 })(window, document); | |
OLD | NEW |