318 lines
10 KiB
JavaScript
Raw Normal View History

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