UNPKG

@douyinfe/semi-ui

Version:

A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.

295 lines (294 loc) 12.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = require("@tiptap/react"); var _react2 = _interopRequireWildcard(require("react")); var _extensionDocument = _interopRequireDefault(require("@tiptap/extension-document")); var _extensionText = _interopRequireDefault(require("@tiptap/extension-text")); var _extensions = require("@tiptap/extensions"); var _extensionParagraph = _interopRequireDefault(require("@tiptap/extension-paragraph")); var _extensionHardBreak = _interopRequireDefault(require("@tiptap/extension-hard-break")); var _state = require("@tiptap/pm/state"); var _view = require("@tiptap/pm/view"); var _inputSlot = _interopRequireDefault(require("./extension/inputSlot")); var _selectSlot = _interopRequireDefault(require("./extension/selectSlot")); var _skillSlot = _interopRequireDefault(require("./extension/skillSlot")); var _constants = require("@douyinfe/semi-foundation/lib/cjs/aiChatInput/constants"); var _plugins = require("./extension/plugins"); var _statusExtension = _interopRequireDefault(require("./extension/statusExtension")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } const PREFIX = _constants.cssClasses.PREFIX; /** * 复制 tiptap Placeholder 扩展的 preparePlaceholderAttribute 函数,用于规范化 dataAttribute */ function preparePlaceholderAttribute(attr) { return attr // replace whitespace with dashes .replace(/\s+/g, '-') // replace non-alphanumeric characters // or special chars like $, %, &, etc. // but not dashes .replace(/[^a-zA-Z0-9-]/g, '') // and replace any numeric character at the start .replace(/^[0-9-]+/, '') // and finally replace any stray, leading dashes .replace(/^-+/, '').toLowerCase(); } /** * 自定义 Placeholder 扩展,覆盖原插件的 decorations 逻辑,让仅包含 skillSlot 和零宽字符的文档也能显示 placeholder */ const CustomPlaceholder = _extensions.Placeholder.extend({ addProseMirrorPlugins() { const dataAttribute = this.options.dataAttribute ? `data-${preparePlaceholderAttribute(this.options.dataAttribute)}` : 'data-placeholder'; // 自定义函数:检查文档是否“实际为空”——即忽略 skillSlot 节点和零宽字符后没有其他内容 const isDocActuallyEmpty = doc => { let actuallyEmpty = true; doc.descendants((node, pos, parent) => { // 如果已经发现不为空,提前终止遍历 if (!actuallyEmpty) { return false; } // 跳过 skillSlot 节点及其子节点 if (node.type.name === 'skillSlot') { return false; } // 检查文本节点是否只包含零宽字符 if (node.isText) { const textWithoutZeroWidth = (node.text || '').replace(new RegExp(_constants.strings.ZERO_WIDTH_CHAR, 'g'), ''); if (textWithoutZeroWidth.length > 0) { actuallyEmpty = false; return false; } } else if ( // 检查是否是其他非 leaf、非容器的自定义节点(如 inputSlot、selectSlot 等),如果是则不为空 node.type.name !== 'doc' && node.type.name !== 'paragraph') { // 对于 inputSlot/selectSlot 这类非 leaf、非容器的自定义节点,视为有内容 actuallyEmpty = false; return false; } return true; }); return actuallyEmpty; }; // 自定义函数:检查 paragraph 是否“实际为空”——即忽略 skillSlot 节点和零宽字符后没有其他内容 const isParagraphActuallyEmpty = paragraphNode => { let actuallyEmpty = true; paragraphNode.descendants((node, pos, parent) => { // 如果已经发现不为空,提前终止遍历 if (!actuallyEmpty) { return false; } // 跳过 skillSlot 节点及其子节点 if (node.type.name === 'skillSlot') { return false; } // 检查文本节点是否只包含零宽字符 if (node.isText) { const textWithoutZeroWidth = (node.text || '').replace(new RegExp(_constants.strings.ZERO_WIDTH_CHAR, 'g'), ''); if (textWithoutZeroWidth.length > 0) { actuallyEmpty = false; return false; } } else if (node.type.name !== 'paragraph') { // 对于其他自定义节点,视为有内容 actuallyEmpty = false; return false; } return true; }); return actuallyEmpty; }; // 自定义函数:检查 paragraph 是否包含 skillSlot const paragraphHasSkillSlot = paragraphNode => { let hasSkill = false; paragraphNode.descendants((node, pos, parent) => { if (node.type.name === 'skillSlot') { hasSkill = true; return false; } return true; }); return hasSkill; }; return [new _state.Plugin({ key: new _state.PluginKey('custom-placeholder'), props: { decorations: _ref => { let { doc, selection } = _ref; var _a; const active = this.editor.isEditable || !this.options.showOnlyWhenEditable; const { anchor } = selection; const decorations = []; if (!active) { return null; } const showPlaceholderWhenSkillOnly = (_a = this.options.showPlaceholderWhenSkillOnly) !== null && _a !== void 0 ? _a : false; const isEmptyDoc = this.editor.isEmpty || showPlaceholderWhenSkillOnly && isDocActuallyEmpty(doc); doc.descendants((node, pos) => { const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize; // 当开启 showPlaceholderWhenSkillOnly 时,使用自定义的 isParagraphActuallyEmpty const isEmpty = !node.isLeaf && (node.type.name === 'paragraph' ? showPlaceholderWhenSkillOnly ? isParagraphActuallyEmpty(node) : (0, _react.isNodeEmpty)(node) : (0, _react.isNodeEmpty)(node)); if ((hasAnchor || !this.options.showOnlyCurrent) && isEmpty) { const classes = [this.options.emptyNodeClass]; if (isEmptyDoc) { classes.push(this.options.emptyEditorClass); } // 如果开启 showPlaceholderWhenSkillOnly 且 paragraph 包含 skillSlot,添加特殊类 const hasSkill = showPlaceholderWhenSkillOnly && node.type.name === 'paragraph' && paragraphHasSkillSlot(node); if (hasSkill) { classes.push('has-skill-slot'); } const attrs = { class: classes.join(' '), [dataAttribute]: typeof this.options.placeholder === 'function' ? this.options.placeholder({ editor: this.editor, node, pos, hasAnchor }) : this.options.placeholder }; const decoration = _view.Decoration.node(pos, pos + node.nodeSize, attrs); decorations.push(decoration); } return this.options.includeChildren; }); return _view.DecorationSet.create(doc, decorations); } } })]; } }); var _default = props => { const { setEditor, onKeyDown, onChange, placeholder, extensions = [], defaultContent, onPaste, onPasteEvent, innerRef, handleKeyDown, onFocus, onBlur, handleCreate, immediatelyRender, showPlaceholderWhenSkillOnly } = props; const handleCompositionEnd = (0, _react2.useCallback)(view => { // Wait for ProseMirror to flush composition mutations before cleaning // zero-width placeholders, otherwise the slot content can be lost. setTimeout(() => { (0, _plugins.handleCompositionEndLogic)(view); }, 60); }, []); const handleTextInput = (0, _react2.useCallback)((view, from, to, text) => { if (view.composing) { return false; } return (0, _plugins.handleTextInputLogic)(view, from, to, text); }, []); const allExtensions = (0, _react2.useMemo)(() => { // 根据 showPlaceholderWhenSkillOnly 决定使用 CustomPlaceholder 还是原生的 Placeholder const customPlaceholderOptions = { placeholder: placeholder, showPlaceholderWhenSkillOnly: true }; const placeholderExtension = showPlaceholderWhenSkillOnly ? CustomPlaceholder.configure(customPlaceholderOptions) : _extensions.Placeholder.configure({ placeholder: placeholder }); return [_extensionDocument.default, _extensionParagraph.default, _extensionText.default, _extensions.UndoRedo, _extensionHardBreak.default, _inputSlot.default, _selectSlot.default, _skillSlot.default, placeholderExtension, _statusExtension.default, ...extensions]; }, [extensions, placeholder, showPlaceholderWhenSkillOnly]); const editorProps = (0, _react2.useMemo)(() => { return { handleKeyDown: handleKeyDown, handlePaste: _plugins.handlePasteLogic, handleTextInput, handleDOMEvents: { compositionend: handleCompositionEnd } }; }, [handleKeyDown, handleTextInput, handleCompositionEnd]); // const onSelectionUpdate = useCallback(({ editor }) => { // // For debug // const fromPos = editor.state.selection.from; // const { $from } = editor.state.selection; // console.log('光标/选区位置', fromPos, editor.state.selection, editor.state.doc); // // console.log('before', $from.nodeBefore, $from.nodeAfter); // }, []); const onCreate = (0, _react2.useCallback)(_ref2 => { let { editor } = _ref2; const { state, view } = editor; const tr = (0, _plugins.handleZeroWidthCharLogic)(state); if (tr) { // 一次性触发,避免多次触发导致 appendTransaction 被多次调用 view.dispatch(tr); } handleCreate(); }, [handleCreate]); const onUpdate = (0, _react2.useCallback)(_ref3 => { let { editor } = _ref3; // The content has changed. const content = editor.getText(); onChange(content); }, [onChange]); const handlePaste = (0, _react2.useCallback)(e => { var _a; // To support file paste const items = (_a = e.clipboardData) === null || _a === void 0 ? void 0 : _a.items; let files = []; if (items) { for (const it of items) { const file = it.getAsFile(); file && files.push(it.getAsFile()); } } if (files.length) { onPaste === null || onPaste === void 0 ? void 0 : onPaste(files); } }, [onPaste]); const editor = (0, _react.useEditor)({ extensions: allExtensions, content: defaultContent !== null && defaultContent !== void 0 ? defaultContent : ``, editorProps: editorProps, immediatelyRender, // onSelectionUpdate, onCreate, onUpdate, onPaste: handlePaste }); (0, _react2.useEffect)(() => { setEditor(editor); }, [editor, setEditor]); if (!editor) { // Prevent rendering until the editor is initialized return null; } return /*#__PURE__*/_react2.default.createElement(_react2.default.Fragment, null, /*#__PURE__*/_react2.default.createElement(_react.EditorContent, { editor: editor, onKeyDown: onKeyDown, onFocus: onFocus, onBlur: onBlur, onPaste: onPasteEvent, ref: innerRef, className: `${PREFIX}-editor-content` })); }; exports.default = _default;