cloneUp.js

import cloneNode from './cloneNode.js';

/**
 * Clones an element and its ancestor chain up to and including the first ancestor that matches the given selector. Optionally includes siblings of each cloned ancestor.
 *
 * @param {Element} element - An Element to start cloning from
 * @param {string} selector - A String CSS selector that determines where to stop cloning ancestors
 * @param {Object} [options={}] - An Object with optional configuration:
 * - `siblings`: Set to `false` to exclude all siblings from being cloned
 * - `siblings.exclude`: A CSS selector string to exclude matching sibling elements from being cloned
 * @returns {Element} The cloned element with its ancestor chain
 */

export default function cloneUp (el, selector, options={}) {
    const clone = cloneNode(el)

    if (el.matches(selector)) {
        return clone
    }

    if (!el.parentElement) {
        return clone
    }

    const parentClone = cloneUp(el.parentElement, selector, options)

    // If siblings option is explicitly false, only add current element
    if (options.siblings === false) {
        parentClone.append(clone)
        return clone
    }

    const shouldExclude = (node) =>
        options.siblings?.exclude &&
        node instanceof Element &&
        node.matches(options.siblings.exclude)

    // Clone previous siblings
    let cursor = el.previousSibling
    while (cursor) {
        if (!shouldExclude(cursor)) {
            parentClone.prepend(cloneNode(cursor, true))
        }
        cursor = cursor.previousSibling
    }

    // Add current element clone
    parentClone.append(clone)

    // Clone next siblings
    cursor = el.nextSibling
    while (cursor) {
        if (!shouldExclude(cursor)) {
            parentClone.append(cloneNode(cursor, true))
        }
        cursor = cursor.nextSibling
    }

    return clone
}