ourbigbook_runtime.js
// https://docs.ourbigbook.com#insane-link-parsing-rules
// We got these to work perfectly at one point with webpack style-loader.
// But we just want the separate .css.
//import "./ourbigbook.scss";
//import "katex/dist/katex.min.css";
//import "normalize.css/normalize.css";
// With import gave Uncaught ReferenceError: Tablesort is not defined
// but this worked https://github.com/tristen/tablesort/issues/165
const Tablesort = require('tablesort')
if (typeof window !== 'undefined') {
window.Tablesort = Tablesort
require('tablesort/src/sorts/tablesort.date.js')
require('tablesort/src/sorts/tablesort.dotsep.js')
require('tablesort/src/sorts/tablesort.filesize.js')
require('tablesort/src/sorts/tablesort.monthname.js')
require('tablesort/src/sorts/tablesort.number.js')
}
let myDocument
const SELFLINK_CLASS = 'selflink'
// TODO CSS variable duplication.
// max-mobile-width
const CSS_MAX_MOBILE_WIDTH = 635
// toplevel: if given, is an Element (not document) under which OurBigBook Marktup runtime will take effect.
export function ourbigbook_runtime(toplevel) {
if (toplevel === undefined) {
toplevel = document;
myDocument = document
} else {
myDocument = toplevel.ownerDocument
}
if (
window.ourbigbook_split_headers &&
window.location.hash &&
!window.location.hash.startsWith(':~:text=')
) {
const hash = window.location.hash.substring(1)
if(!myDocument.getElementById(hash)) {
const dest = window.ourbigbook_redirect_prefix + hash + (window.ourbigbook_html_x_extension ? '.html' : '')
window.location.replace(dest)
}
}
// ToC interaction.
const CLOSE_CLASS = 'close';
const TOC_CONTAINER_CLASS = 'toc-container';
const toc_arrows = toplevel.querySelectorAll(`.${TOC_CONTAINER_CLASS} div.arrow`);
for (const toc_arrow of toc_arrows) {
toc_arrow.addEventListener('click', () => {
// https://docs.ourbigbook.com#table-of-contents-javascript-open-close-interaction
const parent_li = toc_arrow.parentElement.parentElement;
let all_children_closed = true;
let all_children_open = true;
let was_open;
if (parent_li.classList.contains(CLOSE_CLASS)) {
was_open = false;
// Open self.
parent_li.classList.remove(CLOSE_CLASS);
} else {
was_open = true;
// Check if all children are closed.
for (const toc_arrow_child of parent_li.childNodes) {
if (toc_arrow_child.tagName === 'UL') {
for (const toc_arrow_child_2 of toc_arrow_child.childNodes) {
if (toc_arrow_child_2.tagName === 'LI') {
if (toc_arrow_child_2.classList.contains(CLOSE_CLASS)) {
all_children_open = false;
} else if (toc_arrow_child_2.classList.contains('has-child')) {
all_children_closed = false;
}
}
}
}
}
}
for (const toc_arrow_child of parent_li.childNodes) {
if (toc_arrow_child.tagName === 'UL') {
for (const toc_arrow_child_2 of toc_arrow_child.childNodes) {
if (toc_arrow_child_2.tagName === 'LI') {
if (!was_open || (was_open && !all_children_closed)) {
toc_arrow_child_2.classList.add(CLOSE_CLASS);
} else {
toc_arrow_child_2.classList.remove(CLOSE_CLASS);
}
}
}
}
}
});
}
// Open ToC when jumping to it from header.
const h_to_tocs = toplevel.getElementsByClassName('ourbigbook-h-to-toc');
for (const h_to_toc of h_to_tocs) {
h_to_toc.addEventListener('click', () => {
let cur_elem = myDocument.getElementById(h_to_toc.getAttribute('href').slice(1)).parentElement;
while (!cur_elem.classList.contains(TOC_CONTAINER_CLASS)) {
cur_elem.classList.remove(CLOSE_CLASS);
cur_elem = cur_elem.parentElement;
}
});
}
// Video click to play.
// https://github.com/ourbigbook/ourbigbook/issues/122
const videos = toplevel.getElementsByTagName('video');
for(const video of videos) {
const parentNode = video.parentNode;
let first = true;
video.addEventListener('click', () => {
if (first) {
video.play();
}
first = false;
});
}
// tablesort
const tables = toplevel.getElementsByTagName('table');
for(const table of tables) {
new Tablesort(table);
}
// Set repetitive titles from Js to save HTML space.
// On hover things make 0 difference to user experience basically, so it is a no brainer,
// the only concern is slowing down Js.
function setTitles(selector, title) {
const elems = toplevel.querySelectorAll(`.${TOC_CONTAINER_CLASS} ${selector}`);
for (const elem of elems) {
elem.title = title
}
}
setTitles('.c', 'link to this ToC entry')
setTitles('.u', 'parent ToC entry')
for (const e of toplevel.querySelectorAll('span.wcntr')) {
e.title = 'word count for this article + all descendants'
}
for (const e of toplevel.querySelectorAll('span.wcnt')) {
e.title = 'word count for this article'
}
for (const e of toplevel.querySelectorAll('span.dcnt')) {
e.title = 'descendant article count'
}
for (const e of toplevel.querySelectorAll('a.split')) {
e.title = 'view one header per page'
}
for (const e of toplevel.querySelectorAll('a.nosplit')) {
e.title = 'view all headers in a single page'
}
for (const e of toplevel.querySelectorAll('.h-nav .toc')) {
e.title = 'table of contents entry for this header'
}
for (const e of toplevel.querySelectorAll('.h-nav .u')) {
// .u for Up
e.title = 'parent header'
}
for (const e of toplevel.querySelectorAll('.h-nav .wiki')) {
e.title = 'Wikipedia article about the same topic as this header'
}
for (const e of toplevel.querySelectorAll('.h-nav .tags')) {
e.title = 'tags this header is tagged with'
}
// On-hover links.
for (const elem of toplevel.querySelectorAll('.ourbigbook > *')) {
if (elem.id) {
elem.addEventListener('mouseenter', (e) => {
if (CSS_MAX_MOBILE_WIDTH < window.innerWidth) {
const a = myDocument.createElement('a')
a.href = `#${e.target.id}`
a.className = SELFLINK_CLASS
e.target.prepend(a)
}
})
elem.addEventListener('mouseleave', (e) => {
const t = e.target
const firstChild = t.children[0]
if (firstChild.className === SELFLINK_CLASS) {
t.removeChild(firstChild)
}
})
}
}
// JsCanvasDemo
const ourbigbook_canvas_demo_elems = toplevel.getElementsByClassName('ourbigbook-js-canvas-demo');
const ourbigbook_canvas_demo_weakmap = new WeakMap();
for (const ourbigbook_canvas_demo_elem of ourbigbook_canvas_demo_elems) {
ourbigbook_canvas_demo_weakmap.set(ourbigbook_canvas_demo_elem, false);
const observer = new IntersectionObserver(
(entries, observer) => {
entries.forEach(entry => {
if (entry.intersectionRatio > 0.0) {
if (!ourbigbook_canvas_demo_weakmap.get(ourbigbook_canvas_demo_elem)) {
ourbigbook_canvas_demo_weakmap.set(ourbigbook_canvas_demo_elem, true);
eval(ourbigbook_canvas_demo_elem.childNodes[0].textContent).do_init(ourbigbook_canvas_demo_elem);
}
}
});
},
{}
)
observer.observe(ourbigbook_canvas_demo_elem);
}
}
// Load required scripts dynamically:
//
// * https://stackoverflow.com/questions/7308908/waiting-for-dynamically-loaded-script/57267538#57267538
// * https://stackoverflow.com/questions/14521108/dynamically-load-js-inside-js/14521482#14521482
// * https://stackoverflow.com/questions/10004112/how-can-i-wait-for-set-of-asynchronous-callback-functions
//
// We use to reduce the initial load time.
//
// Each script is loaded only once after it has finished loading for the first time,
// even if this function is called multiple times.
async function ourbigbook_load_scripts(script_urls) {
function load(script_url) {
return new Promise(function(resolve, reject) {
if (ourbigbook_load_scripts.loaded.has(script_url)) {
resolve();
} else {
var script = myDocument.createElement('script');
script.onload = resolve;
script.src = script_url
myDocument.head.appendChild(script);
}
});
}
var promises = [];
for (const script_url of script_urls) {
promises.push(load(script_url));
}
await Promise.all(promises);
for (const script_url of script_urls) {
ourbigbook_load_scripts.loaded.add(script_url);
}
}
ourbigbook_load_scripts.loaded = new Set();
// Create some nice controls for a canvas demo!
// TODO currently disabled on HTML because it would cause reflows on lower IDs.
// What we should do instead, is to only add the new elements on hover, this
// keeps thing simple, but still works.
class OurbigbookCanvasDemo {
addInputAfterEnable(label, attributes) {
var input = document.createElement('input');
for (var key in attributes) {
input.setAttribute(key, attributes[key]);
}
var div = document.createElement('div');
div.appendChild(document.createTextNode(label + ': '));
div.appendChild(input);
this.enable_div.parentNode.insertBefore(div, this.enable_div.nextSibling);
return input;
}
addInfoSpanAfterEnable(label) {
var span = document.createElement('span');
var div = document.createElement('div');
div.appendChild(document.createTextNode(label + ': '));
div.appendChild(span);
this.enable_div.parentNode.insertBefore(div, this.enable_div.nextSibling);
return span;
}
animate() {
var fps_limit = parseFloat(this.fps_limit_input.value);
if (!isNaN(fps_limit)) {
var fps_limit_time_millis = 1000.0 / fps_limit;
var now = Date.now();
var fps_limit_elapsed = now - this.fps_limit_then;
}
if (isNaN(fps_limit) || (fps_limit_elapsed > fps_limit_time_millis)) {
if (!isNaN(fps_limit)) {
this.fps_limit_then = now - (fps_limit_elapsed % fps_limit_time_millis);
}
this.resizeCanvas();
this.draw();
// Save the images to files.
// https://stackoverflow.com/questions/19235286/convert-html5-canvas-sequence-to-a-video-file/57153718#57153718
if (this.save_images_input.checked) {
this.canvas.toBlob(this.constructor.createBlobFunc(this.demo_id, this.time));
}
this.time++;
this.total_frames_span.innerHTML = this.time.toString();
/* FPS calculation. */
var fps_granule = parseInt(this.fps_granule_input.value);
if (this.time % fps_granule == 0) {
var fps_date = Date.now();
this.fps_span.innerHTML = (1000.0 * fps_granule / (fps_date - this.fps_last_date)).toFixed(2);
this.fps_last_date = fps_date;
}
}
if (this.enable_input.checked) {
window.requestAnimationFrame(this.animate.bind(this));
}
}
// We need this to fix time because toBlob calls are asynchronous.
static createBlobFunc(demo_id, time) {
return (blob) => {
// From FileSaver.js.
saveAs(blob, `canvas-${demo_id}-${time}.png`);
};
}
// Do the actual drawing.
draw() {
throw new Error('unimplemented');
}
// Change the state from the old to the new.
//
// If it was disabled and now became enabled, start the animation.
//
// async because it may do long async steps like loading external libraries
// on the first enable.
async enableStateChange(new_state, old_state) {
if (new_state) {
if (!old_state) {
this.enable_input.checked = true;
if (!this.init_done) {
await ourbigbook_load_scripts(['https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js']);
this.init_done = true;
}
console.log(`${this.myclass} starting: ${this.demo_id}`);
window.requestAnimationFrame(this.animate.bind(this));
}
} else {
if (old_state) {
console.log(`${this.myclass} stopping: ${this.demo_id}`);
this.enable_input.checked = false;
}
}
}
do_init(demo_element) {
this.demo_element = demo_element;
this.init();
}
init(demo_id, options) {
this.demo_id = demo_id;
this.options = Object.assign({}, options);
if (!('enabled' in this.options)) {
this.options.enabled = false;
}
if (!('context_type' in this.options)) {
this.options.context_type = '2d';
}
// Members.
this.time = 0;
// Random variables.
// https://stackoverflow.com/questions/19764018/controlling-fps-with-requestanimationframe
this.fps_limit_then = Date.now();
this.fps_last_date = new Date();
// HTML.
const canvas_wrapper = document.createElement('div');
this.myclass = 'canvas-demo'
canvas_wrapper.setAttribute('class', this.myclass);
// Enable disable.
this.enable_input = document.createElement('input');
this.enable_input.setAttribute('type', 'checkbox');
this.enable_input.setAttribute('value', '5');
this.enable_input.setAttribute('min', '1');
this.enable_input.addEventListener('change', async () => {
this.enableStateChange(this.enable_input.checked, !this.enable_input.checked);
});
const enable_label = document.createElement('label');
enable_label.appendChild(document.createTextNode('Enable: '));
enable_label.appendChild(this.enable_input);
enable_label.appendChild(document.createTextNode('<-- (click this to run!!!)'));
this.enable_div = document.createElement('div');
this.enable_div.appendChild(enable_label);
canvas_wrapper.appendChild(this.enable_div);
// All inputs and info entries.
this.fps_span = this.addInfoSpanAfterEnable('FPS');
this.total_frames_span = this.addInfoSpanAfterEnable('Total frames');
this.fps_granule_input = this.addInputAfterEnable(
'FPS granule',
{
'min': '1',
'type': 'number',
'value': '5'
}
);
this.fps_limit_input = this.addInputAfterEnable(
'FPS limit',
{
'min': '1',
'type': 'number'
}
);
this.save_images_input = this.addInputAfterEnable(
'Save images',
{
'type': 'checkbox',
}
);
this.canvas_width_input = this.addInputAfterEnable(
'canvas width',
{
'min': '1',
'type': 'number',
'value': '128'
}
);
// Canvas.
this.canvas = document.createElement('canvas');
this.canvas.setAttribute('style', 'border:1px solid black;');
canvas_wrapper.appendChild(this.canvas);
this.ctx = this.canvas.getContext(this.options.context_type);
this.resizeCanvas();
// Add the canvas_wrapper.
this.demo_element.parentNode.insertBefore(
canvas_wrapper, this.demo_element
);
// Auto enable animations when they come into the viewport,
// and disable them when they leave the viewport!!!
//
// This is done to prevent JavaScript animations from slowing the page down too much,
// while still not requiring the user to click to enable all of them all the time.
//
// If the user explicitly disables then once, they are not automatically enabled anymore.
//
// https://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport/15203639#15203639
this.first_observer_init = true;
const observer = new IntersectionObserver(
(entries, observer) => {
entries.forEach(entry => {
if (entry.intersectionRatio > 0.0) {
// Just entered the viewport.
if (this.first_observer_init) {
this.first_observer_init = false;
this.enableStateChange(true, this.enable_input.checked);
}
} else {
// Just left the viewport.
this.enableStateChange(false, this.enable_input.checked);
}
});
},
{}
)
observer.observe(canvas_wrapper);
// Finish initialization.
this.init_done = false;
this.enableStateChange(this.options.enabled, this.enable_input.checked);
}
resizeCanvas() {
const canvas_width = parseInt(this.canvas_width_input.value);
if (isNaN(canvas_width)) {
canvas_width = parseInt(this.canvas_width_input.getAttribute('value'));
}
this.canvas.width = canvas_width;
this.canvas.height = canvas_width;
this.width = canvas_width
this.height = canvas_width
}
}