/** 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 /** The API manager instance. Later populated when setApiManager is called @type {ApiManager} */ this.apiManager = null; 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, HTMLElement):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 `span` to the overlay. * This `span` element will have properties shared between all `span` elements in the overlay. * You can override the shared properties by using a callback. * @param {Object.} [additionalProperties={}] - The DOM properties of the `span` that are NOT shared between all overlay `span` elements. These should be camelCase. * @param {function(Overlay, HTMLSpanElement):void} [callback=()=>{}] - Additional JS modification to the `span`. * @returns {Overlay} Overlay class instance (this) * @since 0.55.8 * @example * // Assume all elements have a shared class (e.g. {'className': 'bar'}) * overlay.addSpan({'id': 'foo', 'textContent': 'Foobar.'}).buildOverlay(document.body); * // Output: * // (Assume already exists in the webpage) * * Foobar. * */ addSpan(additionalProperties = {}, callback = () => {}) { const properties = {}; // Shared DOM properties const span = this.#createElement('span', properties, additionalProperties); // Creates the element callback(this, span); // 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, HTMLDetailsElement):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, HTMLElement):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 `form` to the overlay. * This `form` element will have properties shared between all `form` elements in the overlay. * You can override the shared properties by using a callback. * @param {Object.} [additionalProperties={}] - The DOM properties of the `form` that are NOT shared between all overlay `form` elements. These should be camelCase. * @param {function(Overlay, HTMLFormElement):void} [callback=()=>{}] - Additional JS modification to the `form`. * @returns {Overlay} Overlay class instance (this) * @since 0.88.246 * @example * // Assume all
elements have a shared class (e.g. {'className': 'bar'}) * overlay.addForm({'id': 'foo'}).buildOverlay(document.body); * // Output: * // (Assume already exists in the webpage) * *
* */ addForm(additionalProperties = {}, callback = () => {}) { const properties = {}; // Shared
DOM properties const form = this.#createElement('form', properties, additionalProperties); // Creates the element callback(this, form); // Runs any script passed in through the callback return this; } /** Adds a `fieldset` to the overlay. * This `fieldset` element will have properties shared between all `fieldset` elements in the overlay. * You can override the shared properties by using a callback. * @param {Object.} [additionalProperties={}] - The DOM properties of the `fieldset` that are NOT shared between all overlay `fieldset` elements. These should be camelCase. * @param {function(Overlay, HTMLFieldSetElement):void} [callback=()=>{}] - Additional JS modification to the `fieldset`. * @returns {Overlay} Overlay class instance (this) * @since 0.88.246 * @example * // Assume all
elements have a shared class (e.g. {'className': 'bar'}) * overlay.addFieldset({'id': 'foo'}).buildOverlay(document.body); * // Output: * // (Assume already exists in the webpage) * *
* */ addFieldset(additionalProperties = {}, callback = () => {}) { const properties = {}; // Shared
DOM properties const fieldset = this.#createElement('fieldset', properties, additionalProperties); // Creates the
element callback(this, fieldset); // Runs any script passed in through the callback return this; } /** Adds a `legend` to the overlay. * This `legend` element will have properties shared between all `legend` elements in the overlay. * You can override the shared properties by using a callback. * @param {Object.} [additionalProperties={}] - The DOM properties of the `legend` that are NOT shared between all overlay `legend` elements. These should be camelCase. * @param {function(Overlay, HTMLLegendElement):void} [callback=()=>{}] - Additional JS modification to the `legend`. * @returns {Overlay} Overlay class instance (this) * @since 0.88.246 * @example * // Assume all elements have a shared class (e.g. {'className': 'bar'}) * overlay.addLegend({'id': 'foo', textContent: 'Foobar.'}).buildOverlay(document.body); * // Output: * // (Assume already exists in the webpage) * * * "Foobar." * * */ addLegend(additionalProperties = {}, callback = () => {}) { const properties = {}; // Shared DOM properties const legend = this.#createElement('legend', properties, additionalProperties); // Creates the element callback(this, legend); // 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 label & select element to the overlay. * This select element will have properties shared between all select elements in the overlay. * You can override the shared properties by using a callback. * @param {Object.} [additionalProperties={}] - The DOM properties of the checkbox that are NOT shared between all overlay select elements. These should be camelCase. * @param {function(Overlay, HTMLLabelElement, HTMLSelectElement):void} [callback=()=>{}] - Additional JS modification to the label/select elements. * @returns {Overlay} Overlay class instance (this) * @since 0.88.243 * @example * // Assume all select elements have a shared class (e.g. {'className': 'bar'}) * overlay.addSelect({'id': 'foo', 'textContent': 'Foobar: '}).buildOverlay(document.body); * // Output: * // (Assume already exists in the webpage) * * * * */ addSelect(additionalProperties = {}, callback = () => {}) { const properties = {}; // Shared select DOM properties const label = this.#createElement('label', {'textContent': additionalProperties['textContent'] ?? '', 'for': additionalProperties['id'] ?? ''}); // Creates the label element delete additionalProperties['textContent']; // Deletes 'textContent' DOM property before adding the properties to the select this.buildElement(); // Signifies that we are done adding children to the label const select = this.#createElement('select', properties, additionalProperties); // Creates the select element callback(this, label, select); // Runs any script passed in through the callback return this; } /** Adds an option to the overlay. * This `option` element will have properties shared between all `option` elements in the overlay. * You can override the shared properties by using a callback. * @param {Object.} [additionalProperties={}] - The DOM properties of the `option` that are NOT shared between all overlay `option` elements. These should be camelCase. * @param {function(Overlay, HTMLOptionElement):void} [callback=()=>{}] - Additional JS modification to the `option`. * @returns {Overlay} Overlay class instance (this) * @since 0.88.244 * @example * // Assume all * */ addOption(additionalProperties = {}, callback = () => {}) { const properties = {}; // Shared