Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(131)

Side by Side Diff: polymer_0.5.4/bower_components/app-router/src/app-router.js

Issue 897763002: Updated app-router to 2.4.0 (Closed) Base URL: https://chromium.googlesource.com/infra/third_party/npm_modules.git@master
Patch Set: Created 5 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « polymer_0.5.4/bower_components/app-router/src/app-router.html ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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);
OLDNEW
« no previous file with comments | « polymer_0.5.4/bower_components/app-router/src/app-router.html ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698