/** 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)
*
*
*
*
Your Overlay
*
This is your overlay. It is versatile.
*
*
*
*
*/
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
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