2021-03-26 02:58:50 +01:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2021-04-16 12:39:28 +02:00
|
|
|
const stringToHtml = function(string, query) {
|
|
|
|
return (new DOMParser()).parseFromString(string, "text/html").body.querySelector(query);
|
2021-03-26 02:58:50 +01:00
|
|
|
}
|
|
|
|
|
2020-05-03 00:25:55 +02:00
|
|
|
/**
|
|
|
|
* Alias for `document.querySelector`.
|
|
|
|
*
|
|
|
|
* @param q {string} the query string
|
|
|
|
* @returns {HTMLElement} the element identified by the query string
|
|
|
|
*/
|
2021-04-15 23:02:30 +02:00
|
|
|
const $ = q => document.querySelector(q);
|
2020-05-03 00:25:55 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Runs the given function once the page is loaded.
|
|
|
|
*
|
2021-04-13 20:12:52 +02:00
|
|
|
* 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.
|
2020-05-03 00:25:55 +02:00
|
|
|
*
|
|
|
|
* @param fun {function(...*): *} the function to run
|
|
|
|
*/
|
2021-04-16 12:39:28 +02:00
|
|
|
const doAfterLoad = function(fun) {
|
2021-04-13 20:12:52 +02:00
|
|
|
if (document.readyState === "complete") {
|
|
|
|
fun();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-04-15 23:02:30 +02:00
|
|
|
const oldOnLoad = onload || (() => {
|
2020-05-03 00:25:55 +02:00
|
|
|
});
|
|
|
|
|
2021-04-15 23:02:30 +02:00
|
|
|
onload = (() => {
|
2020-05-03 00:25:55 +02:00
|
|
|
oldOnLoad();
|
|
|
|
fun();
|
|
|
|
});
|
|
|
|
};
|
2020-05-02 23:02:42 +02:00
|
|
|
|
|
|
|
|
2020-05-03 18:02:48 +02:00
|
|
|
/**
|
|
|
|
* Creates a navigation element for navigating through the website.
|
|
|
|
*
|
2020-05-05 00:19:06 +02:00
|
|
|
* Fetches entries asynchronously from the website's API.
|
|
|
|
*
|
2020-07-20 12:54:32 +02:00
|
|
|
* @param [highlightPath] {String} the path to highlight together with its parents
|
2021-05-03 19:20:54 +02:00
|
|
|
* @param [cb] {Function} the callback to execute on the fetched entries, to prevent the need to re-fetch elsewhere
|
2020-05-05 00:50:29 +02:00
|
|
|
* @returns {HTMLElement} a base navigation element that will eventually be filled with contents
|
2020-05-03 18:02:48 +02:00
|
|
|
*/
|
2021-05-03 19:20:54 +02:00
|
|
|
const nav = function(highlightPath = "", cb = undefined) {
|
2021-03-27 21:51:01 +01:00
|
|
|
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"
|
|
|
|
);
|
2020-05-05 00:50:29 +02:00
|
|
|
|
|
|
|
fetch("https://fwdekker.com/api/nav/")
|
2020-05-05 00:19:06 +02:00
|
|
|
.then(it => it.json())
|
2021-03-26 02:58:50 +01:00
|
|
|
.then(json => {
|
2021-05-03 19:20:54 +02:00
|
|
|
if (cb !== undefined) cb(json);
|
|
|
|
|
|
|
|
json.entries.forEach(entry => base.appendChild(stringToHtml(unpackEntry(entry, "/", highlightPath), "li")));
|
2021-03-26 02:58:50 +01:00
|
|
|
})
|
2020-05-05 00:19:06 +02:00
|
|
|
.catch(e => {
|
|
|
|
console.error("Failed to fetch navigation elements", e);
|
|
|
|
return [];
|
|
|
|
});
|
|
|
|
|
2021-04-22 17:00:13 +02:00
|
|
|
const nav = stringToHtml(
|
|
|
|
`<nav>` +
|
|
|
|
`<input id="nav-hamburger-checkbox" type="checkbox" hidden />` +
|
|
|
|
`<label id="nav-hamburger-label" for="nav-hamburger-checkbox">☰</label>` +
|
|
|
|
`</nav>`,
|
|
|
|
"nav"
|
|
|
|
);
|
2021-03-26 02:58:50 +01:00
|
|
|
nav.appendChild(base);
|
|
|
|
return nav;
|
2020-05-05 00:19:06 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Unpacks a navigation entry returned from the navigation API into an HTML element.
|
|
|
|
*
|
|
|
|
* @param entry {Object} the entry to unpack
|
2020-07-20 12:54:32 +02:00
|
|
|
* @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
|
2021-03-26 02:58:50 +01:00
|
|
|
* @returns {string} the navigation list entry as HTML, described by its children
|
2020-05-05 00:19:06 +02:00
|
|
|
*/
|
2021-04-16 12:39:28 +02:00
|
|
|
const unpackEntry = function(entry, path = "/", highlightPath = "") {
|
2020-07-20 12:54:32 +02:00
|
|
|
const shouldHighlight = highlightPath.startsWith(`${path + entry.name}/`);
|
2021-04-22 18:59:41 +02:00
|
|
|
const isExternalLink = !(/^https:\/\/.*fwdekker.com/.test(entry.link)) && entry.link !== "#";
|
2021-04-13 20:04:48 +02:00
|
|
|
const formattedName = (isExternalLink ? "⎋ " : "") + entry.name;
|
2020-07-20 12:54:32 +02:00
|
|
|
|
2020-05-05 00:19:06 +02:00
|
|
|
if (entry.entries.length === 0)
|
2021-04-28 12:26:01 +02:00
|
|
|
return "" +
|
|
|
|
`<li ${shouldHighlight ? "class=\"currentPage\"" : ""}><a href="${entry.link}">${formattedName}</a></li>`;
|
2020-05-05 00:19:06 +02:00
|
|
|
|
2021-03-26 02:58:50 +01:00
|
|
|
const depth = path.split("/").length - 2; // -1 because count parts, then another -1 because of leading `/`
|
2020-07-20 12:54:32 +02:00
|
|
|
const arrow = depth === 0 ? "▾" : "▸";
|
2020-05-17 21:06:26 +02:00
|
|
|
|
2021-03-27 21:51:01 +01:00
|
|
|
return "" +
|
|
|
|
`<li class="${shouldHighlight ? "currentPage" : ""}">` +
|
2021-04-13 20:04:48 +02:00
|
|
|
`<a href="${entry.link}">${formattedName} ${arrow}</a>` +
|
2021-03-27 21:51:01 +01:00
|
|
|
`<ul>${entry.entries.map(it => unpackEntry(it, `${path + entry.name}/`, highlightPath)).join("")}</ul>` +
|
|
|
|
`</li>`;
|
2020-05-03 18:02:48 +02:00
|
|
|
};
|
|
|
|
|
2020-05-05 00:19:06 +02:00
|
|
|
|
2020-05-02 23:02:42 +02:00
|
|
|
/**
|
|
|
|
* Creates a header element with the given title and description.
|
|
|
|
*
|
2020-05-16 18:13:19 +02:00
|
|
|
* @param [title] {string} the title to display, possibly including HTML
|
2020-05-03 23:46:18 +02:00
|
|
|
* @param [description] {string} the description to display, possibly including HTML
|
2020-05-02 23:02:42 +02:00
|
|
|
* @returns {HTMLElement} a header element
|
|
|
|
*/
|
2021-04-16 12:39:28 +02:00
|
|
|
const header = function({title, description}) {
|
2020-05-17 16:45:46 +02:00
|
|
|
if (title === undefined && description === undefined)
|
2021-04-16 12:39:28 +02:00
|
|
|
return stringToHtml(`<header></header>`, "header");
|
2021-03-26 02:58:50 +01:00
|
|
|
|
2021-03-27 21:51:01 +01:00
|
|
|
return stringToHtml(
|
2021-04-16 12:39:28 +02:00
|
|
|
`<header><section class="container">` +
|
2021-03-27 21:51:01 +01:00
|
|
|
(title !== undefined ? `<h1>${title}</h1>` : "") +
|
|
|
|
(description !== undefined ? `<p><em>${description}</em></p>` : "") +
|
|
|
|
`</section></header>`,
|
|
|
|
"header"
|
|
|
|
);
|
2020-05-03 00:25:55 +02:00
|
|
|
};
|
2020-05-02 23:02:42 +02:00
|
|
|
|
2020-05-05 00:19:06 +02:00
|
|
|
|
2020-05-02 23:02:42 +02:00
|
|
|
/**
|
|
|
|
* Creates a footer element with the given data.
|
|
|
|
*
|
2021-04-15 23:37:33 +02:00
|
|
|
* 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
|
2020-05-02 23:02:42 +02:00
|
|
|
* @returns {HTMLElement} a footer element
|
|
|
|
*/
|
2021-04-16 12:39:28 +02:00
|
|
|
const footer = function(
|
2020-09-15 15:27:13 +02:00
|
|
|
{
|
2021-04-15 23:37:33 +02:00
|
|
|
author = undefined,
|
|
|
|
authorURL = undefined,
|
|
|
|
license = undefined,
|
|
|
|
licenseURL = undefined,
|
|
|
|
vcs = undefined,
|
|
|
|
vcsURL = undefined,
|
|
|
|
version = undefined,
|
2020-09-15 15:27:13 +02:00
|
|
|
privacyPolicyURL = undefined
|
|
|
|
}) {
|
2021-04-23 14:45:56 +02:00
|
|
|
if (author === undefined) author = "F.W. Dekker";
|
2021-04-15 23:37:33 +02:00
|
|
|
if (authorURL === undefined) authorURL = "https://fwdekker.com/";
|
2021-04-23 14:45:56 +02:00
|
|
|
if (license === undefined) license = "MIT License";
|
2021-04-15 23:37:33 +02:00
|
|
|
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/";
|
|
|
|
|
2021-03-27 21:51:01 +01:00
|
|
|
return stringToHtml(
|
2021-04-16 12:39:28 +02:00
|
|
|
`<footer><section class="container">` +
|
2021-03-27 21:51:01 +01:00
|
|
|
footerLink("Made by ", author, authorURL, ". ") +
|
|
|
|
footerLink("Licensed under the ", license, licenseURL, ". ") +
|
2021-06-18 11:52:24 +02:00
|
|
|
footerLink("Source code and issue tracker on ", vcs, vcsURL, ". ") +
|
2021-04-15 23:37:33 +02:00
|
|
|
footerLink("Consider reading the ", privacyPolicyURL && "privacy policy", privacyPolicyURL, ". ") +
|
2021-04-24 16:22:39 +02:00
|
|
|
`<div id="footerVersion">${version || ""}</div>` +
|
2021-03-27 21:51:01 +01:00
|
|
|
`</section></footer>`,
|
|
|
|
"footer");
|
2020-05-03 00:25:55 +02:00
|
|
|
};
|
2020-05-02 23:02:42 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructs a link that is used in footers.
|
|
|
|
*
|
|
|
|
* @param prefix {string} the text to display before the text if the text is not undefined
|
2021-04-15 23:37:33 +02:00
|
|
|
* @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
|
2020-05-02 23:02:42 +02:00
|
|
|
* @param suffix {string} the text to display after the text if the text is not undefined
|
2021-03-26 02:58:50 +01:00
|
|
|
* @returns {string} a footer link element
|
2020-05-02 23:02:42 +02:00
|
|
|
*/
|
2021-04-16 12:39:28 +02:00
|
|
|
const footerLink = function(prefix, text, url, suffix) {
|
2021-04-15 23:37:33 +02:00
|
|
|
if (text === null) return "";
|
2021-03-26 02:58:50 +01:00
|
|
|
|
2021-04-15 23:37:33 +02:00
|
|
|
return `${prefix}${url !== null ? `<a href="${url}">${text}</a>` : text}${suffix}`;
|
2020-05-03 00:25:55 +02:00
|
|
|
};
|
2021-03-22 14:18:05 +01:00
|
|
|
|
|
|
|
|
2021-06-08 22:56:51 +02:00
|
|
|
/**
|
|
|
|
* Runs the functions `nav`, `header`, and `footer` after the page has loaded using properties defined in the meta tags.
|
|
|
|
*
|
|
|
|
* The remaining functions are invoked only if the corresponding `target` meta property is set to select an existing
|
|
|
|
* element. The HTML element returned by the function is then added as a child to the element specified by the query
|
|
|
|
* selector in the `target` property. The parameters to the function invocation can be set using other meta properties.
|
|
|
|
* The format of meta properties is `fwd:<function>:<property>`, where `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`.
|
|
|
|
*
|
|
|
|
* Meta properties can be set by including `<meta name="fwd:<function>:<property>" content="<value>" />` in the HTML
|
|
|
|
* page on which this module is included. The `<value>` is then passed without modification as a parameter to the
|
|
|
|
* function. Leaving out the `<value>` by writing `<meta name="fwd:<function>:property" />` instead results in passing
|
|
|
|
* `null` as the value. Not including the meta tag at all corresponds to passing `undefined` to the function. See the
|
|
|
|
* documentation of the respective functions for more details on the parameters that they accept.
|
|
|
|
*
|
|
|
|
* Note that the function is invoked only if `fwd:<function>:target` is a query selector for an existing element. This
|
|
|
|
* means that it is possible to mix how the functions are invoked; for example, one can use meta properties to pass
|
|
|
|
* parameters to `nav`, but also invoke `header` in a separate function manually.
|
|
|
|
*/
|
|
|
|
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.appendChild(nav(getMetaProperty("fwd:nav:highlight-path")));
|
|
|
|
}
|
|
|
|
|
|
|
|
const headerTarget = $(getMetaProperty("fwd:header:target"));
|
|
|
|
if (headerTarget !== null) {
|
|
|
|
headerTarget.appendChild(header({
|
|
|
|
title: getMetaProperty("fwd:header:title"),
|
|
|
|
description: getMetaProperty("fwd:header:description"),
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
const footerTarget = $(getMetaProperty("fwd:footer:target"));
|
|
|
|
if (footerTarget !== null) {
|
|
|
|
footerTarget.appendChild(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"),
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2021-04-15 23:02:30 +02:00
|
|
|
// Export to namespace
|
2021-04-28 13:55:06 +02:00
|
|
|
fwdekker = {stringToHtml, $, doAfterLoad, nav, header, footer};
|