/** * TODO: override the marker.js renderer for urls * to enable proper anchor support. * * Probably just need to check if the text starts * with a # or not to cover most cases. */ class Docs { /** * @param {string} defaultView * @param {string} routesPath */ constructor(defaultViewPath, fetchPath = 'content', routesPath = './assets/data/routes.json') { /** @type {string} */ this.defaultViewPath = defaultViewPath; /** @type {string} */ this.fetchPath = fetchPath; /** @type {string} */ this.routesPath = routesPath; /** @type {Array.} */ this.routesData = []; /** @type {Array.<(Route, RoutingNode)} */ this.routes = []; /** @type {Route} */ this.currentRoute = undefined; this.onExec(); } async onExec() { // Get routes. this.routesData = await this.fetchRoutes(); // Generate nav based on routes. this.generateNav(this.routesData, document.querySelector('aside')); // Add listener for hash change. window.addEventListener('hashchange', () => this.handleLocation(window.location)); if (window.location.hash) { this.handleLocation(window.location); } else if (typeof this.defaultViewPath !== 'undefined') { // TODO: Do it. console.log("Missing Feature !\nShould load the default view"); } } /** * @returns {Array.} The array containing the routes. */ async fetchRoutes() { const response = await fetch(this.routesPath); const json = await response.json(); return json["routes"]; } /** * @param {Array.} routes * @param {HTMLElement} target */ generateNav(routesData, target) { /** @type {Array.} */ const routes = []; const nav = document.createElement("nav"); routesData.forEach(routeData => { if ("routes" in routeData) { routes.push(new RoutingNode(routeData["name"], routeData["hidden"], routeData["routes"])); } else { routes.push(new Route(routeData["name"], routeData["hidden"], routeData["filepath"])); } }); routes.forEach(routingItem => { if (!routingItem.hasOwnProperty("hidden") || !routingItem["hidden"]) { nav.appendChild(routingItem.buildSelf()); } }); target.appendChild(nav); this.routes = routes; } /** * @param {Location} location */ handleLocation(location) { let hashes = location.hash.split('#'); hashes.splice(0, 1); console.log(hashes); if (typeof this.currentRoute === "undefined" || this.currentRoute.filepath !== hashes[0]) { const route = this.findMatchingRoute(hashes[0], this.routes); this.currentRoute = route; this.displayRoute(route); const activeLinks = document.querySelectorAll('a.active'); const newActiveLinks = document.querySelectorAll(`a[href='#${hashes[0]}']`); if (activeLinks.length > 0) { activeLinks.forEach(link => link.classList.remove('active')); } newActiveLinks.forEach(newLink => newLink.classList.add('active')); // TODO: update active route in the page's navbar if (hashes.length > 1) { this.handleLocation(window.location); } } else { const el = document.querySelector(`#${hashes[1]}`); el.scrollIntoView({ behavior: 'smooth', block: 'start' }); } } /** * @param {string} filepath The filepath to search. * @returns {Route} The matching route. */ findMatchingRoute(filepath, routes) { console.log("filepath:", filepath); let match = undefined; for (let routingItem of routes) { // If the routingItem is a route and have what we are looking for. if (routingItem.hasOwnProperty('filepath') && routingItem['filepath'] === filepath) { match = routingItem; break; } // Recursive search to go deeper. for (let v of Object.keys(routingItem)) { if (typeof routingItem[v] === 'object' && v === 'routes') { const o = this.findMatchingRoute(filepath, routingItem[v]); if (o != null) { match = o; break; } } } } return match; } /** * @param {Route} route The route to load. */ async displayRoute(route) { const content = await route.loadSelf(this.fetchPath); const contentOutlet = document.querySelector('#router-outlet'); this.removeChildren(contentOutlet); contentOutlet.innerHTML = marked(content); Prism.highlightAll(); // edited } removeChildren(element) { let count = element.childNodes.length; while (count--) { element.removeChild(element.lastChild); } } } class RoutingItem { /** * @param {string} name * @param {boolean} hidden */ constructor(name, hidden) { this.name = name; this.hidden = hidden; } buildSelf() { const element = document.createElement("div"); element.innerText = this.name; return element; } } class Route extends RoutingItem { /** * @param {string} name * @param {boolean} hidden * @param {string} filepath */ constructor(name, hidden, filepath) { super(name, hidden); /** @type {string} */ this.filepath = filepath; } /** * @returns {HTMLAnchorElement} The Route as an HTML link. */ buildSelf() { const element = document.createElement("a"); element.href = `#${this.filepath}`; element.innerText = this.name; return element; } /** * @returns {string} The document content to load as a string. */ async loadSelf(prefix, suffix) { if (typeof prefix === 'undefined') { prefix = ''; } else { prefix += '/'; } if (typeof suffix === 'undefined') { suffix = ''; } else { suffix += '/'; } const response = await fetch(`${prefix}${this.filepath}${suffix}`); const text = await response.text(); return text; } } class RoutingNode extends RoutingItem { /** * @param {string} name * @param {boolean} hidden * @param {Array.} routesData */ constructor(name, hidden, routesData) { super(name, hidden); /** @type {Array.} */ this.routesData = routesData; /** @type {Array.<(Route,RoutingNode)>} */ this.routes = []; this.onExec(); } onExec() { this.routesData.forEach(routeData => { if ("routes" in routeData) { this.routes.push(new RoutingNode(routeData["name"], routeData["hidden"], routeData["routes"])); } else { this.routes.push(new Route(routeData["name"], routeData["hidden"], routeData["filepath"])); } }); } /** * @returns {HTMLUListElement} The RoutingNode as an HTML unordered list. */ buildSelf(prefix, suffix) { if (typeof prefix === 'undefined') { prefix = ''; } else { prefix += '/'; } if (typeof suffix === 'undefined') { suffix = ''; } else { suffix += '/'; } const element = document.createElement('section'); element.classList.add("node","hide"); const heading = document.createElement('div'); heading.innerText = this.name; heading.addEventListener('click', toggleDisplay); const list = document.createElement('ul'); // list.classList.add("hide"); this.routes.forEach(entry => { const childEl = document.createElement('li'); childEl.appendChild(entry.buildSelf()); list.appendChild(childEl); }); element.appendChild(heading); element.appendChild(list); return element; } } // TODO: find them a new home /** * @param {Event} event */ function toggleDisplay(event) { const dropdown = event.currentTarget.parentNode; // const list = dropdown.querySelector('ul'); toggleClass(dropdown, 'hide'); }; /** * @param {HTMLElement} el * @param {string} className */ function toggleClass(el, className) { if (el.classList.contains(className)) { el.classList.remove(className); } else { el.classList.add(className); } };