一个函数帮你省不少功夫——复制dom节点及样式

184 阅读5分钟

函数如下

function copyElementWithAllStyles(element) {
    const clone = element.cloneNode(true);
    const usedClasses = new Set();

    function processNode(node) {
        if (node.nodeType !== 1) return;

        // 优先用 classList,兼容 SVG/HTMLElement
        if (node.classList && node.classList.length > 0) {
            node.classList.forEach(cls => usedClasses.add(cls));
        } else if (typeof node.className === "string" && node.className.trim()) {
            node.className.split(/\s+/).forEach(cls => usedClasses.add(cls));
        }

        const computedStyle = window.getComputedStyle(node);
        for (let i = 0; i < computedStyle.length; i++) {
            const property = computedStyle[i];
            node.style.setProperty(
                property, 
                computedStyle.getPropertyValue(property),
                computedStyle.getPropertyPriority(property)
            );
        }
        Array.from(node.children).forEach(processNode);
    }
    processNode(clone);

    function escapeClassForSelector(cls) {
        if (window.CSS && window.CSS.escape) {
            return '.' + window.CSS.escape(cls);
        }
        return '.' + cls.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1');
    }

    const styleSheet = document.createElement('style');
    let cssText = '';

    Array.from(document.styleSheets).forEach(sheet => {
        let rules;
        try {
            rules = sheet.rules || sheet.cssRules;
        } catch (e) {
            return;
        }
        if (!rules) return;
        Array.from(rules).forEach(rule => {
            if (rule.type === CSSRule.STYLE_RULE) {
                const selector = rule.selectorText;
                for (const cls of usedClasses) {
                    const escaped = escapeClassForSelector(cls);
                    if (selector && selector.includes(escaped)) {
                        cssText += rule.cssText + '\n';
                        break;
                    }
                }
            }
        });
    });

    if (cssText) {
        styleSheet.textContent = cssText;
        if (clone.firstChild) {
            clone.insertBefore(styleSheet, clone.firstChild);
        } else {
            clone.appendChild(styleSheet);
        }
    }
    return clone;
}

详解能复制 DOM 元素及内部所有子元素样式的函数

在前端开发中,复制 DOM 元素是一个常见需求,但仅仅复制元素结构往往不够,还需要保留其样式,尤其是当元素样式依赖于复杂的 CSS 规则、计算样式时,复制样式就成了一个难题。今天要介绍的copyElementWithAllStyles函数,就能完美解决这个问题,它可以复制 DOM 元素及其内部所有子元素的样式,让复制后的元素和原元素看起来一模一样。

函数解决的核心问题

在常规的元素复制中,使用cloneNode(true)只能复制元素的结构和属性,对于通过 CSS 类定义的样式、继承的样式以及计算后的样式,往往无法完整复制。这会导致复制后的元素失去原有的样式表现,而copyElementWithAllStyles函数就是为了弥补这一缺陷,确保复制后元素在样式上与原元素保持一致。

函数实现原理详解

元素克隆

函数首先通过const clone = element.cloneNode(true);克隆原元素及其所有子元素,这一步得到了与原元素结构完全一致的克隆元素。

收集使用的类名并处理样式

函数定义了processNode函数,用于处理每个节点。在这个函数中:

  • 首先判断节点类型,如果不是元素节点则返回。的收集元素使用的类名,将其存储在usedClasses集合中,方便后续处理与这些类名相关的 CSS 规则。
  • 通过window.getComputedStyle(node)获取元素的计算样式,计算样式包含了元素所有最终应用的样式,包括继承的样式、通过 CSS 类定义的样式等。
  • 遍历计算样式的每个属性,通过node.style.setProperty将计算样式应用到克隆元素的内联样式中,确保元素的基本样式得以保留。
  • 递归处理元素的所有子元素,保证子元素的样式也能被正确处理。

Tailwind JIT 类名转义

由于 Tailwind JIT 生成的类名可能包含特殊字符,如bg-[#0086ff],这些特殊字符在 CSS 选择器中需要进行转义才能正确识别。函数中的escapeClassForSelector函数就负责处理类名的转义:

  • 首先检查是否有window.CSS.escape方法,如果有则使用该方法进行转义,这是符合 CSS 规范的转义方式。
  • 如果没有window.CSS.escape方法,则使用手动转义的方式,将特殊字符进行转义,确保生成的 CSS 选择器是有效的。

提取相关 CSS 规则并应用

为了确保与元素使用的类名相关的 CSS 规则也能被复制,函数进行了以下操作:

  • 创建一个style元素,用于存储提取到的 CSS 规则。
  • 遍历文档中所有的样式表,对于每个样式表,尝试获取其 CSS 规则。需要注意的是,跨域的样式表无法访问其规则,这里通过try-catch语句捕获异常并跳过。
  • 对于每个 CSS 规则,判断是否为样式规则。如果是,遍历之前收集的usedClasses集合,检查规则的选择器是否包含与这些类名相关的转义后的选择器。如果包含,则将该 CSS 规则的文本添加到cssText中。
  • 最后,如果cssText不为空,将其设置为style元素的内容,并将style元素插入到克隆元素中,确保与类名相关的 CSS 规则也能应用到克隆元素上。

使用场景

  • 在需要动态复制元素并保持样式一致的场景中,如拖拽复制元素、生成元素的副本用于预览等。
  • 在一些富文本编辑器中,当需要复制粘贴带有复杂样式的内容时,该函数可以确保样式的完整性。
  • 在进行页面快照生成或元素备份时,能保证复制的元素在样式上与原元素一致。

注意事项

  • 该函数无法处理跨域的 CSS 样式表,因为跨域的样式表规则无法访问,可能会导致部分与跨域样式相关的样式无法复制。
  • 对于一些通过 JavaScript 动态添加的样式,如果没有反映在计算样式中,可能无法被正确复制。
  • 由于函数会将计算样式转换为内联样式,并添加相关的 CSS 规则,可能会导致复制后的元素样式代码较为冗余。

总结

copyElementWithAllStyles函数通过巧妙的方式实现了 DOM 元素及其内部所有子元素样式的完整复制,解决了常规元素复制中样式丢失的问题。它的实现逻辑清晰,涵盖了元素克隆、样式处理、类名转义、CSS 规则提取等多个关键环节,在实际开发中具有很高的实用价值。不过在使用时,也需要注意其存在的一些限制,根据具体场景合理运用。