template/src/main/js/Template.js

180 lines
6.2 KiB
JavaScript

import h from "hyperscript";
import "normalize.css/normalize.css";
import "milligram/dist/milligram.css";
import "../css/common.css";
import "../css/nav.css";
import "../css/overrides.css";
/**
* Alias for `document.querySelector`.
*
* @param q {string} the query string
* @returns {HTMLElement} the element identified by the query string
*/
export const $ = q => document.querySelector(q);
/**
* 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.
*
* @param fun {function(...*): *} the function to run
*/
export const doAfterLoad = function (fun) {
const oldOnLoad = window.onload || (() => {
});
window.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
* @returns {HTMLElement} a base navigation element that will eventually be filled with contents
*/
export const nav = function (highlightPath = "") {
const base = h("ul",
h("li", h("a", {href: "https://fwdekker.com/"},
h("div.logo", h("img.logo", {src: "https://fwdekker.com/favicon.png"})),
h("b", "FWDekker")
))
);
fetch("https://fwdekker.com/api/nav/")
.then(it => it.json())
.then(json => json.entries.forEach(entry => base.appendChild(unpackEntry(entry, "/", highlightPath))))
.catch(e => {
console.error("Failed to fetch navigation elements", e);
return [];
});
return h("nav.nav", base);
};
/**
* 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 {HTMLElement} the navigation list entry as HTML, described by its children
*/
const unpackEntry = function (entry, path = "/", highlightPath = "") {
const shouldHighlight = highlightPath.startsWith(`${path + entry.name}/`);
if (entry.entries.length === 0)
return h("li",
h("a", {href: entry.link, innerHTML: entry.name}),
{className: shouldHighlight ? "currentPage" : ""}
);
const depth = path.split("/").length - 2; // -1 because count parts, then another -1 because of leading `/`
const arrow = depth === 0 ? "▾" : "▸";
return h("li",
h("a", {href: entry.link, innerHTML: `${entry.name} ${arrow}`}),
h("ul", entry.entries.map(it => unpackEntry(it, `${path + entry.name}/`, highlightPath))),
{className: shouldHighlight ? "currentPage" : ""}
);
};
/**
* Creates a header element with the given title and description.
*
* @param [title] {string} the title to display, possibly including HTML
* @param [description] {string} the description to display, possibly including HTML
* @returns {HTMLElement} a header element
*/
export const header = function ({title, description}) {
if (title === undefined && description === undefined)
return h("header.header");
return h("header.header",
h("section.container",
title !== undefined ? h("h1", {innerHTML: title}) : undefined,
description !== undefined ? h("p", h("em", {innerHTML: description})) : undefined
)
);
};
/**
* Creates a footer element with the given data.
*
* @param [author] {string|undefined} the author
* @param [authorURL] {string|undefined} the URL to link the author's name to
* @param [license] {string|undefined} the type of license
* @param [licenseURL] {string|undefined} the URL to the license file
* @param [vcs] {string|undefined} the type of version control
* @param [vcsURL] {string|undefined} the URL to the repository
* @param [version] {string|undefined} the page version
* @param [privacyPolicyURL] {string|null|undefined} the URL to the privacy policy, or `null` if there should be no
* privacy policy, or `undefined` if the default privacy policy should be used
* @returns {HTMLElement} a footer element
*/
export const footer = function (
{
author, authorURL, license, licenseURL, vcs, vcsURL, version,
privacyPolicyURL = undefined
}) {
return h("footer.footer",
h("section.container",
footerLink("Made by ", author, authorURL, ". "),
footerLink("Licensed under the ", license, licenseURL, ". "),
footerLink("Source code available on ", vcs, vcsURL, ". "),
footerLink(
"Consider reading the ",
privacyPolicyURL === null ? undefined : "privacy policy",
privacyPolicyURL === undefined ? "https://fwdekker.com/privacy/" : privacyPolicyURL,
". "
),
h("div", version || "", {style: {"float": "right"}})
)
);
};
/**
* 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|undefined} the text to display, or `undefined` if the returned element should be empty
* @param url {string|undefined} the URL to link the text to, or `undefined` 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 {HTMLElement} a footer link element
*/
const footerLink = function (prefix, text, url, suffix) {
if (text === undefined) return h("span");
return h("span",
h("span", prefix),
url !== undefined
? h("a", text, {href: url})
: h("span", text),
h("span", suffix)
);
};
/**
* Unhides the main element on the page and applies default display styling.
*/
export const showPage = function () {
// Flex-based footer positioning, taken from https://stackoverflow.com/a/12253099
const main = $("main");
main.style.display = "flex";
main.style.flexDirection = "column";
main.style.minHeight = "100%";
$("#contents").style.flex = "1";
}