函数如下
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 规则提取等多个关键环节,在实际开发中具有很高的实用价值。不过在使用时,也需要注意其存在的一些限制,根据具体场景合理运用。