npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@blockquote/frontend-utilities

v1.0.2

Published

Utilities for managing the DOM, handling events, and performing various common tasks in frontend development.

Downloads

260

Readme

Utilities for managing the DOM, handling events, and performing various common tasks in frontend development.

This repository provides a set of utility functions designed to facilitate DOM manipulation, event handling, and other common tasks in frontend development. These functions address various needs such as re-dispatching events, checking element visibility, and traversing the DOM tree.

Table of Contents

Re-dispatch Event Functions

redispatchEvent

/**
 * Re-dispatches an event from the provided element.
 *
 * This function is useful for forwarding non-composed events, such as `change`
 * events.
 *
 * @example
 * class MyInput extends LitElement {
 *   render() {
 *     return html`<input @change=${this.redispatchEvent}>`;
 *   }
 *
 *   protected redispatchEvent(event: Event) {
 *     redispatchEvent(this, event);
 *   }
 * }
 *
 * @example
 * class MyDialog extends LitElement {
 *   render() {
 *     return html`<dialog @close=${this.redispatchEvent}>...</dialog>`;
 *   }
 *
 *   protected redispatchEvent(ev: Event) {
 *     redispatchEvent(this, ev, { cancelable: true });
 *   }
 * }
 *
 * @param {Element} element - The element to dispatch the event from.
 * @param {Event|string} ev - The event to re-dispatch. If it's a string, a new Event is created.
 * @param {Object} [options={}] - An object with properties to override in the new event.
 * @returns {boolean} - Whether or not the event was dispatched (if cancelable).
 */
export const redispatchEvent = (element, ev, options) => {
  if (typeof ev === 'string') {
    const eventType = ev;
    const newEvent = new CustomEvent(eventType);
    return redispatchEventFromEvent(element, newEvent, options);
  }
  return redispatchEventFromEvent(element, ev, options);
};

Element Visibility and Focus Functions

isElementInvisible

/**
 * Determines if an element should be ignored by a screen reader.
 * @param {Element} element - The DOM element to check.
 * @param {Array} [exceptions=['dialog', '[popover]']] - Array of Elements to ignore when checking the element.
 * @returns {boolean} True if the element should be ignored by a screen reader, false otherwise.
 */
export const isElementInvisible = (element, exceptions = ['dialog', '[popover]']) => {
  if (!element || !(element instanceof HTMLElement)) {
    return false;
  }

  if (element.matches(exceptions.join(','))) {
    return false;
  }

  const {display, visibility, opacity} = window.getComputedStyle(element);
  const isStyleHidden =
    display === 'none' || visibility === 'hidden' || visibility === 'collapse' || opacity === '0';
  const isAttributeHidden = element.matches('[disabled], [hidden], [inert], [aria-hidden="true"]');

  return isStyleHidden || isAttributeHidden;
};

isFocusable

/**
 * Checks if an element is focusable. An element is considered focusable if it matches
 * standard focusable elements criteria (such as buttons, inputs, etc., that are not disabled
 * and do not have a negative tabindex) or is a custom element with a shadow root that delegates focus.
 *
 * @param {Element} element - The DOM element to check for focusability.
 * @returns {boolean} True if the element is focusable, false otherwise.
 */
export const isFocusable = (element) => {
  if (!(element instanceof HTMLElement)) {
    return false;
  }

  // https://stackoverflow.com/a/30753870/76472
  const knownFocusableElements = `a[href],area[href],button:not([disabled]),details,iframe,object,input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[contentEditable="true"],[tabindex]:not([tabindex^="-"]),audio[controls],video[controls]`;

  if (element.matches(knownFocusableElements)) {
    return true;
  }

  const isDisabledCustomElement =
    element.localName.includes('-') && element.matches('[disabled], [aria-disabled="true"]');
  if (isDisabledCustomElement) {
    return false;
  }
  return /** @type {ShadowRoot | *} */ (element.shadowRoot)?.delegatesFocus ?? false;
};

DOM Traversal Functions

walkComposedTree

/**
 * Traverse the composed tree from the root, selecting elements that meet the provided filter criteria.
 * You can pass [NodeFilter](https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter) or 0 to retrieve all nodes.
 *
 * @param {Object} options - The options object.
 * @param {Node} options.root - The root node for traversal. **Required**.
 * @param {number} [options.whatToShow=0] - NodeFilter code for node types to include. Defaults to `0`, which selects all node types.
 * @param {function} [options.filterAccept=(node: Node) => true] - Filters nodes. Child nodes are considered even if the parent does not satisfy the filter. Defaults to a function that always returns `true`.
 * @param {function} [options.filterReject=(node: Node) => false] - Filters nodes to reject. Any node in the subtree based at this node is not included. Defaults to a function that always returns `false`.
 * @returns {IterableIterator<Node>} An iterator yielding nodes meeting the filter criteria.
 */
export function* walkComposedTree({
  root,
  whatToShow = 0,
  filterAccept = () => true,
  filterReject = () => false,
}) {
  if ((whatToShow && root.nodeType !== whatToShow) || filterReject(root)) {
    return;
  }

  if (filterAccept(root)) {
    yield root;
  }

  const children =
    root instanceof HTMLElement && root.shadowRoot
      ? root.shadowRoot.children
      : root instanceof HTMLSlotElement
        ? root.assignedNodes({flatten: true})
        : root.childNodes;

  for (const child of children) {
    yield* walkComposedTree({root: child, whatToShow, filterAccept, filterReject});
  }
}

