@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.
450 lines • 19.2 kB
JavaScript
var __rest = this && this.__rest || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]];
}
return t;
};
import React, { useCallback, useMemo, useState } from 'react';
import { TextStyleKit } from '@tiptap/extension-text-style';
import StarterKit from '@tiptap/starter-kit';
import { Image } from "@tiptap/extension-image";
import { Mark } from '@tiptap/core';
import { EditorContent, useEditor, useEditorState } from '@tiptap/react';
import { Button, Toast, Divider, Dropdown, Input, Collapse } from '../../index';
import { cssClasses } from '@douyinfe/semi-foundation/lib/es/sidebar/constants';
import { IconH1, IconHn, IconH2, IconH3, IconH4, IconH5, IconH6, IconList, IconOrderedList, IconQuote, IconLink, IconItalic, IconStrikeThrough, IconText, IconBold, IconCode, IconMinus, IconUndo, IconRedo, IconCheckCircleStroked, IconDeleteStroked, IconAlignLeft, IconAlignJustify, IconAlignCenter, IconAlignRight, IconImage } from '@douyinfe/semi-icons';
import cls from 'classnames';
import { CollapseHeader } from './code';
import { TextAlign } from '@tiptap/extension-text-align';
import { ImageUploadNode } from './imageSlot';
import LocaleConsumer from '../../locale/localeConsumer';
const collapseCls = cssClasses.COLLAPSE;
const prefixCls = cssClasses.FILE;
// 用于保证在输入link ,选区 UI 对用户保持一致性,否则会在输入时候,因为富文本本区域焦点丢失而看不到选区
const SelectionMark = Mark.create({
name: 'selectionMark',
inclusive: false,
parseHTML() {
return [{
tag: 'span.select'
}];
},
renderHTML() {
return ['span', {
class: 'select'
}, 0];
}
});
const ConfigureButton = /*#__PURE__*/React.memo(props => {
const {
active,
className
} = props,
rest = __rest(props, ["active", "className"]);
return /*#__PURE__*/React.createElement(Button, Object.assign({}, rest, {
theme: 'borderless',
type: 'tertiary',
className: cls(`${prefixCls}-menu-bar-btn`, {
[`${prefixCls}-menu-bar-btn-active`]: active,
[className]: className
})
}));
});
const ConfigureDropdownItem = /*#__PURE__*/React.memo(props => {
const {
active,
children
} = props,
rest = __rest(props, ["active", "children"]);
return /*#__PURE__*/React.createElement(Dropdown.Item, Object.assign({
className: cls(`${prefixCls}-menu-bar-dropdown-item`, {
[`${prefixCls}-menu-bar-dropdown-item-active`]: active
})
}, rest), children);
});
function MenuBar(_ref) {
let {
editor,
className
} = _ref;
const [linkDropdownVisible, setLinkDropdownVisible] = useState(false);
const [linkInputValue, setLinkInputValue] = useState('');
const [linkSelectionRange, setLinkSelectionRange] = useState(null);
const editorState = useEditorState({
editor,
selector: ctx => {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4;
const {
from,
to
} = ctx.editor.state.selection;
const hasSelection = from !== to;
const hasCursor = from === to && ctx.editor.isFocused;
return {
isBold: (_a = ctx.editor.isActive('bold')) !== null && _a !== void 0 ? _a : false,
canBold: (_b = ctx.editor.can().chain().toggleBold().run()) !== null && _b !== void 0 ? _b : false,
isItalic: (_c = ctx.editor.isActive('italic')) !== null && _c !== void 0 ? _c : false,
canItalic: (_d = ctx.editor.can().chain().toggleItalic().run()) !== null && _d !== void 0 ? _d : false,
isStrike: (_e = ctx.editor.isActive('strike')) !== null && _e !== void 0 ? _e : false,
canStrike: (_f = ctx.editor.can().chain().toggleStrike().run()) !== null && _f !== void 0 ? _f : false,
isCode: (_g = ctx.editor.isActive('code')) !== null && _g !== void 0 ? _g : false,
canCode: (_h = ctx.editor.can().chain().toggleCode().run()) !== null && _h !== void 0 ? _h : false,
canClearMarks: (_j = ctx.editor.can().chain().unsetAllMarks().run()) !== null && _j !== void 0 ? _j : false,
isParagraph: (_k = ctx.editor.isActive('paragraph')) !== null && _k !== void 0 ? _k : false,
isHeading: (_l = ctx.editor.isActive('heading')) !== null && _l !== void 0 ? _l : false,
isHeading1: (_m = ctx.editor.isActive('heading', {
level: 1
})) !== null && _m !== void 0 ? _m : false,
isHeading2: (_o = ctx.editor.isActive('heading', {
level: 2
})) !== null && _o !== void 0 ? _o : false,
isHeading3: (_p = ctx.editor.isActive('heading', {
level: 3
})) !== null && _p !== void 0 ? _p : false,
isHeading4: (_q = ctx.editor.isActive('heading', {
level: 4
})) !== null && _q !== void 0 ? _q : false,
isHeading5: (_r = ctx.editor.isActive('heading', {
level: 5
})) !== null && _r !== void 0 ? _r : false,
isHeading6: (_s = ctx.editor.isActive('heading', {
level: 6
})) !== null && _s !== void 0 ? _s : false,
isBulletList: (_t = ctx.editor.isActive('bulletList')) !== null && _t !== void 0 ? _t : false,
isOrderedList: (_u = ctx.editor.isActive('orderedList')) !== null && _u !== void 0 ? _u : false,
isCodeBlock: (_v = ctx.editor.isActive('codeBlock')) !== null && _v !== void 0 ? _v : false,
isBlockquote: (_w = ctx.editor.isActive('blockquote')) !== null && _w !== void 0 ? _w : false,
isLink: (_x = ctx.editor.isActive('link')) !== null && _x !== void 0 ? _x : false,
canLink: hasSelection || hasCursor,
canUndo: (_y = ctx.editor.can().chain().undo().run()) !== null && _y !== void 0 ? _y : false,
canRedo: (_z = ctx.editor.can().chain().redo().run()) !== null && _z !== void 0 ? _z : false,
isAlignLeft: (_0 = ctx.editor.isActive({
textAlign: 'left'
})) !== null && _0 !== void 0 ? _0 : false,
isAlignCenter: (_1 = ctx.editor.isActive({
textAlign: 'center'
})) !== null && _1 !== void 0 ? _1 : false,
isAlignRight: (_2 = ctx.editor.isActive({
textAlign: 'right'
})) !== null && _2 !== void 0 ? _2 : false,
isAlignJustify: (_3 = ctx.editor.isActive({
textAlign: 'justify'
})) !== null && _3 !== void 0 ? _3 : false,
isImage: (_4 = ctx.editor.isActive("imageUpload")) !== null && _4 !== void 0 ? _4 : false,
canInsertImage: ctx.editor.can().insertContent({
type: "imageUpload"
})
};
}
});
const handleConfirmLink = useCallback(locale => {
const href = linkInputValue.trim();
if (!href) {
return;
}
const {
from,
to
} = linkSelectionRange !== null && linkSelectionRange !== void 0 ? linkSelectionRange : editor.state.selection;
const chain = editor.chain().focus();
if (from !== to) {
// With a selection area, set a link for the currently selected text.
chain.setTextSelection({
from,
to
}).extendMarkRange('link').setLink({
href
}).unsetMark('selectionMark').run();
} else {
// No selection area but cursor: Insert a linked text at the cursor position.
chain.setTextSelection(from).insertContent({
type: 'text',
text: href,
marks: [{
type: 'link',
attrs: {
href
}
}]
}).unsetMark('selectionMark').run();
}
Toast.success(locale.linkAddSuccess);
setLinkDropdownVisible(false);
setLinkSelectionRange(null);
}, [editor, linkInputValue, linkSelectionRange]);
const handleUnsetLink = useCallback(locale => {
editor.chain().focus().unsetLink().unsetMark('selectionMark').run();
Toast.success(locale.linkRemoveSuccess);
setLinkDropdownVisible(false);
setLinkSelectionRange(null);
}, [editor]);
const handleLinkInputKeyDown = useCallback((e, locale) => {
if (e.key === 'Enter') {
handleConfirmLink(locale);
}
}, [handleConfirmLink]);
const handleImageAdd = useCallback(() => {
if (!editor) {
return false;
}
try {
editor.chain().focus().insertContent({
type: "imageUpload"
}).run();
} catch (_a) {
return false;
}
return true;
}, [editor]);
return /*#__PURE__*/React.createElement("div", {
className: className
}, /*#__PURE__*/React.createElement(ConfigureButton, {
icon: /*#__PURE__*/React.createElement(IconUndo, null),
onClick: () => editor.chain().focus().undo().run(),
disabled: !editorState.canUndo
}), /*#__PURE__*/React.createElement(ConfigureButton, {
icon: /*#__PURE__*/React.createElement(IconRedo, null),
onClick: () => editor.chain().focus().redo().run(),
disabled: !editorState.canRedo
}), /*#__PURE__*/React.createElement(Divider, {
layout: "vertical"
}), /*#__PURE__*/React.createElement(Dropdown, {
render: /*#__PURE__*/React.createElement(Dropdown.Menu, null, /*#__PURE__*/React.createElement(ConfigureDropdownItem, {
className: editorState.isHeading1 ? `${prefixCls}-menu-bar-dropdown-item-active` : '',
onClick: () => editor.chain().focus().toggleHeading({
level: 1
}).run()
}, /*#__PURE__*/React.createElement(IconH1, null)), /*#__PURE__*/React.createElement(ConfigureDropdownItem, {
className: editorState.isHeading2 ? `${prefixCls}-menu-bar-dropdown-item-active` : '',
onClick: () => editor.chain().focus().toggleHeading({
level: 2
}).run()
}, /*#__PURE__*/React.createElement(IconH2, null)), /*#__PURE__*/React.createElement(ConfigureDropdownItem, {
className: editorState.isHeading3 ? `${prefixCls}-menu-bar-dropdown-item-active` : '',
onClick: () => editor.chain().focus().toggleHeading({
level: 3
}).run()
}, /*#__PURE__*/React.createElement(IconH3, null)), /*#__PURE__*/React.createElement(ConfigureDropdownItem, {
className: editorState.isHeading4 ? `${prefixCls}-menu-bar-dropdown-item-active` : '',
onClick: () => editor.chain().focus().toggleHeading({
level: 4
}).run()
}, /*#__PURE__*/React.createElement(IconH4, null)), /*#__PURE__*/React.createElement(ConfigureDropdownItem, {
className: editorState.isHeading5 ? `${prefixCls}-menu-bar-dropdown-item-active` : '',
onClick: () => editor.chain().focus().toggleHeading({
level: 5
}).run()
}, /*#__PURE__*/React.createElement(IconH5, null)), /*#__PURE__*/React.createElement(ConfigureDropdownItem, {
className: editorState.isHeading6 ? `${prefixCls}-menu-bar-dropdown-item-active` : '',
onClick: () => editor.chain().focus().toggleHeading({
level: 6
}).run()
}, /*#__PURE__*/React.createElement(IconH6, null)))
}, /*#__PURE__*/React.createElement("span", null, /*#__PURE__*/React.createElement(ConfigureButton, {
icon: /*#__PURE__*/React.createElement(IconHn, null),
active: editorState.isHeading
}))), /*#__PURE__*/React.createElement(ConfigureButton, {
icon: /*#__PURE__*/React.createElement(IconText, null),
onClick: () => editor.chain().focus().setParagraph().run(),
active: editorState.isParagraph
}), /*#__PURE__*/React.createElement(ConfigureButton, {
icon: /*#__PURE__*/React.createElement(IconList, null),
onClick: () => editor.chain().focus().toggleBulletList().run(),
active: editorState.isBulletList
}), /*#__PURE__*/React.createElement(ConfigureButton, {
icon: /*#__PURE__*/React.createElement(IconOrderedList, null),
onClick: () => editor.chain().focus().toggleOrderedList().run(),
active: editorState.isOrderedList
}), /*#__PURE__*/React.createElement(ConfigureButton, {
icon: /*#__PURE__*/React.createElement(IconQuote, null),
active: editorState.isBlockquote,
onClick: () => editor.chain().focus().setBlockquote().run()
}), /*#__PURE__*/React.createElement(ConfigureButton, {
active: editorState.isCodeBlock,
className: `${prefixCls}-menu-bar-btn-codeblock`,
onClick: () => editor.chain().focus().toggleCodeBlock().run()
}, "CB"), /*#__PURE__*/React.createElement(ConfigureButton, {
icon: /*#__PURE__*/React.createElement(IconMinus, null),
onClick: () => editor.chain().focus().setHorizontalRule().run()
}), /*#__PURE__*/React.createElement(Divider, {
layout: "vertical"
}), /*#__PURE__*/React.createElement(ConfigureButton, {
active: editorState.isAlignLeft,
icon: /*#__PURE__*/React.createElement(IconAlignLeft, null),
onClick: () => editor.chain().focus().setTextAlign('left').run()
}), /*#__PURE__*/React.createElement(ConfigureButton, {
active: editorState.isAlignCenter,
icon: /*#__PURE__*/React.createElement(IconAlignCenter, null),
onClick: () => editor.chain().focus().setTextAlign('center').run()
}), /*#__PURE__*/React.createElement(ConfigureButton, {
active: editorState.isAlignRight,
icon: /*#__PURE__*/React.createElement(IconAlignRight, null),
onClick: () => editor.chain().focus().setTextAlign('right').run()
}), /*#__PURE__*/React.createElement(ConfigureButton, {
active: editorState.isAlignJustify,
icon: /*#__PURE__*/React.createElement(IconAlignJustify, null),
onClick: () => editor.chain().focus().setTextAlign('justify').run()
}), /*#__PURE__*/React.createElement(Divider, {
layout: "vertical"
}), /*#__PURE__*/React.createElement(ConfigureButton, {
icon: /*#__PURE__*/React.createElement(IconBold, null),
active: editorState.isBold,
onClick: () => editor.chain().focus().toggleBold().run()
}), /*#__PURE__*/React.createElement(ConfigureButton, {
icon: /*#__PURE__*/React.createElement(IconItalic, null),
onClick: () => editor.chain().focus().toggleItalic().run(),
active: editorState.isItalic,
disabled: !editorState.canItalic
}), /*#__PURE__*/React.createElement(ConfigureButton, {
icon: /*#__PURE__*/React.createElement(IconStrikeThrough, null),
onClick: () => editor.chain().focus().toggleStrike().run(),
active: editorState.isStrike,
disabled: !editorState.canStrike
}), /*#__PURE__*/React.createElement(ConfigureButton, {
icon: /*#__PURE__*/React.createElement(IconCode, null),
onClick: () => editor.chain().focus().toggleCode().run(),
active: editorState.isCode,
disabled: !editorState.canCode
}), /*#__PURE__*/React.createElement(Dropdown, {
trigger: "click",
visible: linkDropdownVisible,
onVisibleChange: visible => {
var _a;
setLinkDropdownVisible(visible);
if (visible) {
const {
from,
to
} = editor.state.selection;
setLinkSelectionRange({
from,
to
});
if (from !== to) {
editor.chain().focus().setMark('selectionMark').run();
}
const currentHref = ((_a = editor.getAttributes('link')) === null || _a === void 0 ? void 0 : _a.href) || '';
setLinkInputValue(currentHref);
} else {
editor.chain().focus().unsetMark('selectionMark').run();
setLinkSelectionRange(null);
}
},
render: /*#__PURE__*/React.createElement("div", {
className: `${prefixCls}-menu-bar-link-dropdown`
}, /*#__PURE__*/React.createElement(LocaleConsumer, {
componentName: "Sidebar"
}, locale => (/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Input, {
size: "small",
placeholder: locale.enterLinkAddress,
value: linkInputValue,
onChange: setLinkInputValue,
onKeyDown: e => handleLinkInputKeyDown(e, locale),
className: `${prefixCls}-menu-bar-link-input`
}), /*#__PURE__*/React.createElement(Button, {
size: "small",
theme: "borderless",
type: "tertiary",
icon: /*#__PURE__*/React.createElement(IconCheckCircleStroked, null),
onClick: e => handleConfirmLink(locale),
disabled: !linkInputValue.trim()
}), /*#__PURE__*/React.createElement(Button, {
size: "small",
theme: "borderless",
icon: /*#__PURE__*/React.createElement(IconDeleteStroked, null),
onClick: e => handleUnsetLink(locale),
disabled: !editorState.isLink
})))))
}, /*#__PURE__*/React.createElement("span", null, /*#__PURE__*/React.createElement(ConfigureButton, {
icon: /*#__PURE__*/React.createElement(IconLink, null),
active: editorState.isLink
}))), /*#__PURE__*/React.createElement(Divider, {
layout: "vertical"
}), /*#__PURE__*/React.createElement(ConfigureButton, {
icon: /*#__PURE__*/React.createElement(IconImage, null),
disabled: !editorState.canInsertImage,
onClick: handleImageAdd
}));
}
export const FileItem = /*#__PURE__*/React.memo(props => {
const {
editable = true,
content,
onContentChange,
extensions = [],
className,
style,
imgUploadProps
} = props;
const defaultExtensions = useMemo(() => [TextStyleKit, StarterKit.configure({
link: {
openOnClick: false,
enableClickSelection: true
}
}), Image, SelectionMark, TextAlign.configure({
types: ["heading", "paragraph"]
}), ImageUploadNode.configure(imgUploadProps)], [imgUploadProps]);
const allExtensions = useMemo(() => [...defaultExtensions, ...extensions], [defaultExtensions, extensions]);
const editor = useEditor({
extensions: allExtensions,
editable: editable,
content: content,
onUpdate: _ref2 => {
let {
editor
} = _ref2;
onContentChange === null || onContentChange === void 0 ? void 0 : onContentChange(editor.getHTML());
}
});
if (!editor) {
return null;
}
return /*#__PURE__*/React.createElement("div", {
className: cls(prefixCls, {
[className]: className
}),
style: style
}, editable && /*#__PURE__*/React.createElement(MenuBar, {
editor: editor,
className: `${prefixCls}-menu-bar`
}), /*#__PURE__*/React.createElement(EditorContent, {
editor: editor,
className: `${prefixCls}-editor`
}));
});
const FileContent = /*#__PURE__*/React.memo(props => {
const {
activeKey,
files = [],
onExpand,
style,
className,
onChange
} = props;
return /*#__PURE__*/React.createElement(Collapse, {
className: cls(collapseCls, `${collapseCls}-file`, {
[className]: className
}),
style: style,
onChange: onChange,
activeKey: activeKey,
clickHeaderToExpand: false
}, files.map(file => {
return /*#__PURE__*/React.createElement(Collapse.Panel, {
header: /*#__PURE__*/React.createElement(CollapseHeader, {
content: file,
onExpand: onExpand,
mode: 'file'
}),
itemKey: file.key,
key: file.key
}, /*#__PURE__*/React.createElement(FileItem, {
key: file.key,
content: file.content,
editable: false
}));
}));
});
export default FileContent;