UNPKG

@ant-design/x

Version:

Craft AI-driven interfaces effortlessly

94 lines (90 loc) 3.27 kB
import { useEvent } from '@rc-component/util'; import { Button, Flex } from 'antd'; import React from 'react'; import { useLocale } from "../locale"; import en_US from "../locale/en_US"; /** * 判断块级元素(跨浏览器) * div.contentEditable 在换行时会注入块级元素以达成换行效果 * 编辑后提取格式化纯文本,需要识别出这些块级元素并替换为 \n * */ function isBlock(el) { const d = getComputedStyle(el).display; return d === 'block' || d === 'flex' || d === 'list-item' || d === 'table'; } function getPlainTextWithFormat(dom) { const lines = ['']; const walker = document.createTreeWalker(dom, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT); while (walker.nextNode()) { const node = walker.currentNode; if (node.nodeType === Node.TEXT_NODE) { // textContent 拒绝直接 xss lines[lines.length - 1] += node.textContent; continue; } // 单纯空行结构 <div><br></div>(chrome/edge/safari/firefox),仅保留一个换行 if (node.tagName === 'BR' && node.parentNode?.childElementCount === 1) { continue; } // 换行 if (node.tagName === 'BR' || isBlock(node)) { lines.push(''); } } return lines.join('\n'); } export const EditableContent = ({ content, prefixCls, okText, cancelText, onEditConfirm, onEditCancel }) => { const mockInputRef = React.useRef(null); const [contextLocale] = useLocale('Bubble', en_US.Bubble); const onConfirm = useEvent(() => { // 但 onEditing 端应该对入参做 xss 防护 onEditConfirm?.(getPlainTextWithFormat(mockInputRef.current)); }); const onCancel = useEvent(() => onEditCancel?.()); React.useEffect(() => { mockInputRef.current.textContent = content; mockInputRef.current.focus(); const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(mockInputRef.current); range.collapse(false); selection.removeAllRanges(); selection.addRange(range); }, []); // 拒绝非 string content,保证 div 渲染纯文本(Text Node)而不是 HTML if (typeof content !== 'string') throw new Error('Content of editable Bubble should be string'); // 避免组件更新,影响光标位置 // 初始化文本使用 content,后续由编辑内容确定 const memoedMockInput = React.useMemo(() => /*#__PURE__*/ /** * 为什么使用 div * input、textarea 是固定行为、固定宽高的元素,无法对内容自适应,体验差 * div.contentEditable 提供了编辑 innerHTML 的能力,同时具备内容自适应能力,体验好 */ React.createElement("div", { ref: mockInputRef, contentEditable: true }), []); return /*#__PURE__*/React.createElement(React.Fragment, null, memoedMockInput, /*#__PURE__*/React.createElement(Flex, { rootClassName: `${prefixCls}-editing-opts`, gap: 8 }, /*#__PURE__*/React.createElement(Button, { type: "primary", shape: "round", size: "small", onClick: onConfirm }, okText || contextLocale.editableOk), /*#__PURE__*/React.createElement(Button, { type: "text", shape: "round", size: "small", onClick: onCancel }, cancelText || contextLocale.editableCancel))); };