Website/docs/assets/js/docs.js
2023-03-02 21:10:18 -05:00

341 lines
8.8 KiB
JavaScript

/**
* 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.<JSON>} */
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.<JSON>} The array containing the routes.
*/
async fetchRoutes() {
const response = await fetch(this.routesPath);
const json = await response.json();
return json["routes"];
}
/**
* @param {Array.<JSON>} routes
* @param {HTMLElement} target
*/
generateNav(routesData, target) {
/** @type {Array.<RoutingItem>} */
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.<JSON>} routesData
*/
constructor(name, hidden, routesData) {
super(name, hidden);
/** @type {Array.<JSON>} */
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);
}
};