/** The overlay builder for the Blue Marble script. * @description This class handles the overlay UI for the Blue Marble script. * @class Overlay * @since 0.0.2 * @example * const overlay = new Overlay(); * overlay.addDiv({ 'id': 'overlay' }) * .addDiv({ 'id': 'header' }) * .addHeader(1, {'textContent': 'Your Overlay'}).buildElement() * .addP({'textContent': 'This is your overlay. It is versatile.'}).buildElement() * .buildElement() // Marks the end of the header
* .addHr().buildElement() * .buildOverlay(document.body); * // Output: * // (Assume already exists in the webpage) * *
* *
*
* */ export default class Overlay { /** Constructor for the Overlay class. * @param {string} name - The name of the userscript * @param {string} version - The version of the userscript * @since 0.0.2 * @see {@link Overlay} */ constructor(name, version) { this.name = name; // Name of userscript this.version = version; // Version of userscript this.apiManager = null; // The API manager instance. Later populated when setApiManager is called this.outputStatusId = 'bm-output-status'; // ID for status element this.overlay = null; // The overlay root DOM HTMLElement this.currentParent = null; // The current parent HTMLElement in the overlay this.parentStack = []; // Tracks the parent elements BEFORE the currentParent so we can nest elements } /** Populates the apiManager variable with the apiManager class. * @param {apiManager} apiManager - The apiManager class instance * @since 0.41.4 */ setApiManager(apiManager) {this.apiManager = apiManager;} /** Creates an element. * For **internal use** of the {@link Overlay} class. * @param {string} tag - The tag name as a string. * @param {Object.} [properties={}] - The DOM properties of the element. * @returns {HTMLElement} HTML Element * @since 0.43.2 */ #createElement(tag, properties = {}, additionalProperties={}) { const element = document.createElement(tag); // Creates the element // If this is the first element made... if (!this.overlay) { this.overlay = element; // Declare it the highest overlay element this.currentParent = element; } else { this.currentParent?.appendChild(element); // ...else delcare it the child of the last element this.parentStack.push(this.currentParent); this.currentParent = element; } // For every passed in property (shared by all like-elements), apply the it to the element for (const [property, value] of Object.entries(properties)) { this.#applyAttribute(element, property, value); } // For every passed in additional property, apply the it to the element for (const [property, value] of Object.entries(additionalProperties)) { this.#applyAttribute(element, property, value); } return element; } /** Applies an attribute to an element * @param {HTMLElement} element - The element to apply the attribute to * @param {String} property - The name of the attribute to apply * @param {String} value - The value of the attribute * @since 0.88.136 */ #applyAttribute(element, property, value) { if (property == 'class') { element.classList.add(...value.split(/\s+/)); // converts `'foo bar'` to `'foo', 'bar'` which is accepted } else if (property == 'for') { element.htmlFor = value; } else if (property == 'tabindex') { element.tabIndex = Number(value); } else if (property == 'readonly') { element.readOnly = ((value == 'true') || (value == '1')); } else if (property == 'maxlength') { element.maxLength = Number(value); } else if (property.startsWith('data')) { element.dataset[ property.slice(5).split('-').map( (part, i) => (i == 0) ? part : part[0].toUpperCase() + part.slice(1) ).join('') ] = value; } else if (property.startsWith('aria')) { const camelCase = property.slice(5).split('-').map( (part, i) => (i == 0) ? part : part[0].toUpperCase() + part.slice(1) ).join(''); element['aria' + camelCase[0].toUpperCase() + camelCase.slice(1)] = value; } else { element[property] = value; } } /** Finishes building an element. * Call this after you are finished adding children. * If the element will have no children, call it anyways. * @returns {Overlay} Overlay class instance (this) * @since 0.43.2 * @example * overlay * .addDiv() * .addHeader(1).buildElement() // Breaks out of the

* .addP().buildElement() // Breaks out of the

* .buildElement() // Breaks out of the

* .addHr() // Since there are no more elements, calling buildElement() is optional * .buildOverlay(document.body); */ buildElement() { if (this.parentStack.length > 0) { this.currentParent = this.parentStack.pop(); } return this; } /** Finishes building the overlay and displays it. * Call this when you are done chaining methods. * @param {HTMLElement} parent - The parent HTMLElement this overlay should be appended to as a child. * @since 0.43.2 * @example * overlay * .addDiv() * .addP().buildElement() * .buildElement() * .buildOverlay(document.body); // Adds DOM structure to document body * //

*/ buildOverlay(parent) { parent?.appendChild(this.overlay); // Resets the class-bound variables of this class instance back to default so overlay can be build again later this.overlay = null; this.currentParent = null; this.parentStack = []; } /** Adds a `div` to the overlay. * This `div` element will have properties shared between all `div` elements in the overlay. * You can override the shared properties by using a callback. * @param {Object.} [additionalProperties={}] - The DOM properties of the `div` that are NOT shared between all overlay `div` elements. These should be camelCase. * @param {function(Overlay, HTMLDivElement):void} [callback=()=>{}] - Additional JS modification to the `div`. * @returns {Overlay} Overlay class instance (this) * @since 0.43.2 * @example * // Assume all
elements have a shared class (e.g. {'className': 'bar'}) * overlay.addDiv({'id': 'foo'}).buildOverlay(document.body); * // Output: * // (Assume already exists in the webpage) * *
* */ addDiv(additionalProperties = {}, callback = () => {}) { const properties = {}; // Shared
DOM properties const div = this.#createElement('div', properties, additionalProperties); // Creates the
element callback(this, div); // Runs any script passed in through the callback return this; } /** Adds a `p` to the overlay. * This `p` element will have properties shared between all `p` elements in the overlay. * You can override the shared properties by using a callback. * @param {Object.} [additionalProperties={}] - The DOM properties of the `p` that are NOT shared between all overlay `p` elements. These should be camelCase. * @param {function(Overlay, HTMLParagraphElement):void} [callback=()=>{}] - Additional JS modification to the `p`. * @returns {Overlay} Overlay class instance (this) * @since 0.43.2 * @example * // Assume all

elements have a shared class (e.g. {'className': 'bar'}) * overlay.addP({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body); * // Output: * // (Assume already exists in the webpage) * *

Foobar.

* */ addP(additionalProperties = {}, callback = () => {}) { const properties = {}; // Shared

DOM properties const p = this.#createElement('p', properties, additionalProperties); // Creates the

element callback(this, p); // Runs any script passed in through the callback return this; } /** Adds a `small` to the overlay. * This `small` element will have properties shared between all `small` elements in the overlay. * You can override the shared properties by using a callback. * @param {Object.} [additionalProperties={}] - The DOM properties of the `small` that are NOT shared between all overlay `small` elements. These should be camelCase. * @param {function(Overlay, HTMLParagraphElement):void} [callback=()=>{}] - Additional JS modification to the `small`. * @returns {Overlay} Overlay class instance (this) * @since 0.55.8 * @example * // Assume all elements have a shared class (e.g. {'className': 'bar'}) * overlay.addSmall({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body); * // Output: * // (Assume already exists in the webpage) * * Foobar. * */ addSmall(additionalProperties = {}, callback = () => {}) { const properties = {}; // Shared DOM properties const small = this.#createElement('small', properties, additionalProperties); // Creates the element callback(this, small); // Runs any script passed in through the callback return this; } /** Adds a `details` to the overlay. * This `details` element will have properties shared between all `details` elements in the overlay. * You can override the shared properties by using a callback. * @param {Object.} [additionalProperties={}] - The DOM properties of the `details` that are NOT shared between all overlay `details` elements. These should be camelCase. * @param {function(Overlay, HTMLParagraphElement):void} [callback=()=>{}] - Additional JS modification to the `details`. * @returns {Overlay} Overlay class instance (this) * @since 0.88.96 * @example * // Assume all

elements have a shared class (e.g. {'className': 'bar'}) * overlay.addDetails({'id': 'foo'}).buildOverlay(document.body); * // Output: * // (Assume already exists in the webpage) * *
* */ addDetails(additionalProperties = {}, callback = () => {}) { const properties = {}; // Shared
DOM properties const details = this.#createElement('details', properties, additionalProperties); // Creates the
element callback(this, details); // Runs any script passed in through the callback return this; } /** Adds a `summary` to the overlay. * This `summary` element will have properties shared between all `summary` elements in the overlay. * You can override the shared properties by using a callback. * @param {Object.} [additionalProperties={}] - The DOM properties of the `summary` that are NOT shared between all overlay `summary` elements. These should be camelCase. * @param {function(Overlay, HTMLParagraphElement):void} [callback=()=>{}] - Additional JS modification to the `summary`. * @returns {Overlay} Overlay class instance (this) * @since 0.88.96 * @example * // Assume all elements have a shared class (e.g. {'className': 'bar'}) * overlay.addSummary({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body); * // Output: * // (Assume already exists in the webpage) * * Foobar. * */ addSummary(additionalProperties = {}, callback = () => {}) { const properties = {}; // Shared DOM properties const summary = this.#createElement('summary', properties, additionalProperties); // Creates the element callback(this, summary); // Runs any script passed in through the callback return this; } /** Adds a `img` to the overlay. * This `img` element will have properties shared between all `img` elements in the overlay. * You can override the shared properties by using a callback. * @param {Object.} [additionalProperties={}] - The DOM properties of the `img` that are NOT shared between all overlay `img` elements. These should be camelCase. * @param {function(Overlay, HTMLImageElement):void} [callback=()=>{}] - Additional JS modification to the `img`. * @returns {Overlay} Overlay class instance (this) * @since 0.43.2 * @example * // Assume all elements have a shared class (e.g. {'className': 'bar'}) * overlay.addimg({'id': 'foo', 'src': './img.png'}).buildOverlay(document.body); * // Output: * // (Assume already exists in the webpage) * * * */ addImg(additionalProperties = {}, callback = () => {}) { const properties = {}; // Shared DOM properties const img = this.#createElement('img', properties, additionalProperties); // Creates the element callback(this, img); // Runs any script passed in through the callback return this; } /** Adds a header to the overlay. * This header element will have properties shared between all header elements in the overlay. * You can override the shared properties by using a callback. * @param {number} level - The header level. Must be between 1 and 6 (inclusive) * @param {Object.} [additionalProperties={}] - The DOM properties of the header that are NOT shared between all overlay header elements. These should be camelCase. * @param {function(Overlay, HTMLHeadingElement):void} [callback=()=>{}] - Additional JS modification to the header. * @returns {Overlay} Overlay class instance (this) * @since 0.43.7 * @example * // Assume all header elements have a shared class (e.g. {'className': 'bar'}) * overlay.addHeader(6, {'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body); * // Output: * // (Assume already exists in the webpage) * *
Foobar.
* */ addHeader(level, additionalProperties = {}, callback = () => {}) { const properties = {}; // Shared header DOM properties const header = this.#createElement('h' + level, properties, additionalProperties); // Creates the header element callback(this, header); // Runs any script passed in through the callback return this; } /** Adds a `hr` to the overlay. * This `hr` element will have properties shared between all `hr` elements in the overlay. * You can override the shared properties by using a callback. * @param {Object.} [additionalProperties={}] - The DOM properties of the `hr` that are NOT shared between all overlay `hr` elements. These should be camelCase. * @param {function(Overlay, HTMLHRElement):void} [callback=()=>{}] - Additional JS modification to the `hr`. * @returns {Overlay} Overlay class instance (this) * @since 0.43.7 * @example * // Assume all
elements have a shared class (e.g. {'className': 'bar'}) * overlay.addhr({'id': 'foo'}).buildOverlay(document.body); * // Output: * // (Assume already exists in the webpage) * *
* */ addHr(additionalProperties = {}, callback = () => {}) { const properties = {}; // Shared
DOM properties const hr = this.#createElement('hr', properties, additionalProperties); // Creates the
element callback(this, hr); // Runs any script passed in through the callback return this; } /** Adds a `br` to the overlay. * This `br` element will have properties shared between all `br` elements in the overlay. * You can override the shared properties by using a callback. * @param {Object.} [additionalProperties={}] - The DOM properties of the `br` that are NOT shared between all overlay `br` elements. These should be camelCase. * @param {function(Overlay, HTMLBRElement):void} [callback=()=>{}] - Additional JS modification to the `br`. * @returns {Overlay} Overlay class instance (this) * @since 0.43.11 * @example * // Assume all
elements have a shared class (e.g. {'className': 'bar'}) * overlay.addbr({'id': 'foo'}).buildOverlay(document.body); * // Output: * // (Assume already exists in the webpage) * *
* */ addBr(additionalProperties = {}, callback = () => {}) { const properties = {}; // Shared
DOM properties const br = this.#createElement('br', properties, additionalProperties); // Creates the
element callback(this, br); // Runs any script passed in through the callback return this; } /** Adds a checkbox to the overlay. * This checkbox element will have properties shared between all checkbox elements in the overlay. * You can override the shared properties by using a callback. Note: the checkbox element is inside a label element. * @param {Object.} [additionalProperties={}] - The DOM properties of the checkbox that are NOT shared between all overlay checkbox elements. These should be camelCase. * @param {function(Overlay, HTMLLabelElement, HTMLInputElement):void} [callback=()=>{}] - Additional JS modification to the checkbox. * @returns {Overlay} Overlay class instance (this) * @since 0.43.10 * @example * // Assume all checkbox elements have a shared class (e.g. {'className': 'bar'}) * overlay.addCheckbox({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body); * // Output: * // (Assume already exists in the webpage) * * * */ addCheckbox(additionalProperties = {}, callback = () => {}) { const properties = {'type': 'checkbox'}; // Shared checkbox DOM properties const label = this.#createElement('label', {'textContent': additionalProperties['textContent'] ?? ''}); // Creates the label element delete additionalProperties['textContent']; // Deletes 'textContent' DOM property before adding the properties to the checkbox const checkbox = this.#createElement('input', properties, additionalProperties); // Creates the checkbox element label.insertBefore(checkbox, label.firstChild); // Makes the checkbox the first child of the label (before the text content) this.buildElement(); // Signifies that we are done adding children to the checkbox callback(this, label, checkbox); // Runs any script passed in through the callback return this; } /** Adds a `button` to the overlay. * This `button` element will have properties shared between all `button` elements in the overlay. * You can override the shared properties by using a callback. * @param {Object.} [additionalProperties={}] - The DOM properties of the `button` that are NOT shared between all overlay `button` elements. These should be camelCase. * @param {function(Overlay, HTMLButtonElement):void} [callback=()=>{}] - Additional JS modification to the `button`. * @returns {Overlay} Overlay class instance (this) * @since 0.43.12 * @example * // Assume all * */ addButton(additionalProperties = {}, callback = () => {}) { const properties = {}; // Shared * * @example * // Assume all help button elements have a shared class (e.g. {'className': 'bar'}) * overlay.addButtonHelp({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body); * // Output: * // (Assume already exists in the webpage) * * * */ addButtonHelp(additionalProperties = {}, callback = () => {}) { const tooltip = additionalProperties['title'] ?? additionalProperties['textContent'] ?? 'Help: No info'; // Retrieves the tooltip // Makes sure the tooltip is stored in the title property delete additionalProperties['textContent']; additionalProperties['title'] = `Help: ${tooltip}`; // Shared help button DOM properties const properties = { 'textContent': '?', 'className': 'bm-help', 'onclick': () => { this.updateInnerHTML(this.outputStatusId, tooltip); } }; const help = this.#createElement('button', properties, additionalProperties); // Creates the *
* */ addInputFile(additionalProperties = {}, callback = () => {}) { const properties = { 'type': 'file', 'tabindex': '-1', 'aria-hidden': 'true', 'style': 'display: none !important; visibility: hidden !important; position: absolute !important; left: -9999px !important; width: 0 !important; height: 0 !important; opacity: 0 !important;' }; // Complete file input hiding to prevent native browser text interference const text = additionalProperties['textContent'] ?? ''; // Retrieves the text content delete additionalProperties['textContent']; // Deletes the text content before applying the additional properties to the file input const container = this.#createElement('div'); // Container for file input const input = this.#createElement('input', properties, additionalProperties); // Creates the file input this.buildElement(); // Signifies that we are done adding children to the file input const button = this.#createElement('button', {'textContent': text}); this.buildElement(); // Signifies that we are done adding children to the button this.buildElement(); // Signifies that we are done adding children to the container // Prevent file input from being accessible or visible by screen-readers and tabbing //input.setAttribute('tabindex', '-1'); //input.setAttribute('aria-hidden', 'true'); button.addEventListener('click', () => { input.click(); // Clicks the file input }); // Update button text when file is selected input.addEventListener('change', () => { button.style.maxWidth = `${button.offsetWidth}px`; if (input.files.length > 0) { button.textContent = input.files[0].name; } else { button.textContent = text; } }); callback(this, container, input, button); // Runs any script passed in through the callback return this; } /** Adds a `textarea` to the overlay. * This `textarea` element will have properties shared between all `textarea` elements in the overlay. * You can override the shared properties by using a callback. * @param {Object.} [additionalProperties={}] - The DOM properties of the `textarea` that are NOT shared between all overlay `textarea` elements. These should be camelCase. * @param {function(Overlay, HTMLTextAreaElement):void} [callback=()=>{}] - Additional JS modification to the `textarea`. * @returns {Overlay} Overlay class instance (this) * @since 0.43.13 * @example * // Assume all * */ addTextarea(additionalProperties = {}, callback = () => {}) { const properties = {}; // Shared