template/src/main/js/template.js

229 lines
9.1 KiB
JavaScript

/**
* Converts the given string to an HTML element.
*
* @param string the string to convert to an HTML element
* @param query the type of element to return
* @returns {HTMLElement} the HTML element described by the given string
*/
const stringToHtml = function(string, query) {
return (new DOMParser()).parseFromString(string, "text/html").body.querySelector(query);
}
/**
* Alias for `root.querySelector(query)`.
*
* @param query {string} the query string
* @param root {HTMLElement} the element to start searching in, or `undefined` if searching should start in `document`
* @returns {HTMLElement} the element identified by `query` in `root`
*/
const $ = (query, root) => root === undefined ? document.querySelector(query) : root.querySelector(query);
/**
* Alias for `root.querySelectorAll(query)`.
*
* @param query {string} the query string
* @param root {HTMLElement} the element to start searching in, or `undefined` if searching should start in `document`
* @returns {NodeListOf<HTMLElement>} the elements identified by `query` in `root`
*/
const $a = (query, root) => root === undefined ? document.querySelectorAll(query) : root.querySelectorAll(query);
/**
* Runs the given function once the page is loaded.
*
* This function can be used multiple times. It does not overwrite existing callbacks for the page load event. If the
* page has already loaded when this function is invoked, `fun` is invoked immediately inside this function.
*
* @param fun {function(...*): *} the function to run
*/
const doAfterLoad = function(fun) {
if (document.readyState === "complete") {
fun();
return;
}
const oldOnLoad = onload || (() => {
});
onload = (() => {
oldOnLoad();
fun();
});
};
/**
* Creates a navigation element for navigating through the website.
*
* Fetches entries asynchronously from the website's API.
*
* @param [highlightPath] {String} the path to highlight together with its parents
* @param [cb] {Function} the callback to execute on the fetched entries, to prevent the need to re-fetch elsewhere
* @returns {HTMLElement} a base navigation element that will eventually be filled with contents
*/
const nav = function(highlightPath = "", cb = undefined) {
const base = stringToHtml(
`<ul><li><a href="https://fwdekker.com/">` +
`<div class="logo"><img class="logo" src="https://fwdekker.com/favicon.png" alt="FWDekker" /></div>` +
`<b>FWDekker</b>` +
`</a></li></ul>`,
"ul"
);
fetch("https://fwdekker.com/api/nav/")
.then(it => it.json())
.then(json => {
if (cb !== undefined) cb(json);
json.entries.forEach(entry => base.appendChild(stringToHtml(unpackEntry(entry, "/", highlightPath), "li")));
})
.catch(e => {
console.error("Failed to fetch navigation elements", e);
return [];
});
const nav = stringToHtml(
`<nav class="fwd-nav">` +
`<input id="nav-hamburger-checkbox" type="checkbox" hidden />` +
`<label id="nav-hamburger-label" for="nav-hamburger-checkbox">&#9776;</label>` +
`</nav>`,
"nav"
);
nav.appendChild(base);
return nav;
};
/**
* Unpacks a navigation entry returned from the navigation API into an HTML element.
*
* @param entry {Object} the entry to unpack
* @param [path] {number} the current path traversed, found by joining the names of the entries with `/`s; always starts
* and ends with a `/`
* @param [highlightPath] {String} the path to highlight together with its parents
* @returns {string} the navigation list entry as HTML, described by its children
*/
const unpackEntry = function(entry, path = "/", highlightPath = "") {
const shouldHighlight = highlightPath.startsWith(`${path + entry.name}/`);
const isExternalLink = !(/^https:\/\/.*fwdekker.com/i.test(entry.link)) && entry.link !== "#";
if (entry.entries.length === 0)
return "" +
`<li ${shouldHighlight ? "class=\"currentPage\"" : ""}>` +
`<a href="${entry.link}" ${isExternalLink ? "target=\"_blank\"" : ""}>${entry.name}</a>` +
`</li>`;
const depth = path.split("/").length - 2; // -1 because count parts, then another -1 because of leading `/`
const arrow = depth === 0 ? "&#9662;" : "&#9656;";
return "" +
`<li class="${shouldHighlight ? "currentPage" : ""}">` +
`<a href="${entry.link}">${entry.name} ${arrow}</a>` +
`<ul>${entry.entries.map(it => unpackEntry(it, `${path + entry.name}/`, highlightPath)).join("")}</ul>` +
`</li>`;
};
/**
* Creates a footer element with the given data.
*
* Setting an argument to `undefined` or not giving that argument will cause the default value to be used. Setting an
* argument to `null` will result in a footer without the corresponding element.
*
* @param [author] {string|null|undefined} the author
* @param [authorURL] {string|null|undefined} the URL to link the author's name to
* @param [license] {string|null|undefined} the type of license
* @param [licenseURL] {string|null|undefined} the URL to the license file
* @param [vcs] {string|null|undefined} the type of version control
* @param [vcsURL] {string|null|undefined} the URL to the repository
* @param [version] {string|null|undefined} the page version
* @param [privacyPolicyURL] {string|null|undefined} the URL to the privacy policy
* @returns {HTMLElement} a footer element
*/
const footer = function(
{
author = undefined,
authorURL = undefined,
license = undefined,
licenseURL = undefined,
vcs = undefined,
vcsURL = undefined,
version = undefined,
privacyPolicyURL = undefined
}) {
if (author === undefined) author = "Florine&nbsp;W.&nbsp;Dekker";
if (authorURL === undefined) authorURL = "https://fwdekker.com/";
if (license === undefined) license = "MIT";
if (licenseURL === undefined && vcsURL !== undefined) licenseURL = `${vcsURL}src/branch/master/LICENSE`;
if (vcs === undefined && vcsURL !== undefined) vcs = "git";
if (privacyPolicyURL === undefined) privacyPolicyURL = "https://fwdekker.com/privacy/";
return stringToHtml(
`<footer class="fwd-footer"><hr /><small>` +
footerLink("Made by ", author, authorURL, ". ") +
footerLink("Licensed ", license, licenseURL, ". ") +
footerLink("Source and support on ", vcs, vcsURL, ". ") +
footerLink("Read the ", privacyPolicyURL && "privacy policy", privacyPolicyURL, ". ") +
`</small><div id="fwd-footer-version"><small>${version || ""}</small></div></footer>`,
"footer");
};
/**
* Constructs a link that is used in footers.
*
* @param prefix {string} the text to display before the text if the text is not undefined
* @param text {string|null} the text to display, or `null` if the returned element should be empty
* @param url {string|null} the URL to link the text to, or `null` if the text should not be a link
* @param suffix {string} the text to display after the text if the text is not undefined
* @returns {string} a footer link element
*/
const footerLink = function(prefix, text, url, suffix) {
if (text === null) return "";
return `${prefix}${url !== null ? `<a href="${url}">${text}</a>` : text}${suffix}`;
};
/**
* Runs the functions `nav` and `footer` after the page has loaded using properties defined in the meta tags.
*
* Meta tags are read as interpreted as `<meta name="fwd:<function>:<property>" content="<value>" />` from the
* document's head. Given a `function` of `nav` or `footer`, if the `value` of `fwd:<function>:target` is the ID of an
* element in the page, that element is replaced by the output of `function`. The `function` is invoked with parameters
* also read from meta elements, where each `property` is the same as the name of the parameter of that function, except
* that instead of camelcase words are separated by dashes. For example, `vcsURL` becomes `vcs-url`.
*/
doAfterLoad(() => {
const getMetaProperty = (name) => {
const element = $(`meta[name="${name}"]`);
return element === null ? undefined : element.getAttribute("content");
};
const navTarget = $(getMetaProperty("fwd:nav:target"));
if (navTarget !== null) {
navTarget.parentElement.replaceChild(
nav(getMetaProperty("fwd:nav:highlight-path")),
navTarget
);
}
const footerTarget = $(getMetaProperty("fwd:footer:target"));
if (footerTarget !== null) {
footerTarget.parentElement.replaceChild(
footer({
author: getMetaProperty("fwd:footer:author"),
authorURL: getMetaProperty("fwd:footer:author-url"),
license: getMetaProperty("fwd:footer:license"),
licenseURL: getMetaProperty("fwd:footer:license-url"),
vcs: getMetaProperty("fwd:footer:vcs"),
vcsURL: getMetaProperty("fwd:footer:vcs-url"),
version: getMetaProperty("fwd:footer:version"),
privacyPolicyURL: getMetaProperty("fwd:footer:privacy-policy-url"),
}),
footerTarget
);
}
});
// Export to namespace
window.fwdekker = {stringToHtml, $, $a, doAfterLoad};