Index: polymer_0.5.4/bower_components/app-router/src/app-router.js |
diff --git a/polymer_0.5.4/bower_components/app-router/src/app-router.js b/polymer_0.5.4/bower_components/app-router/src/app-router.js |
index 912ba1cf31739e8fe30b80eb04d86b7b1b7c6c5b..f4f3df833d97ce94d77b3913214020b4fb62eed7 100644 |
--- a/polymer_0.5.4/bower_components/app-router/src/app-router.js |
+++ b/polymer_0.5.4/bower_components/app-router/src/app-router.js |
@@ -1,254 +1,479 @@ |
+// @license Copyright (C) 2015 Erik Ringsmuth - MIT license |
(function(window, document) { |
- // <app-route path="/path" [import="/page/cust-el.html"] [element="cust-el"] [template]></app-route> |
- document.registerElement('app-route', { |
- prototype: Object.create(HTMLElement.prototype) |
- }); |
- |
- // <active-route></active-route> holds the active route's content when `shadow` is not enabled |
- document.registerElement('active-route', { |
- prototype: Object.create(HTMLElement.prototype) |
- }); |
- |
- // <app-router [shadow] [trailingSlash="strict|ignore"] [init="auto|manual"]></app-router> |
- var router = Object.create(HTMLElement.prototype); |
- |
+ var utilities = {}; |
var importedURIs = {}; |
var isIE = 'ActiveXObject' in window; |
+ var previousUrl = {}; |
- // fire(type, detail, node) - Fire a new CustomEvent(type, detail) on the node |
- // |
- // listen with document.querySelector('app-router').addEventListener(type, function(event) { |
- // event.detail, event.preventDefault() |
- // }) |
- function fire(type, detail, node) { |
- // create a CustomEvent the old way for IE9/10 support |
- var event = document.createEvent('CustomEvent'); |
- |
- // initCustomEvent(type, bubbles, cancelable, detail) |
- event.initCustomEvent(type, false, true, detail); |
+ // <app-router [init="auto|manual"] [mode="auto|hash|pushstate"] [trailingSlash="strict|ignore"] [shadow]></app-router> |
+ var AppRouter = Object.create(HTMLElement.prototype); |
+ AppRouter.util = utilities; |
- // returns false when event.preventDefault() is called, true otherwise |
- return node.dispatchEvent(event); |
- } |
+ // <app-route path="/path" [import="/page/cust-el.html"] [element="cust-el"] [template]></app-route> |
+ document.registerElement('app-route', { |
+ prototype: Object.create(HTMLElement.prototype) |
+ }); |
// Initial set up when attached |
- router.attachedCallback = function() { |
+ AppRouter.attachedCallback = function() { |
+ // init="auto|manual" |
if(this.getAttribute('init') !== 'manual') { |
this.init(); |
} |
}; |
// Initialize the router |
- router.init = function() { |
- if (this.isInitialized) { |
+ AppRouter.init = function() { |
+ var router = this; |
+ if (router.isInitialized) { |
return; |
} |
- this.isInitialized = true; |
- this.activeRoute = document.createElement('app-route'); |
+ router.isInitialized = true; |
- // Listen for URL change events. |
- this.stateChangeHandler = this.go.bind(this); |
- window.addEventListener('popstate', this.stateChangeHandler, false); |
- if (isIE) { |
- // IE is truly special! A hashchange is supposed to trigger a popstate, making popstate the only |
- // even you should need to listen to. Not the case in IE! Make another event listener for it! |
- window.addEventListener('hashchange', this.stateChangeHandler, false); |
+ // trailingSlash="strict|ignore" |
+ if (!router.hasAttribute('trailingSlash')) { |
+ router.setAttribute('trailingSlash', 'strict'); |
+ } |
+ |
+ // mode="auto|hash|pushstate" |
+ if (!router.hasAttribute('mode')) { |
+ router.setAttribute('mode', 'auto'); |
} |
- // set up an <active-route> element for the active route's content |
- this.activeRouteContent = document.createElement('active-route'); |
- this.appendChild(this.activeRouteContent); |
- if (this.hasAttribute('shadow')) { |
- this.activeRouteContent = this.activeRouteContent.createShadowRoot(); |
+ // typecast="auto|string" |
+ if (!router.hasAttribute('typecast')) { |
+ router.setAttribute('typecast', 'auto'); |
} |
- // load the web component for the active route |
- this.go(); |
+ // <app-router core-animated-pages transitions="hero-transition cross-fade"> |
+ if (router.hasAttribute('core-animated-pages')) { |
+ // use shadow DOM to wrap the <app-route> elements in a <core-animated-pages> element |
+ // <app-router> |
+ // # shadowRoot |
+ // <core-animated-pages> |
+ // # content in the light DOM |
+ // <app-route element="home-page"> |
+ // <home-page> |
+ // </home-page> |
+ // </app-route> |
+ // </core-animated-pages> |
+ // </app-router> |
+ router.createShadowRoot(); |
+ router.coreAnimatedPages = document.createElement('core-animated-pages'); |
+ router.coreAnimatedPages.appendChild(document.createElement('content')); |
+ |
+ // don't know why it needs to be static, but absolute doesn't display the page |
+ router.coreAnimatedPages.style.position = 'static'; |
+ |
+ // toggle the selected page using selected="path" instead of selected="integer" |
+ router.coreAnimatedPages.setAttribute('valueattr', 'path'); |
+ |
+ // pass the transitions attribute from <app-router core-animated-pages transitions="hero-transition cross-fade"> |
+ // to <core-animated-pages transitions="hero-transition cross-fade"> |
+ router.coreAnimatedPages.setAttribute('transitions', router.getAttribute('transitions')); |
+ |
+ // set the shadow DOM's content |
+ router.shadowRoot.appendChild(router.coreAnimatedPages); |
+ |
+ // when a transition finishes, remove the previous route's content. there is a temporary overlap where both |
+ // the new and old route's content is in the DOM to animate the transition. |
+ router.coreAnimatedPages.addEventListener('core-animated-pages-transition-end', function() { |
+ transitionAnimationEnd(router.previousRoute); |
+ }); |
+ } |
+ |
+ // listen for URL change events |
+ router.stateChangeHandler = stateChange.bind(null, router); |
+ window.addEventListener('popstate', router.stateChangeHandler, false); |
+ if (isIE) { |
+ // IE bug. A hashchange is supposed to trigger a popstate event, making popstate the only event you |
+ // need to listen to. That's not the case in IE so we make another event listener for it. |
+ window.addEventListener('hashchange', router.stateChangeHandler, false); |
+ } |
+ |
+ // load the web component for the current route |
+ stateChange(router); |
}; |
// clean up global event listeners |
- router.detachedCallback = function() { |
+ AppRouter.detachedCallback = function() { |
window.removeEventListener('popstate', this.stateChangeHandler, false); |
if (isIE) { |
window.removeEventListener('hashchange', this.stateChangeHandler, false); |
} |
}; |
- // go() - Find the first <app-route> that matches the current URL and change the active route |
- router.go = function() { |
- var urlPath = this.parseUrlPath(window.location.href); |
+ // go(path, options) Navigate to the path |
+ // |
+ // options = { |
+ // replace: true |
+ // } |
+ AppRouter.go = function(path, options) { |
+ if (this.getAttribute('mode') !== 'pushstate') { |
+ // mode == auto or hash |
+ path = '#' + path; |
+ } |
+ if (options && options.replace === true) { |
+ window.history.replaceState(null, null, path); |
+ } else { |
+ window.history.pushState(null, null, path); |
+ } |
+ |
+ // dispatch a popstate event |
+ try { |
+ var popstateEvent = new PopStateEvent('popstate', { |
+ bubbles: false, |
+ cancelable: false, |
+ state: {} |
+ }); |
+ |
+ if ('dispatchEvent_' in window) { |
+ // FireFox with polyfill |
+ window.dispatchEvent_(popstateEvent); |
+ } else { |
+ // normal |
+ window.dispatchEvent(popstateEvent); |
+ } |
+ } catch(error) { |
+ // Internet Exploder |
+ var fallbackEvent = document.createEvent('CustomEvent'); |
+ fallbackEvent.initCustomEvent('popstate', false, false, { state: {} }); |
+ window.dispatchEvent(fallbackEvent); |
+ } |
+ }; |
+ |
+ // fire(type, detail, node) - Fire a new CustomEvent(type, detail) on the node |
+ // |
+ // listen with document.querySelector('app-router').addEventListener(type, function(event) { |
+ // event.detail, event.preventDefault() |
+ // }) |
+ function fire(type, detail, node) { |
+ // create a CustomEvent the old way for IE9/10 support |
+ var event = document.createEvent('CustomEvent'); |
+ |
+ // initCustomEvent(type, bubbles, cancelable, detail) |
+ event.initCustomEvent(type, false, true, detail); |
+ |
+ // returns false when event.preventDefault() is called, true otherwise |
+ return node.dispatchEvent(event); |
+ } |
+ |
+ // Find the first <app-route> that matches the current URL and change the active route |
+ function stateChange(router) { |
+ var url = utilities.parseUrl(window.location.href, router.getAttribute('mode')); |
+ |
+ // don't load a new route if only the hash fragment changed |
+ if (url.hash !== previousUrl.hash && url.path === previousUrl.path && url.search === previousUrl.search && url.isHashPath === previousUrl.isHashPath) { |
+ scrollToHash(url.hash); |
+ return; |
+ } |
+ previousUrl = url; |
+ |
+ // fire a state-change event on the app-router and return early if the user called event.preventDefault() |
var eventDetail = { |
- path: urlPath |
+ path: url.path |
}; |
- if (!fire('state-change', eventDetail, this)) { |
+ if (!fire('state-change', eventDetail, router)) { |
return; |
} |
- var routes = this.querySelectorAll('app-route'); |
- for (var i = 0; i < routes.length; i++) { |
- if (this.testRoute(routes[i].getAttribute('path'), urlPath, this.getAttribute('trailingSlash'), routes[i].hasAttribute('regex'))) { |
- this.activateRoute(routes[i], urlPath); |
+ |
+ // find the first matching route |
+ var route = router.firstElementChild; |
+ while (route) { |
+ if (route.tagName === 'APP-ROUTE' && utilities.testRoute(route.getAttribute('path'), url.path, router.getAttribute('trailingSlash'), route.hasAttribute('regex'))) { |
+ activateRoute(router, route, url); |
return; |
} |
+ route = route.nextSibling; |
+ } |
+ |
+ fire('not-found', eventDetail, router); |
+ } |
+ |
+ // Activate the route |
+ function activateRoute(router, route, url) { |
+ if (route.hasAttribute('redirect')) { |
+ router.go(route.getAttribute('redirect'), {replace: true}); |
+ return; |
} |
- fire('not-found', eventDetail, this); |
- }; |
- // activateRoute(route, urlPath) - Activate the route |
- router.activateRoute = function(route, urlPath) { |
var eventDetail = { |
- path: urlPath, |
+ path: url.path, |
route: route, |
- oldRoute: this.activeRoute |
+ oldRoute: router.activeRoute |
}; |
- if (!fire('activate-route-start', eventDetail, this)) { |
+ if (!fire('activate-route-start', eventDetail, router)) { |
return; |
} |
if (!fire('activate-route-start', eventDetail, route)) { |
return; |
} |
- |
- this.activeRoute.removeAttribute('active'); |
- route.setAttribute('active', 'active'); |
- this.activeRoute = route; |
- var importUri = route.getAttribute('import'); |
- var routePath = route.getAttribute('path'); |
- var isRegExp = route.hasAttribute('regex'); |
- var elementName = route.getAttribute('element'); |
- var isTemplate = route.hasAttribute('template'); |
- var isElement = !isTemplate; |
+ // update the references to the activeRoute and previousRoute. if you switch between routes quickly you may go to a |
+ // new route before the previous route's transition animation has completed. if that's the case we need to remove |
+ // the previous route's content before we replace the reference to the previous route. |
+ if (router.previousRoute && router.previousRoute.transitionAnimationInProgress) { |
+ transitionAnimationEnd(router.previousRoute); |
+ } |
+ if (router.activeRoute) { |
+ router.activeRoute.removeAttribute('active'); |
+ } |
+ router.previousRoute = router.activeRoute; |
+ router.activeRoute = route; |
+ router.activeRoute.setAttribute('active', 'active'); |
- // import custom element |
- if (isElement && importUri) { |
- this.importAndActivateCustomElement(importUri, elementName, routePath, urlPath, isRegExp, eventDetail); |
+ // import custom element or template |
+ if (route.hasAttribute('import')) { |
+ importAndActivate(router, route.getAttribute('import'), route, url, eventDetail); |
} |
// pre-loaded custom element |
- else if (isElement && !importUri && elementName) { |
- this.activateCustomElement(elementName, routePath, urlPath, isRegExp, eventDetail); |
+ else if (route.hasAttribute('element')) { |
+ activateCustomElement(router, route.getAttribute('element'), route, url, eventDetail); |
} |
- // import template |
- else if (isTemplate && importUri) { |
- this.importAndActivateTemplate(importUri, route, eventDetail); |
+ // inline template |
+ else if (route.firstElementChild && route.firstElementChild.tagName === 'TEMPLATE') { |
+ activateTemplate(router, route.firstElementChild, route, url, eventDetail); |
} |
- // pre-loaded template |
- else if (isTemplate && !importUri) { |
- this.activateTemplate(route, eventDetail); |
+ } |
+ |
+ // Import and activate a custom element or template |
+ function importAndActivate(router, importUri, route, url, eventDetail) { |
+ var importLink; |
+ function importLoadedCallback() { |
+ activateImport(router, importLink, importUri, route, url, eventDetail); |
} |
- }; |
- // importAndActivateCustomElement(importUri, elementName, routePath, urlPath, isRegExp, eventDetail) - Import the custom element then replace the active route |
- // with a new instance of the custom element |
- router.importAndActivateCustomElement = function(importUri, elementName, routePath, urlPath, isRegExp, eventDetail) { |
if (!importedURIs.hasOwnProperty(importUri)) { |
+ // hasn't been imported yet |
importedURIs[importUri] = true; |
- var elementLink = document.createElement('link'); |
- elementLink.setAttribute('rel', 'import'); |
- elementLink.setAttribute('href', importUri); |
- document.head.appendChild(elementLink); |
+ importLink = document.createElement('link'); |
+ importLink.setAttribute('rel', 'import'); |
+ importLink.setAttribute('href', importUri); |
+ importLink.addEventListener('load', importLoadedCallback); |
+ document.head.appendChild(importLink); |
+ } else { |
+ // previously imported. this is an async operation and may not be complete yet. |
+ importLink = document.querySelector('link[href="' + importUri + '"]'); |
+ if (importLink.import) { |
+ // import complete |
+ importLoadedCallback(); |
+ } else { |
+ // wait for `onload` |
+ importLink.addEventListener('load', importLoadedCallback); |
+ } |
} |
- this.activateCustomElement(elementName || importUri.split('/').slice(-1)[0].replace('.html', ''), routePath, urlPath, isRegExp, eventDetail); |
- }; |
+ } |
- // activateCustomElement(elementName, routePath, urlPath, isRegExp, eventDetail) - Replace the active route with a new instance of the custom element |
- router.activateCustomElement = function(elementName, routePath, urlPath, isRegExp, eventDetail) { |
- var resourceEl = document.createElement(elementName); |
- var routeArgs = this.routeArguments(routePath, urlPath, window.location.href, isRegExp); |
- for (var arg in routeArgs) { |
- if (routeArgs.hasOwnProperty(arg)) { |
- resourceEl[arg] = routeArgs[arg]; |
+ // Activate the imported custom element or template |
+ function activateImport(router, importLink, importUri, route, url, eventDetail) { |
+ // make sure the user didn't navigate to a different route while it loaded |
+ if (route.hasAttribute('active')) { |
+ if (route.hasAttribute('template')) { |
+ // template |
+ activateTemplate(router, importLink.import.querySelector('template'), route, url, eventDetail); |
+ } else { |
+ // custom element |
+ activateCustomElement(router, route.getAttribute('element') || importUri.split('/').slice(-1)[0].replace('.html', ''), route, url, eventDetail); |
} |
} |
- this.activeElement(resourceEl, eventDetail); |
- }; |
+ } |
- // importAndActivateTemplate(importUri, route, eventDetail) - Import the template then replace the active route with a clone of the template's content |
- router.importAndActivateTemplate = function(importUri, route, eventDetail) { |
- if (importedURIs.hasOwnProperty(importUri)) { |
- // previously imported. this is an async operation and may not be complete yet. |
- var previousLink = document.querySelector('link[href="' + importUri + '"]'); |
- if (previousLink.import) { |
- // the import is complete |
- this.activeElement(document.importNode(previousLink.import.querySelector('template').content, true), eventDetail); |
- } else { |
- // wait for `onload` |
- previousLink.onload = function() { |
- if (route.hasAttribute('active')) { |
- this.activeElement(document.importNode(previousLink.import.querySelector('template').content, true), eventDetail); |
- } |
- }.bind(this); |
+ // Data bind the custom element then activate it |
+ function activateCustomElement(router, elementName, route, url, eventDetail) { |
+ var customElement = document.createElement(elementName); |
+ var model = createModel(router, route, url, eventDetail); |
+ for (var property in model) { |
+ if (model.hasOwnProperty(property)) { |
+ customElement[property] = model[property]; |
} |
+ } |
+ activateElement(router, customElement, url, eventDetail); |
+ } |
+ |
+ // Create an instance of the template |
+ function activateTemplate(router, template, route, url, eventDetail) { |
+ var templateInstance; |
+ if ('createInstance' in template) { |
+ // template.createInstance(model) is a Polymer method that binds a model to a template and also fixes |
+ // https://github.com/erikringsmuth/app-router/issues/19 |
+ var model = createModel(router, route, url, eventDetail); |
+ templateInstance = template.createInstance(model); |
} else { |
- // template hasn't been loaded yet |
- importedURIs[importUri] = true; |
- var templateLink = document.createElement('link'); |
- templateLink.setAttribute('rel', 'import'); |
- templateLink.setAttribute('href', importUri); |
- templateLink.onload = function() { |
- if (route.hasAttribute('active')) { |
- this.activeElement(document.importNode(templateLink.import.querySelector('template').content, true), eventDetail); |
- } |
- }.bind(this); |
- document.head.appendChild(templateLink); |
+ templateInstance = document.importNode(template.content, true); |
} |
- }; |
+ activateElement(router, templateInstance, url, eventDetail); |
+ } |
- // activateTemplate(route, eventDetail) - Replace the active route with a clone of the template's content |
- router.activateTemplate = function(route, eventDetail) { |
- var clone = document.importNode(route.querySelector('template').content, true); |
- this.activeElement(clone, eventDetail); |
- }; |
+ // Create the route's model |
+ function createModel(router, route, url, eventDetail) { |
+ var model = utilities.routeArguments(route.getAttribute('path'), url.path, url.search, route.hasAttribute('regex'), router.getAttribute('typecast') === 'auto'); |
+ if (route.hasAttribute('bindRouter') || router.hasAttribute('bindRouter')) { |
+ model.router = router; |
+ } |
+ eventDetail.model = model; |
+ fire('before-data-binding', eventDetail, router); |
+ fire('before-data-binding', eventDetail, eventDetail.route); |
+ return eventDetail.model; |
+ } |
+ |
+ // Replace the active route's content with the new element |
+ function activateElement(router, element, url, eventDetail) { |
+ // core-animated-pages temporarily needs the old and new route in the DOM at the same time to animate the transition, |
+ // otherwise we can remove the old route's content right away. |
+ // UNLESS |
+ // if the route we're navigating to matches the same app-route (ex: path="/article/:id" navigating from /article/0 to |
+ // /article/1), then we have to simply replace the route's content instead of animating a transition. |
+ if (!router.hasAttribute('core-animated-pages') || eventDetail.route === eventDetail.oldRoute) { |
+ removeRouteContent(router.previousRoute); |
+ } |
+ |
+ // add the new content |
+ router.activeRoute.appendChild(element); |
+ |
+ // animate the transition if core-animated-pages are being used |
+ if (router.hasAttribute('core-animated-pages')) { |
+ router.coreAnimatedPages.selected = router.activeRoute.getAttribute('path'); |
+ |
+ // we already wired up transitionAnimationEnd() in init() |
- // activeElement(element, eventDetail) - Replace the active route's content with the new element |
- router.activeElement = function(element, eventDetail) { |
- while (this.activeRouteContent.firstChild) { |
- this.activeRouteContent.removeChild(this.activeRouteContent.firstChild); |
+ // use to check if the previous route has finished animating before being removed |
+ if (router.previousRoute) { |
+ router.previousRoute.transitionAnimationInProgress = true; |
+ } |
+ } |
+ |
+ // scroll to the URL hash if it's present |
+ if (url.hash && !router.hasAttribute('core-animated-pages')) { |
+ scrollToHash(url.hash); |
} |
- this.activeRouteContent.appendChild(element); |
- fire('activate-route-end', eventDetail, this); |
+ |
+ fire('activate-route-end', eventDetail, router); |
fire('activate-route-end', eventDetail, eventDetail.route); |
- }; |
+ } |
- // urlPath(url) - Parses the url to get the path |
+ // Call when the previousRoute has finished the transition animation out |
+ function transitionAnimationEnd(previousRoute) { |
+ if (previousRoute) { |
+ previousRoute.transitionAnimationInProgress = false; |
+ removeRouteContent(previousRoute); |
+ } |
+ } |
+ |
+ // Remove the route's content (but not the <template> if it exists) |
+ function removeRouteContent(route) { |
+ if (route) { |
+ var node = route.firstChild; |
+ while (node) { |
+ var nodeToRemove = node; |
+ node = node.nextSibling; |
+ if (nodeToRemove.tagName !== 'TEMPLATE') { |
+ route.removeChild(nodeToRemove); |
+ } |
+ } |
+ } |
+ } |
+ |
+ // scroll to the element with id="hash" or name="hash" |
+ function scrollToHash(hash) { |
+ if (!hash) return; |
+ |
+ // wait for the browser's scrolling to finish before we scroll to the hash |
+ // ex: http://example.com/#/page1#middle |
+ // the browser will scroll to an element with id or name `/page1#middle` when the page finishes loading. if it doesn't exist |
+ // 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 |
+ // before we scroll to the element with id or name `middle`. |
+ setTimeout(function() { |
+ var hashElement = document.querySelector('html /deep/ ' + hash) || document.querySelector('html /deep/ [name="' + hash.substring(1) + '"]'); |
+ if (hashElement && hashElement.scrollIntoView) { |
+ hashElement.scrollIntoView(true); |
+ } |
+ }, 0); |
+ } |
+ |
+ // parseUrl(location, mode) - Augment the native URL() constructor to get info about hash paths |
// |
- // This will return the hash path if it exists or return the real path if no hash path exists. |
+ // Example parseUrl('http://domain.com/other/path?queryParam3=false#/example/path?queryParam1=true&queryParam2=example%20string#middle', 'auto') |
// |
- // Example URL = 'http://domain.com/other/path?queryParam3=false#/example/path?queryParam1=true&queryParam2=example%20string' |
- // path = '/example/path' |
+ // returns { |
+ // path: '/example/path', |
+ // hash: '#middle' |
+ // search: '?queryParam1=true&queryParam2=example%20string', |
+ // isHashPath: true |
+ // } |
// |
- // Note: The URL must contain the protocol like 'http(s)://' |
- router.parseUrlPath = function(url) { |
- // The relative URI is everything after the third slash including the third slash |
- // Example relativeUri = '/other/path?queryParam3=false#/example/path?queryParam1=true&queryParam2=example%20string' |
- var splitUrl = url.split('/'); |
- var relativeUri = '/' + splitUrl.splice(3, splitUrl.length - 3).join('/'); |
- |
- // The path is everything in the relative URI up to the first ? or # |
- // Example path = '/other/path' |
- var path = relativeUri.split(/[\?#]/)[0]; |
- |
- // The hash is everything from the first # up to the the search starting with ? if it exists |
- // Example hash = '#/example/path' |
- var hashIndex = relativeUri.indexOf('#'); |
- if (hashIndex !== -1) { |
- var hash = relativeUri.substring(hashIndex).split('?')[0]; |
- if (hash.substring(0, 2) === '#/') { |
- // Hash path |
- path = hash.substring(1); |
- } else if (hash.substring(0, 3) === '#!/') { |
- // Hashbang path |
- path = hash.substring(2); |
+ // Note: The location must be a fully qualified URL with a protocol like 'http(s)://' |
+ utilities.parseUrl = function(location, mode) { |
+ var url = { |
+ isHashPath: mode === 'hash' |
+ }; |
+ |
+ if (typeof URL === 'function') { |
+ // browsers that support `new URL()` |
+ var nativeUrl = new URL(location); |
+ url.path = nativeUrl.pathname; |
+ url.hash = nativeUrl.hash; |
+ url.search = nativeUrl.search; |
+ } else { |
+ // IE |
+ var anchor = document.createElement('a'); |
+ anchor.href = location; |
+ url.path = anchor.pathname; |
+ if (url.path.charAt(0) !== '/') { |
+ url.path = '/' + url.path; |
} |
+ url.hash = anchor.hash; |
+ url.search = anchor.search; |
} |
- return path; |
+ if (mode !== 'pushstate') { |
+ // auto or hash |
+ |
+ // check for a hash path |
+ if (url.hash.substring(0, 2) === '#/') { |
+ // hash path |
+ url.isHashPath = true; |
+ url.path = url.hash.substring(1); |
+ } else if (url.hash.substring(0, 3) === '#!/') { |
+ // hashbang path |
+ url.isHashPath = true; |
+ url.path = url.hash.substring(2); |
+ } else if (url.isHashPath) { |
+ // still use the hash if mode="hash" |
+ if (url.hash.length === 0) { |
+ url.path = '/'; |
+ } else { |
+ url.path = url.hash.substring(1); |
+ } |
+ } |
+ |
+ if (url.isHashPath) { |
+ url.hash = ''; |
+ |
+ // hash paths might have an additional hash in the hash path for scrolling to a specific part of the page #/hash/path#elementId |
+ var secondHashIndex = url.path.indexOf('#'); |
+ if (secondHashIndex !== -1) { |
+ url.hash = url.path.substring(secondHashIndex); |
+ url.path = url.path.substring(0, secondHashIndex); |
+ } |
+ |
+ // hash paths get the search from the hash if it exists |
+ var searchIndex = url.path.indexOf('?'); |
+ if (searchIndex !== -1) { |
+ url.search = url.path.substring(searchIndex); |
+ url.path = url.path.substring(0, searchIndex); |
+ } |
+ } |
+ } |
+ |
+ return url; |
}; |
- // router.testRoute(routePath, urlPath, trailingSlashOption, isRegExp) - Test if the route's path matches the URL's path |
+ // testRoute(routePath, urlPath, trailingSlashOption, isRegExp) - Test if the route's path matches the URL's path |
// |
- // Example routePath: '/example/*' |
- // Example urlPath = '/example/path' |
- router.testRoute = function(routePath, urlPath, trailingSlashOption, isRegExp) { |
- // This algorithm tries to fail or succeed as quickly as possible for the most common cases. |
+ // Example routePath: '/user/:userId/**' |
+ // Example urlPath = '/user/123/bio' |
+ utilities.testRoute = function(routePath, urlPath, trailingSlashOption, isRegExp) { |
+ // try to fail or succeed as quickly as possible for the most common cases |
// handle trailing slashes (options: strict (default), ignore) |
if (trailingSlashOption === 'ignore') { |
@@ -261,113 +486,85 @@ |
} |
} |
+ // test regular expressions |
if (isRegExp) { |
- // parse HTML attribute path="/^\/\w+\/\d+$/i" to a regular expression `new RegExp('^\/\w+\/\d+$', 'i')` |
- // note that 'i' is the only valid option. global 'g', multiline 'm', and sticky 'y' won't be valid matchers for a path. |
- if (routePath.charAt(0) !== '/') { |
- // must start with a slash |
- return false; |
- } |
- routePath = routePath.slice(1); |
- var options = ''; |
- if (routePath.slice(-1) === '/') { |
- routePath = routePath.slice(0, -1); |
- } |
- else if (routePath.slice(-2) === '/i') { |
- routePath = routePath.slice(0, -2); |
- options = 'i'; |
- } |
- else { |
- // must end with a slash followed by zero or more options |
- return false; |
- } |
- return new RegExp(routePath, options).test(urlPath); |
+ return utilities.testRegExString(routePath, urlPath); |
} |
- // If the urlPath is an exact match or '*' then the route is a match |
+ // if the urlPath is an exact match or '*' then the route is a match |
if (routePath === urlPath || routePath === '*') { |
return true; |
} |
- // Look for wildcards |
- if (routePath.indexOf('*') === -1 && routePath.indexOf(':') === -1) { |
- // No wildcards and we already made sure it wasn't an exact match so the test fails |
- return false; |
+ // relative routes a/b/c are the same as routes that start with a globstar /**/a/b/c |
+ if (routePath.charAt(0) !== '/') { |
+ routePath = '/**/' + routePath; |
} |
- // Example urlPathSegments = ['', example', 'path'] |
- var urlPathSegments = urlPath.split('/'); |
+ // recursively test if the segments match (start at 1 because 0 is always an empty string) |
+ return segmentsMatch(routePath.split('/'), 1, urlPath.split('/'), 1) |
+ }; |
- // Example routePathSegments = ['', 'example', '*'] |
- var routePathSegments = routePath.split('/'); |
+ // segmentsMatch(routeSegments, routeIndex, urlSegments, urlIndex, pathVariables) |
+ // recursively test the route segments against the url segments in place (without creating copies of the arrays |
+ // for each recursive call) |
+ // |
+ // example routeSegments ['', 'user', ':userId', '**'] |
+ // example urlSegments ['', 'user', '123', 'bio'] |
+ function segmentsMatch(routeSegments, routeIndex, urlSegments, urlIndex, pathVariables) { |
+ var routeSegment = routeSegments[routeIndex]; |
+ var urlSegment = urlSegments[urlIndex]; |
+ |
+ // if we're at the last route segment and it is a globstar, it will match the rest of the url |
+ if (routeSegment === '**' && routeIndex === routeSegments.length - 1) { |
+ return true; |
+ } |
- // There must be the same number of path segments or it isn't a match |
- if (urlPathSegments.length !== routePathSegments.length) { |
- return false; |
+ // we hit the end of the route segments or the url segments |
+ if (typeof routeSegment === 'undefined' || typeof urlSegment === 'undefined') { |
+ // return true if we hit the end of both at the same time meaning everything else matched, else return false |
+ return routeSegment === urlSegment; |
} |
- // Check equality of each path segment |
- for (var i = 0; i < routePathSegments.length; i++) { |
- // The path segments must be equal, be a wildcard segment '*', or be a path parameter like ':id' |
- var routeSegment = routePathSegments[i]; |
- if (routeSegment !== urlPathSegments[i] && routeSegment !== '*' && routeSegment.charAt(0) !== ':') { |
- // The path segment wasn't the same string and it wasn't a wildcard or parameter |
- return false; |
+ // if the current segments match, recursively test the remaining segments |
+ if (routeSegment === urlSegment || routeSegment === '*' || routeSegment.charAt(0) === ':') { |
+ // store the path variable if we have a pathVariables object |
+ if (routeSegment.charAt(0) === ':' && typeof pathVariables !== 'undefined') { |
+ pathVariables[routeSegment.substring(1)] = urlSegments[urlIndex]; |
} |
+ return segmentsMatch(routeSegments, routeIndex + 1, urlSegments, urlIndex + 1, pathVariables); |
} |
- // Nothing failed. The route matches the URL. |
- return true; |
- }; |
+ // globstars can match zero to many URL segments |
+ if (routeSegment === '**') { |
+ // test if the remaining route segments match any combination of the remaining url segments |
+ for (var i = urlIndex; i < urlSegments.length; i++) { |
+ if (segmentsMatch(routeSegments, routeIndex + 1, urlSegments, i, pathVariables)) { |
+ return true; |
+ } |
+ } |
+ } |
- // router.routeArguments(routePath, urlPath, url, isRegExp) - Gets the path variables and query parameter values from the URL |
- router.routeArguments = function routeArguments(routePath, urlPath, url, isRegExp) { |
- var args = {}; |
+ // all tests failed, the route segments do not match the url segments |
+ return false; |
+ } |
- // Example urlPathSegments = ['', example', 'path'] |
- var urlPathSegments = urlPath.split('/'); |
+ // routeArguments(routePath, urlPath, search, isRegExp) - Gets the path variables and query parameter values from the URL |
+ utilities.routeArguments = function(routePath, urlPath, search, isRegExp, typecast) { |
+ var args = {}; |
+ // regular expressions can't have path variables |
if (!isRegExp) { |
- // Example routePathSegments = ['', 'example', '*'] |
- var routePathSegments = routePath.split('/'); |
- |
- // Get path variables |
+ // relative routes a/b/c are the same as routes that start with a globstar /**/a/b/c |
+ if (routePath.charAt(0) !== '/') { |
+ routePath = '/**/' + routePath; |
+ } |
+ |
+ // get path variables |
// urlPath '/customer/123' |
// routePath '/customer/:id' |
// parses id = '123' |
- for (var index = 0; index < routePathSegments.length; index++) { |
- var routeSegment = routePathSegments[index]; |
- if (routeSegment.charAt(0) === ':') { |
- args[routeSegment.substring(1)] = urlPathSegments[index]; |
- } |
- } |
- } |
- |
- // Get the query parameter values |
- // The search is the query parameters including the leading '?' |
- var searchIndex = url.indexOf('?'); |
- var search = ''; |
- if (searchIndex !== -1) { |
- search = url.substring(searchIndex); |
- var hashIndex = search.indexOf('#'); |
- if (hashIndex !== -1) { |
- search = search.substring(0, hashIndex); |
- } |
- } |
- // If it's a hash URL we need to get the search from the hash |
- var hashPathIndex = url.indexOf('#/'); |
- var hashBangPathIndex = url.indexOf('#!/'); |
- if (hashPathIndex !== -1 || hashBangPathIndex !== -1) { |
- var hash = ''; |
- if (hashPathIndex !== -1) { |
- hash = url.substring(hashPathIndex); |
- } else { |
- hash = url.substring(hashBangPathIndex); |
- } |
- searchIndex = hash.indexOf('?'); |
- if (searchIndex !== -1) { |
- search = hash.substring(searchIndex); |
- } |
+ segmentsMatch(routePath.split('/'), 1, urlPath.split('/'), 1, args); |
} |
var queryParameters = search.substring(1).split('&'); |
@@ -381,26 +578,62 @@ |
args[queryParameterParts[0]] = queryParameterParts.splice(1, queryParameterParts.length - 1).join('='); |
} |
- // Parse the arguments into unescaped strings, numbers, or booleans |
- for (var arg in args) { |
- var value = args[arg]; |
- if (value === 'true') { |
- args[arg] = true; |
- } else if (value === 'false') { |
- args[arg] = false; |
- } else if (!isNaN(value) && value !== '') { |
- // numeric |
- args[arg] = +value; |
- } else { |
- // string |
- args[arg] = decodeURIComponent(value); |
+ if (typecast) { |
+ // parse the arguments into unescaped strings, numbers, or booleans |
+ for (var arg in args) { |
+ args[arg] = utilities.typecast(args[arg]); |
} |
} |
return args; |
}; |
+ // typecast(value) - Typecast the string value to an unescaped string, number, or boolean |
+ utilities.typecast = function(value) { |
+ // bool |
+ if (value === 'true') { |
+ return true; |
+ } |
+ if (value === 'false') { |
+ return false; |
+ } |
+ |
+ // number |
+ if (!isNaN(value) && value !== '' && value.charAt(0) !== '0') { |
+ return +value; |
+ } |
+ |
+ // string |
+ return decodeURIComponent(value); |
+ }; |
+ |
+ // testRegExString(pattern, value) - Parse HTML attribute path="/^\/\w+\/\d+$/i" to a regular |
+ // expression `new RegExp('^\/\w+\/\d+$', 'i')` and test against it. |
+ // |
+ // note that 'i' is the only valid option. global 'g', multiline 'm', and sticky 'y' won't be valid matchers for a path. |
+ utilities.testRegExString = function(pattern, value) { |
+ if (pattern.charAt(0) !== '/') { |
+ // must start with a slash |
+ return false; |
+ } |
+ pattern = pattern.slice(1); |
+ var options = ''; |
+ if (pattern.slice(-1) === '/') { |
+ pattern = pattern.slice(0, -1); |
+ } |
+ else if (pattern.slice(-2) === '/i') { |
+ pattern = pattern.slice(0, -2); |
+ options = 'i'; |
+ } |
+ else { |
+ // must end with a slash followed by zero or more options |
+ return false; |
+ } |
+ return new RegExp(pattern, options).test(value); |
+ }; |
+ |
document.registerElement('app-router', { |
- prototype: router |
+ prototype: AppRouter |
}); |
+ |
})(window, document); |