@ant-design/x
Version:
Craft AI-driven interfaces effortlessly
94 lines (90 loc) • 3.27 kB
JavaScript
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)));
};