@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
JavaScript
;
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;