404 lines
11 KiB
JavaScript
404 lines
11 KiB
JavaScript
// TODO: Override the marker.js renderer for urls to enable proper anchor support.
|
|
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* Called on instanciation by the constructor.
|
|
*/
|
|
async onExec() {
|
|
this.routesData = await this.fetchRoutes();
|
|
|
|
this.updateRoutes(this.routesData);
|
|
|
|
this.generateNav(this.routes, document.querySelector('aside'));
|
|
|
|
window.addEventListener('hashchange', () => this.handleLocation(window.location));
|
|
|
|
// Check for initial condition.
|
|
if (window.location.hash) {
|
|
// There is a hash in the address bar, load the view.
|
|
this.handleLocation(window.location);
|
|
} else if (typeof this.defaultViewPath !== 'undefined') {
|
|
// TODO: Load a default view.
|
|
console.log('Missing Feature !\nShould load the default view');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch the json file containing the routes.
|
|
* @returns {Array.<JSON>} The array containing the routes.
|
|
*/
|
|
async fetchRoutes() {
|
|
const response = await fetch(this.routesPath);
|
|
const json = await response.json();
|
|
|
|
return json['routes'];
|
|
}
|
|
|
|
/**
|
|
* Update the routes.
|
|
* @param {Array.<JSON>} routesData
|
|
*/
|
|
updateRoutes(routesData) {
|
|
const routes = [];
|
|
|
|
routesData.forEach(routeData => {
|
|
if ('routes' in routeData) {
|
|
const node = new RoutingNode(
|
|
routeData['name'],
|
|
routeData['hidden'],
|
|
routeData['routes']
|
|
);
|
|
|
|
routes.push(node);
|
|
} else {
|
|
const route = new Route(
|
|
routeData['name'],
|
|
routeData['hidden'],
|
|
routeData['filepath']
|
|
);
|
|
|
|
routes.push(route);
|
|
}
|
|
});
|
|
|
|
this.routes = routes;
|
|
}
|
|
|
|
/**
|
|
* Generate the side nav HTML based on routing data.
|
|
* @param {Array.<RoutingItem>} routes
|
|
* @param {HTMLElement} target
|
|
*/
|
|
generateNav(routes, target) {
|
|
const nav = document.createElement('nav');
|
|
|
|
routes.forEach(routingItem => {
|
|
if (routingItem['hidden'] === false || typeof routingItem['hidden'] === 'undefined') {
|
|
const el = routingItem.buildSelf();
|
|
|
|
if (typeof el !== 'undefined') {
|
|
nav.appendChild(el);
|
|
}
|
|
}
|
|
});
|
|
|
|
target.appendChild(nav);
|
|
}
|
|
|
|
/**
|
|
* Handle if the view needs to be changed or to move to in-page anchor
|
|
* base on the location data.
|
|
* @param {Location} location
|
|
*/
|
|
handleLocation(location) {
|
|
const hashes = location.hash.split('#');
|
|
hashes.splice(0, 1);
|
|
|
|
if (typeof this.currentRoute === 'undefined' || this.currentRoute.filepath !== hashes[0]) {
|
|
// TODO: add check in case of no matching routes.
|
|
const route = this.findMatchingRoute(hashes[0], this.routes);
|
|
|
|
this.currentRoute = route;
|
|
|
|
// Scroll back to top of the content outlet.
|
|
document.querySelector('#router-outlet').scrollTop = 0;
|
|
|
|
this.displayRoute(route);
|
|
|
|
// Changing classes based on the new active 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'));
|
|
|
|
if (hashes.length > 1) {
|
|
this.handleLocation(window.location);
|
|
}
|
|
} else {
|
|
// Page don't need to be changed, just move to anchor.
|
|
const el = document.querySelector(`#${hashes[1]}`);
|
|
|
|
el.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'start'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recursively search for a matching route based on a filepath.
|
|
* @param {string} filepath The filepath to search.
|
|
* @returns {Route} The matching route.
|
|
*/
|
|
findMatchingRoute(filepath, routes) {
|
|
// TODO: Break the loops in a more elegant way.
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Load and displays a route's content.
|
|
* @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
|
|
}
|
|
|
|
/**
|
|
* Utilitary function to remove any content inside an HTML element
|
|
* @param {HTMLElement} element
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} force Wether to force generation or not despite the hidden property.
|
|
* @returns {HTMLAnchorElement} The Route as an HTML link.
|
|
*/
|
|
buildSelf(force = false) {
|
|
if (this.hidden === false || typeof this.hidden === 'undefined') {
|
|
force = true
|
|
}
|
|
|
|
if (force) {
|
|
const element = document.createElement('a');
|
|
|
|
element.href = `#${this.filepath}`;
|
|
element.innerText = this.name;
|
|
|
|
return element;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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(force = false) {
|
|
if (this.hidden === false || typeof this.hidden === 'undefined') {
|
|
force = true
|
|
}
|
|
|
|
if (force) {
|
|
const element = document.createElement('section');
|
|
element.classList.add('node', 'hide');
|
|
|
|
const heading = this.buildSelf_Heading(this.name, toggleDisplay);
|
|
const list = this.buildSelf_List(this.routes);
|
|
|
|
element.appendChild(heading);
|
|
element.appendChild(list);
|
|
|
|
return element;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
buildSelf_Heading(text, onClickCallback) {
|
|
const wrapper = document.createElement('div');
|
|
const headingText = document.createElement('span');
|
|
const headingIcon = document.createElement('img');
|
|
|
|
headingText.innerText = text;
|
|
|
|
headingIcon.src = './assets/img/icons/baseline_expand_more_white_24dp.png';
|
|
|
|
wrapper.appendChild(headingText);
|
|
wrapper.appendChild(headingIcon);
|
|
|
|
if (typeof onClickCallback === "function") {
|
|
wrapper.addEventListener('click', onClickCallback);
|
|
}
|
|
|
|
return wrapper;
|
|
}
|
|
|
|
/**
|
|
* @param {Array.<(Route,RoutingNode)>} entries
|
|
*/
|
|
buildSelf_List(entries) {
|
|
const list = document.createElement('ul');
|
|
|
|
entries.forEach(entry => {
|
|
const listItem = document.createElement('li');
|
|
const itemContent = entry.buildSelf();
|
|
|
|
if (typeof itemContent !== 'undefined') {
|
|
listItem.appendChild(itemContent);
|
|
|
|
list.appendChild(listItem);
|
|
}
|
|
});
|
|
|
|
return list;
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}; |