OLD | NEW |
| 1 // @license Copyright (C) 2015 Erik Ringsmuth - MIT license |
1 (function(window, document) { | 2 (function(window, document) { |
| 3 var utilities = {}; |
| 4 var importedURIs = {}; |
| 5 var isIE = 'ActiveXObject' in window; |
| 6 var previousUrl = {}; |
| 7 |
| 8 // <app-router [init="auto|manual"] [mode="auto|hash|pushstate"] [trailingSlas
h="strict|ignore"] [shadow]></app-router> |
| 9 var AppRouter = Object.create(HTMLElement.prototype); |
| 10 AppRouter.util = utilities; |
| 11 |
2 // <app-route path="/path" [import="/page/cust-el.html"] [element="cust-el"] [
template]></app-route> | 12 // <app-route path="/path" [import="/page/cust-el.html"] [element="cust-el"] [
template]></app-route> |
3 document.registerElement('app-route', { | 13 document.registerElement('app-route', { |
4 prototype: Object.create(HTMLElement.prototype) | 14 prototype: Object.create(HTMLElement.prototype) |
5 }); | 15 }); |
6 | 16 |
7 // <active-route></active-route> holds the active route's content when `shadow
` is not enabled | 17 // Initial set up when attached |
8 document.registerElement('active-route', { | 18 AppRouter.attachedCallback = function() { |
9 prototype: Object.create(HTMLElement.prototype) | 19 // init="auto|manual" |
10 }); | 20 if(this.getAttribute('init') !== 'manual') { |
11 | 21 this.init(); |
12 // <app-router [shadow] [trailingSlash="strict|ignore"] [init="auto|manual"]><
/app-router> | 22 } |
13 var router = Object.create(HTMLElement.prototype); | 23 }; |
14 | 24 |
15 var importedURIs = {}; | 25 // Initialize the router |
16 var isIE = 'ActiveXObject' in window; | 26 AppRouter.init = function() { |
| 27 var router = this; |
| 28 if (router.isInitialized) { |
| 29 return; |
| 30 } |
| 31 router.isInitialized = true; |
| 32 |
| 33 // trailingSlash="strict|ignore" |
| 34 if (!router.hasAttribute('trailingSlash')) { |
| 35 router.setAttribute('trailingSlash', 'strict'); |
| 36 } |
| 37 |
| 38 // mode="auto|hash|pushstate" |
| 39 if (!router.hasAttribute('mode')) { |
| 40 router.setAttribute('mode', 'auto'); |
| 41 } |
| 42 |
| 43 // typecast="auto|string" |
| 44 if (!router.hasAttribute('typecast')) { |
| 45 router.setAttribute('typecast', 'auto'); |
| 46 } |
| 47 |
| 48 // <app-router core-animated-pages transitions="hero-transition cross-fade"> |
| 49 if (router.hasAttribute('core-animated-pages')) { |
| 50 // use shadow DOM to wrap the <app-route> elements in a <core-animated-pag
es> element |
| 51 // <app-router> |
| 52 // # shadowRoot |
| 53 // <core-animated-pages> |
| 54 // # content in the light DOM |
| 55 // <app-route element="home-page"> |
| 56 // <home-page> |
| 57 // </home-page> |
| 58 // </app-route> |
| 59 // </core-animated-pages> |
| 60 // </app-router> |
| 61 router.createShadowRoot(); |
| 62 router.coreAnimatedPages = document.createElement('core-animated-pages'); |
| 63 router.coreAnimatedPages.appendChild(document.createElement('content')); |
| 64 |
| 65 // don't know why it needs to be static, but absolute doesn't display the
page |
| 66 router.coreAnimatedPages.style.position = 'static'; |
| 67 |
| 68 // toggle the selected page using selected="path" instead of selected="int
eger" |
| 69 router.coreAnimatedPages.setAttribute('valueattr', 'path'); |
| 70 |
| 71 // pass the transitions attribute from <app-router core-animated-pages tra
nsitions="hero-transition cross-fade"> |
| 72 // to <core-animated-pages transitions="hero-transition cross-fade"> |
| 73 router.coreAnimatedPages.setAttribute('transitions', router.getAttribute('
transitions')); |
| 74 |
| 75 // set the shadow DOM's content |
| 76 router.shadowRoot.appendChild(router.coreAnimatedPages); |
| 77 |
| 78 // when a transition finishes, remove the previous route's content. there
is a temporary overlap where both |
| 79 // the new and old route's content is in the DOM to animate the transition
. |
| 80 router.coreAnimatedPages.addEventListener('core-animated-pages-transition-
end', function() { |
| 81 transitionAnimationEnd(router.previousRoute); |
| 82 }); |
| 83 } |
| 84 |
| 85 // listen for URL change events |
| 86 router.stateChangeHandler = stateChange.bind(null, router); |
| 87 window.addEventListener('popstate', router.stateChangeHandler, false); |
| 88 if (isIE) { |
| 89 // IE bug. A hashchange is supposed to trigger a popstate event, making po
pstate the only event you |
| 90 // need to listen to. That's not the case in IE so we make another event l
istener for it. |
| 91 window.addEventListener('hashchange', router.stateChangeHandler, false); |
| 92 } |
| 93 |
| 94 // load the web component for the current route |
| 95 stateChange(router); |
| 96 }; |
| 97 |
| 98 // clean up global event listeners |
| 99 AppRouter.detachedCallback = function() { |
| 100 window.removeEventListener('popstate', this.stateChangeHandler, false); |
| 101 if (isIE) { |
| 102 window.removeEventListener('hashchange', this.stateChangeHandler, false); |
| 103 } |
| 104 }; |
| 105 |
| 106 // go(path, options) Navigate to the path |
| 107 // |
| 108 // options = { |
| 109 // replace: true |
| 110 // } |
| 111 AppRouter.go = function(path, options) { |
| 112 if (this.getAttribute('mode') !== 'pushstate') { |
| 113 // mode == auto or hash |
| 114 path = '#' + path; |
| 115 } |
| 116 if (options && options.replace === true) { |
| 117 window.history.replaceState(null, null, path); |
| 118 } else { |
| 119 window.history.pushState(null, null, path); |
| 120 } |
| 121 |
| 122 // dispatch a popstate event |
| 123 try { |
| 124 var popstateEvent = new PopStateEvent('popstate', { |
| 125 bubbles: false, |
| 126 cancelable: false, |
| 127 state: {} |
| 128 }); |
| 129 |
| 130 if ('dispatchEvent_' in window) { |
| 131 // FireFox with polyfill |
| 132 window.dispatchEvent_(popstateEvent); |
| 133 } else { |
| 134 // normal |
| 135 window.dispatchEvent(popstateEvent); |
| 136 } |
| 137 } catch(error) { |
| 138 // Internet Exploder |
| 139 var fallbackEvent = document.createEvent('CustomEvent'); |
| 140 fallbackEvent.initCustomEvent('popstate', false, false, { state: {} }); |
| 141 window.dispatchEvent(fallbackEvent); |
| 142 } |
| 143 }; |
17 | 144 |
18 // fire(type, detail, node) - Fire a new CustomEvent(type, detail) on the node | 145 // fire(type, detail, node) - Fire a new CustomEvent(type, detail) on the node |
19 // | 146 // |
20 // listen with document.querySelector('app-router').addEventListener(type, fun
ction(event) { | 147 // listen with document.querySelector('app-router').addEventListener(type, fun
ction(event) { |
21 // event.detail, event.preventDefault() | 148 // event.detail, event.preventDefault() |
22 // }) | 149 // }) |
23 function fire(type, detail, node) { | 150 function fire(type, detail, node) { |
24 // create a CustomEvent the old way for IE9/10 support | 151 // create a CustomEvent the old way for IE9/10 support |
25 var event = document.createEvent('CustomEvent'); | 152 var event = document.createEvent('CustomEvent'); |
26 | 153 |
27 // initCustomEvent(type, bubbles, cancelable, detail) | 154 // initCustomEvent(type, bubbles, cancelable, detail) |
28 event.initCustomEvent(type, false, true, detail); | 155 event.initCustomEvent(type, false, true, detail); |
29 | 156 |
30 // returns false when event.preventDefault() is called, true otherwise | 157 // returns false when event.preventDefault() is called, true otherwise |
31 return node.dispatchEvent(event); | 158 return node.dispatchEvent(event); |
32 } | 159 } |
33 | 160 |
34 // Initial set up when attached | 161 // Find the first <app-route> that matches the current URL and change the acti
ve route |
35 router.attachedCallback = function() { | 162 function stateChange(router) { |
36 if(this.getAttribute('init') !== 'manual') { | 163 var url = utilities.parseUrl(window.location.href, router.getAttribute('mode
')); |
37 this.init(); | 164 |
38 } | 165 // don't load a new route if only the hash fragment changed |
39 }; | 166 if (url.hash !== previousUrl.hash && url.path === previousUrl.path && url.se
arch === previousUrl.search && url.isHashPath === previousUrl.isHashPath) { |
40 | 167 scrollToHash(url.hash); |
41 // Initialize the router | 168 return; |
42 router.init = function() { | 169 } |
43 if (this.isInitialized) { | 170 previousUrl = url; |
44 return; | 171 |
45 } | 172 // fire a state-change event on the app-router and return early if the user
called event.preventDefault() |
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 = { | 173 var eventDetail = { |
81 path: urlPath | 174 path: url.path |
82 }; | 175 }; |
83 if (!fire('state-change', eventDetail, this)) { | 176 if (!fire('state-change', eventDetail, router)) { |
84 return; | 177 return; |
85 } | 178 } |
86 var routes = this.querySelectorAll('app-route'); | 179 |
87 for (var i = 0; i < routes.length; i++) { | 180 // find the first matching route |
88 if (this.testRoute(routes[i].getAttribute('path'), urlPath, this.getAttrib
ute('trailingSlash'), routes[i].hasAttribute('regex'))) { | 181 var route = router.firstElementChild; |
89 this.activateRoute(routes[i], urlPath); | 182 while (route) { |
| 183 if (route.tagName === 'APP-ROUTE' && utilities.testRoute(route.getAttribut
e('path'), url.path, router.getAttribute('trailingSlash'), route.hasAttribute('r
egex'))) { |
| 184 activateRoute(router, route, url); |
90 return; | 185 return; |
91 } | 186 } |
92 } | 187 route = route.nextSibling; |
93 fire('not-found', eventDetail, this); | 188 } |
94 }; | 189 |
95 | 190 fire('not-found', eventDetail, router); |
96 // activateRoute(route, urlPath) - Activate the route | 191 } |
97 router.activateRoute = function(route, urlPath) { | 192 |
| 193 // Activate the route |
| 194 function activateRoute(router, route, url) { |
| 195 if (route.hasAttribute('redirect')) { |
| 196 router.go(route.getAttribute('redirect'), {replace: true}); |
| 197 return; |
| 198 } |
| 199 |
98 var eventDetail = { | 200 var eventDetail = { |
99 path: urlPath, | 201 path: url.path, |
100 route: route, | 202 route: route, |
101 oldRoute: this.activeRoute | 203 oldRoute: router.activeRoute |
102 }; | 204 }; |
103 if (!fire('activate-route-start', eventDetail, this)) { | 205 if (!fire('activate-route-start', eventDetail, router)) { |
104 return; | 206 return; |
105 } | 207 } |
106 if (!fire('activate-route-start', eventDetail, route)) { | 208 if (!fire('activate-route-start', eventDetail, route)) { |
107 return; | 209 return; |
108 } | 210 } |
109 | 211 |
110 this.activeRoute.removeAttribute('active'); | 212 // update the references to the activeRoute and previousRoute. if you switch
between routes quickly you may go to a |
111 route.setAttribute('active', 'active'); | 213 // new route before the previous route's transition animation has completed.
if that's the case we need to remove |
112 this.activeRoute = route; | 214 // the previous route's content before we replace the reference to the previ
ous route. |
113 | 215 if (router.previousRoute && router.previousRoute.transitionAnimationInProgre
ss) { |
114 var importUri = route.getAttribute('import'); | 216 transitionAnimationEnd(router.previousRoute); |
115 var routePath = route.getAttribute('path'); | 217 } |
116 var isRegExp = route.hasAttribute('regex'); | 218 if (router.activeRoute) { |
117 var elementName = route.getAttribute('element'); | 219 router.activeRoute.removeAttribute('active'); |
118 var isTemplate = route.hasAttribute('template'); | 220 } |
119 var isElement = !isTemplate; | 221 router.previousRoute = router.activeRoute; |
120 | 222 router.activeRoute = route; |
121 // import custom element | 223 router.activeRoute.setAttribute('active', 'active'); |
122 if (isElement && importUri) { | 224 |
123 this.importAndActivateCustomElement(importUri, elementName, routePath, url
Path, isRegExp, eventDetail); | 225 // import custom element or template |
| 226 if (route.hasAttribute('import')) { |
| 227 importAndActivate(router, route.getAttribute('import'), route, url, eventD
etail); |
124 } | 228 } |
125 // pre-loaded custom element | 229 // pre-loaded custom element |
126 else if (isElement && !importUri && elementName) { | 230 else if (route.hasAttribute('element')) { |
127 this.activateCustomElement(elementName, routePath, urlPath, isRegExp, even
tDetail); | 231 activateCustomElement(router, route.getAttribute('element'), route, url, e
ventDetail); |
128 } | 232 } |
129 // import template | 233 // inline template |
130 else if (isTemplate && importUri) { | 234 else if (route.firstElementChild && route.firstElementChild.tagName === 'TEM
PLATE') { |
131 this.importAndActivateTemplate(importUri, route, eventDetail); | 235 activateTemplate(router, route.firstElementChild, route, url, eventDetail)
; |
132 } | 236 } |
133 // pre-loaded template | 237 } |
134 else if (isTemplate && !importUri) { | 238 |
135 this.activateTemplate(route, eventDetail); | 239 // Import and activate a custom element or template |
136 } | 240 function importAndActivate(router, importUri, route, url, eventDetail) { |
137 }; | 241 var importLink; |
138 | 242 function importLoadedCallback() { |
139 // importAndActivateCustomElement(importUri, elementName, routePath, urlPath,
isRegExp, eventDetail) - Import the custom element then replace the active route | 243 activateImport(router, importLink, importUri, route, url, eventDetail); |
140 // with a new instance of the custom element | 244 } |
141 router.importAndActivateCustomElement = function(importUri, elementName, route
Path, urlPath, isRegExp, eventDetail) { | 245 |
142 if (!importedURIs.hasOwnProperty(importUri)) { | 246 if (!importedURIs.hasOwnProperty(importUri)) { |
| 247 // hasn't been imported yet |
143 importedURIs[importUri] = true; | 248 importedURIs[importUri] = true; |
144 var elementLink = document.createElement('link'); | 249 importLink = document.createElement('link'); |
145 elementLink.setAttribute('rel', 'import'); | 250 importLink.setAttribute('rel', 'import'); |
146 elementLink.setAttribute('href', importUri); | 251 importLink.setAttribute('href', importUri); |
147 document.head.appendChild(elementLink); | 252 importLink.addEventListener('load', importLoadedCallback); |
148 } | 253 document.head.appendChild(importLink); |
149 this.activateCustomElement(elementName || importUri.split('/').slice(-1)[0].
replace('.html', ''), routePath, urlPath, isRegExp, eventDetail); | 254 } else { |
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. | 255 // previously imported. this is an async operation and may not be complete
yet. |
168 var previousLink = document.querySelector('link[href="' + importUri + '"]'
); | 256 importLink = document.querySelector('link[href="' + importUri + '"]'); |
169 if (previousLink.import) { | 257 if (importLink.import) { |
170 // the import is complete | 258 // import complete |
171 this.activeElement(document.importNode(previousLink.import.querySelector
('template').content, true), eventDetail); | 259 importLoadedCallback(); |
172 } else { | 260 } else { |
173 // wait for `onload` | 261 // wait for `onload` |
174 previousLink.onload = function() { | 262 importLink.addEventListener('load', importLoadedCallback); |
175 if (route.hasAttribute('active')) { | 263 } |
176 this.activeElement(document.importNode(previousLink.import.querySele
ctor('template').content, true), eventDetail); | 264 } |
177 } | 265 } |
178 }.bind(this); | 266 |
179 } | 267 // Activate the imported custom element or template |
| 268 function activateImport(router, importLink, importUri, route, url, eventDetail
) { |
| 269 // make sure the user didn't navigate to a different route while it loaded |
| 270 if (route.hasAttribute('active')) { |
| 271 if (route.hasAttribute('template')) { |
| 272 // template |
| 273 activateTemplate(router, importLink.import.querySelector('template'), ro
ute, url, eventDetail); |
| 274 } else { |
| 275 // custom element |
| 276 activateCustomElement(router, route.getAttribute('element') || importUri
.split('/').slice(-1)[0].replace('.html', ''), route, url, eventDetail); |
| 277 } |
| 278 } |
| 279 } |
| 280 |
| 281 // Data bind the custom element then activate it |
| 282 function activateCustomElement(router, elementName, route, url, eventDetail) { |
| 283 var customElement = document.createElement(elementName); |
| 284 var model = createModel(router, route, url, eventDetail); |
| 285 for (var property in model) { |
| 286 if (model.hasOwnProperty(property)) { |
| 287 customElement[property] = model[property]; |
| 288 } |
| 289 } |
| 290 activateElement(router, customElement, url, eventDetail); |
| 291 } |
| 292 |
| 293 // Create an instance of the template |
| 294 function activateTemplate(router, template, route, url, eventDetail) { |
| 295 var templateInstance; |
| 296 if ('createInstance' in template) { |
| 297 // template.createInstance(model) is a Polymer method that binds a model t
o a template and also fixes |
| 298 // https://github.com/erikringsmuth/app-router/issues/19 |
| 299 var model = createModel(router, route, url, eventDetail); |
| 300 templateInstance = template.createInstance(model); |
180 } else { | 301 } else { |
181 // template hasn't been loaded yet | 302 templateInstance = document.importNode(template.content, true); |
182 importedURIs[importUri] = true; | 303 } |
183 var templateLink = document.createElement('link'); | 304 activateElement(router, templateInstance, url, eventDetail); |
184 templateLink.setAttribute('rel', 'import'); | 305 } |
185 templateLink.setAttribute('href', importUri); | 306 |
186 templateLink.onload = function() { | 307 // Create the route's model |
187 if (route.hasAttribute('active')) { | 308 function createModel(router, route, url, eventDetail) { |
188 this.activeElement(document.importNode(templateLink.import.querySelect
or('template').content, true), eventDetail); | 309 var model = utilities.routeArguments(route.getAttribute('path'), url.path, u
rl.search, route.hasAttribute('regex'), router.getAttribute('typecast') === 'aut
o'); |
| 310 if (route.hasAttribute('bindRouter') || router.hasAttribute('bindRouter')) { |
| 311 model.router = router; |
| 312 } |
| 313 eventDetail.model = model; |
| 314 fire('before-data-binding', eventDetail, router); |
| 315 fire('before-data-binding', eventDetail, eventDetail.route); |
| 316 return eventDetail.model; |
| 317 } |
| 318 |
| 319 // Replace the active route's content with the new element |
| 320 function activateElement(router, element, url, eventDetail) { |
| 321 // core-animated-pages temporarily needs the old and new route in the DOM at
the same time to animate the transition, |
| 322 // otherwise we can remove the old route's content right away. |
| 323 // UNLESS |
| 324 // if the route we're navigating to matches the same app-route (ex: path="/a
rticle/:id" navigating from /article/0 to |
| 325 // /article/1), then we have to simply replace the route's content instead o
f animating a transition. |
| 326 if (!router.hasAttribute('core-animated-pages') || eventDetail.route === eve
ntDetail.oldRoute) { |
| 327 removeRouteContent(router.previousRoute); |
| 328 } |
| 329 |
| 330 // add the new content |
| 331 router.activeRoute.appendChild(element); |
| 332 |
| 333 // animate the transition if core-animated-pages are being used |
| 334 if (router.hasAttribute('core-animated-pages')) { |
| 335 router.coreAnimatedPages.selected = router.activeRoute.getAttribute('path'
); |
| 336 |
| 337 // we already wired up transitionAnimationEnd() in init() |
| 338 |
| 339 // use to check if the previous route has finished animating before being
removed |
| 340 if (router.previousRoute) { |
| 341 router.previousRoute.transitionAnimationInProgress = true; |
| 342 } |
| 343 } |
| 344 |
| 345 // scroll to the URL hash if it's present |
| 346 if (url.hash && !router.hasAttribute('core-animated-pages')) { |
| 347 scrollToHash(url.hash); |
| 348 } |
| 349 |
| 350 fire('activate-route-end', eventDetail, router); |
| 351 fire('activate-route-end', eventDetail, eventDetail.route); |
| 352 } |
| 353 |
| 354 // Call when the previousRoute has finished the transition animation out |
| 355 function transitionAnimationEnd(previousRoute) { |
| 356 if (previousRoute) { |
| 357 previousRoute.transitionAnimationInProgress = false; |
| 358 removeRouteContent(previousRoute); |
| 359 } |
| 360 } |
| 361 |
| 362 // Remove the route's content (but not the <template> if it exists) |
| 363 function removeRouteContent(route) { |
| 364 if (route) { |
| 365 var node = route.firstChild; |
| 366 while (node) { |
| 367 var nodeToRemove = node; |
| 368 node = node.nextSibling; |
| 369 if (nodeToRemove.tagName !== 'TEMPLATE') { |
| 370 route.removeChild(nodeToRemove); |
189 } | 371 } |
190 }.bind(this); | 372 } |
191 document.head.appendChild(templateLink); | 373 } |
192 } | 374 } |
| 375 |
| 376 // scroll to the element with id="hash" or name="hash" |
| 377 function scrollToHash(hash) { |
| 378 if (!hash) return; |
| 379 |
| 380 // wait for the browser's scrolling to finish before we scroll to the hash |
| 381 // ex: http://example.com/#/page1#middle |
| 382 // the browser will scroll to an element with id or name `/page1#middle` whe
n the page finishes loading. if it doesn't exist |
| 383 // it will scroll to the top of the page. let the browser finish the current
event loop and scroll to the top of the page |
| 384 // before we scroll to the element with id or name `middle`. |
| 385 setTimeout(function() { |
| 386 var hashElement = document.querySelector('html /deep/ ' + hash) || documen
t.querySelector('html /deep/ [name="' + hash.substring(1) + '"]'); |
| 387 if (hashElement && hashElement.scrollIntoView) { |
| 388 hashElement.scrollIntoView(true); |
| 389 } |
| 390 }, 0); |
| 391 } |
| 392 |
| 393 // parseUrl(location, mode) - Augment the native URL() constructor to get info
about hash paths |
| 394 // |
| 395 // Example parseUrl('http://domain.com/other/path?queryParam3=false#/example/p
ath?queryParam1=true&queryParam2=example%20string#middle', 'auto') |
| 396 // |
| 397 // returns { |
| 398 // path: '/example/path', |
| 399 // hash: '#middle' |
| 400 // search: '?queryParam1=true&queryParam2=example%20string', |
| 401 // isHashPath: true |
| 402 // } |
| 403 // |
| 404 // Note: The location must be a fully qualified URL with a protocol like 'http
(s)://' |
| 405 utilities.parseUrl = function(location, mode) { |
| 406 var url = { |
| 407 isHashPath: mode === 'hash' |
| 408 }; |
| 409 |
| 410 if (typeof URL === 'function') { |
| 411 // browsers that support `new URL()` |
| 412 var nativeUrl = new URL(location); |
| 413 url.path = nativeUrl.pathname; |
| 414 url.hash = nativeUrl.hash; |
| 415 url.search = nativeUrl.search; |
| 416 } else { |
| 417 // IE |
| 418 var anchor = document.createElement('a'); |
| 419 anchor.href = location; |
| 420 url.path = anchor.pathname; |
| 421 if (url.path.charAt(0) !== '/') { |
| 422 url.path = '/' + url.path; |
| 423 } |
| 424 url.hash = anchor.hash; |
| 425 url.search = anchor.search; |
| 426 } |
| 427 |
| 428 if (mode !== 'pushstate') { |
| 429 // auto or hash |
| 430 |
| 431 // check for a hash path |
| 432 if (url.hash.substring(0, 2) === '#/') { |
| 433 // hash path |
| 434 url.isHashPath = true; |
| 435 url.path = url.hash.substring(1); |
| 436 } else if (url.hash.substring(0, 3) === '#!/') { |
| 437 // hashbang path |
| 438 url.isHashPath = true; |
| 439 url.path = url.hash.substring(2); |
| 440 } else if (url.isHashPath) { |
| 441 // still use the hash if mode="hash" |
| 442 if (url.hash.length === 0) { |
| 443 url.path = '/'; |
| 444 } else { |
| 445 url.path = url.hash.substring(1); |
| 446 } |
| 447 } |
| 448 |
| 449 if (url.isHashPath) { |
| 450 url.hash = ''; |
| 451 |
| 452 // hash paths might have an additional hash in the hash path for scrolli
ng to a specific part of the page #/hash/path#elementId |
| 453 var secondHashIndex = url.path.indexOf('#'); |
| 454 if (secondHashIndex !== -1) { |
| 455 url.hash = url.path.substring(secondHashIndex); |
| 456 url.path = url.path.substring(0, secondHashIndex); |
| 457 } |
| 458 |
| 459 // hash paths get the search from the hash if it exists |
| 460 var searchIndex = url.path.indexOf('?'); |
| 461 if (searchIndex !== -1) { |
| 462 url.search = url.path.substring(searchIndex); |
| 463 url.path = url.path.substring(0, searchIndex); |
| 464 } |
| 465 } |
| 466 } |
| 467 |
| 468 return url; |
193 }; | 469 }; |
194 | 470 |
195 // activateTemplate(route, eventDetail) - Replace the active route with a clon
e of the template's content | 471 // testRoute(routePath, urlPath, trailingSlashOption, isRegExp) - Test if the
route's path matches the URL's path |
196 router.activateTemplate = function(route, eventDetail) { | 472 // |
197 var clone = document.importNode(route.querySelector('template').content, tru
e); | 473 // Example routePath: '/user/:userId/**' |
198 this.activeElement(clone, eventDetail); | 474 // Example urlPath = '/user/123/bio' |
199 }; | 475 utilities.testRoute = function(routePath, urlPath, trailingSlashOption, isRegE
xp) { |
200 | 476 // try to fail or succeed as quickly as possible for the most common cases |
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 | 477 |
253 // handle trailing slashes (options: strict (default), ignore) | 478 // handle trailing slashes (options: strict (default), ignore) |
254 if (trailingSlashOption === 'ignore') { | 479 if (trailingSlashOption === 'ignore') { |
255 // remove trailing / from the route path and URL path | 480 // remove trailing / from the route path and URL path |
256 if(urlPath.slice(-1) === '/') { | 481 if(urlPath.slice(-1) === '/') { |
257 urlPath = urlPath.slice(0, -1); | 482 urlPath = urlPath.slice(0, -1); |
258 } | 483 } |
259 if(routePath.slice(-1) === '/' && !isRegExp) { | 484 if(routePath.slice(-1) === '/' && !isRegExp) { |
260 routePath = routePath.slice(0, -1); | 485 routePath = routePath.slice(0, -1); |
261 } | 486 } |
262 } | 487 } |
263 | 488 |
| 489 // test regular expressions |
264 if (isRegExp) { | 490 if (isRegExp) { |
265 // parse HTML attribute path="/^\/\w+\/\d+$/i" to a regular expression `ne
w RegExp('^\/\w+\/\d+$', 'i')` | 491 return utilities.testRegExString(routePath, urlPath); |
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 } | 492 } |
286 | 493 |
287 // If the urlPath is an exact match or '*' then the route is a match | 494 // if the urlPath is an exact match or '*' then the route is a match |
288 if (routePath === urlPath || routePath === '*') { | 495 if (routePath === urlPath || routePath === '*') { |
289 return true; | 496 return true; |
290 } | 497 } |
291 | 498 |
292 // Look for wildcards | 499 // relative routes a/b/c are the same as routes that start with a globstar /
**/a/b/c |
293 if (routePath.indexOf('*') === -1 && routePath.indexOf(':') === -1) { | 500 if (routePath.charAt(0) !== '/') { |
294 // No wildcards and we already made sure it wasn't an exact match so the t
est fails | 501 routePath = '/**/' + routePath; |
295 return false; | |
296 } | 502 } |
297 | 503 |
298 // Example urlPathSegments = ['', example', 'path'] | 504 // recursively test if the segments match (start at 1 because 0 is always an
empty string) |
299 var urlPathSegments = urlPath.split('/'); | 505 return segmentsMatch(routePath.split('/'), 1, urlPath.split('/'), 1) |
| 506 }; |
300 | 507 |
301 // Example routePathSegments = ['', 'example', '*'] | 508 // segmentsMatch(routeSegments, routeIndex, urlSegments, urlIndex, pathVariabl
es) |
302 var routePathSegments = routePath.split('/'); | 509 // recursively test the route segments against the url segments in place (with
out creating copies of the arrays |
| 510 // for each recursive call) |
| 511 // |
| 512 // example routeSegments ['', 'user', ':userId', '**'] |
| 513 // example urlSegments ['', 'user', '123', 'bio'] |
| 514 function segmentsMatch(routeSegments, routeIndex, urlSegments, urlIndex, pathV
ariables) { |
| 515 var routeSegment = routeSegments[routeIndex]; |
| 516 var urlSegment = urlSegments[urlIndex]; |
303 | 517 |
304 // There must be the same number of path segments or it isn't a match | 518 // if we're at the last route segment and it is a globstar, it will match th
e rest of the url |
305 if (urlPathSegments.length !== routePathSegments.length) { | 519 if (routeSegment === '**' && routeIndex === routeSegments.length - 1) { |
306 return false; | 520 return true; |
307 } | 521 } |
308 | 522 |
309 // Check equality of each path segment | 523 // we hit the end of the route segments or the url segments |
310 for (var i = 0; i < routePathSegments.length; i++) { | 524 if (typeof routeSegment === 'undefined' || typeof urlSegment === 'undefined'
) { |
311 // The path segments must be equal, be a wildcard segment '*', or be a pat
h parameter like ':id' | 525 // return true if we hit the end of both at the same time meaning everythi
ng else matched, else return false |
312 var routeSegment = routePathSegments[i]; | 526 return routeSegment === urlSegment; |
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 } | 527 } |
318 | 528 |
319 // Nothing failed. The route matches the URL. | 529 // if the current segments match, recursively test the remaining segments |
320 return true; | 530 if (routeSegment === urlSegment || routeSegment === '*' || routeSegment.char
At(0) === ':') { |
321 }; | 531 // store the path variable if we have a pathVariables object |
| 532 if (routeSegment.charAt(0) === ':' && typeof pathVariables !== 'undefined'
) { |
| 533 pathVariables[routeSegment.substring(1)] = urlSegments[urlIndex]; |
| 534 } |
| 535 return segmentsMatch(routeSegments, routeIndex + 1, urlSegments, urlIndex
+ 1, pathVariables); |
| 536 } |
322 | 537 |
323 // router.routeArguments(routePath, urlPath, url, isRegExp) - Gets the path va
riables and query parameter values from the URL | 538 // globstars can match zero to many URL segments |
324 router.routeArguments = function routeArguments(routePath, urlPath, url, isReg
Exp) { | 539 if (routeSegment === '**') { |
325 var args = {}; | 540 // test if the remaining route segments match any combination of the remai
ning url segments |
326 | 541 for (var i = urlIndex; i < urlSegments.length; i++) { |
327 // Example urlPathSegments = ['', example', 'path'] | 542 if (segmentsMatch(routeSegments, routeIndex + 1, urlSegments, i, pathVar
iables)) { |
328 var urlPathSegments = urlPath.split('/'); | 543 return true; |
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 } | 544 } |
343 } | 545 } |
344 } | 546 } |
345 | 547 |
346 // Get the query parameter values | 548 // all tests failed, the route segments do not match the url segments |
347 // The search is the query parameters including the leading '?' | 549 return false; |
348 var searchIndex = url.indexOf('?'); | 550 } |
349 var search = ''; | 551 |
350 if (searchIndex !== -1) { | 552 // routeArguments(routePath, urlPath, search, isRegExp) - Gets the path variab
les and query parameter values from the URL |
351 search = url.substring(searchIndex); | 553 utilities.routeArguments = function(routePath, urlPath, search, isRegExp, type
cast) { |
352 var hashIndex = search.indexOf('#'); | 554 var args = {}; |
353 if (hashIndex !== -1) { | 555 |
354 search = search.substring(0, hashIndex); | 556 // regular expressions can't have path variables |
| 557 if (!isRegExp) { |
| 558 // relative routes a/b/c are the same as routes that start with a globstar
/**/a/b/c |
| 559 if (routePath.charAt(0) !== '/') { |
| 560 routePath = '/**/' + routePath; |
355 } | 561 } |
356 } | 562 |
357 // If it's a hash URL we need to get the search from the hash | 563 // get path variables |
358 var hashPathIndex = url.indexOf('#/'); | 564 // urlPath '/customer/123' |
359 var hashBangPathIndex = url.indexOf('#!/'); | 565 // routePath '/customer/:id' |
360 if (hashPathIndex !== -1 || hashBangPathIndex !== -1) { | 566 // parses id = '123' |
361 var hash = ''; | 567 segmentsMatch(routePath.split('/'), 1, urlPath.split('/'), 1, args); |
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 } | 568 } |
372 | 569 |
373 var queryParameters = search.substring(1).split('&'); | 570 var queryParameters = search.substring(1).split('&'); |
374 // split() on an empty string has a strange behavior of returning [''] inste
ad of [] | 571 // split() on an empty string has a strange behavior of returning [''] inste
ad of [] |
375 if (queryParameters.length === 1 && queryParameters[0] === '') { | 572 if (queryParameters.length === 1 && queryParameters[0] === '') { |
376 queryParameters = []; | 573 queryParameters = []; |
377 } | 574 } |
378 for (var i = 0; i < queryParameters.length; i++) { | 575 for (var i = 0; i < queryParameters.length; i++) { |
379 var queryParameter = queryParameters[i]; | 576 var queryParameter = queryParameters[i]; |
380 var queryParameterParts = queryParameter.split('='); | 577 var queryParameterParts = queryParameter.split('='); |
381 args[queryParameterParts[0]] = queryParameterParts.splice(1, queryParamete
rParts.length - 1).join('='); | 578 args[queryParameterParts[0]] = queryParameterParts.splice(1, queryParamete
rParts.length - 1).join('='); |
382 } | 579 } |
383 | 580 |
384 // Parse the arguments into unescaped strings, numbers, or booleans | 581 if (typecast) { |
385 for (var arg in args) { | 582 // parse the arguments into unescaped strings, numbers, or booleans |
386 var value = args[arg]; | 583 for (var arg in args) { |
387 if (value === 'true') { | 584 args[arg] = utilities.typecast(args[arg]); |
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 } | 585 } |
398 } | 586 } |
399 | 587 |
400 return args; | 588 return args; |
401 }; | 589 }; |
402 | 590 |
| 591 // typecast(value) - Typecast the string value to an unescaped string, number,
or boolean |
| 592 utilities.typecast = function(value) { |
| 593 // bool |
| 594 if (value === 'true') { |
| 595 return true; |
| 596 } |
| 597 if (value === 'false') { |
| 598 return false; |
| 599 } |
| 600 |
| 601 // number |
| 602 if (!isNaN(value) && value !== '' && value.charAt(0) !== '0') { |
| 603 return +value; |
| 604 } |
| 605 |
| 606 // string |
| 607 return decodeURIComponent(value); |
| 608 }; |
| 609 |
| 610 // testRegExString(pattern, value) - Parse HTML attribute path="/^\/\w+\/\d+$/
i" to a regular |
| 611 // expression `new RegExp('^\/\w+\/\d+$', 'i')` and test against it. |
| 612 // |
| 613 // note that 'i' is the only valid option. global 'g', multiline 'm', and stic
ky 'y' won't be valid matchers for a path. |
| 614 utilities.testRegExString = function(pattern, value) { |
| 615 if (pattern.charAt(0) !== '/') { |
| 616 // must start with a slash |
| 617 return false; |
| 618 } |
| 619 pattern = pattern.slice(1); |
| 620 var options = ''; |
| 621 if (pattern.slice(-1) === '/') { |
| 622 pattern = pattern.slice(0, -1); |
| 623 } |
| 624 else if (pattern.slice(-2) === '/i') { |
| 625 pattern = pattern.slice(0, -2); |
| 626 options = 'i'; |
| 627 } |
| 628 else { |
| 629 // must end with a slash followed by zero or more options |
| 630 return false; |
| 631 } |
| 632 return new RegExp(pattern, options).test(value); |
| 633 }; |
| 634 |
403 document.registerElement('app-router', { | 635 document.registerElement('app-router', { |
404 prototype: router | 636 prototype: AppRouter |
405 }); | 637 }); |
| 638 |
406 })(window, document); | 639 })(window, document); |
OLD | NEW |