@lesjoursfr/browser-tools
Version:
Some browser tools for events & DOM manipulation.
596 lines (595 loc) • 19.2 kB
JavaScript
/**
* 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);
}
}
}