export default class DOMManager { /** Document/window width */ static get screenWidth() {return Math.max(document.documentElement.clientWidth, window.innerWidth || 0);} /** Document/window height */ static get screenHeight() {return Math.max(document.documentElement.clientHeight, window.innerHeight || 0);} static get bdHead() {return this.getElement("bd-head");} static get bdBody() {return this.getElement("bd-body");} static get bdScripts() {return this.getElement("bd-scripts");} static get bdStyles() {return this.getElement("bd-styles");} static get bdThemes() {return this.getElement("bd-themes");} static get bdCustomCSS() {return this.getElement("#customcss");} static get bdTooltips() {return this.getElement("bd-tooltips") || this.createElement("bd-tooltips").appendTo(this.bdBody);} static get bdModals() {return this.getElement("bd-modals") || this.createElement("bd-modals").appendTo(this.bdBody);} static get bdToasts() {return this.getElement("bd-toasts") || this.createElement("bd-toasts").appendTo(this.bdBody);} static initialize() { // this.createElement("bd-head", {target: document.head}); // this.createElement("bd-body", {target: document.body}); // this.createElement("bd-scripts", {target: this.bdHead}); // this.createElement("bd-styles", {target: this.bdHead}); // this.createElement("bd-themes", {target: this.bdHead}); // this.createElement("style", {id: "customcss", target: this.bdHead}); } static escapeID(id) { return id.replace(/^[^a-z]+|[^\w-]+/gi, "-"); } static getElement(e, baseElement = document) { if (e instanceof Node) return e; return baseElement.querySelector(e); } static createElement(tag, options = {}, child = null) { const {className, id, target} = options; const element = document.createElement(tag); if (className) element.className = className; if (id) element.id = id; if (child) element.append(child); if (target) this.getElement(target).append(element); return element; } /** * Parses a string of HTML and returns the results. If the second parameter is true, * the parsed HTML will be returned as a document fragment {@see https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment}. * This is extremely useful if you have a list of elements at the top level, they can then be appended all at once to another node. * * If the second parameter is false, then the return value will be the list of parsed * nodes and there were multiple top level nodes, otherwise the single node is returned. * @param {string} html - HTML to be parsed * @param {boolean} [fragment=false] - Whether or not the return should be the raw `DocumentFragment` * @returns {(DocumentFragment|NodeList|HTMLElement)} - The result of HTML parsing */ static parseHTML(html, fragment = false) { const template = document.createElement("template"); template.innerHTML = html; const node = template.content.cloneNode(true); if (fragment) return node; return node.childNodes.length > 1 ? node.childNodes : node.childNodes[0]; } static removeStyle(id) { id = this.escapeID(id); const exists = this.getElement(`#${id}`, this.bdStyles); if (exists) exists.remove(); } static injectStyle(id, css) { id = this.escapeID(id); const style = this.getElement(`#${id}`, this.bdStyles) || this.createElement("style", {id}); style.textContent = css; this.bdStyles.append(style); } static unlinkStyle(id) { return this.removeStyle(id); } static linkStyle(id, url, {documentHead = false} = {}) { id = this.escapeID(id); return new Promise(resolve => { const link = this.getElement(`#${id}`, this.bdStyles) || this.createElement("link", {id}); link.rel = "stylesheet"; link.href = url; link.onload = resolve; const target = documentHead ? document.head : this.bdStyles; target.append(link); }); } static removeTheme(id) { id = this.escapeID(id); const exists = this.getElement(`#${id}`, this.bdThemes); if (exists) exists.remove(); } static injectTheme(id, css) { id = this.escapeID(id); const style = this.getElement(`#${id}`, this.bdThemes) || this.createElement("style", {id}); style.textContent = css; this.bdThemes.append(style); } static updateCustomCSS(css) { this.bdCustomCSS.textContent = css; } static removeScript(id) { id = this.escapeID(id); const exists = this.getElement(`#${id}`, this.bdScripts); if (exists) exists.remove(); } static injectScript(id, url) { id = this.escapeID(id); return new Promise((resolve, reject) => { const script = this.getElement(`#${id}`, this.bdScripts) || this.createElement("script", {id}); script.src = url; script.onload = resolve; script.onerror = reject; this.bdScripts.append(script); }); } // https://javascript.info/js-animation static animate({timing = _ => _, update, duration}) { const start = performance.now(); requestAnimationFrame(function animate(time) { // timeFraction goes from 0 to 1 let timeFraction = (time - start) / duration; if (timeFraction > 1) timeFraction = 1; // calculate the current animation state const progress = timing(timeFraction); update(progress); // draw it if (timeFraction < 1) requestAnimationFrame(animate); }); } /** * Adds a listener for when a node matching a selector is added to the document body. * The listener is automatically removed upon firing. * @param {string} selector - node to wait for * @param {callable} callback - function to be performed on event */ static onAdded(selector, callback) { if (document.body.querySelector(selector)) return callback(document.body.querySelector(selector)); const observer = new MutationObserver((mutations) => { for (let m = 0; m < mutations.length; m++) { for (let i = 0; i < mutations[m].addedNodes.length; i++) { const mutation = mutations[m].addedNodes[i]; if (mutation.nodeType === 3) continue; // ignore text const directMatch = mutation.matches(selector) && mutation; const childrenMatch = mutation.querySelector(selector); if (directMatch || childrenMatch) { observer.disconnect(); return callback(directMatch ?? childrenMatch); } } } }); observer.observe(document.body, {subtree: true, childList: true}); return () => {observer.disconnect();}; } /** * Adds a listener for when the node is removed from the document body. * The listener is automatically removed upon firing. * @param {HTMLElement} node - node to wait for * @param {callable} callback - function to be performed on event */ static onRemoved(node, callback) { const observer = new MutationObserver((mutations) => { for (let m = 0; m < mutations.length; m++) { const mutation = mutations[m]; const nodes = Array.from(mutation.removedNodes); const directMatch = nodes.indexOf(node) > -1; const parentMatch = nodes.some(parent => parent.contains(node)); if (directMatch || parentMatch) { observer.disconnect(); callback(); } } }); observer.observe(document.body, {subtree: true, childList: true}); return () => {observer.disconnect();}; } } DOMManager.createElement("bd-head", {target: document.body}); DOMManager.createElement("bd-body", {target: document.body}); DOMManager.createElement("bd-scripts", {target: DOMManager.bdHead}); DOMManager.createElement("bd-styles", {target: DOMManager.bdHead}); DOMManager.createElement("bd-themes", {target: DOMManager.bdHead}); DOMManager.createElement("style", {id: "customcss", target: DOMManager.bdHead});