UNPKG

@lesjoursfr/browser-tools

Version:

Some browser tools for events & DOM manipulation.

596 lines (595 loc) 19.2 kB
/** * Convert a dashed string to camelCase */ function dashedToCamel(string) { return string.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase()); } /** * Check if an node is a tag element */ function isTagElement(node, tag) { return isHTMLElement(node) && node.tagName === tag.toUpperCase(); } /** * Check if a node is an HTML Element. * @param {Node} node the node to test * @returns {boolean} true if the node is an HTMLElement */ export function isCommentNode(node) { return node.nodeType === Node.COMMENT_NODE; } /** * Check if a node is an HTML Element. * @param {Node} node the node to test * @returns {boolean} true if the node is an HTMLElement */ export function isTextNode(node) { return node.nodeType === Node.TEXT_NODE; } /** * Check if a node is an HTML Element. * @param {Node} node the node to test * @returns {boolean} true if the node is an HTMLElement */ export function isHTMLElement(node) { return node.nodeType === Node.ELEMENT_NODE; } /** * Create an HTMLElement from the HTML template. * @param {string} template the HTML template * @returns {DocumentFragment} The created DocumentFragment */ export function createFragmentFromTemplate(template) { const range = document.createRange(); range.selectNode(document.body); return range.createContextualFragment(template); } /** * Create an HTMLElement from the HTML template. * @param {string} template the HTML template * @returns {HTMLElement} the created HTMLElement */ export function createFromTemplate(template) { return createFragmentFromTemplate(template).children[0]; } /** * Update the given CSS property. * If the value is `null` the property will be removed. * @param {HTMLElement} node the node to update * @param {string|{ [key: string]: string|null }} property multi-word property names are hyphenated (kebab-case) and not camel-cased. * @param {string|null} value (default to `null`) * @returns {HTMLElement} the element */ export function updateCSS(node, property, value = null) { if (typeof property !== "string") { for (const [key, val] of Object.entries(property)) { if (val !== null) { node.style.setProperty(key, val); } else { node.style.removeProperty(key); } } } else { if (value !== null) { node.style.setProperty(property, value); } else { node.style.removeProperty(property); } } return node; } /** * Update the given CSS property. * If the value is `null` the property will be removed. * @param {string} id the id of the node to update * @param {string|{ [key: string]: string|null }} property multi-word property names are hyphenated (kebab-case) and not camel-cased. * @param {string|null} value (default to `null`) */ export function updateCSSOfElement(id, property, value = null) { const node = document.getElementById(id); if (node === null) { return; } if (typeof property !== "string") { for (const [key, val] of Object.entries(property)) { if (val !== null) { node.style.setProperty(key, val); } else { node.style.removeProperty(key); } } } else { if (value !== null) { node.style.setProperty(property, value); } else { node.style.removeProperty(property); } } } /** * Update the given CSS property. * If the value is `null` the property will be removed. * @param {string} selector the CSS selector of the nodes to update * @param {string|{ [key: string]: string|null }} property multi-word property names are hyphenated (kebab-case) and not camel-cased. * @param {string|null} value (default to `null`) */ export function updateCSSOfElements(selector, property, value = null) { const nodes = document.querySelectorAll(selector); if (typeof property !== "string") { for (const node of nodes) { for (const [key, val] of Object.entries(property)) { if (val !== null) { node.style.setProperty(key, val); } else { node.style.removeProperty(key); } } } } else { for (const node of nodes) { if (value !== null) { node.style.setProperty(property, value); } else { node.style.removeProperty(property); } } } } /** * Check if the node has the given attribute. * @param {HTMLElement} node * @param {string} attribute * @returns {boolean} true or false */ export function hasAttribute(node, attribute) { return node.hasAttribute(attribute); } /** * Get the given attribute. * @param {HTMLElement} node * @param {string} attribute * @returns {string|null} the value */ export function getAttribute(node, attribute) { return node.getAttribute(attribute); } /** * Set the given attribute. * If the value is `null` the attribute will be removed. * @param {HTMLElement} node * @param {string} attribute * @param {string|null} value * @returns {HTMLElement} the element */ export function setAttribute(node, attribute, value) { if (value === null) { node.removeAttribute(attribute); } else { node.setAttribute(attribute, value); } return node; } /** * Get the given data. * This function does not change the DOM. * If there is no key this function return all data * @param {HTMLElement} node * @param {string|undefined} key * @returns {BrowserToolsDataType|null} the value */ export function getData(node, key) { if (node.ljbtData === undefined) { node.ljbtData = {}; for (const [k, v] of Object.entries(node.dataset)) { if (v === undefined) { continue; } node.ljbtData[dashedToCamel(k)] = v; } } return key === undefined ? node.ljbtData : (node.ljbtData[dashedToCamel(key)] ?? null); } /** * Set the given data. * If the value is `null` the data will be removed. * This function does not change the DOM. * @param {HTMLElement} node * @param {string} key * @param {BrowserToolsDataType|null} value * @returns {HTMLElement} the element */ export function setData(node, key, value) { if (node.ljbtData === undefined) { node.ljbtData = {}; } if (value === null) { delete node.ljbtData[dashedToCamel(key)]; } else { node.ljbtData[dashedToCamel(key)] = value; } return node; } /** * Check if the node has the given tag name, or if its tag name is in the given list. * @param {HTMLElement} node the element to check * @param {string|Array<string>} tags a tag name or a list of tag name * @returns {boolean} true if the node has the given tag name */ export function hasTagName(node, tags) { if (typeof tags === "string") { return node.tagName === tags.toUpperCase(); } return tags.some((tag) => node.tagName === tag.toUpperCase()); } /** * Check if the node has the given class name. * @param {HTMLElement} node the element to check * @param {string} className a class name * @returns {boolean} true if the node has the given class name */ export function hasClass(node, className) { return node.classList.contains(className); } /** * Add the class to the node's class attribute. * @param {HTMLElement} node * @param {string|Array<string>} className * @returns {HTMLElement} the element */ export function addClass(node, className) { if (typeof className === "string") { node.classList.add(className); } else { node.classList.add(...className); } return node; } /** * Add the class to the node's class attribute with the given id. * @param {string} id * @param {string|Array<string>} className */ export function addClassToElement(id, className) { const node = document.getElementById(id); if (node === null) { return; } if (typeof className === "string") { node.classList.add(className); } else { node.classList.add(...className); } } /** * Add the class to the nodes' class attribute that match the given CSS selector. * @param {string} selector * @param {string|Array<string>} className */ export function addClassToElements(selector, className) { const nodes = document.querySelectorAll(selector); if (typeof className === "string") { for (const node of nodes) { node.classList.add(className); } } else { for (const node of nodes) { node.classList.add(...className); } } } /** * Remove the class from the node's class attribute. * @param {HTMLElement} node * @param {string|Array<string>} className * @returns {HTMLElement} the element */ export function removeClass(node, className) { if (typeof className === "string") { node.classList.remove(className); } else { node.classList.remove(...className); } return node; } /** * Remove the class from the node's class attribute with the given id. * @param {string} id * @param {string|Array<string>} className */ export function removeClassFromElement(id, className) { const node = document.getElementById(id); if (node === null) { return; } if (typeof className === "string") { node.classList.remove(className); } else { node.classList.remove(...className); } } /** * Remove the class from the nodes' class attribute that match the given CSS selector. * @param {string} selector * @param {string|Array<string>} className */ export function removeClassFromElements(selector, className) { const nodes = document.querySelectorAll(selector); if (typeof className === "string") { for (const node of nodes) { node.classList.remove(className); } } else { for (const node of nodes) { node.classList.remove(...className); } } } /** * Test if the node match the given selector. * @param {HTMLElement} node * @param {string} selector * @returns {boolean} true or false */ export function is(node, selector) { return node.matches(selector); } /** * Get the node's offset. * @param {HTMLElement} node * @returns {{ top: number, left: number }} The node's offset */ export function offset(node) { const rect = node.getBoundingClientRect(); const win = node.ownerDocument.defaultView; return { top: rect.top + win.scrollY, left: rect.left + win.scrollX, }; } /** * Create a new node. * @param {string} tag the tag name of the node * @param {object} options optional parameters * @param {string} options.innerHTML the HTML code of the node * @param {string} options.textContent the text content of the node * @param {object} options.attributes attributes of the node * @returns {HTMLElement} the created node */ export function createNodeWith(tag, { innerHTML, textContent, attributes, } = {}) { const node = document.createElement(tag); if (attributes) { for (const key in attributes) { if (Object.hasOwnProperty.call(attributes, key)) { node.setAttribute(key, attributes[key]); } } } if (typeof innerHTML === "string") { node.innerHTML = innerHTML; } else if (typeof textContent === "string") { node.textContent = textContent; } return node; } /** * Replace a node. * @param {HTMLElement} node the node to replace * @param {HTMLElement} replacement the new node * @returns {HTMLElement} the new node */ export function replaceNodeWith(node, replacement) { node.replaceWith(replacement); return replacement; } /** * Replace the node by its child nodes. * @param {HTMLElement} node the node to replace * @returns {Array<ChildNode>} its child nodes */ export function unwrapNode(node) { const newNodes = [...node.childNodes]; node.replaceWith(...newNodes); return newNodes; } /** * Replace the node by its text content. * @param {HTMLElement} node the node to replace * @returns {Text} the created Text node */ export function textifyNode(node) { const newNode = document.createTextNode(node.textContent ?? ""); node.replaceWith(newNode); return newNode; } /** * Know if a tag si a self-closing tag * @param {string} tagName * @returns {boolean} */ export function isSelfClosing(tagName) { return [ "AREA", "BASE", "BR", "COL", "EMBED", "HR", "IMG", "INPUT", "KEYGEN", "LINK", "META", "PARAM", "SOURCE", "TRACK", "WBR", ].includes(tagName); } /** * Remove all node's child nodes that pass the test implemented by the provided function. * @param {ChildNode} node the node to process * @param {Function} callbackFn the predicate */ export function removeNodes(node, callbackFn) { for (const el of [...node.childNodes]) { if (callbackFn(el)) { el.remove(); } } } /** * Remove recursively all node's child nodes that pass the test implemented by the provided function. * @param {ChildNode} node the node to process * @param {Function} callbackFn the predicate */ export function removeNodesRecursively(node, callbackFn) { // Remove the node if it meets the condition if (callbackFn(node)) { node.remove(); return; } // Loop through the node’s children for (const el of [...node.childNodes]) { // Execute the same function if it’s an element node removeNodesRecursively(el, callbackFn); } } /** * Remove all node's child nodes that are empty text nodes. * @param {ChildNode} node the node to process */ export function removeEmptyTextNodes(node) { removeNodes(node, (el) => isTextNode(el) && (el.textContent === null || el.textContent.trim().length === 0)); } /** * Remove all node's child nodes that are comment nodes. * @param {ChildNode} node the node to process */ export function removeCommentNodes(node) { removeNodes(node, (el) => isCommentNode(el)); } /** * Reset all node's attributes to the given list. * @param {HTMLElement} node the node * @param {object} targetAttributes the requested node's attributes */ export function resetAttributesTo(node, targetAttributes) { for (const name of node.getAttributeNames()) { if (targetAttributes[name] === undefined) { node.removeAttribute(name); } } for (const name of Object.keys(targetAttributes)) { node.setAttribute(name, targetAttributes[name]); } } /** * Replace the node's style attribute by some regular nodes (`<b>`, `<i>`, `<u>` or `<s>`). * @param {HTMLElement} node the node to process * @returns {HTMLElement} the new node */ export function replaceNodeStyleByTag(node) { // Get the style const styleAttr = node.getAttribute("style") || ""; // Check if a tag is override by the style attribute if ((hasTagName(node, "b") && styleAttr.match(/font-weight\s*:\s*(normal|400);/)) || (hasTagName(node, "i") && styleAttr.match(/font-style\s*:\s*normal;/)) || (hasTagName(node, ["u", "s"]) && styleAttr.match(/text-decoration\s*:\s*none;/))) { node = replaceNodeWith(node, createNodeWith("span", { attributes: { style: styleAttr }, innerHTML: node.innerHTML })); } // Infer the tag from the style if (styleAttr.match(/font-weight\s*:\s*(bold|700|800|900);/)) { node = replaceNodeWith(node, createNodeWith("b", { innerHTML: `<span style="${styleAttr.replace(/font-weight\s*:\s*(bold|700|800|900);/, "")}">${node.innerHTML}</span>`, })); } else if (styleAttr.match(/font-style\s*:\s*italic;/)) { node = replaceNodeWith(node, createNodeWith("i", { innerHTML: `<span style="${styleAttr.replace(/font-style\s*:\s*italic;/, "")}">${node.innerHTML}</span>`, })); } else if (styleAttr.match(/text-decoration\s*:\s*underline;/)) { node = replaceNodeWith(node, createNodeWith("u", { innerHTML: `<span style="${styleAttr.replace(/text-decoration\s*:\s*underline;/, "")}">${node.innerHTML}</span>`, })); } else if (styleAttr.match(/text-decoration\s*:\s*line-through;/)) { node = replaceNodeWith(node, createNodeWith("s", { innerHTML: `<span style="${styleAttr.replace(/text-decoration\s*:\s*line-through;/, "")}">${node.innerHTML}</span>`, })); } // Return the node return node; } /** * Remove all leading & trailing node's child nodes that match the given tag. * @param {HTMLElement} node the node to process * @param {string} tag the tag */ export function trimTag(node, tag) { // Children const children = node.childNodes; // Remove Leading while (children.length > 0 && isTagElement(children[0], tag)) { children[0].remove(); } // Remove Trailing while (children.length > 0 && isTagElement(children[children.length - 1], tag)) { children[children.length - 1].remove(); } } /** * Replaces text in a string, using a regular expression or search string. * @param {HTMLElement} node the node to process * @param {string | RegExp} searchValue A string or regular expression to search for. If searchValue is a regex, then it must have the global (g) flag set, or a TypeError is thrown. * @param {string | Function} replacer A string containing the text to replace or a function that returns the replacement text. * @param {boolean} textOnly If true, any HTML will be rendered as text. Defaults to false */ export function replaceAllText(node, searchValue, // eslint-disable-next-line @typescript-eslint/no-explicit-any replacer, textOnly = false) { for (const child of [...node.childNodes]) { // Check the type of the node if (child.nodeType === Node.TEXT_NODE) { // The original node value. const val = child.textContent; // The new value. // @ts-expect-error: Weird TS error on the replacer parameter const newVal = val.replaceAll(searchValue, replacer); // Only replace text if the new value is actually different! if (newVal !== val) { if (!textOnly && (/</.test(newVal) || /&[^;]+;/.test(newVal))) { // The new value contains HTML or an HTML entity, we need to replace the node child.parentNode.insertBefore(createFragmentFromTemplate(newVal), child); child.remove(); } else { // The new value contains no HTML, so it can be set in the same node child.textContent = newVal; } } } else if (child.nodeType === Node.ELEMENT_NODE) { // Let's process the Node with the same parameters replaceAllText(child, searchValue, replacer, textOnly); } } }