如何在基于Slate.js开发的笔记应用中实现类Twitter的股票提及功能?
实现Slate.js股票代码提及节点的可行方案
首先明确:用normalizeNode不是错误思路,但确实不是最适合这个场景的方案。normalizeNode的核心作用是维护文档结构的合法性(比如纠正嵌套错误、补全必填属性),而你要做的是将用户输入的特定文本实时转换为自定义节点,这属于交互层面的输入处理,用onChange钩子结合文本匹配,或者采用「实时高亮+触发转换」的模式会更合适,也更符合用户的输入体验。
下面给你两种可行的实现方案:
方案一:实时自动转换(输入完成后自动替换)
这种方案会在用户输入暂停时(比如选区折叠),自动检测文本中的股票代码并转换为提及节点。
步骤1:定义提及节点类型
首先要注册一个mention类型的节点,用于渲染股票代码:
// 定义Mention节点配置 const MentionNode = { type: 'mention', inline: true, // 标记为行内节点,和文本在同一行显示 renderElement: (props) => { const { symbol } = props.element; // 自定义样式,比如蓝色背景、圆角等 return ( <span className="mention-node" data-symbol={symbol} style={{ backgroundColor: '#e8f4fd', padding: '2px 4px', borderRadius: '4px' }} > ${symbol} </span> ); }, };
步骤2:扩展编辑器的onChange钩子
通过withPlugins创建一个编辑器扩展,在onChange中检测并转换文本:
const withStockMention = (editor) => { const { onChange } = editor; editor.onChange = () => { // 先执行原有的onChange逻辑 onChange(); // 只在选区折叠时处理(避免用户选中文本时误触发) if (editor.selection && Range.isCollapsed(editor.selection)) { // 查找所有包含股票代码的文本节点 const [match] = Editor.nodes(editor, { match: (n) => Text.isText(n) && /\$[a-zA-Z]{1,6}/.test(n.text), mode: 'lowest', // 只找最底层的文本节点 }); if (match) { const [textNode, nodePath] = match; const fullText = textNode.text; const regex = /\$([a-zA-Z]{1,6})/g; let matchResult; let offset = 0; // 遍历所有匹配的股票代码 while ((matchResult = regex.exec(fullText)) !== null) { const symbol = matchResult[1]; const startPos = matchResult.index; const endPos = startPos + symbol.length + 1; // 包含$符号的总长度 // 用Transact包裹操作,确保所有修改作为一个原子操作 Transact(editor, () => { // 分割文本节点,把匹配的部分单独拆出来 Editor.splitNode(editor, nodePath, { at: startPos }); Editor.splitNode(editor, [nodePath[0], 1], { at: endPos - startPos }); // 删除拆分出来的匹配文本 Editor.removeNodes(editor, { at: [nodePath[0], 1] }); // 插入自定义的mention节点 Editor.insertNodes(editor, { type: 'mention', symbol: symbol, children: [{ text: '' }], // Slate节点必须包含children数组 }, { at: [nodePath[0], 1] }); }); } } } }; return editor; };
步骤3:注册插件到编辑器
在初始化Slate编辑器时,把这两个插件加进去:
const editor = useSlate({ plugins: [withStockMention, MentionNode], // 其他配置... });
方案二:实时高亮+手动触发转换(更贴合Twitter体验)
这种方案不会自动转换,而是在用户输入$后实时高亮匹配的文本,当用户按下空格/回车时再转换为提及节点,避免打断输入流程。
步骤1:添加文本高亮逻辑
用Slate的decorate函数标记匹配的文本范围,然后在renderLeaf中渲染高亮样式:
// 自定义decorate函数,标记需要高亮的股票代码 const decorateMention = ([node, path]) => { if (!Text.isText(node)) return []; const highlightRanges = []; const regex = /\$[a-zA-Z]{1,6}/g; let match; while ((match = regex.exec(node.text)) !== null) { highlightRanges.push({ anchor: { path, offset: match.index }, focus: { path, offset: match.index + match[0].length }, type: 'stock-mention-preview', // 自定义标记类型 }); } return highlightRanges; }; // 自定义renderLeaf,处理高亮样式 const renderCustomLeaf = (props) => { const { leaf, attributes, children } = props; if (leaf.type === 'stock-mention-preview') { return ( <span {...attributes} style={{ backgroundColor: '#e8f4fd', borderBottom: '1px solid #1da1f2' }} > {children} </span> ); } return <span {...attributes}>{children}</span>; };
步骤2:监听键盘事件触发转换
当用户按下空格或回车时,检查当前选区是否在高亮的文本上,然后执行转换:
const handleKeyDown = (event) => { const editor = useSlate(); // 只在按下空格或回车时触发 if ([' ', 'Enter'].includes(event.key)) { const [match] = Editor.nodes(editor, { match: (n) => Text.isText(n) && /\$[a-zA-Z]{1,6}$/.test(n.text), mode: 'lowest', }); if (match) { event.preventDefault(); // 阻止默认的空格/回车行为 const [textNode, nodePath] = match; const matchResult = textNode.text.match(/\$([a-zA-Z]{1,6})$/); if (!matchResult) return; const symbol = matchResult[1]; const startPos = matchResult.index; Transact(editor, () => { // 分割并删除匹配的文本 Editor.splitNode(editor, nodePath, { at: startPos }); Editor.removeNodes(editor, { at: [nodePath[0], 1] }); // 插入mention节点 Editor.insertNodes(editor, { type: 'mention', symbol: symbol, children: [{ text: '' }], }, { at: [nodePath[0], 1] }); // 插入空格(如果是空格触发的话) if (event.key === ' ') { Editor.insertNodes(editor, { text: ' ' }); } }); } } };
步骤3:关联到编辑器组件
把decorate、renderLeaf和键盘事件绑定到编辑器:
<Slate editor={editor} initialValue={initialValue}> <Editable decorate={decorateMention} renderLeaf={renderCustomLeaf} onKeyDown={handleKeyDown} // 其他配置... /> </Slate>
为什么不推荐用normalizeNode?
normalizeNode的触发时机是文档结构发生变化且不符合预设规则时,它的设计目的是「修正错误」,而不是「处理用户输入交互」。如果用它来转换文本节点,会有两个问题:
- 容易触发无限循环:比如你在normalizeNode中删除文本节点并插入mention节点,会再次触发normalizeNode,导致死循环。
- 打断用户输入:用户正在输入
$AAP还没输完$AAPL时,normalizeNode可能就触发转换,打断输入流程。
所以更推荐用上面两种方案,尤其是方案二,更符合用户的输入习惯。
内容的提问来源于stack exchange,提问作者Filuren
相关产品推荐
相关产品推荐