getFirstAndLastItems

/**
 * Retrieves the first and last children of a node using a TreeWalker.
 *
 * @param {IterableIterator<HTMLElement>} walker - The TreeWalker object used to traverse the node's children.
 * @returns {[first: HTMLElement|null, last: HTMLElement|null]} An array containing the first and last children. If no children are found, `null` is returned for both.
 */
export const getFirstAndLastItems = (walker) => {
  let firstFocusableChild = null;
  let lastFocusableChild = null;

  for (const currentNode of walker) {
    if (!firstFocusableChild) {
      firstFocusableChild = currentNode;
    }
    lastFocusableChild = currentNode;
  }

  return [firstFocusableChild, lastFocusableChild];
};

getDeepActiveElement

/**
 * Returns the deepest active element, considering Shadow DOM subtrees
 * @param {Document | ShadowRoot} root - The root element to start the search from.
 * @returns {Element} The deepest active element or body element if no active element is found.
 */
export const getDeepActiveElement = (root = document) => {
  const activeEl = root?.activeElement;
  if (activeEl) {
    if (activeEl.shadowRoot) {
      return getDeepActiveElement(activeEl.shadowRoot) ?? activeEl;
    }
    return activeEl;
  }
  return document.body;
};

deepContains

/**
 * Returns true if the first node contains the second, even if the second node
 * is in a shadow tree.
 *
 * The standard Node.contains() function does not account for Shadow DOM, and
 * returns false if the supplied target node is sitting inside a shadow tree
 * within the container.
 *
 * @param {Node} container - The container to search within.
 * @param {Node} target - The node that may be inside the container.
 * @returns {boolean} - True if the container contains the target node.
 */
export const deepContains = (container, target) => {
  /** @type {any} */
  let current = target;
  while (current) {
    const parent = current.assignedSlot || current.parentNode || current.host;
    if (parent === container) {
      return true;
    }
    current = parent;
  }
  return false;
};

composedAncestors

/**
 * Return the ancestors of the given node in the composed tree.
 *
 * In the composed tree, the ancestor of a node assigned to a slot is that slot,
 * not the node's DOM ancestor. The ancestor of a shadow root is its host.
 *
 * @author Jan Miksovsky
 * @see https://github.com/JanMiksovsky/elix/blob/main/src/core/dom.js
 *
 * @param {Node} node
 * @returns {Iterable<Node>}
 */
export function* composedAncestors(node) {
  for (let /** @type {Node|null} */ current = node; current; ) {
    const next =
      current instanceof HTMLElement && current.assignedSlot
        ? current.assignedSlot
        : current instanceof ShadowRoot
          ? current.host
          : current.parentNode;
    if (next) {
      yield next;
    }
    current = next;
  }
}

Event Handling Functions

isClickInsideRect

/**
 * Checks if a click event occurred inside a given bounding rectangle.
 *
 * @param {DOMRect} rect - The bounding rectangle, typically obtained from `element.getBoundingClientRect()`.
 * @param {PointerEvent} ev - The click event.
 * @returns {boolean} True if the click occurred inside the rectangle, false otherwise.
 */
export const isClickInsideRect = (rect, ev) => {
  const {top, left, height, width} = rect;
  const {clientX, clientY} = ev;
  return clientY >= top && clientY <= top + height && clientX >= left && clientX <= left + width;
};

Snapshots Functions

htmlStructureSnapshot

/**
 * Returns the outerHTML or innerHTML of a node after removing specified attributes from it and its children.
 *
 * If the initial node is an HTMLElement, the function returns the outerHTML of the node.
 * If the initial node is a ShadowRoot, the function returns the innerHTML of the node.
 *
 * @param {Node} node - The initial node.
 * @param {string[]} [ignoreAttributes=[]] - An optional array of strings representing attributes to ignore.
 * @returns {string} The outerHTML or innerHTML of the node after removing the specified attributes.
 */
export const htmlStructureSnapshot = (node, ignoreAttributes = []) => {
  const isHTMLElement = node instanceof HTMLElement;
  const isShadowRoot = node instanceof ShadowRoot;

  if (isHTMLElement || isShadowRoot) {
    if (ignoreAttributes.length > 0) {
      removeAttributes(node, ignoreAttributes);
    }

    const htmlContent = isShadowRoot ? node.innerHTML : node.outerHTML;
    return removeCommentsAndFormat(htmlContent);
  }
  return '';
};

Miscellaneous Functions

randomID

/**
 * Generates a random alphanumeric string of a specified length.
 *
 * @param {number} [length=10] - The length of the random string to generate. Default is 10.
 * @returns {string} A random alphanumeric string of the specified length.
 */
export const randomID = (length = 10) => Math.random().toString(36).substring(2, length);

urlToPlainObject

/**
 * Converts a URL object to a plain object.
 *
 * @author Cory LaViska
 * @see https://www.abeautifulsite.net/posts/converting-a-url-object-to-a-plain-object-in-java-script
 *
 * @param {URL|string} url - The URL object to parse.
 * @returns {Object} An object representing the parsed URL.
 */
export const urlToPlainObject = (url) => {
  const urlObject = typeof url === 'string' ? new URL(url) : url;
  const plainObject = {};

  for (const key in urlObject) {
    if (typeof urlObject[key] === 'string') {
      plainObject[key] = urlObject[key];
    }
  }
  return plainObject;
};

Author Information

  • Original Authors: @material/web, Jan Miksovsky, Cory LaViska

Scaffold generated using:

npm init @blockquote/wc