import { IgniteElement } from "../ignite-html/ignite-element.js"; import { IgniteTemplate, slot, div, html } from "../ignite-html/ignite-template.js"; class RouterLink extends IgniteElement { constructor() { super(); this.pushStateListener = () => this.update(); this.popStateListener = () => this.update(); window.addEventListener("popstate", this.popStateListener); window.addEventListener("pushstate", this.pushStateListener); } get properties() { return { active: false, routes: [], target: null }; } get styles() { return /*css*/` router-link { display: flex; flex-direction: row; align-items: center; justify-content: center; } `; } render() { return this.template .onClick((event) => this.onClick(event)) .class(this.active, value => value ? "active" : null) .child(new slot(this).class(this.active, value => value ? "active" : null)); } ready() { this.update(); } update() { var routeMatches = false; //Check the target first. routeMatches = Router.matches(this.target); //Check optional routes next. for (var i = 0; i < this.routes.length && !routeMatches; i++) { routeMatches = Router.matches(this.routes[i]); } if (routeMatches && !this.active) { this.active = true; } else if (!routeMatches && this.active) { this.active = false; } } onClick(event) { event.preventDefault(); Router.navigate(this.target, false); } cleanup() { window.removeEventListener("popstate", this.popStateListener); window.removeEventListener("pushstate", this.pushStateListener); } } class RouterView extends IgniteElement { constructor() { super(); this.pushStateListener = () => this.update(); this.popStateListener = () => this.update(); window.addEventListener("popstate", this.popStateListener); window.addEventListener("pushstate", this.pushStateListener); } get properties() { return { show: false, routes: [], strict: false }; } render() { return this.template .child(new slot(this)) .style("display", this.show, null, value => value ? null : "none"); } ready() { this.update(); } update() { var routeMatches = false; //Based on whether we are strict matching or not check if we have a match. if (!this.strict) { for (var i = 0; i < this.routes.length && !routeMatches; i++) { routeMatches = Router.matches(this.routes[i]); } } else { routeMatches = true; for (var i = 0; i < this.routes.length && routeMatches; i++) { routeMatches = Router.matches(this.routes[i]); } } //If we found a match show this router view if it's not already visible, otherwise hide it. if (routeMatches && !this.show) { this.show = true; } else if (!routeMatches && this.show) { this.show = false; } } cleanup() { window.removeEventListener("popstate", this.popStateListener); window.removeEventListener("pushstate", this.pushStateListener); } } class RouterLinkTemplate extends IgniteTemplate { /** * Initializes a new router link template. * @param {String} target The target route when the link is clicked. * @param {String|String[]} routes Optional routes that can be used to control the active state of the link. * @param {...any} elements Elements to render within the link. */ constructor(target, routes, ...elements) { super("router-link", elements); if (!routes) { routes = []; } if (!Array.isArray(routes)) { routes = [routes]; } this.property("target", target); this.property("routes", routes); } } class RouterViewTemplate extends IgniteTemplate { /** * Initializes a new router view. * @param {String|String[]} routes Single or multiple routes to trigger this view to render. * @param {any|any[]} elements Elements to render within the view. * @param {Boolean} strict If true all routes must match before this view becomes visible. */ constructor(routes, elements, strict = false) { super("router-view", Array.isArray(elements) ? elements : [elements]); if (!Array.isArray(routes)) { routes = [routes]; } this.property("routes", routes); this.property("strict", strict); } } class Router { /** * Navigates to a given route and refreshes the page if requested. * @param {String} route The route to navigate to. * @param {Boolean} refresh Whether or not to refresh the page, default is false. */ static navigate(route, refresh = false) { if (refresh) { if (Router.hashMode) { window.location.hash = route; window.location.reload(); } else { window.location.href = route; } } else { if (Router.hashMode) { window.location.hash = route; Router.states.push(route); } else { window.history.pushState(route, route, route); Router.states.push(route); } window.dispatchEvent(new Event("pushstate")); } } /** * Navigates back one with the ability to have a fallback route or refresh the page. * @param {String} fallback The fallback route incase there is no history, default is null. * @param {Boolean} refresh Whether or not to refresh the page, default is false. */ static back(fallback = null, refresh = false) { if (Router.states && Router.states.length > 1) { Router.states.pop(); //Pop the current state. Router.navigate(Router.states.pop(), refresh); //Navigate to the previous state. } else { if (fallback) { Router.navigate(fallback, refresh); } else { window.history.back(); } } } static matches(route) { //Get the path parts from the window var pathParts = []; //If hash mode is set and we have a hash location, get it and split it. if (Router.hashMode && window.location.hash && window.location.hash.length > 0) { pathParts = window.location.hash.substr(1).split("/"); if (pathParts.length > 0 && pathParts[0].length == 0) { pathParts.splice(1); } } else if (!Router.hashMode) { pathParts = window.location.pathname.split("/").splice(1); } //Get the route parts var fromRoot = (route.trim().startsWith("/")); var routeParts = (fromRoot ? route.trim().split("/").splice(1) : route.trim().split("/")); //Check to see if we have a trailing route part, if so, remove it. if (pathParts.length > 0 && pathParts[pathParts.length - 1] == "") { pathParts.pop(); } if (routeParts.length > 0 && routeParts[routeParts.length - 1] == "") { routeParts.pop(); } //If path parts is 0 and route parts is 0 we have a match. if (pathParts.length == 0 && routeParts.length == 0) { return true; } //If path parts is 0, and the route part is ** then this is a match. else if (pathParts.length == 0 && routeParts.length == 1 && (routeParts[0] == "**" || routeParts[0] == "*")) { return true; } //If path parts is 0 and the route starts with ! and is a length of 1 then this is a match. else if (pathParts.length == 0 && routeParts.length == 1 && routeParts[0].startsWith("!")) { return true; } //If the path parts is 0 and the route is !*/** then this is a match else if (pathParts.length == 0 && routeParts.length == 2 && routeParts[0].startsWith("!") && routeParts[1] == "**") { return true; } //Check the route parts against the path parts. var max = Math.min(pathParts.length, routeParts.length); if (fromRoot) { for (var i = 0; i < max; i++) { if (routeParts[i].startsWith("!") && pathParts[i] == routeParts[i].substring(1)) { return false; } else if (routeParts[i] == "**") { return true; } else if (routeParts[i] != pathParts[i] && routeParts[i] != "*" && !routeParts[i].startsWith("!")) { return false; } else if (i + 2 == routeParts.length && i + 1 == pathParts.length && routeParts[i + 1] == "**") { return true; } } if (routeParts.length > pathParts.length) { return false; } else if (pathParts.length > routeParts.length && routeParts[routeParts.length - 1] != "**") { return false; } else { return true; } } else { for (var offset = 0; offset < pathParts.length; offset++) { for (var i = 0; i < max; i++) { if (i + offset >= pathParts.length) { return false; } if (routeParts[i].startsWith("!") && pathParts[i + offset] == routeParts[i].substring(1)) { break; } else if (routeParts[i] == "**") { return true; } else if (routeParts[i] != pathParts[i + offset] && routeParts[i] != "*" && !routeParts[i].startsWith("!")) { break; } else if (i + 1 == routeParts.length && offset + routeParts.length == pathParts.length) { return true; } else if (i + 2 == routeParts.length && offset + i + 1 == pathParts.length && routeParts[i + 1] == "**") { return true; } } } return false; } } } Router.states = []; Router.hashMode = false; customElements.define("router-link", RouterLink); customElements.define("router-view", RouterView); window.Router = Router; export { RouterLinkTemplate as RouterLink, RouterViewTemplate as RouterView, Router }