| 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);
|
|
|