富文本编辑器中为多个独立span元素批量应用样式的问题
富文本编辑器中为多个独立span元素批量应用样式的问题
最近我打算自己实现一个富文本编辑器,一开始用execCommand的版本我还挺满意,但还是想自己从头实现一遍,折腾一下。
下面是我目前能正常运行的版本代码:
import React, { useRef } from "react"; import "./TextEditor_v2.css"; const ModernTextEditor = () => { const editorRef = useRef(); const applyStyle = (style, value) => { const selection = window.getSelection(); if (!selection.rangeCount) return; const range = selection.getRangeAt(0); if (range.collapsed) return; const selectedContent = range.cloneContents(); const fragment = document.createDocumentFragment(); const mergeStyles = (existingStyles, newStyles) => { const tempSpan = document.createElement("span"); tempSpan.style.cssText = existingStyles; if (style === "bold") tempSpan.style.fontWeight = value ? "bold" : "normal"; if (style === "italic") tempSpan.style.fontStyle = value ? "italic" : "normal"; if (style === "color") tempSpan.style.color = value; if (style === "fontSize") tempSpan.style.fontSize = value + "px"; if (style === "fontFamily") tempSpan.style.fontFamily = value; return tempSpan.style.cssText; }; const processNode = (node, parentStyles = "") => { if (node.nodeType === Node.TEXT_NODE) { const span = document.createElement("span"); span.style.cssText = mergeStyles(parentStyles, style); span.textContent = node.textContent; return span; } else if (node.nodeType === Node.ELEMENT_NODE) { const combinedStyles = mergeStyles(node.style.cssText, style); const span = document.createElement("span"); span.style.cssText = combinedStyles; Array.from(node.childNodes).forEach((child) => { span.appendChild(processNode(child, combinedStyles)); }); return span; } return node; }; Array.from(selectedContent.childNodes).forEach((node) => { fragment.appendChild(processNode(node)); }); range.deleteContents(); range.insertNode(fragment); selection.removeAllRanges(); }; const handleCommand = (command, value = null) => { applyStyle(command, value); }; return ( <div className="text-editor"> <div className="toolbar"> <button onClick={() => handleCommand("bold", true)}>Bold</button> <button onClick={() => handleCommand("italic", true)}>Italic</button> <input type="color" onChange={(e) => handleCommand("color", e.target.value)} /> <select onChange={(e) => handleCommand("fontSize", e.target.value)} defaultValue="16" > {[12, 14, 16, 18, 20, 24, 28].map((size) => ( <option key={size} value={size}> {size}px </option> ))} </select> <select onChange={(e) => handleCommand("fontFamily", e.target.value)} defaultValue="Arial" > {["Arial", "Georgia", "Courier New", "Verdana"].map((font) => ( <option key={font} value={font}> {font} </option> ))} </select> </div> <div ref={editorRef} className="editor" contentEditable={true} suppressContentEditableWarning={true} ></div> </div> ); }; export default ModernTextEditor;
对应的样式文件:
.text-editor { border: 1px solid #ccc; border-radius: 5px; width: 80%; margin: 20px auto; padding: 10px; } .toolbar { display: flex; gap: 10px; margin-bottom: 10px; } .editor { min-height: 200px; border: 1px solid #eee; padding: 10px; font-family: Arial, sans-serif; font-size: 16px; outline: none; cursor: text; }
这个版本是基于contentEditable的div来实现的,应用样式时会把选中的文本包裹在带有对应样式的span标签里。但我特别不爽的是,每次应用新样式都会嵌套一层新的span,所以我尝试重写了applyStyle方法:
const applyStyle = (style, value) => { const selection = window.getSelection(); if (!selection.rangeCount) return; const range = selection.getRangeAt(0); if (range.collapsed) return; // No text selected const selectedText = range.extractContents(); const parentNode = range.startContainer.parentNode; const processNode = (node) => { if (node.nodeType === Node.TEXT_NODE) { // Create a span to wrap this text node with the new style const span = document.createElement("span"); // Preserve existing styles if (parentNode.nodeName === "SPAN") { span.style.cssText = parentNode.style.cssText; } // Apply the new style if (style === "bold") span.style.fontWeight = value ? "bold" : "normal"; if (style === "italic") span.style.fontStyle = value ? "italic" : "normal"; if (style === "color") span.style.color = value; if (style === "fontSize") span.style.fontSize = value + "px"; if (style === "fontFamily") span.style.fontFamily = value; span.textContent = node.textContent; return span; } else if (node.nodeName === "SPAN") { // If the node is a span, recursively process its children const newSpan = document.createElement("span"); newSpan.style.cssText = node.style.cssText; // Apply the new style to this span if (style === "bold") newSpan.style.fontWeight = value ? "bold" : "normal"; if (style === "italic") newSpan.style.fontStyle = value ? "italic" : "normal"; if (style === "color") newSpan.style.color = value; if (style === "fontSize") newSpan.style.fontSize = value + "px"; if (style === "fontFamily") newSpan.style.fontFamily = value; Array.from(node.childNodes).forEach((child) => { newSpan.appendChild(processNode(child)); }); return newSpan; } return node; }; const newNodes = []; Array.from(selectedText.childNodes).forEach((node) => { newNodes.push(processNode(node)); }); // Split existing span into 3 parts if selection is in the middle const startOffset = range.startOffset; const endOffset = range.endOffset; if (parentNode.nodeName === "SPAN") { const textContent = parentNode.textContent; const beforeText = textContent.slice(0, startOffset); const afterText = textContent.slice(endOffset); if (beforeText) { const beforeSpan = document.createElement("span"); beforeSpan.style.cssText = parentNode.style.cssText; beforeSpan.textContent = beforeText; parentNode.parentNode.insertBefore(beforeSpan, parentNode); } if (afterText) { const afterSpan = document.createElement("span"); afterSpan.style.cssText = parentNode.style.cssText; afterSpan.textContent = afterText; parentNode.parentNode.insertBefore(afterSpan, parentNode.nextSibling); } parentNode.parentNode.removeChild(parentNode); } // Insert the new styled nodes newNodes.forEach((node) => { range.insertNode(node); }); // Clean up selection selection.removeAllRanges(); };
这个新版本的方法能处理不少场景,比如当我只选中一个span中间的部分时,它会把这个span拆分成三个独立的span(选中部分前后保留原样式,中间应用新样式)。但现在遇到了一个棘手的问题:当我选中多个独立的span元素(比如先写两个单词,分别给它们设置不同的样式,然后同时选中这两个单词想修改它们的颜色),结果就变得一团糟,样式完全乱了套。
备注:内容来源于stack exchange,提问作者Araders
相关产品推荐
相关产品推荐

