318 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			318 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 
								 | 
							
								(function () {
							 | 
						||
| 
								 | 
							
								    let head_include = document.body == null,
							 | 
						||
| 
								 | 
							
								        style = document.createElement("style");
							 | 
						||
| 
								 | 
							
								    style.textContent = `[data-route]:not(.route-visible){display:none}`;
							 | 
						||
| 
								 | 
							
								    document.head.appendChild(style);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    class EventEmitter {
							 | 
						||
| 
								 | 
							
								        constructor() {
							 | 
						||
| 
								 | 
							
								            this.event_handlers = {};
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        on(event_name, func) {
							 | 
						||
| 
								 | 
							
								            if (typeof func != "function") {
							 | 
						||
| 
								 | 
							
								                throw "Event handler has to be a function.";
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            this.event_handlers[event_name] = func;
							 | 
						||
| 
								 | 
							
								            return this;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        off(event_name) {
							 | 
						||
| 
								 | 
							
								            delete this.event_handlers[event_name];
							 | 
						||
| 
								 | 
							
								            return this;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        fire(event_name, args) {
							 | 
						||
| 
								 | 
							
								            if (event_name in this.event_handlers) {
							 | 
						||
| 
								 | 
							
								                this.event_handlers[event_name].call(this, args);
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            return this;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    class Route extends EventEmitter {
							 | 
						||
| 
								 | 
							
								        constructor(overlay, elm, paths) {
							 | 
						||
| 
								 | 
							
								            super();
							 | 
						||
| 
								 | 
							
								            this.overlay = overlay;
							 | 
						||
| 
								 | 
							
								            this.elm = elm;
							 | 
						||
| 
								 | 
							
								            this.paths = paths;
							 | 
						||
| 
								 | 
							
								            this.title = undefined;
							 | 
						||
| 
								 | 
							
								            if (elm.hasAttribute("data-title")) {
							 | 
						||
| 
								 | 
							
								                this.title = this.elm.getAttribute("data-title");
							 | 
						||
| 
								 | 
							
								                this.elm.removeAttribute("data-title");
							 | 
						||
| 
								 | 
							
								            } else if (document.querySelector("title") != null) {
							 | 
						||
| 
								 | 
							
								                this.title = document.querySelector("title").textContent;
							 | 
						||
| 
								 | 
							
								            } else {
							 | 
						||
| 
								 | 
							
								                this.title = this.paths[0];
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        get element() {
							 | 
						||
| 
								 | 
							
								            return this.elm;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        isCurrent() {
							 | 
						||
| 
								 | 
							
								            return this.elm.classList.contains("route-current");
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        isVisible() {
							 | 
						||
| 
								 | 
							
								            return this.elm.classList.contains("route-visible");
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    class MultiRoute extends Route {
							 | 
						||
| 
								 | 
							
								        constructor(overlay, elm, paths_data) {
							 | 
						||
| 
								 | 
							
								            if (overlay) {
							 | 
						||
| 
								 | 
							
								                paths_data = paths_data.substr(9);
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            let paths = [];
							 | 
						||
| 
								 | 
							
								            paths_data.split(",").forEach(name => {
							 | 
						||
| 
								 | 
							
								                paths.push(name.trim());
							 | 
						||
| 
								 | 
							
								            });
							 | 
						||
| 
								 | 
							
								            super(overlay, elm, paths);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        getCanonicalPath() {
							 | 
						||
| 
								 | 
							
								            if (this.paths[0].substr(0, 1) == "/") {
							 | 
						||
| 
								 | 
							
								                return this.paths[0];
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            if (this.paths.length > 1) {
							 | 
						||
| 
								 | 
							
								                return this.paths[1];
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            return "/" + this.paths[0];
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    class StandardRoute extends MultiRoute {
							 | 
						||
| 
								 | 
							
								        constructor(overlay, elm, paths) {
							 | 
						||
| 
								 | 
							
								            super(overlay, elm, elm.getAttribute("data-route"));
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    class RegexRoute extends Route {
							 | 
						||
| 
								 | 
							
								        constructor(overlay, elm) {
							 | 
						||
| 
								 | 
							
								            let regexp = elm.getAttribute("data-route").substr(2);
							 | 
						||
| 
								 | 
							
								            if (overlay) {
							 | 
						||
| 
								 | 
							
								                regexp = regexp.substr(9);
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            super(overlay, elm, [regexp]);
							 | 
						||
| 
								 | 
							
								            this.regex = new RegExp(regexp);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        getArgs(path) {
							 | 
						||
| 
								 | 
							
								            if (path === undefined) {
							 | 
						||
| 
								 | 
							
								                path = single.getCurrentPath();
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            let res = this.regex.exec(path);
							 | 
						||
| 
								 | 
							
								            if (res && res.length > 0) {
							 | 
						||
| 
								 | 
							
								                return res;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            return false;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    class SingleApp extends EventEmitter {
							 | 
						||
| 
								 | 
							
								        constructor() {
							 | 
						||
| 
								 | 
							
								            super();
							 | 
						||
| 
								 | 
							
								            this.routes = [];
							 | 
						||
| 
								 | 
							
								            this.routes_populated = false;
							 | 
						||
| 
								 | 
							
								            if (!head_include) {
							 | 
						||
| 
								 | 
							
								                this.populateRoutes();
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            window.onpopstate = event => {
							 | 
						||
| 
								 | 
							
								                event.preventDefault();
							 | 
						||
| 
								 | 
							
								                single.loadRoute();
							 | 
						||
| 
								 | 
							
								            };
							 | 
						||
| 
								 | 
							
								            this.timeouts = [];
							 | 
						||
| 
								 | 
							
								            this.intervals = [];
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        populateRoutes() {
							 | 
						||
| 
								 | 
							
								            if (this.routes_populated) {
							 | 
						||
| 
								 | 
							
								                return;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            document.body.querySelectorAll("[data-route]").forEach(elm => {
							 | 
						||
| 
								 | 
							
								                let data = elm.getAttribute("data-route"),
							 | 
						||
| 
								 | 
							
								                    overlay = false;
							 | 
						||
| 
								 | 
							
								                if (data.substr(0, 9) == "overlay: ") {
							 | 
						||
| 
								 | 
							
								                    data = data.substr(9);
							 | 
						||
| 
								 | 
							
								                    overlay = true;
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								                if (data.substr(0, 2) == "~ ") {
							 | 
						||
| 
								 | 
							
								                    this.routes.push(new RegexRoute(overlay, elm));
							 | 
						||
| 
								 | 
							
								                } else {
							 | 
						||
| 
								 | 
							
								                    this.routes.push(new StandardRoute(overlay, elm));
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								            });
							 | 
						||
| 
								 | 
							
								            if (this.routes.length == 0) {
							 | 
						||
| 
								 | 
							
								                console.error("[single.js] You need to define at least one route");
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            this.routes.forEach(route => {
							 | 
						||
| 
								 | 
							
								                route.paths.forEach(path => {
							 | 
						||
| 
								 | 
							
								                    for (let i = 0; i < this.routes; i++) {
							 | 
						||
| 
								 | 
							
								                        if (this.routes[i] !== route && this.routes[i].paths.indexOf(path) > -1) {
							 | 
						||
| 
								 | 
							
								                            console.error("[single.js] Duplicate path: " + path);
							 | 
						||
| 
								 | 
							
								                        }
							 | 
						||
| 
								 | 
							
								                    }
							 | 
						||
| 
								 | 
							
								                });
							 | 
						||
| 
								 | 
							
								            });
							 | 
						||
| 
								 | 
							
								            document.body.addEventListener("click", event => {
							 | 
						||
| 
								 | 
							
								                let elm = event.target;
							 | 
						||
| 
								 | 
							
								                while (elm && !(elm instanceof HTMLAnchorElement)) {
							 | 
						||
| 
								 | 
							
								                    elm = elm.parentNode;
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								                if (
							 | 
						||
| 
								 | 
							
								                    elm instanceof HTMLAnchorElement &&
							 | 
						||
| 
								 | 
							
								                    !elm.hasAttribute("target") &&
							 | 
						||
| 
								 | 
							
								                    elm.hasAttribute("href") &&
							 | 
						||
| 
								 | 
							
								                    elm.getAttribute("href").substr(0, 1) == "/"
							 | 
						||
| 
								 | 
							
								                ) {
							 | 
						||
| 
								 | 
							
								                    event.preventDefault();
							 | 
						||
| 
								 | 
							
								                    single.loadRoute(new URL(elm.href));
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								            });
							 | 
						||
| 
								 | 
							
								            this.routes_populated = true;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        getRoute(route) {
							 | 
						||
| 
								 | 
							
								            this.populateRoutes();
							 | 
						||
| 
								 | 
							
								            let is_elm = route instanceof HTMLElement;
							 | 
						||
| 
								 | 
							
								            if (is_elm) {
							 | 
						||
| 
								 | 
							
								                if (!route.hasAttribute("data-route")) {
							 | 
						||
| 
								 | 
							
								                    throw "Invalid route element: " + route;
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								                route = route.getAttribute("data-route");
							 | 
						||
| 
								 | 
							
								                if (route.substr(0, 9) == "overlay: ") {
							 | 
						||
| 
								 | 
							
								                    route = route.substr(9);
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								                if (route.substr(0, 2) == "~ ") {
							 | 
						||
| 
								 | 
							
								                    route = route.substr(2);
							 | 
						||
| 
								 | 
							
								                } else {
							 | 
						||
| 
								 | 
							
								                    route = route.split(",")[0];
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								            } else {
							 | 
						||
| 
								 | 
							
								                if (route.substr(0, 9) == "overlay: ") {
							 | 
						||
| 
								 | 
							
								                    route = route.substr(9);
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								                if (route.substr(0, 2) == "~ ") {
							 | 
						||
| 
								 | 
							
								                    route = route.substr(2);
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            for (let i = 0; i < this.routes.length; i++) {
							 | 
						||
| 
								 | 
							
								                if (this.routes[i].paths.indexOf(route) > -1) {
							 | 
						||
| 
								 | 
							
								                    return this.routes[i];
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            if (!is_elm) {
							 | 
						||
| 
								 | 
							
								                return this.getRoutes(route)[0];
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        getRoutes(route) {
							 | 
						||
| 
								 | 
							
								            let routes = [];
							 | 
						||
| 
								 | 
							
								            try {
							 | 
						||
| 
								 | 
							
								                document.querySelectorAll(route).forEach(elm => {
							 | 
						||
| 
								 | 
							
								                    try {
							 | 
						||
| 
								 | 
							
								                        let route = this.getRoute(elm);
							 | 
						||
| 
								 | 
							
								                        if (route) {
							 | 
						||
| 
								 | 
							
								                            routes.push(route);
							 | 
						||
| 
								 | 
							
								                        }
							 | 
						||
| 
								 | 
							
								                    } catch (ignored) {}
							 | 
						||
| 
								 | 
							
								                });
							 | 
						||
| 
								 | 
							
								            } catch (ignored) {}
							 | 
						||
| 
								 | 
							
								            return routes;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        loadRoute(path) {
							 | 
						||
| 
								 | 
							
								            this.populateRoutes();
							 | 
						||
| 
								 | 
							
								            this.timeouts.forEach(clearTimeout);
							 | 
						||
| 
								 | 
							
								            this.intervals.forEach(clearInterval);
							 | 
						||
| 
								 | 
							
								            if (path === undefined) {
							 | 
						||
| 
								 | 
							
								                path = new URL(location.href);
							 | 
						||
| 
								 | 
							
								            } else if (typeof path == "string" && path.substr(0, 1) == "/") {
							 | 
						||
| 
								 | 
							
								                path = new URL(location.protocol + location.hostname + path);
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            let route,
							 | 
						||
| 
								 | 
							
								                args = false,
							 | 
						||
| 
								 | 
							
								                urlextra = "";
							 | 
						||
| 
								 | 
							
								            if (path instanceof URL) {
							 | 
						||
| 
								 | 
							
								                urlextra = path.search + path.hash;
							 | 
						||
| 
								 | 
							
								                path = decodeURIComponent(path.pathname);
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            for (let i = 0; i < this.routes.length; i++) {
							 | 
						||
| 
								 | 
							
								                if (this.routes[i] instanceof RegexRoute) {
							 | 
						||
| 
								 | 
							
								                    args = this.routes[i].getArgs(path);
							 | 
						||
| 
								 | 
							
								                    if (args !== false) {
							 | 
						||
| 
								 | 
							
								                        route = this.routes[i];
							 | 
						||
| 
								 | 
							
								                        break;
							 | 
						||
| 
								 | 
							
								                    }
							 | 
						||
| 
								 | 
							
								                } else if (this.routes[i].paths.indexOf(path) > -1) {
							 | 
						||
| 
								 | 
							
								                    route = this.routes[i];
							 | 
						||
| 
								 | 
							
								                    break;
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            if (route === undefined) {
							 | 
						||
| 
								 | 
							
								                route = this.getRoute("404");
							 | 
						||
| 
								 | 
							
								                if (route === null) {
							 | 
						||
| 
								 | 
							
								                    route = this.routes[0];
							 | 
						||
| 
								 | 
							
								                    path = route.getCanonicalPath();
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            if (path.substr(0, 1) != "/") {
							 | 
						||
| 
								 | 
							
								                path = route.getCanonicalPath();
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            if (args === false) {
							 | 
						||
| 
								 | 
							
								                args = [path];
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            route.fire("beforeload", args);
							 | 
						||
| 
								 | 
							
								            this.fire("route_beforeload", {
							 | 
						||
| 
								 | 
							
								                route: route,
							 | 
						||
| 
								 | 
							
								                args: args
							 | 
						||
| 
								 | 
							
								            });
							 | 
						||
| 
								 | 
							
								            this.routes.forEach(r => {
							 | 
						||
| 
								 | 
							
								                if (r !== route) {
							 | 
						||
| 
								 | 
							
								                    r.elm.classList.remove("route-current");
							 | 
						||
| 
								 | 
							
								                    if (!route.overlay || r.overlay) {
							 | 
						||
| 
								 | 
							
								                        r.elm.classList.remove("route-visible");
							 | 
						||
| 
								 | 
							
								                    }
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								            });
							 | 
						||
| 
								 | 
							
								            route.elm.classList.add("route-current", "route-visible");
							 | 
						||
| 
								 | 
							
								            path += urlextra;
							 | 
						||
| 
								 | 
							
								            if (this.getCurrentPath() != path) {
							 | 
						||
| 
								 | 
							
								                history.pushState({}, route.title, path);
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            document.querySelector("title").textContent = route.title;
							 | 
						||
| 
								 | 
							
								            this.fire("route_load", {
							 | 
						||
| 
								 | 
							
								                route: route,
							 | 
						||
| 
								 | 
							
								                args: args
							 | 
						||
| 
								 | 
							
								            });
							 | 
						||
| 
								 | 
							
								            route.fire("load", args);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        getCurrentRoute() {
							 | 
						||
| 
								 | 
							
								            return this.getRoute(".route-current");
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        getCurrentPath() {
							 | 
						||
| 
								 | 
							
								            return location.pathname + location.search + location.hash;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        setTimeout(f, i) {
							 | 
						||
| 
								 | 
							
								            this.timeouts.push(window.setTimeout(f, i));
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        setInterval(f, i) {
							 | 
						||
| 
								 | 
							
								            this.intervals.push(window.setInterval(f, i));
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    console.assert(!("single" in window));
							 | 
						||
| 
								 | 
							
								    window.single = new SingleApp();
							 | 
						||
| 
								 | 
							
								    if (["interactive", "complete"].indexOf(document.readyState) > -1) {
							 | 
						||
| 
								 | 
							
								        window.single.loadRoute();
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								        document.addEventListener("DOMContentLoaded", () => window.single.loadRoute());
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								})();
							 |