feat: improve tiptap editor

This commit is contained in:
fantasticit 2022-03-24 21:37:32 +08:00
parent 2ad6518a51
commit cd22f259fa
70 changed files with 303 additions and 128 deletions

View File

@ -33,6 +33,7 @@ import { OrderedList } from './extensions/orderedList';
import { Paragraph } from './extensions/paragraph'; import { Paragraph } from './extensions/paragraph';
import { Placeholder } from './extensions/placeholder'; import { Placeholder } from './extensions/placeholder';
import { SearchNReplace } from './extensions/search'; import { SearchNReplace } from './extensions/search';
import { SelectionExtension } from './extensions/selection';
import { Status } from './extensions/status'; import { Status } from './extensions/status';
import { Strike } from './extensions/strike'; import { Strike } from './extensions/strike';
import { Table } from './extensions/table'; import { Table } from './extensions/table';
@ -85,6 +86,7 @@ export const BaseKit = [
Paragraph, Paragraph,
Placeholder, Placeholder,
SearchNReplace, SearchNReplace,
SelectionExtension,
Status, Status,
Strike, Strike,
Table, Table,

View File

@ -1,6 +1,6 @@
import { Node, mergeAttributes } from '@tiptap/core'; import { Node, mergeAttributes } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react'; import { ReactNodeViewRenderer } from '@tiptap/react';
import { AttachmentWrapper } from '../components/attachment'; import { AttachmentWrapper } from '../wrappers/attachment';
import { getDatasetAttribute } from '../services/dataset'; import { getDatasetAttribute } from '../services/dataset';
declare module '@tiptap/core' { declare module '@tiptap/core' {
@ -68,7 +68,6 @@ export const Attachment = Node.create({
}; };
}, },
// @ts-ignore
addCommands() { addCommands() {
return { return {
setAttachment: setAttachment:
@ -78,6 +77,7 @@ export const Attachment = Node.create({
}, },
}; };
}, },
addNodeView() { addNodeView() {
return ReactNodeViewRenderer(AttachmentWrapper); return ReactNodeViewRenderer(AttachmentWrapper);
}, },

View File

@ -1,6 +1,6 @@
import { Node, Command, mergeAttributes, wrappingInputRule } from '@tiptap/core'; import { Node, Command, mergeAttributes, wrappingInputRule } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react'; import { ReactNodeViewRenderer } from '@tiptap/react';
import { BannerWrapper } from '../components/banner'; import { BannerWrapper } from '../wrappers/banner';
import { typesAvailable } from '../services/markdown/markdownToHTML/markdownBanner'; import { typesAvailable } from '../services/markdown/markdownToHTML/markdownBanner';
import { getDatasetAttribute } from '../services/dataset'; import { getDatasetAttribute } from '../services/dataset';
@ -54,7 +54,6 @@ export const Banner = Node.create({
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]; return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
}, },
// @ts-ignore
addCommands() { addCommands() {
return { return {
setBanner: setBanner:

View File

@ -3,7 +3,7 @@ import { Node, textblockTypeInputRule, mergeAttributes } from '@tiptap/core';
import { Plugin, PluginKey, TextSelection } from 'prosemirror-state'; import { Plugin, PluginKey, TextSelection } from 'prosemirror-state';
import { ReactNodeViewRenderer } from '@tiptap/react'; import { ReactNodeViewRenderer } from '@tiptap/react';
import { LowlightPlugin } from '../services/lowlightPlugin'; import { LowlightPlugin } from '../services/lowlightPlugin';
import { CodeBlockWrapper } from '../components/codeBlock'; import { CodeBlockWrapper } from '../wrappers/codeBlock';
export interface CodeBlockOptions { export interface CodeBlockOptions {
/** /**

View File

@ -1,6 +1,6 @@
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core'; import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react'; import { ReactNodeViewRenderer } from '@tiptap/react';
import { DocumentChildrenWrapper } from '../components/documentChildren'; import { DocumentChildrenWrapper } from '../wrappers/documentChildren';
import { getDatasetAttribute } from '../services/dataset'; import { getDatasetAttribute } from '../services/dataset';
declare module '@tiptap/core' { declare module '@tiptap/core' {
@ -16,10 +16,16 @@ export const DocumentChildrenInputRegex = /^documentChildren\$$/;
export const DocumentChildren = Node.create({ export const DocumentChildren = Node.create({
name: 'documentChildren', name: 'documentChildren',
group: 'block', group: 'block',
draggable: true,
selectable: true,
atom: true, atom: true,
addOptions() {
return {
HTMLAttributes: {
class: 'documentChildren',
},
};
},
addAttributes() { addAttributes() {
return { return {
wikiId: { wikiId: {
@ -32,18 +38,11 @@ export const DocumentChildren = Node.create({
}, },
}; };
}, },
addOptions() {
return {
HTMLAttributes: {
class: 'documentChildren',
},
};
},
parseHTML() { parseHTML() {
return [ return [
{ {
tag: 'div', tag: 'div.documentChildren',
}, },
]; ];
}, },
@ -51,7 +50,7 @@ export const DocumentChildren = Node.create({
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]; return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
}, },
// @ts-ignore
addCommands() { addCommands() {
return { return {
setDocumentChildren: setDocumentChildren:

View File

@ -1,6 +1,6 @@
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core'; import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react'; import { ReactNodeViewRenderer } from '@tiptap/react';
import { DocumentReferenceWrapper } from '../components/documentReference'; import { DocumentReferenceWrapper } from '../wrappers/documentReference';
import { getDatasetAttribute } from '../services/dataset'; import { getDatasetAttribute } from '../services/dataset';
declare module '@tiptap/core' { declare module '@tiptap/core' {
@ -16,9 +16,7 @@ export const DocumentReferenceInputRegex = /^documentReference\$$/;
export const DocumentReference = Node.create({ export const DocumentReference = Node.create({
name: 'documentReference', name: 'documentReference',
group: 'block', group: 'block',
draggable: true,
atom: true, atom: true,
selectable: true,
addAttributes() { addAttributes() {
return { return {
@ -48,7 +46,7 @@ export const DocumentReference = Node.create({
parseHTML() { parseHTML() {
return [ return [
{ {
tag: 'div', tag: 'div.documentReference',
}, },
]; ];
}, },
@ -57,7 +55,6 @@ export const DocumentReference = Node.create({
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]; return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
}, },
// @ts-ignore
addCommands() { addCommands() {
return { return {
setDocumentReference: setDocumentReference:

View File

@ -4,8 +4,8 @@ import { Plugin, PluginKey } from 'prosemirror-state';
import { Decoration, DecorationSet } from 'prosemirror-view'; import { Decoration, DecorationSet } from 'prosemirror-view';
import Suggestion from '@tiptap/suggestion'; import Suggestion from '@tiptap/suggestion';
import tippy from 'tippy.js'; import tippy from 'tippy.js';
import { EmojiList } from '../components/emojiList'; import { EmojiList } from '../wrappers/emojiList';
import { emojiSearch, emojisToName } from '../components/emojiList/emojis'; import { emojiSearch, emojisToName } from '../wrappers/emojiList/emojis';
declare module '@tiptap/core' { declare module '@tiptap/core' {
interface Commands<ReturnType> { interface Commands<ReturnType> {

View File

@ -4,7 +4,7 @@ import { Plugin, PluginKey } from 'prosemirror-state';
import { Decoration, DecorationSet } from 'prosemirror-view'; import { Decoration, DecorationSet } from 'prosemirror-view';
import Suggestion from '@tiptap/suggestion'; import Suggestion from '@tiptap/suggestion';
import tippy from 'tippy.js'; import tippy from 'tippy.js';
import { MenuList } from '../components/menuList'; import { MenuList } from '../wrappers/menuList';
import { EVOKE_MENU_ITEMS } from '../menus/evokeMenu'; import { EVOKE_MENU_ITEMS } from '../menus/evokeMenu';
export const EvokeMenuPluginKey = new PluginKey('evokeMenu'); export const EvokeMenuPluginKey = new PluginKey('evokeMenu');

View File

@ -16,6 +16,7 @@ declare module '@tiptap/core' {
export const HorizontalRule = Node.create<HorizontalRuleOptions>({ export const HorizontalRule = Node.create<HorizontalRuleOptions>({
name: 'horizontalRule', name: 'horizontalRule',
group: 'block', group: 'block',
selectable: true,
addOptions() { addOptions() {
return { return {

View File

@ -1,6 +1,6 @@
import { Node, mergeAttributes } from '@tiptap/core'; import { Node, mergeAttributes } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react'; import { ReactNodeViewRenderer } from '@tiptap/react';
import { IframeWrapper } from '../components/iframe'; import { IframeWrapper } from '../wrappers/iframe';
import { getDatasetAttribute } from '../services/dataset'; import { getDatasetAttribute } from '../services/dataset';
declare module '@tiptap/core' { declare module '@tiptap/core' {
@ -16,7 +16,7 @@ export const Iframe = Node.create({
content: '', content: '',
marks: '', marks: '',
group: 'block', group: 'block',
draggable: true, selectable: true,
atom: true, atom: true,
addOptions() { addOptions() {
@ -56,7 +56,6 @@ export const Iframe = Node.create({
return ['iframe', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]; return ['iframe', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
}, },
// @ts-ignore
addCommands() { addCommands() {
return { return {
setIframe: setIframe:

View File

@ -1,6 +1,6 @@
import { Image as BuiltInImage } from '@tiptap/extension-image'; import { Image as BuiltInImage } from '@tiptap/extension-image';
import { ReactNodeViewRenderer } from '@tiptap/react'; import { ReactNodeViewRenderer } from '@tiptap/react';
import { ImageWrapper } from '../components/image'; import { ImageWrapper } from '../wrappers/image';
const resolveImageEl = (element) => (element.nodeName === 'IMG' ? element : element.querySelector('img')); const resolveImageEl = (element) => (element.nodeName === 'IMG' ? element : element.querySelector('img'));
@ -20,10 +20,12 @@ export const Image = BuiltInImage.extend({
content: '', content: '',
marks: '', marks: '',
group: 'block', group: 'block',
draggable: true, draggable: false,
selectable: true,
atom: true, atom: true,
}; };
}, },
addAttributes() { addAttributes() {
return { return {
...this.parent?.(), ...this.parent?.(),
@ -59,6 +61,7 @@ export const Image = BuiltInImage.extend({
}, },
}; };
}, },
addCommands() { addCommands() {
return { return {
...this.parent?.(), ...this.parent?.(),
@ -69,6 +72,7 @@ export const Image = BuiltInImage.extend({
}, },
}; };
}, },
addNodeView() { addNodeView() {
return ReactNodeViewRenderer(ImageWrapper); return ReactNodeViewRenderer(ImageWrapper);
}, },

View File

@ -1,6 +1,6 @@
import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core'; import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react'; import { ReactNodeViewRenderer } from '@tiptap/react';
import { KatexWrapper } from '../components/katex'; import { KatexWrapper } from '../wrappers/katex';
declare module '@tiptap/core' { declare module '@tiptap/core' {
interface Commands<ReturnType> { interface Commands<ReturnType> {
@ -46,11 +46,10 @@ export const Katex = Node.create({
return ['span', mergeAttributes((this.options && this.options.HTMLAttributes) || {}, HTMLAttributes)]; return ['span', mergeAttributes((this.options && this.options.HTMLAttributes) || {}, HTMLAttributes)];
}, },
// @ts-ignore
addCommands() { addCommands() {
return { return {
setKatex: setKatex:
(options) => (options = {}) =>
({ commands }) => { ({ commands }) => {
return commands.insertContent({ return commands.insertContent({
type: this.name, type: this.name,

View File

@ -1,6 +1,6 @@
import { Node } from '@tiptap/core'; import { Node } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react'; import { ReactNodeViewRenderer } from '@tiptap/react';
import { LoadingWrapper } from '../components/loading'; import { LoadingWrapper } from '../wrappers/loading';
export const Loading = Node.create({ export const Loading = Node.create({
name: 'loading', name: 'loading',

View File

@ -1,7 +1,6 @@
import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core'; import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react'; import { ReactNodeViewRenderer } from '@tiptap/react';
import { safeJSONParse } from 'helpers/json'; import { MindWrapper } from '../wrappers/mind';
import { MindWrapper } from '../components/mind';
import { getDatasetAttribute } from '../services/dataset'; import { getDatasetAttribute } from '../services/dataset';
const DEFAULT_MIND_DATA = { const DEFAULT_MIND_DATA = {
@ -27,7 +26,7 @@ export const Mind = Node.create({
content: '', content: '',
marks: '', marks: '',
group: 'block', group: 'block',
draggable: true, selectable: true,
atom: true, atom: true,
addAttributes() { addAttributes() {
@ -67,7 +66,6 @@ export const Mind = Node.create({
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]; return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
}, },
// @ts-ignore
addCommands() { addCommands() {
return { return {
setMind: setMind:

View File

@ -0,0 +1,60 @@
import { Extension } from '@tiptap/core';
import { Plugin, PluginKey, NodeSelection, TextSelection, Selection, AllSelection } from 'prosemirror-state';
import { Decoration, DecorationSet } from 'prosemirror-view';
import { EXTENSION_PRIORITY_HIGHEST } from '../constants';
export const selectionPluginKey = new PluginKey('selection');
export const getTopLevelNodesFromSelection = (selection: Selection, doc) => {
const nodes: { node; pos: number }[] = [];
if (selection.from !== selection.to) {
const { from, to } = selection;
doc.nodesBetween(from, to, (node, pos) => {
const withinSelection = from <= pos && pos + node.nodeSize <= to;
if (node && node.type.name !== 'paragraph' && !node.isText && withinSelection) {
nodes.push({ node, pos });
return false;
}
return true;
});
}
return nodes;
};
export const getDecorations = (doc, selection: Selection): DecorationSet => {
if (selection instanceof NodeSelection) {
return DecorationSet.create(doc, [
Decoration.node(selection.from, selection.to, {
class: 'selected-node',
}),
]);
}
if (selection instanceof TextSelection || selection instanceof AllSelection) {
const decorations = getTopLevelNodesFromSelection(selection, doc).map(({ node, pos }) => {
return Decoration.node(pos, pos + node.nodeSize, {
class: 'selected-node',
});
});
return DecorationSet.create(doc, decorations);
}
return DecorationSet.empty;
};
export const SelectionExtension = Extension.create({
name: 'selection',
priority: EXTENSION_PRIORITY_HIGHEST,
addProseMirrorPlugins() {
return [
new Plugin({
key: selectionPluginKey,
props: {
decorations(state) {
const { doc, selection } = state;
const decorationSet = getDecorations(doc, selection);
return decorationSet;
},
},
}),
];
},
});

View File

@ -1,6 +1,6 @@
import { Node, mergeAttributes } from '@tiptap/core'; import { Node, mergeAttributes } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react'; import { ReactNodeViewRenderer } from '@tiptap/react';
import { StatusWrapper } from '../components/status'; import { StatusWrapper } from '../wrappers/status';
import { getDatasetAttribute } from '../services/dataset'; import { getDatasetAttribute } from '../services/dataset';
declare module '@tiptap/core' { declare module '@tiptap/core' {
@ -41,20 +41,19 @@ export const Status = Node.create({
parseHTML() { parseHTML() {
return [ return [
{ {
tag: 'div', tag: 'span.status',
}, },
]; ];
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]; return ['span', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
}, },
// @ts-ignore
addCommands() { addCommands() {
return { return {
setStatus: setStatus:
(options) => (options = {}) =>
({ commands }) => { ({ commands }) => {
return commands.insertContent({ return commands.insertContent({
type: this.name, type: this.name,

View File

@ -40,17 +40,18 @@ export const Table = BuiltInTable.extend({
if (fixedWidth && totalWidth > 0) { if (fixedWidth && totalWidth > 0) {
HTMLAttributes.style = `width: ${totalWidth}px;`; HTMLAttributes.style = `width: ${totalWidth}px;`;
} else if (totalWidth && totalWidth > 0) { } else if (totalWidth && totalWidth > 0) {
HTMLAttributes.style = `min-width: 100%`; HTMLAttributes.style = `min-width: ${totalWidth}px`;
} else { } else {
HTMLAttributes.style = null; HTMLAttributes.style = null;
} }
return [ return [
'div', 'div',
{ class: 'tableWrapper' }, { class: 'tableWrapper adas' },
['table', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), ['tbody', 0]], ['table', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), ['tbody', 0]],
]; ];
}, },
}).configure({ }).configure({
resizable: true, resizable: true,
cellMinWidth: 50,
}); });

View File

@ -4,7 +4,7 @@ import { TaskItem as BuiltInTaskItem } from '@tiptap/extension-task-item';
import { Plugin } from 'prosemirror-state'; import { Plugin } from 'prosemirror-state';
import { findParentNodeClosestToPos } from 'prosemirror-utils'; import { findParentNodeClosestToPos } from 'prosemirror-utils';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants'; import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
import { TaskItemWrapper } from '../components/taskItem'; import { TaskItemWrapper } from '../wrappers/taskItem';
const CustomTaskItem = BuiltInTaskItem.extend({ const CustomTaskItem = BuiltInTaskItem.extend({
parseHTML() { parseHTML() {

View File

@ -3,10 +3,10 @@ import { Space, Button } from '@douyinfe/semi-ui';
import { IconUndo, IconRedo } from '@douyinfe/semi-icons'; import { IconUndo, IconRedo } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { IconClear } from 'components/icons'; import { IconClear } from 'components/icons';
import { Divider } from './components/divider'; import { Divider } from './wrappers/divider';
import { MediaInsertMenu } from './menus/mediaInsert'; import { MediaInsertMenu } from './menus/mediaInsert';
import { Paragraph } from './menus/components/paragraph'; import { Paragraph } from './menus/paragraph';
import { FontSize } from './menus/components/fontSize'; import { FontSize } from './menus/fontSize';
import { BaseMenu } from './menus/baseMenu'; import { BaseMenu } from './menus/baseMenu';
import { AlignMenu } from './menus/align'; import { AlignMenu } from './menus/align';
import { ListMenu } from './menus/list'; import { ListMenu } from './menus/list';

View File

@ -29,12 +29,15 @@ export const AlignMenu = ({ editor }) => {
<Tooltip content="左对齐"> <Tooltip content="左对齐">
<Button onClick={toggle('left')} icon={<IconAlignLeft />} type="tertiary" theme="borderless" /> <Button onClick={toggle('left')} icon={<IconAlignLeft />} type="tertiary" theme="borderless" />
</Tooltip> </Tooltip>
<Tooltip content="居中"> <Tooltip content="居中">
<Button onClick={toggle('center')} icon={<IconAlignCenter />} type="tertiary" theme="borderless" /> <Button onClick={toggle('center')} icon={<IconAlignCenter />} type="tertiary" theme="borderless" />
</Tooltip> </Tooltip>
<Tooltip content="右对齐"> <Tooltip content="右对齐">
<Button onClick={toggle('right')} icon={<IconAlignRight />} type="tertiary" theme="borderless" /> <Button onClick={toggle('right')} icon={<IconAlignRight />} type="tertiary" theme="borderless" />
</Tooltip> </Tooltip>
<Tooltip content="两端对齐"> <Tooltip content="两端对齐">
<Button onClick={toggle('justify')} icon={<IconAlignJustify />} type="tertiary" theme="borderless" /> <Button onClick={toggle('justify')} icon={<IconAlignJustify />} type="tertiary" theme="borderless" />
</Tooltip> </Tooltip>

View File

@ -1,8 +1,8 @@
import { Space, Button } from '@douyinfe/semi-ui'; import { Space, Button } from '@douyinfe/semi-ui';
import { IconDelete, IconTickCircle, IconAlertTriangle, IconClear, IconInfoCircle } from '@douyinfe/semi-icons'; import { IconDelete, IconTickCircle, IconAlertTriangle, IconClear, IconInfoCircle } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { BubbleMenu } from './components/bubbleMenu'; import { BubbleMenu } from '../views/bubbleMenu';
import { Divider } from '../components/divider'; import { Divider } from '../wrappers/divider';
import { Banner } from '../extensions/banner'; import { Banner } from '../extensions/banner';
import { deleteNode } from '../services/deleteNode'; import { deleteNode } from '../services/deleteNode';

View File

@ -3,7 +3,7 @@ import { Button } from '@douyinfe/semi-ui';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { IconQuote, IconLink, IconHorizontalRule } from 'components/icons'; import { IconQuote, IconLink, IconHorizontalRule } from 'components/icons';
import { isTitleActive } from '../services/isActive'; import { isTitleActive } from '../services/isActive';
import { Emoji } from './components/emoji'; import { Emoji } from './emoji';
import { Search } from './search'; import { Search } from './search';
export const BaseInsertMenu: React.FC<{ editor: any }> = ({ editor }) => { export const BaseInsertMenu: React.FC<{ editor: any }> = ({ editor }) => {

View File

@ -3,7 +3,7 @@ import { Button } from '@douyinfe/semi-ui';
import { IconFont, IconMark } from '@douyinfe/semi-icons'; import { IconFont, IconMark } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { isTitleActive } from '../services/isActive'; import { isTitleActive } from '../services/isActive';
import { Color } from './components/color'; import { ColorPicker } from './colorPicker';
export const ColorMenu: React.FC<{ editor: any }> = ({ editor }) => { export const ColorMenu: React.FC<{ editor: any }> = ({ editor }) => {
const { color, backgroundColor } = editor.getAttributes('textStyle'); const { color, backgroundColor } = editor.getAttributes('textStyle');
@ -14,7 +14,7 @@ export const ColorMenu: React.FC<{ editor: any }> = ({ editor }) => {
return ( return (
<> <>
<Color <ColorPicker
onSetColor={(color) => { onSetColor={(color) => {
editor.chain().focus().setColor(color).run(); editor.chain().focus().setColor(color).run();
}} }}
@ -45,8 +45,9 @@ export const ColorMenu: React.FC<{ editor: any }> = ({ editor }) => {
disabled={isTitleActive(editor)} disabled={isTitleActive(editor)}
/> />
</Tooltip> </Tooltip>
</Color> </ColorPicker>
<Color
<ColorPicker
onSetColor={(color) => { onSetColor={(color) => {
editor.chain().focus().setBackgroundColor(color).run(); editor.chain().focus().setBackgroundColor(color).run();
}} }}
@ -71,7 +72,7 @@ export const ColorMenu: React.FC<{ editor: any }> = ({ editor }) => {
disabled={isTitleActive(editor)} disabled={isTitleActive(editor)}
/> />
</Tooltip> </Tooltip>
</Color> </ColorPicker>
</> </>
); );
}; };

View File

@ -26,7 +26,7 @@ const colors = [
'rgb(234, 230, 255)', 'rgb(234, 230, 255)',
]; ];
export const Color: React.FC<{ export const ColorPicker: React.FC<{
onSetColor; onSetColor;
disabled?: boolean; disabled?: boolean;
}> = ({ children, onSetColor, disabled = false }) => { }> = ({ children, onSetColor, disabled = false }) => {

View File

@ -31,6 +31,7 @@ export const EVOKE_MENU_ITEMS = [
), ),
command: (editor: Editor) => editor.chain().focus().toggleHeading({ level: 1 }).run(), command: (editor: Editor) => editor.chain().focus().toggleHeading({ level: 1 }).run(),
}, },
{ {
key: '标题2', key: '标题2',
label: ( label: (
@ -41,6 +42,7 @@ export const EVOKE_MENU_ITEMS = [
), ),
command: (editor: Editor) => editor.chain().focus().toggleHeading({ level: 2 }).run(), command: (editor: Editor) => editor.chain().focus().toggleHeading({ level: 2 }).run(),
}, },
{ {
key: '标题1', key: '标题1',
label: ( label: (
@ -51,6 +53,7 @@ export const EVOKE_MENU_ITEMS = [
), ),
command: (editor: Editor) => editor.chain().focus().toggleHeading({ level: 3 }).run(), command: (editor: Editor) => editor.chain().focus().toggleHeading({ level: 3 }).run(),
}, },
{ {
key: '无序列表', key: '无序列表',
label: ( label: (
@ -61,6 +64,7 @@ export const EVOKE_MENU_ITEMS = [
), ),
command: (editor: Editor) => editor.chain().focus().toggleBulletList().run(), command: (editor: Editor) => editor.chain().focus().toggleBulletList().run(),
}, },
{ {
key: '有序列表', key: '有序列表',
label: ( label: (
@ -71,6 +75,7 @@ export const EVOKE_MENU_ITEMS = [
), ),
command: (editor: Editor) => editor.chain().focus().toggleOrderedList().run(), command: (editor: Editor) => editor.chain().focus().toggleOrderedList().run(),
}, },
{ {
key: '任务列表', key: '任务列表',
label: ( label: (
@ -81,6 +86,7 @@ export const EVOKE_MENU_ITEMS = [
), ),
command: (editor: Editor) => editor.chain().focus().toggleTaskList().run(), command: (editor: Editor) => editor.chain().focus().toggleTaskList().run(),
}, },
{ {
key: '链接', key: '链接',
label: ( label: (
@ -91,6 +97,7 @@ export const EVOKE_MENU_ITEMS = [
), ),
command: (editor: Editor) => editor.chain().focus().toggleLink({ href: '' }).run(), command: (editor: Editor) => editor.chain().focus().toggleLink({ href: '' }).run(),
}, },
{ {
key: '引用', key: '引用',
label: ( label: (
@ -101,6 +108,7 @@ export const EVOKE_MENU_ITEMS = [
), ),
command: (editor: Editor) => editor.chain().focus().toggleBlockquote().run(), command: (editor: Editor) => editor.chain().focus().toggleBlockquote().run(),
}, },
{ {
key: '分割线', key: '分割线',
label: ( label: (
@ -111,6 +119,7 @@ export const EVOKE_MENU_ITEMS = [
), ),
command: (editor: Editor) => editor.chain().focus().setHorizontalRule().run(), command: (editor: Editor) => editor.chain().focus().setHorizontalRule().run(),
}, },
{ {
key: '表格', key: '表格',
label: ( label: (
@ -121,6 +130,7 @@ export const EVOKE_MENU_ITEMS = [
), ),
command: (editor: Editor) => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(), command: (editor: Editor) => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(),
}, },
{ {
key: '代码块', key: '代码块',
label: ( label: (
@ -131,6 +141,7 @@ export const EVOKE_MENU_ITEMS = [
), ),
command: (editor: Editor) => editor.chain().focus().toggleCodeBlock().run(), command: (editor: Editor) => editor.chain().focus().toggleCodeBlock().run(),
}, },
{ {
key: '图片', key: '图片',
label: () => ( label: () => (
@ -141,6 +152,7 @@ export const EVOKE_MENU_ITEMS = [
), ),
command: (editor: Editor) => editor.chain().focus().setEmptyImage().run(), command: (editor: Editor) => editor.chain().focus().setEmptyImage().run(),
}, },
{ {
key: '附件', key: '附件',
label: () => ( label: () => (
@ -151,6 +163,7 @@ export const EVOKE_MENU_ITEMS = [
), ),
command: (editor: Editor) => editor.chain().focus().setAttachment().run(), command: (editor: Editor) => editor.chain().focus().setAttachment().run(),
}, },
{ {
key: '外链', key: '外链',
label: ( label: (
@ -161,6 +174,7 @@ export const EVOKE_MENU_ITEMS = [
), ),
command: (editor: Editor) => editor.chain().focus().setIframe({ url: '' }).run(), command: (editor: Editor) => editor.chain().focus().setIframe({ url: '' }).run(),
}, },
{ {
key: '思维导图', key: '思维导图',
label: ( label: (
@ -171,6 +185,7 @@ export const EVOKE_MENU_ITEMS = [
), ),
command: (editor: Editor) => editor.chain().focus().setMind().run(), command: (editor: Editor) => editor.chain().focus().setMind().run(),
}, },
{ {
key: '数学公式', key: '数学公式',
label: ( label: (
@ -181,6 +196,7 @@ export const EVOKE_MENU_ITEMS = [
), ),
command: (editor: Editor) => editor.chain().focus().setKatex().run(), command: (editor: Editor) => editor.chain().focus().setKatex().run(),
}, },
{ {
key: '状态', key: '状态',
label: ( label: (
@ -191,6 +207,7 @@ export const EVOKE_MENU_ITEMS = [
), ),
command: (editor: Editor) => editor.chain().focus().setStatus().run(), command: (editor: Editor) => editor.chain().focus().setStatus().run(),
}, },
{ {
key: '信息框', key: '信息框',
label: ( label: (
@ -201,6 +218,7 @@ export const EVOKE_MENU_ITEMS = [
), ),
command: (editor: Editor) => editor.chain().focus().setBanner({ type: 'info' }).run(), command: (editor: Editor) => editor.chain().focus().setBanner({ type: 'info' }).run(),
}, },
{ {
key: '文档', key: '文档',
label: ( label: (
@ -211,6 +229,7 @@ export const EVOKE_MENU_ITEMS = [
), ),
command: (editor: Editor) => editor.chain().focus().setDocumentReference().run(), command: (editor: Editor) => editor.chain().focus().setDocumentReference().run(),
}, },
{ {
key: '子文档', key: '子文档',
label: ( label: (

View File

@ -1,6 +1,6 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { Select } from '@douyinfe/semi-ui'; import { Select } from '@douyinfe/semi-ui';
import { isTitleActive } from '../../services/isActive'; import { isTitleActive } from '../services/isActive';
export const FONT_SIZES = [12, 13, 14, 15, 16, 19, 22, 24, 29, 32, 40, 48]; export const FONT_SIZES = [12, 13, 14, 15, 16, 19, 22, 24, 29, 32, 40, 48];

View File

@ -3,8 +3,8 @@ import { Space, Button, InputNumber, Typography } from '@douyinfe/semi-ui';
import { IconAlignLeft, IconAlignCenter, IconAlignRight, IconUpload, IconDelete } from '@douyinfe/semi-icons'; import { IconAlignLeft, IconAlignCenter, IconAlignRight, IconUpload, IconDelete } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { Upload } from 'components/upload'; import { Upload } from 'components/upload';
import { BubbleMenu } from './components/bubbleMenu'; import { BubbleMenu } from '../views/bubbleMenu';
import { Divider } from '../components/divider'; import { Divider } from '../wrappers/divider';
import { Image } from '../extensions/image'; import { Image } from '../extensions/image';
import { getImageOriginSize } from '../services/image'; import { getImageOriginSize } from '../services/image';
@ -51,6 +51,7 @@ export const ImageBubbleMenu = ({ editor }) => {
size="small" size="small"
/> />
</Tooltip> </Tooltip>
<Tooltip content="居中"> <Tooltip content="居中">
<Button <Button
onClick={() => { onClick={() => {
@ -69,6 +70,7 @@ export const ImageBubbleMenu = ({ editor }) => {
size="small" size="small"
/> />
</Tooltip> </Tooltip>
<Tooltip content="右对齐"> <Tooltip content="右对齐">
<Button <Button
onClick={() => { onClick={() => {
@ -87,7 +89,9 @@ export const ImageBubbleMenu = ({ editor }) => {
size="small" size="small"
/> />
</Tooltip> </Tooltip>
<Divider /> <Divider />
<Text></Text> <Text></Text>
<InputNumber <InputNumber
size="small" size="small"
@ -106,6 +110,7 @@ export const ImageBubbleMenu = ({ editor }) => {
.run(); .run();
}} }}
/> />
<Text></Text> <Text></Text>
<InputNumber <InputNumber
size="small" size="small"
@ -124,30 +129,9 @@ export const ImageBubbleMenu = ({ editor }) => {
.run(); .run();
}} }}
/> />
<Divider /> <Divider />
<Upload
accept="image/*"
onOK={async (url, fileName) => {
const { width, height } = await getImageOriginSize(url);
editor
.chain()
.updateAttributes(Image.name, {
src: url,
alt: fileName,
width,
height,
})
.setNodeSelection(editor.state.selection.from)
.focus()
.run();
}}
>
{() => (
<Tooltip content="上传图片">
<Button size="small" type="tertiary" theme="borderless" icon={<IconUpload />} />
</Tooltip>
)}
</Upload>
<Tooltip content="删除" hideOnClick> <Tooltip content="删除" hideOnClick>
<Button <Button
size="small" size="small"

View File

@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { Space, Button, Input } from '@douyinfe/semi-ui'; import { Space, Button, Input } from '@douyinfe/semi-ui';
import { IconExternalOpen, IconUnlink, IconTickCircle } from '@douyinfe/semi-icons'; import { IconExternalOpen, IconUnlink, IconTickCircle } from '@douyinfe/semi-icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { BubbleMenu } from './components/bubbleMenu'; import { BubbleMenu } from '../views/bubbleMenu';
import { Link } from '../extensions/link'; import { Link } from '../extensions/link';
export const LinkBubbleMenu = ({ editor }) => { export const LinkBubbleMenu = ({ editor }) => {
@ -28,11 +28,11 @@ export const LinkBubbleMenu = ({ editor }) => {
onChange={(v) => setUrl(v)} onChange={(v) => setUrl(v)}
placeholder={'输入链接'} placeholder={'输入链接'}
onEnterPress={(e) => { onEnterPress={(e) => {
// @ts-ignore const url = (e.target as HTMLInputElement).value;
const url = e.target.value;
setUrl(url); setUrl(url);
}} }}
/> />
<Tooltip content="设置链接"> <Tooltip content="设置链接">
<Button <Button
size="small" size="small"
@ -51,6 +51,7 @@ export const LinkBubbleMenu = ({ editor }) => {
}} }}
/> />
</Tooltip> </Tooltip>
<Tooltip content="去除链接"> <Tooltip content="去除链接">
<Button <Button
onClick={() => { onClick={() => {
@ -62,6 +63,7 @@ export const LinkBubbleMenu = ({ editor }) => {
size="small" size="small"
/> />
</Tooltip> </Tooltip>
<Tooltip content="访问链接"> <Tooltip content="访问链接">
<Button <Button
size="small" size="small"

View File

@ -50,7 +50,7 @@ export const MediaInsertMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
</div> </div>
} }
> >
<Dropdown.Item onClick={() => editor.chain().focus().toggleCodeBlock().run()}> <Dropdown.Item>
<IconTable /> <IconTable />
</Dropdown.Item> </Dropdown.Item>
</Popover> </Popover>
@ -63,6 +63,7 @@ export const MediaInsertMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
<IconImage /> <IconImage />
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item onClick={() => editor.chain().focus().setAttachment().run()}> <Dropdown.Item onClick={() => editor.chain().focus().setAttachment().run()}>
<IconAttachment /> <IconAttachment />
@ -71,6 +72,7 @@ export const MediaInsertMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
<Dropdown.Item onClick={() => editor.chain().focus().setIframe({ url: '' }).run()}> <Dropdown.Item onClick={() => editor.chain().focus().setIframe({ url: '' }).run()}>
<IconLink /> <IconLink />
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item onClick={() => editor.chain().focus().setMind().run()}> <Dropdown.Item onClick={() => editor.chain().focus().setMind().run()}>
<IconMind /> <IconMind />
</Dropdown.Item> </Dropdown.Item>
@ -81,18 +83,22 @@ export const MediaInsertMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
<Dropdown.Divider /> <Dropdown.Divider />
<Dropdown.Title></Dropdown.Title> <Dropdown.Title></Dropdown.Title>
<Dropdown.Item onClick={() => editor.chain().focus().setStatus().run()}> <Dropdown.Item onClick={() => editor.chain().focus().setStatus().run()}>
<IconStatus /> <IconStatus />
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item onClick={() => editor.chain().focus().setBanner({ type: 'info' }).run()}> <Dropdown.Item onClick={() => editor.chain().focus().setBanner({ type: 'info' }).run()}>
<IconInfo /> <IconInfo />
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Divider /> <Dropdown.Divider />
<Dropdown.Title></Dropdown.Title> <Dropdown.Title></Dropdown.Title>
<Dropdown.Item onClick={() => editor.chain().focus().setDocumentReference().run()}> <Dropdown.Item onClick={() => editor.chain().focus().setDocumentReference().run()}>
<IconDocument /> <IconDocument />
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item onClick={() => editor.chain().focus().setDocumentChildren().run()}> <Dropdown.Item onClick={() => editor.chain().focus().setDocumentChildren().run()}>
<IconDocument /> <IconDocument />
</Dropdown.Item> </Dropdown.Item>

View File

@ -1,6 +1,6 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { Select } from '@douyinfe/semi-ui'; import { Select } from '@douyinfe/semi-ui';
import { isTitleActive } from '../../services/isActive'; import { isTitleActive } from '../services/isActive';
const getCurrentCaretTitle = (editor) => { const getCurrentCaretTitle = (editor) => {
if (editor.isActive('heading', { level: 1 })) return 1; if (editor.isActive('heading', { level: 1 })) return 1;

View File

@ -53,12 +53,15 @@ export const Search = ({ editor }) => {
<Button disabled={!results.length} onClick={() => editor.commands.replaceAll()}> <Button disabled={!results.length} onClick={() => editor.commands.replaceAll()}>
</Button> </Button>
<Button disabled={!results.length} onClick={() => editor.commands.replace()}> <Button disabled={!results.length} onClick={() => editor.commands.replace()}>
</Button> </Button>
<Button disabled={!results.length} onClick={() => editor.commands.goToPrevSearchResult()}> <Button disabled={!results.length} onClick={() => editor.commands.goToPrevSearchResult()}>
</Button> </Button>
<Button disabled={!results.length} onClick={() => editor.commands.goToNextSearchResult()}> <Button disabled={!results.length} onClick={() => editor.commands.goToNextSearchResult()}>
</Button> </Button>

View File

@ -11,8 +11,8 @@ import {
IconDeleteTable, IconDeleteTable,
} from 'components/icons'; } from 'components/icons';
import { Tooltip } from 'components/tooltip'; import { Tooltip } from 'components/tooltip';
import { Divider } from '../components/divider'; import { Divider } from '../wrappers/divider';
import { BubbleMenu } from './components/bubbleMenu'; import { BubbleMenu } from '../views/bubbleMenu';
import { Table } from '../extensions/table'; import { Table } from '../extensions/table';
export const TableBubbleMenu = ({ editor }) => { export const TableBubbleMenu = ({ editor }) => {

View File

@ -102,14 +102,14 @@ export function LowlightPlugin({
// (for example, a transaction that affects the entire document). // (for example, a transaction that affects the entire document).
// Such transactions can happen during collab syncing via y-prosemirror, for example. // Such transactions can happen during collab syncing via y-prosemirror, for example.
transaction.steps.some((step) => { transaction.steps.some((step) => {
// @ts-ignore
return ( return (
// @ts-ignore
step.from !== undefined && step.from !== undefined &&
// @ts-ignore // @ts-ignore
step.to !== undefined && step.to !== undefined &&
oldNodes.some((node) => { oldNodes.some((node) => {
// @ts-ignore
return ( return (
// @ts-ignore
node.pos >= step.from && node.pos >= step.from &&
// @ts-ignore // @ts-ignore
node.pos + node.node.nodeSize <= step.to node.pos + node.node.nodeSize <= step.to

View File

@ -1,4 +1,5 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import cls from 'classnames';
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react'; import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
import { Button, Typography, Spin, Collapsible, Space } from '@douyinfe/semi-ui'; import { Button, Typography, Spin, Collapsible, Space } from '@douyinfe/semi-ui';
import { import {
@ -47,7 +48,7 @@ const getFileTypeIcon = (type: FileType) => {
}; };
export const AttachmentWrapper = ({ editor, node, updateAttributes }) => { export const AttachmentWrapper = ({ editor, node, updateAttributes }) => {
const $upload = useRef(); const $upload = useRef<HTMLInputElement>();
const isEditable = editor.isEditable; const isEditable = editor.isEditable;
const { hasTrigger, fileName, fileSize, fileExt, fileType, url, error } = node.attrs; const { hasTrigger, fileName, fileSize, fileExt, fileType, url, error } = node.attrs;
const [loading, toggleLoading] = useToggle(false); const [loading, toggleLoading] = useToggle(false);
@ -55,7 +56,6 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => {
const selectFile = () => { const selectFile = () => {
if (!isEditable || error || url) return; if (!isEditable || error || url) return;
// @ts-ignore
isEditable && $upload.current.click(); isEditable && $upload.current.click();
}; };
@ -90,7 +90,7 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => {
const content = (() => { const content = (() => {
if (error) { if (error) {
return ( return (
<div className={styles.wrap} onClick={selectFile}> <div className={cls(styles.wrap, 'render-wrapper')} onClick={selectFile}>
<Text>{error}</Text> <Text>{error}</Text>
</div> </div>
); );
@ -99,7 +99,7 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => {
if (url) { if (url) {
return ( return (
<> <>
<div className={styles.wrap} onClick={selectFile}> <div className={cls(styles.wrap, 'render-wrapper')} onClick={selectFile}>
<Space> <Space>
{getFileTypeIcon(type)} {getFileTypeIcon(type)}
{fileName}.{fileExt} {fileName}.{fileExt}
@ -139,7 +139,7 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => {
if (isEditable && !url) { if (isEditable && !url) {
return ( return (
<div className={styles.wrap} onClick={selectFile}> <div className={cls(styles.wrap, 'render-wrapper')} onClick={selectFile}>
<Spin spinning={loading}> <Spin spinning={loading}>
<Text style={{ cursor: 'pointer' }}>{loading ? '正在上传中' : '请选择文件'}</Text> <Text style={{ cursor: 'pointer' }}>{loading ? '正在上传中' : '请选择文件'}</Text>
<input ref={$upload} type="file" hidden onChange={handleFile} /> <input ref={$upload} type="file" hidden onChange={handleFile} />

View File

@ -1,10 +1,11 @@
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react'; import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
import { Banner as SemiBanner } from '@douyinfe/semi-ui'; import { Banner as SemiBanner } from '@douyinfe/semi-ui';
import cls from 'classnames';
import styles from './index.module.scss'; import styles from './index.module.scss';
export const BannerWrapper = ({ node }) => { export const BannerWrapper = ({ node }) => {
return ( return (
<NodeViewWrapper id="js-bannber-container" className={styles.wrap}> <NodeViewWrapper id="js-bannber-container" className={cls(styles.wrap, 'render-wrapper')}>
<SemiBanner type={node.attrs.type} description={<NodeViewContent />} closeIcon={null} fullMode={false} /> <SemiBanner type={node.attrs.type} description={<NodeViewContent />} closeIcon={null} fullMode={false} />
</NodeViewWrapper> </NodeViewWrapper>
); );

View File

@ -1,4 +1,5 @@
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import cls from 'classnames';
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react'; import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
import { Button, Select, Tooltip } from '@douyinfe/semi-ui'; import { Button, Select, Tooltip } from '@douyinfe/semi-ui';
import { IconCopy } from '@douyinfe/semi-icons'; import { IconCopy } from '@douyinfe/semi-icons';
@ -17,7 +18,7 @@ export const CodeBlockWrapper = ({
const $container = useRef<HTMLPreElement>(); const $container = useRef<HTMLPreElement>();
return ( return (
<NodeViewWrapper className={styles.wrap}> <NodeViewWrapper className={cls(styles.wrap, 'render-wrapper')}>
<div className={styles.handleWrap}> <div className={styles.handleWrap}>
{isEditable && ( {isEditable && (
<Select <Select

View File

@ -1,14 +1,21 @@
.wrap { .wrap {
margin: 28px 0 16px; margin-top: 12px;
padding-top: 12px; padding: 12px;
border-top: 1px solid var(--semi-color-border); border: 1px solid var(--semi-color-border);
border-left: 0;
border-right: 0;
&.isEditable { &.isEditable {
border: 0;
background-color: var(--semi-color-fill-0);
&:hover { &:hover {
outline: 1px solid var(--semi-color-link); background-color: var(--semi-color-fill-1);
} }
.itemWrap { .itemWrap {
pointer-events: none;
&:hover { &:hover {
color: var(--semi-color-text-1); color: var(--semi-color-text-1);
border-color: var(--semi-color-border); border-color: var(--semi-color-border);
@ -23,6 +30,8 @@
text-decoration: none; text-decoration: none;
color: var(--semi-color-text-1); color: var(--semi-color-text-1);
border-radius: var(--border-radius); border-radius: var(--border-radius);
border: 1px solid var(--semi-color-border);
cursor: pointer;
&:hover { &:hover {
color: var(--semi-color-link); color: var(--semi-color-link);

View File

@ -34,7 +34,10 @@ export const DocumentChildrenWrapper = ({ editor, node, updateAttributes }) => {
}, [node.attrs, wikiId, documentId]); }, [node.attrs, wikiId, documentId]);
return ( return (
<NodeViewWrapper as="div" className={cls(styles.wrap, isEditable && styles.isEditable)}> <NodeViewWrapper
as="div"
className={cls('render-wrapper', styles.wrap, isEditable && styles.isEditable, 'documentChildren')}
>
<div> <div>
<div> <div>
<Text type="tertiary"></Text> <Text type="tertiary"></Text>

View File

@ -1,12 +1,17 @@
.wrap { .wrap {
margin: 8px 0; margin-top: 12px;
&.isEditable { &.isEditable {
padding: 12px;
background-color: var(--semi-color-fill-0);
&:hover { &:hover {
outline: 1px solid var(--semi-color-link); background-color: var(--semi-color-fill-1);
} }
.itemWrap { .itemWrap {
pointer-events: none;
&:hover { &:hover {
color: var(--semi-color-text-1); color: var(--semi-color-text-1);
border-color: var(--semi-color-border); border-color: var(--semi-color-border);

View File

@ -22,7 +22,7 @@ export const DocumentReferenceWrapper = ({ editor, node, updateAttributes }) =>
}; };
return ( return (
<NodeViewWrapper as="div" className={cls(styles.wrap, isEditable && styles.isEditable)}> <NodeViewWrapper as="div" className={cls('render-wrapper', styles.wrap, isEditable && styles.isEditable)}>
<div> <div>
{isEditable && ( {isEditable && (
<DataRender <DataRender

View File

@ -1,4 +1,5 @@
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react'; import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
import cls from 'classnames';
import { Input } from '@douyinfe/semi-ui'; import { Input } from '@douyinfe/semi-ui';
import { Resizeable } from 'components/resizeable'; import { Resizeable } from 'components/resizeable';
import styles from './index.module.scss'; import styles from './index.module.scss';
@ -7,13 +8,11 @@ export const IframeWrapper = ({ editor, node, updateAttributes }) => {
const isEditable = editor.isEditable; const isEditable = editor.isEditable;
const { url, width, height } = node.attrs; const { url, width, height } = node.attrs;
console.log('render iframe', node.attrs);
const onResize = (size) => { const onResize = (size) => {
updateAttributes({ width: size.width, height: size.height }); updateAttributes({ width: size.width, height: size.height });
}; };
const content = ( const content = (
<NodeViewContent as="div" className={styles.wrap}> <NodeViewContent as="div" className={cls(styles.wrap, 'render-wrapper')}>
{isEditable && ( {isEditable && (
<div className={styles.handlerWrap}> <div className={styles.handlerWrap}>
<Input placeholder={'输入外链地址'} value={url} onChange={(url) => updateAttributes({ url })}></Input> <Input placeholder={'输入外链地址'} value={url} onChange={(url) => updateAttributes({ url })}></Input>

View File

@ -1,6 +1,7 @@
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react'; import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
import { Resizeable } from 'components/resizeable'; import { Resizeable } from 'components/resizeable';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import cls from 'classnames';
import { Typography, Spin } from '@douyinfe/semi-ui'; import { Typography, Spin } from '@douyinfe/semi-ui';
import { useToggle } from 'hooks/useToggle'; import { useToggle } from 'hooks/useToggle';
import { uploadFile } from 'services/file'; import { uploadFile } from 'services/file';
@ -12,7 +13,7 @@ const { Text } = Typography;
export const ImageWrapper = ({ editor, node, updateAttributes }) => { export const ImageWrapper = ({ editor, node, updateAttributes }) => {
const isEditable = editor.isEditable; const isEditable = editor.isEditable;
const { hasTrigger, error, src, alt, title, width, height, textAlign } = node.attrs; const { hasTrigger, error, src, alt, title, width, height, textAlign } = node.attrs;
const $upload = useRef(); const $upload = useRef<HTMLInputElement>();
const [loading, toggleLoading] = useToggle(false); const [loading, toggleLoading] = useToggle(false);
const onResize = (size) => { const onResize = (size) => {
@ -21,7 +22,6 @@ export const ImageWrapper = ({ editor, node, updateAttributes }) => {
const selectFile = () => { const selectFile = () => {
if (!isEditable || error || src) return; if (!isEditable || error || src) return;
// @ts-ignore
isEditable && $upload.current.click(); isEditable && $upload.current.click();
}; };
@ -54,7 +54,7 @@ export const ImageWrapper = ({ editor, node, updateAttributes }) => {
const content = (() => { const content = (() => {
if (error) { if (error) {
return ( return (
<div className={styles.wrap}> <div className={cls(styles.wrap, 'render-wrapper')}>
<Text>{error}</Text> <Text>{error}</Text>
</div> </div>
); );
@ -62,7 +62,7 @@ export const ImageWrapper = ({ editor, node, updateAttributes }) => {
if (!src) { if (!src) {
return ( return (
<div className={styles.wrap} onClick={selectFile}> <div className={cls(styles.wrap, 'render-wrapper')} onClick={selectFile}>
<Spin spinning={loading}> <Spin spinning={loading}>
<Text style={{ cursor: 'pointer' }}>{loading ? '正在上传中' : '请选择图片'}</Text> <Text style={{ cursor: 'pointer' }}>{loading ? '正在上传中' : '请选择图片'}</Text>
<input ref={$upload} accept="image/*" type="file" hidden onChange={handleFile} /> <input ref={$upload} accept="image/*" type="file" hidden onChange={handleFile} />

View File

@ -1,5 +1,6 @@
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react'; import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
import { useEffect, useMemo } from 'react'; import { useMemo } from 'react';
import cls from 'classnames';
import { Popover, TextArea, Typography, Space } from '@douyinfe/semi-ui'; import { Popover, TextArea, Typography, Space } from '@douyinfe/semi-ui';
import { IconHelpCircle } from '@douyinfe/semi-icons'; import { IconHelpCircle } from '@douyinfe/semi-icons';
import katex from 'katex'; import katex from 'katex';
@ -26,7 +27,7 @@ export const KatexWrapper = ({ editor, node, updateAttributes }) => {
); );
return ( return (
<NodeViewWrapper as="span" className={styles.wrap} contentEditable={false}> <NodeViewWrapper as="span" className={cls(styles.wrap, 'render-wrapper')} contentEditable={false}>
{isEditable ? ( {isEditable ? (
<Popover <Popover
showArrow showArrow

View File

@ -5,6 +5,8 @@
line-height: 0; line-height: 0;
overflow: visible; overflow: visible;
outline: none; outline: none;
border-radius: var(--border-radius);
border: 1px solid var(--semi-color-border);
.jsmindWrap { .jsmindWrap {
position: absolute; position: absolute;
@ -29,8 +31,7 @@
position: relative; position: relative;
min-height: 50px; min-height: 50px;
overflow: hidden; overflow: hidden;
border-radius: var(--border-radius);
border: 1px solid var(--semi-color-border);
outline: none; outline: none;
> input { > input {

View File

@ -1,4 +1,5 @@
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react'; import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
import cls from 'classnames';
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { Button } from '@douyinfe/semi-ui'; import { Button } from '@douyinfe/semi-ui';
import { IconMinus, IconPlus } from '@douyinfe/semi-icons'; import { IconMinus, IconPlus } from '@douyinfe/semi-icons';
@ -120,7 +121,7 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
); );
return ( return (
<NodeViewWrapper className={styles.wrap}> <NodeViewWrapper className={cls(styles.wrap, 'render-wrapper')}>
<NodeViewContent as="div"> <NodeViewContent as="div">
{isEditable ? ( {isEditable ? (
<Resizeable width={width} height={height} onChange={onResize}> <Resizeable width={width} height={height} onChange={onResize}>

View File

@ -1,6 +1,6 @@
.wrap { .wrap {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
margin-right: 4px; font-size: 0;
cursor: pointer; cursor: pointer;
} }

View File

@ -1,14 +1,19 @@
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react'; import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
import { Space, Popover, Tag, Input } from '@douyinfe/semi-ui'; import { Space, Popover, Tag, Input } from '@douyinfe/semi-ui';
import cls from 'classnames';
import styles from './index.module.scss'; import styles from './index.module.scss';
export const StatusWrapper = ({ editor, node, updateAttributes }) => { export const StatusWrapper = ({ editor, node, updateAttributes }) => {
const isEditable = editor.isEditable; const isEditable = editor.isEditable;
const { color, text } = node.attrs; const { color, text } = node.attrs;
const content = <Tag color={color}>{text || '点击设置状态'}</Tag>; const content = (
<Tag className="render-wrapper" color={color}>
{text || '点击设置状态'}
</Tag>
);
return ( return (
<NodeViewWrapper as="span" className={styles.wrap}> <NodeViewWrapper as="span" className={cls(styles.wrap, 'status')}>
{isEditable ? ( {isEditable ? (
<Popover <Popover
showArrow showArrow
@ -24,8 +29,7 @@ export const StatusWrapper = ({ editor, node, updateAttributes }) => {
key={color} key={color}
style={{ width: 24, height: 24, cursor: 'pointer' }} style={{ width: 24, height: 24, cursor: 'pointer' }}
type="solid" type="solid"
// @ts-ignore color={color as unknown as any}
color={color}
onClick={() => updateAttributes({ color })} onClick={() => updateAttributes({ color })}
></Tag> ></Tag>
); );

View File

@ -255,8 +255,7 @@
pre { pre {
position: relative; position: relative;
border-radius: var(--border-radius); margin: 0;
margin: 0.75rem 0px;
counter-reset: line 0; counter-reset: line 0;
display: flex; display: flex;
min-width: 48px; min-width: 48px;
@ -264,7 +263,6 @@
line-height: 1.3; line-height: 1.3;
background-color: #0d0d0d; background-color: #0d0d0d;
background-color: var(--semi-color-fill-0); background-color: var(--semi-color-fill-0);
border: 1px solid var(--semi-color-border);
code { code {
color: inherit; color: inherit;
@ -397,6 +395,82 @@
.search-result-current { .search-result-current {
background: rgb(255, 0, 0); background: rgb(255, 0, 0);
} }
/******* 选中样式 *******/
hr.selected-node {
background-color: rgb(0, 101, 255);
}
.node-status {
.semi-tag-default {
border: 1px solid var(--semi-color-border);
}
&.selected-node {
.semi-tag-default {
border: 1px solid rgb(0 101 255);
}
}
}
.node-codeBlock,
.node-documentChildren,
.node-documentReference,
.node-katex {
.render-wrapper {
border: 1px solid transparent;
}
}
.node-katex {
.render-wrapper {
border-radius: var(--border-radius);
}
}
.node-attachment,
.node-banner,
.node-iframe,
.node-image,
.node-katex,
.node-mind,
.node-codeBlock,
.node-documentChildren,
.node-documentReference {
&:not(.has-focus) {
::selection {
background-color: transparent;
}
}
// #e0ebfa
.render-wrapper {
position: relative;
user-select: text;
}
&.selected-node {
.render-wrapper {
border: 1px solid rgb(0 101 255);
background-color: var(--semi-color-info-light-hover);
}
}
}
.tableWrapper {
::selection {
background-color: transparent;
}
&.selected-node {
td,
th {
border-color: rgb(0 101 255);
background-color: var(--semi-color-info-light-hover);
}
}
}
/******* 选中样式 *******/
} }
.resize-cursor { .resize-cursor {