mirror of https://github.com/fantasticit/think.git
refactor: improve render performence
parent
5681cc7bd8
commit
5c0d9f54e4
|
@ -64,7 +64,8 @@ export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, author
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [documentId, currentUser, toggleLoading]);
|
}, [documentId, currentUser, toggleLoading]);
|
||||||
const editor = useEditor({
|
const editor = useEditor(
|
||||||
|
{
|
||||||
editable: authority && authority.editable,
|
editable: authority && authority.editable,
|
||||||
extensions: [
|
extensions: [
|
||||||
...BaseKit,
|
...BaseKit,
|
||||||
|
@ -80,7 +81,9 @@ export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, author
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
}, 50),
|
}, 50),
|
||||||
});
|
},
|
||||||
|
[authority, provider]
|
||||||
|
);
|
||||||
const [mentionUsersSettingVisible, toggleMentionUsersSettingVisible] = useToggle(false);
|
const [mentionUsersSettingVisible, toggleMentionUsersSettingVisible] = useToggle(false);
|
||||||
const [mentionUsers, setMentionUsers] = useState([]);
|
const [mentionUsers, setMentionUsers] = useState([]);
|
||||||
|
|
||||||
|
|
|
@ -120,8 +120,14 @@ export const createKeysLocalStorageLRUCache = (storageKey, capacity) => {
|
||||||
const lruCache = new LRUCache(capacity);
|
const lruCache = new LRUCache(capacity);
|
||||||
|
|
||||||
if (USED_STORAGE_KEYS.includes(storageKey)) {
|
if (USED_STORAGE_KEYS.includes(storageKey)) {
|
||||||
|
// @ts-ignore
|
||||||
|
if (module.hot) {
|
||||||
|
console.error(`Storage Key ${storageKey} has been used!`);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
throw new Error(`Storage Key ${storageKey} has been used!`);
|
throw new Error(`Storage Key ${storageKey} has been used!`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
USED_STORAGE_KEYS.push(storageKey);
|
USED_STORAGE_KEYS.push(storageKey);
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
export const Divider = ({ vertical = false }) => {
|
import React from 'react';
|
||||||
|
|
||||||
|
export const _Divider = ({ vertical = false }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
@ -12,3 +14,7 @@ export const Divider = ({ vertical = false }) => {
|
||||||
></div>
|
></div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Divider = React.memo(_Divider, (prevProps, nextProps) => {
|
||||||
|
return prevProps.vertical === nextProps.vertical;
|
||||||
|
});
|
||||||
|
|
|
@ -3,21 +3,21 @@ import { sinkListItem, liftListItem } from 'prosemirror-schema-list';
|
||||||
import { TextSelection, AllSelection, Transaction } from 'prosemirror-state';
|
import { TextSelection, AllSelection, Transaction } from 'prosemirror-state';
|
||||||
import { isListActive, isListNode, clamp, getNodeType } from 'tiptap/prose-utils';
|
import { isListActive, isListNode, clamp, getNodeType } from 'tiptap/prose-utils';
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
indent: {
|
||||||
|
indent: () => ReturnType;
|
||||||
|
outdent: () => ReturnType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type IndentOptions = {
|
type IndentOptions = {
|
||||||
types: string[];
|
types: string[];
|
||||||
indentLevels: number[];
|
indentLevels: number[];
|
||||||
defaultIndentLevel: number;
|
defaultIndentLevel: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
|
||||||
interface Commands {
|
|
||||||
indent: {
|
|
||||||
indent: () => Command;
|
|
||||||
outdent: () => Command;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum IndentProps {
|
export enum IndentProps {
|
||||||
min = 0,
|
min = 0,
|
||||||
max = 210,
|
max = 210,
|
||||||
|
|
|
@ -43,6 +43,7 @@ interface SearchOptions {
|
||||||
searchResultCurrentClass: string;
|
searchResultCurrentClass: string;
|
||||||
caseSensitive: boolean;
|
caseSensitive: boolean;
|
||||||
disableRegex: boolean;
|
disableRegex: boolean;
|
||||||
|
onChange?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TextNodesWithPosition {
|
interface TextNodesWithPosition {
|
||||||
|
@ -216,6 +217,7 @@ export const SearchNReplace = Extension.create<SearchOptions>({
|
||||||
searchResultCurrentClass: 'search-result-current',
|
searchResultCurrentClass: 'search-result-current',
|
||||||
caseSensitive: false,
|
caseSensitive: false,
|
||||||
disableRegex: false,
|
disableRegex: false,
|
||||||
|
onChange: () => {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -279,7 +281,7 @@ export const SearchNReplace = Extension.create<SearchOptions>({
|
||||||
const { currentIndex, results, searchResultCurrentClass } = this.options;
|
const { currentIndex, results, searchResultCurrentClass } = this.options;
|
||||||
const nextIndex = (currentIndex + results.length - 1) % results.length;
|
const nextIndex = (currentIndex + results.length - 1) % results.length;
|
||||||
this.options.currentIndex = nextIndex;
|
this.options.currentIndex = nextIndex;
|
||||||
|
this.options.onChange && this.options.onChange();
|
||||||
return gotoSearchResult({
|
return gotoSearchResult({
|
||||||
view,
|
view,
|
||||||
tr,
|
tr,
|
||||||
|
@ -294,7 +296,7 @@ export const SearchNReplace = Extension.create<SearchOptions>({
|
||||||
const { currentIndex, results, searchResultCurrentClass } = this.options;
|
const { currentIndex, results, searchResultCurrentClass } = this.options;
|
||||||
const nextIndex = (currentIndex + 1) % results.length;
|
const nextIndex = (currentIndex + 1) % results.length;
|
||||||
this.options.currentIndex = nextIndex;
|
this.options.currentIndex = nextIndex;
|
||||||
|
this.options.onChange && this.options.onChange();
|
||||||
return gotoSearchResult({
|
return gotoSearchResult({
|
||||||
view,
|
view,
|
||||||
tr,
|
tr,
|
||||||
|
@ -331,6 +333,10 @@ export const SearchNReplace = Extension.create<SearchOptions>({
|
||||||
);
|
);
|
||||||
extensionThis.options.results = results;
|
extensionThis.options.results = results;
|
||||||
|
|
||||||
|
if (results.length && searchTerm) {
|
||||||
|
extensionThis.options.onChange && extensionThis.options.onChange();
|
||||||
|
}
|
||||||
|
|
||||||
if (ctx.getMeta('directDecoration')) {
|
if (ctx.getMeta('directDecoration')) {
|
||||||
const { fromPos, toPos, attrs } = ctx.getMeta('directDecoration');
|
const { fromPos, toPos, attrs } = ctx.getMeta('directDecoration');
|
||||||
decorationsToReturn.push(Decoration.inline(fromPos, toPos, attrs));
|
decorationsToReturn.push(Decoration.inline(fromPos, toPos, attrs));
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
|
|
||||||
|
export const useActive = (editor: Editor, ...args) => {
|
||||||
|
const [active, toggleActive] = useToggle(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = () => {
|
||||||
|
// eslint-disable-next-line prefer-spread
|
||||||
|
toggleActive(editor.isActive.apply(editor, args));
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.on('selectionUpdate', listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
editor.off('selectionUpdate', listener);
|
||||||
|
};
|
||||||
|
}, [editor, args, toggleActive]);
|
||||||
|
|
||||||
|
return active;
|
||||||
|
};
|
|
@ -0,0 +1,48 @@
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import deepEqual from 'deep-equal';
|
||||||
|
|
||||||
|
type MapFn<T, R> = (arg: T) => R;
|
||||||
|
|
||||||
|
function mapSelf<T>(d: T): T {
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAttributes<T extends Record<string, unknown>, R>(
|
||||||
|
editor: Editor,
|
||||||
|
attrbute: string,
|
||||||
|
defaultValue?: T,
|
||||||
|
map?: (arg: T) => R
|
||||||
|
) {
|
||||||
|
const mapFn = (map || mapSelf) as MapFn<T, R>;
|
||||||
|
const [value, setValue] = useState<R>(mapFn(defaultValue));
|
||||||
|
const prevValueCache = useRef<R>(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = () => {
|
||||||
|
const attrs = { ...defaultValue, ...editor.getAttributes(attrbute) };
|
||||||
|
Object.keys(attrs).forEach((key) => {
|
||||||
|
if (attrs[key] === null || attrs[key] === undefined) {
|
||||||
|
// @ts-ignore
|
||||||
|
attrs[key] = defaultValue[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const nextAttrs = mapFn(attrs);
|
||||||
|
if (deepEqual(prevValueCache.current, nextAttrs)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setValue(nextAttrs);
|
||||||
|
prevValueCache.current = nextAttrs;
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.on('selectionUpdate', listener);
|
||||||
|
editor.on('transaction', listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
editor.off('selectionUpdate', listener);
|
||||||
|
editor.off('transaction', listener);
|
||||||
|
};
|
||||||
|
}, [editor, defaultValue, attrbute, mapFn]);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
|
@ -44,6 +44,8 @@ import { Table } from './menus/table';
|
||||||
import { Mind } from './menus/mind';
|
import { Mind } from './menus/mind';
|
||||||
|
|
||||||
const _MenuBar: React.FC<{ editor: any }> = ({ editor }) => {
|
const _MenuBar: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
|
if (!editor) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Space spacing={2}>
|
<Space spacing={2}>
|
||||||
|
|
|
@ -1,26 +1,35 @@
|
||||||
import React from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
import { Button, Dropdown, Tooltip } from '@douyinfe/semi-ui';
|
import { Button, Dropdown, Tooltip } from '@douyinfe/semi-ui';
|
||||||
import { IconAlignLeft, IconAlignCenter, IconAlignRight, IconAlignJustify } from '@douyinfe/semi-icons';
|
import { IconAlignLeft, IconAlignCenter, IconAlignRight, IconAlignJustify } from '@douyinfe/semi-icons';
|
||||||
import { isTitleActive } from 'tiptap/prose-utils';
|
import { useActive } from 'tiptap/hooks/use-active';
|
||||||
|
import { Title } from 'tiptap/extensions/title';
|
||||||
|
|
||||||
export const Align: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Align: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
const current = (() => {
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
if (editor.isActive({ textAlign: 'center' })) {
|
const isAlignCenter = useActive(editor, { textAlign: 'center' });
|
||||||
|
const isAlignRight = useActive(editor, { textAlign: 'right' });
|
||||||
|
const isAlignJustify = useActive(editor, { textAlign: 'justify' });
|
||||||
|
|
||||||
|
const current = useMemo(() => {
|
||||||
|
if (isAlignCenter) {
|
||||||
return <IconAlignCenter />;
|
return <IconAlignCenter />;
|
||||||
}
|
}
|
||||||
if (editor.isActive({ textAlign: 'right' })) {
|
if (isAlignRight) {
|
||||||
return <IconAlignRight />;
|
return <IconAlignRight />;
|
||||||
}
|
}
|
||||||
if (editor.isActive({ textAlign: 'justify' })) {
|
if (isAlignJustify) {
|
||||||
return <IconAlignJustify />;
|
return <IconAlignJustify />;
|
||||||
}
|
}
|
||||||
return <IconAlignLeft />;
|
return <IconAlignLeft />;
|
||||||
})();
|
}, [isAlignCenter, isAlignRight, isAlignJustify]);
|
||||||
|
|
||||||
const toggle = (align) => {
|
const toggle = useCallback(
|
||||||
|
(align) => {
|
||||||
return () => editor.chain().focus().setTextAlign(align).run();
|
return () => editor.chain().focus().setTextAlign(align).run();
|
||||||
};
|
},
|
||||||
|
[editor]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
@ -47,7 +56,7 @@ export const Align: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<Tooltip content="对齐方式" spacing={6}>
|
<Tooltip content="对齐方式" spacing={6}>
|
||||||
<Button type="tertiary" theme="borderless" icon={current} disabled={isTitleActive(editor)}></Button>
|
<Button type="tertiary" theme="borderless" icon={current} disabled={isTitleActive}></Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
|
@ -5,37 +5,29 @@ import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
||||||
import { Attachment } from 'tiptap/extensions/attachment';
|
import { Attachment } from 'tiptap/extensions/attachment';
|
||||||
import { copyNode, deleteNode } from 'tiptap/prose-utils';
|
import { copyNode, deleteNode } from 'tiptap/prose-utils';
|
||||||
import { Divider } from 'tiptap/divider';
|
import { Divider } from 'tiptap/divider';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
export const AttachmentBubbleMenu = ({ editor }) => {
|
export const AttachmentBubbleMenu = ({ editor }) => {
|
||||||
|
const copyMe = useCallback(() => copyNode(Attachment.name, editor), [editor]);
|
||||||
|
const deleteMe = useCallback(() => deleteNode(Attachment.name, editor), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BubbleMenu
|
<BubbleMenu
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
pluginKey="document-children-bubble-menu"
|
pluginKey="attachment-bubble-menu"
|
||||||
shouldShow={() => editor.isActive(Attachment.name)}
|
shouldShow={() => editor.isActive(Attachment.name)}
|
||||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space spacing={4}>
|
||||||
<Tooltip content="复制">
|
<Tooltip content="复制">
|
||||||
<Button
|
<Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => copyNode(Attachment.name, editor)}
|
|
||||||
icon={<IconCopy />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Tooltip content="删除节点" hideOnClick>
|
<Tooltip content="删除节点" hideOnClick>
|
||||||
<Button
|
<Button onClick={deleteMe} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => deleteNode(Attachment.name, editor)}
|
|
||||||
icon={<IconDelete />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</BubbleMenu>
|
</BubbleMenu>
|
||||||
|
|
|
@ -3,10 +3,6 @@ import { Editor } from '@tiptap/core';
|
||||||
import { AttachmentBubbleMenu } from './bubble';
|
import { AttachmentBubbleMenu } from './bubble';
|
||||||
|
|
||||||
export const Attachment: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Attachment: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
if (!editor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AttachmentBubbleMenu editor={editor} />
|
<AttachmentBubbleMenu editor={editor} />
|
||||||
|
|
|
@ -1,40 +1,50 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconMark } from '@douyinfe/semi-icons';
|
import { IconMark } from '@douyinfe/semi-icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { isTitleActive } from 'tiptap/prose-utils';
|
import { useAttributes } from 'tiptap/hooks/use-attributes';
|
||||||
|
import { useActive } from 'tiptap/hooks/use-active';
|
||||||
|
import { Title } from 'tiptap/extensions/title';
|
||||||
import { ColorPicker } from '../_components/color-picker';
|
import { ColorPicker } from '../_components/color-picker';
|
||||||
|
|
||||||
export const BackgroundColor: React.FC<{ editor: Editor }> = ({ editor }) => {
|
const FlexStyle: React.CSSProperties = {
|
||||||
const { backgroundColor } = editor.getAttributes('textStyle');
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
export const BackgroundColor: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
<ColorPicker
|
const backgroundColor = useAttributes(
|
||||||
onSetColor={(color) => {
|
editor,
|
||||||
|
'textStyle',
|
||||||
|
{ backgroundColor: 'transparent' },
|
||||||
|
(attrs) => attrs.backgroundColor
|
||||||
|
);
|
||||||
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
|
|
||||||
|
const setBackgroundColor = useCallback(
|
||||||
|
(color) => {
|
||||||
color
|
color
|
||||||
? editor.chain().focus().setBackgroundColor(color).run()
|
? editor.chain().focus().setBackgroundColor(color).run()
|
||||||
: editor.chain().focus().unsetBackgroundColor().run();
|
: editor.chain().focus().unsetBackgroundColor().run();
|
||||||
}}
|
},
|
||||||
disabled={isTitleActive(editor)}
|
[editor]
|
||||||
>
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ColorPicker onSetColor={setBackgroundColor} disabled={isTitleActive}>
|
||||||
<Tooltip content="背景色">
|
<Tooltip content="背景色">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('textStyle') ? 'light' : 'borderless'}
|
theme={editor.isActive('textStyle') ? 'light' : 'borderless'}
|
||||||
type={'tertiary'}
|
type={'tertiary'}
|
||||||
icon={
|
icon={
|
||||||
<div
|
<div style={FlexStyle}>
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconMark />
|
<IconMark />
|
||||||
<span style={{ backgroundColor, width: 12, height: 2 }}></span>
|
<span style={{ backgroundColor, width: 12, height: 2 }}></span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
disabled={isTitleActive(editor)}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ColorPicker>
|
</ColorPicker>
|
||||||
|
|
|
@ -1,24 +1,26 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { IconQuote } from 'components/icons';
|
import { IconQuote } from 'components/icons';
|
||||||
import { isTitleActive } from 'tiptap/prose-utils';
|
import { useActive } from 'tiptap/hooks/use-active';
|
||||||
|
import { Title } from 'tiptap/extensions/title';
|
||||||
|
import { Blockquote as BlockquoteExtension } from 'tiptap/extensions/blockquote';
|
||||||
|
|
||||||
export const Blockquote: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Blockquote: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
if (!editor) {
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
return null;
|
const isBlockquoteActive = useActive(editor, BlockquoteExtension.name);
|
||||||
}
|
|
||||||
|
const toggleBlockquote = useCallback(() => editor.chain().focus().toggleBlockquote().run(), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="插入引用">
|
<Tooltip content="插入引用">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('blockquote') ? 'light' : 'borderless'}
|
theme={isBlockquoteActive ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
icon={<IconQuote />}
|
icon={<IconQuote />}
|
||||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
onClick={toggleBlockquote}
|
||||||
className={editor.isActive('blockquote') ? 'is-active' : ''}
|
disabled={isTitleActive}
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,17 +3,22 @@ import { Editor } from '@tiptap/core';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconBold } from '@douyinfe/semi-icons';
|
import { IconBold } from '@douyinfe/semi-icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { isTitleActive } from 'tiptap/prose-utils';
|
import { useActive } from 'tiptap/hooks/use-active';
|
||||||
|
import { Title } from 'tiptap/extensions/title';
|
||||||
|
import { Bold as BoldExtension } from 'tiptap/extensions/bold';
|
||||||
|
|
||||||
export const Bold: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Bold: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
|
const isBoldActive = useActive(editor, BoldExtension.name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="粗体">
|
<Tooltip content="粗体">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('bold') ? 'light' : 'borderless'}
|
theme={isBoldActive ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
icon={<IconBold />}
|
icon={<IconBold />}
|
||||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||||
disabled={isTitleActive(editor)}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,23 +1,26 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconList } from 'components/icons';
|
import { IconList } from 'components/icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { isTitleActive } from 'tiptap/prose-utils';
|
import { useActive } from 'tiptap/hooks/use-active';
|
||||||
|
import { Title } from 'tiptap/extensions/title';
|
||||||
|
import { BulletList as BulletListExtension } from 'tiptap/extensions/bullet-list';
|
||||||
|
|
||||||
export const BulletList: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const BulletList: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
if (!editor) {
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
return null;
|
const isBulletListActive = useActive(editor, BulletListExtension.name);
|
||||||
}
|
|
||||||
|
const toggleBulletList = useCallback(() => editor.chain().focus().toggleBulletList().run(), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="无序列表">
|
<Tooltip content="无序列表">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('bulletList') ? 'light' : 'borderless'}
|
theme={isBulletListActive ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
icon={<IconList />}
|
icon={<IconList />}
|
||||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
onClick={toggleBulletList}
|
||||||
disabled={isTitleActive(editor)}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
|
@ -32,23 +32,20 @@ export const CalloutBubbleMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
[editor]
|
[editor]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const copyMe = useCallback(() => copyNode(Callout.name, editor), [editor]);
|
||||||
|
const deleteMe = useCallback(() => deleteNode(Callout.name, editor), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BubbleMenu
|
<BubbleMenu
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
pluginKey="banner-bubble-menu"
|
pluginKey="calloyt-bubble-menu"
|
||||||
shouldShow={() => editor.isActive(Callout.name)}
|
shouldShow={() => editor.isActive(Callout.name)}
|
||||||
matchRenderContainer={(node) => node && node.id === 'js-bannber-container'}
|
matchRenderContainer={(node) => node && node.id === 'js-bannber-container'}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space spacing={4}>
|
||||||
<Tooltip content="复制">
|
<Tooltip content="复制">
|
||||||
<Button
|
<Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => copyNode(Callout.name, editor)}
|
|
||||||
icon={<IconCopy />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
|
@ -103,15 +100,11 @@ export const CalloutBubbleMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
>
|
>
|
||||||
<Button icon={<IconDrawBoard />} type="tertiary" theme="borderless" size="small" />
|
<Button icon={<IconDrawBoard />} type="tertiary" theme="borderless" size="small" />
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Tooltip content="删除" hideOnClick>
|
<Tooltip content="删除" hideOnClick>
|
||||||
<Button
|
<Button size="small" type="tertiary" theme="borderless" icon={<IconDelete />} onClick={deleteMe} />
|
||||||
size="small"
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
icon={<IconDelete />}
|
|
||||||
onClick={() => deleteNode(Callout.name, editor)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</BubbleMenu>
|
</BubbleMenu>
|
||||||
|
|
|
@ -3,10 +3,6 @@ import { Editor } from '@tiptap/core';
|
||||||
import { CalloutBubbleMenu } from './bubble';
|
import { CalloutBubbleMenu } from './bubble';
|
||||||
|
|
||||||
export const Callout: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Callout: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
if (!editor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CalloutBubbleMenu editor={editor} />
|
<CalloutBubbleMenu editor={editor} />
|
||||||
|
|
|
@ -1,21 +1,18 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconClear } from 'components/icons';
|
import { IconClear } from 'components/icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
|
||||||
export const CleadrNodeAndMarks: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const CleadrNodeAndMarks: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
return (
|
const clear = useCallback(() => {
|
||||||
<Tooltip content="清除格式">
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
editor.chain().focus().unsetAllMarks().run();
|
editor.chain().focus().unsetAllMarks().run();
|
||||||
editor.chain().focus().clearNodes().run();
|
editor.chain().focus().clearNodes().run();
|
||||||
}}
|
}, [editor]);
|
||||||
icon={<IconClear />}
|
|
||||||
type="tertiary"
|
return (
|
||||||
theme="borderless"
|
<Tooltip content="清除格式">
|
||||||
/>
|
<Button onClick={clear} icon={<IconClear />} type="tertiary" theme="borderless" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
import { Space, Button } from '@douyinfe/semi-ui';
|
import { Space, Button } from '@douyinfe/semi-ui';
|
||||||
import { IconCopy, IconDelete } from '@douyinfe/semi-icons';
|
import { IconCopy, IconDelete } from '@douyinfe/semi-icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
@ -7,36 +8,27 @@ import { copyNode, deleteNode } from 'tiptap/prose-utils';
|
||||||
import { Divider } from 'tiptap/divider';
|
import { Divider } from 'tiptap/divider';
|
||||||
|
|
||||||
export const CodeBlockBubbleMenu = ({ editor }) => {
|
export const CodeBlockBubbleMenu = ({ editor }) => {
|
||||||
|
const copyMe = useCallback(() => copyNode(CodeBlock.name, editor), [editor]);
|
||||||
|
const deleteMe = useCallback(() => deleteNode(CodeBlock.name, editor), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BubbleMenu
|
<BubbleMenu
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
pluginKey="document-children-bubble-menu"
|
pluginKey="code-block-bubble-menu"
|
||||||
shouldShow={() => editor.isActive(CodeBlock.name)}
|
shouldShow={() => editor.isActive(CodeBlock.name)}
|
||||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
matchRenderContainer={(node: HTMLElement) => node && node.classList && node.classList.contains('node-codeBlock')}
|
matchRenderContainer={(node: HTMLElement) => node && node.classList && node.classList.contains('node-codeBlock')}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space spacing={4}>
|
||||||
<Tooltip content="复制">
|
<Tooltip content="复制">
|
||||||
<Button
|
<Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => copyNode(CodeBlock.name, editor)}
|
|
||||||
icon={<IconCopy />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Tooltip content="删除节点" hideOnClick>
|
<Tooltip content="删除节点" hideOnClick>
|
||||||
<Button
|
<Button onClick={deleteMe} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => deleteNode(CodeBlock.name, editor)}
|
|
||||||
icon={<IconDelete />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</BubbleMenu>
|
</BubbleMenu>
|
||||||
|
|
|
@ -3,10 +3,6 @@ import { Editor } from '@tiptap/core';
|
||||||
import { CodeBlockBubbleMenu } from './bubble';
|
import { CodeBlockBubbleMenu } from './bubble';
|
||||||
|
|
||||||
export const CodeBlock: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const CodeBlock: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
if (!editor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CodeBlockBubbleMenu editor={editor} />
|
<CodeBlockBubbleMenu editor={editor} />
|
||||||
|
|
|
@ -3,17 +3,22 @@ import { Editor } from '@tiptap/core';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconCode } from '@douyinfe/semi-icons';
|
import { IconCode } from '@douyinfe/semi-icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { isTitleActive } from 'tiptap/prose-utils';
|
import { useActive } from 'tiptap/hooks/use-active';
|
||||||
|
import { Title } from 'tiptap/extensions/title';
|
||||||
|
import { Code as InlineCode } from 'tiptap/extensions/code';
|
||||||
|
|
||||||
export const Code: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Code: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
|
const isCodeActive = useActive(editor, InlineCode.name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="行内代码">
|
<Tooltip content="行内代码">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('code') ? 'light' : 'borderless'}
|
theme={isCodeActive ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
icon={<IconCode />}
|
icon={<IconCode />}
|
||||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
onClick={() => editor.chain().focus().toggleCode().run()}
|
||||||
disabled={isTitleActive(editor)}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,15 +6,19 @@ import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
||||||
import { Countdown } from 'tiptap/extensions/countdown';
|
import { Countdown } from 'tiptap/extensions/countdown';
|
||||||
import { Divider } from 'tiptap/divider';
|
import { Divider } from 'tiptap/divider';
|
||||||
import { copyNode, deleteNode } from 'tiptap/prose-utils';
|
import { copyNode, deleteNode } from 'tiptap/prose-utils';
|
||||||
|
import { useAttributes } from 'tiptap/hooks/use-attributes';
|
||||||
import { triggerOpenCountSettingModal } from '../_event';
|
import { triggerOpenCountSettingModal } from '../_event';
|
||||||
|
|
||||||
export const CountdownBubbleMenu = ({ editor }) => {
|
export const CountdownBubbleMenu = ({ editor }) => {
|
||||||
const attrs = editor.getAttributes(Countdown.name);
|
const attrs = useAttributes(editor, Countdown.name, {});
|
||||||
|
|
||||||
const openEditLinkModal = useCallback(() => {
|
const openEditLinkModal = useCallback(() => {
|
||||||
triggerOpenCountSettingModal(attrs);
|
triggerOpenCountSettingModal(attrs);
|
||||||
}, [attrs]);
|
}, [attrs]);
|
||||||
|
|
||||||
|
const copyMe = useCallback(() => copyNode(Countdown.name, editor), [editor]);
|
||||||
|
const deleteMe = useCallback(() => deleteNode(Countdown.name, editor), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BubbleMenu
|
<BubbleMenu
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
|
@ -23,15 +27,9 @@ export const CountdownBubbleMenu = ({ editor }) => {
|
||||||
shouldShow={() => editor.isActive(Countdown.name)}
|
shouldShow={() => editor.isActive(Countdown.name)}
|
||||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space spacing={4}>
|
||||||
<Tooltip content="复制">
|
<Tooltip content="复制">
|
||||||
<Button
|
<Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => copyNode(Countdown.name, editor)}
|
|
||||||
icon={<IconCopy />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="编辑">
|
<Tooltip content="编辑">
|
||||||
|
@ -41,13 +39,7 @@ export const CountdownBubbleMenu = ({ editor }) => {
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Tooltip content="删除节点" hideOnClick>
|
<Tooltip content="删除节点" hideOnClick>
|
||||||
<Button
|
<Button onClick={deleteMe} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => deleteNode(Countdown.name, editor)}
|
|
||||||
icon={<IconDelete />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</BubbleMenu>
|
</BubbleMenu>
|
||||||
|
|
|
@ -4,10 +4,6 @@ import { CountdownBubbleMenu } from './bubble';
|
||||||
import { CountdownSettingModal } from './modal';
|
import { CountdownSettingModal } from './modal';
|
||||||
|
|
||||||
export const Countdonw: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Countdonw: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
if (!editor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CountdownBubbleMenu editor={editor} />
|
<CountdownBubbleMenu editor={editor} />
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
import { Space, Button } from '@douyinfe/semi-ui';
|
import { Space, Button } from '@douyinfe/semi-ui';
|
||||||
import { IconCopy, IconDelete } from '@douyinfe/semi-icons';
|
import { IconCopy, IconDelete } from '@douyinfe/semi-icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
@ -7,6 +8,9 @@ import { copyNode, deleteNode } from 'tiptap/prose-utils';
|
||||||
import { Divider } from 'tiptap/divider';
|
import { Divider } from 'tiptap/divider';
|
||||||
|
|
||||||
export const DocumentChildrenBubbleMenu = ({ editor }) => {
|
export const DocumentChildrenBubbleMenu = ({ editor }) => {
|
||||||
|
const copyMe = useCallback(() => copyNode(DocumentChildren.name, editor), [editor]);
|
||||||
|
const deleteMe = useCallback(() => deleteNode(DocumentChildren.name, editor), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BubbleMenu
|
<BubbleMenu
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
|
@ -15,27 +19,15 @@ export const DocumentChildrenBubbleMenu = ({ editor }) => {
|
||||||
shouldShow={() => editor.isActive(DocumentChildren.name)}
|
shouldShow={() => editor.isActive(DocumentChildren.name)}
|
||||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space spacing={4}>
|
||||||
<Tooltip content="复制">
|
<Tooltip content="复制">
|
||||||
<Button
|
<Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => copyNode(DocumentChildren.name, editor)}
|
|
||||||
icon={<IconCopy />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Tooltip content="删除节点" hideOnClick>
|
<Tooltip content="删除节点" hideOnClick>
|
||||||
<Button
|
<Button onClick={deleteMe} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => deleteNode(DocumentChildren.name, editor)}
|
|
||||||
icon={<IconDelete />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</BubbleMenu>
|
</BubbleMenu>
|
||||||
|
|
|
@ -3,10 +3,6 @@ import { Editor } from '@tiptap/core';
|
||||||
import { DocumentChildrenBubbleMenu } from './bubble';
|
import { DocumentChildrenBubbleMenu } from './bubble';
|
||||||
|
|
||||||
export const DocumentChildren: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const DocumentChildren: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
if (!editor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DocumentChildrenBubbleMenu editor={editor} />
|
<DocumentChildrenBubbleMenu editor={editor} />
|
||||||
|
|
|
@ -33,6 +33,9 @@ export const DocumentReferenceBubbleMenu = ({ editor }) => {
|
||||||
[editor]
|
[editor]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const copyMe = useCallback(() => copyNode(DocumentReference.name, editor), [editor]);
|
||||||
|
const deleteMe = useCallback(() => deleteNode(DocumentReference.name, editor), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BubbleMenu
|
<BubbleMenu
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
|
@ -41,15 +44,9 @@ export const DocumentReferenceBubbleMenu = ({ editor }) => {
|
||||||
shouldShow={() => editor.isActive(DocumentReference.name)}
|
shouldShow={() => editor.isActive(DocumentReference.name)}
|
||||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space spacing={4}>
|
||||||
<Tooltip content="复制">
|
<Tooltip content="复制">
|
||||||
<Button
|
<Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => copyNode(DocumentReference.name, editor)}
|
|
||||||
icon={<IconCopy />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
|
@ -97,13 +94,7 @@ export const DocumentReferenceBubbleMenu = ({ editor }) => {
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Tooltip content="删除节点" hideOnClick>
|
<Tooltip content="删除节点" hideOnClick>
|
||||||
<Button
|
<Button onClick={deleteMe} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => deleteNode(DocumentReference.name, editor)}
|
|
||||||
icon={<IconDelete />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</BubbleMenu>
|
</BubbleMenu>
|
||||||
|
|
|
@ -3,10 +3,6 @@ import { Editor } from '@tiptap/core';
|
||||||
import { DocumentReferenceBubbleMenu } from './bubble';
|
import { DocumentReferenceBubbleMenu } from './bubble';
|
||||||
|
|
||||||
export const DocumentReference: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const DocumentReference: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
if (!editor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DocumentReferenceBubbleMenu editor={editor} />
|
<DocumentReferenceBubbleMenu editor={editor} />
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Select } from '@douyinfe/semi-ui';
|
import { Select } from '@douyinfe/semi-ui';
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
import { isTitleActive } from 'tiptap/prose-utils';
|
import { useActive } from 'tiptap/hooks/use-active';
|
||||||
|
import { Title } from 'tiptap/extensions/title';
|
||||||
|
import { useAttributes } from 'tiptap/hooks/use-attributes';
|
||||||
|
|
||||||
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];
|
||||||
|
|
||||||
export const FontSize: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const FontSize: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
const currentFontSizePx = editor.getAttributes('textStyle').fontSize || '16px';
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
const currentFontSize = +currentFontSizePx.replace('px', '');
|
const currentFontSize = useAttributes(editor, 'textStyle', { fontSize: '16px' }, (attrs) =>
|
||||||
|
attrs.fontSize.replace('px', '')
|
||||||
|
);
|
||||||
|
|
||||||
const toggle = useCallback(
|
const toggle = useCallback(
|
||||||
(val) => {
|
(val) => {
|
||||||
|
@ -21,12 +25,7 @@ export const FontSize: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select disabled={isTitleActive} value={+currentFontSize} onChange={toggle} style={{ width: 80, marginRight: 10 }}>
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
value={currentFontSize}
|
|
||||||
onChange={toggle}
|
|
||||||
style={{ width: 80, marginRight: 10 }}
|
|
||||||
>
|
|
||||||
{FONT_SIZES.map((fontSize) => (
|
{FONT_SIZES.map((fontSize) => (
|
||||||
<Select.Option key={fontSize} value={fontSize}>
|
<Select.Option key={fontSize} value={fontSize}>
|
||||||
{fontSize}px
|
{fontSize}px
|
||||||
|
|
|
@ -1,19 +1,27 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
import { Select } from '@douyinfe/semi-ui';
|
import { Select } from '@douyinfe/semi-ui';
|
||||||
import { isTitleActive } from 'tiptap/prose-utils';
|
import { useActive } from 'tiptap/hooks/use-active';
|
||||||
|
import { Title } from 'tiptap/extensions/title';
|
||||||
const getCurrentCaretTitle = (editor) => {
|
|
||||||
if (editor.isActive('heading', { level: 1 })) return 1;
|
|
||||||
if (editor.isActive('heading', { level: 2 })) return 2;
|
|
||||||
if (editor.isActive('heading', { level: 3 })) return 3;
|
|
||||||
if (editor.isActive('heading', { level: 4 })) return 4;
|
|
||||||
if (editor.isActive('heading', { level: 5 })) return 5;
|
|
||||||
if (editor.isActive('heading', { level: 6 })) return 6;
|
|
||||||
return 'paragraph';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Heading: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Heading: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
|
const isH1 = useActive(editor, 'heading', { level: 1 });
|
||||||
|
const isH2 = useActive(editor, 'heading', { level: 2 });
|
||||||
|
const isH3 = useActive(editor, 'heading', { level: 3 });
|
||||||
|
const isH4 = useActive(editor, 'heading', { level: 4 });
|
||||||
|
const isH5 = useActive(editor, 'heading', { level: 5 });
|
||||||
|
const isH6 = useActive(editor, 'heading', { level: 6 });
|
||||||
|
const current = useMemo(() => {
|
||||||
|
if (isH1) return 1;
|
||||||
|
if (isH2) return 2;
|
||||||
|
if (isH3) return 3;
|
||||||
|
if (isH4) return 4;
|
||||||
|
if (isH5) return 5;
|
||||||
|
if (isH6) return 6;
|
||||||
|
return 'paragraph';
|
||||||
|
}, [isH1, isH2, isH3, isH4, isH5, isH6]);
|
||||||
|
|
||||||
const toggle = useCallback(
|
const toggle = useCallback(
|
||||||
(level) => {
|
(level) => {
|
||||||
if (level === 'paragraph') {
|
if (level === 'paragraph') {
|
||||||
|
@ -26,12 +34,7 @@ export const Heading: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select disabled={isTitleActive} value={current} onChange={toggle} style={{ width: 90, marginRight: 10 }}>
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
value={getCurrentCaretTitle(editor)}
|
|
||||||
onChange={toggle}
|
|
||||||
style={{ width: 90, marginRight: 10 }}
|
|
||||||
>
|
|
||||||
<Select.Option value="paragraph">正文</Select.Option>
|
<Select.Option value="paragraph">正文</Select.Option>
|
||||||
<Select.Option value={1}>
|
<Select.Option value={1}>
|
||||||
<h1 style={{ margin: 0, fontSize: '1.3em' }}>标题1</h1>
|
<h1 style={{ margin: 0, fontSize: '1.3em' }}>标题1</h1>
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { IconHorizontalRule } from 'components/icons';
|
import { IconHorizontalRule } from 'components/icons';
|
||||||
import { isTitleActive } from 'tiptap/prose-utils';
|
import { useActive } from 'tiptap/hooks/use-active';
|
||||||
|
import { Title } from 'tiptap/extensions/title';
|
||||||
|
|
||||||
export const HorizontalRule: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const HorizontalRule: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
if (!editor) {
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
return null;
|
|
||||||
}
|
const setHorizontalRule = useCallback(() => editor.chain().focus().setHorizontalRule().run(), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="插入分割线">
|
<Tooltip content="插入分割线">
|
||||||
|
@ -16,8 +17,8 @@ export const HorizontalRule: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
theme={'borderless'}
|
theme={'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
icon={<IconHorizontalRule />}
|
icon={<IconHorizontalRule />}
|
||||||
onClick={() => editor.chain().focus().setHorizontalRule().run()}
|
onClick={setHorizontalRule}
|
||||||
disabled={isTitleActive(editor)}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,40 +1,41 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconIndentLeft, IconIndentRight } from '@douyinfe/semi-icons';
|
import { IconIndentLeft, IconIndentRight } from '@douyinfe/semi-icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { isTitleActive } from 'tiptap/prose-utils';
|
import { useActive } from 'tiptap/hooks/use-active';
|
||||||
|
import { Title } from 'tiptap/extensions/title';
|
||||||
|
|
||||||
export const Ident: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Ident: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
if (!editor) {
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
return null;
|
|
||||||
}
|
const indent = useCallback(() => {
|
||||||
|
editor.chain().focus().indent().run();
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
const outdent = useCallback(() => {
|
||||||
|
editor.chain().focus().outdent().run();
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip content="增加缩进">
|
<Tooltip content="增加缩进">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={indent}
|
||||||
// @ts-ignore
|
|
||||||
editor.chain().focus().indent().run();
|
|
||||||
}}
|
|
||||||
icon={<IconIndentRight />}
|
icon={<IconIndentRight />}
|
||||||
theme={'borderless'}
|
theme={'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
disabled={isTitleActive(editor)}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="减少缩进">
|
<Tooltip content="减少缩进">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={outdent}
|
||||||
// @ts-ignore
|
|
||||||
editor.chain().focus().outdent().run();
|
|
||||||
}}
|
|
||||||
icon={<IconIndentLeft />}
|
icon={<IconIndentLeft />}
|
||||||
theme={'borderless'}
|
theme={'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
disabled={isTitleActive(editor)}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { Tooltip } from 'components/tooltip';
|
||||||
import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
||||||
import { Iframe } from 'tiptap/extensions/iframe';
|
import { Iframe } from 'tiptap/extensions/iframe';
|
||||||
import { copyNode, deleteNode } from 'tiptap/prose-utils';
|
import { copyNode, deleteNode } from 'tiptap/prose-utils';
|
||||||
|
import { useAttributes } from 'tiptap/hooks/use-attributes';
|
||||||
import { Divider } from 'tiptap/divider';
|
import { Divider } from 'tiptap/divider';
|
||||||
import { Size } from '../_components/size';
|
import { Size } from '../_components/size';
|
||||||
|
|
||||||
|
@ -16,8 +17,7 @@ const EXAMPLE_LINK =
|
||||||
'https://proxy.tencentsuite.com/openapi/proxy/v2/addon?uid=144115212008575217&creator=144115212008575217&redirect=https%3A%2F%2Fi.y.qq.com%2Fn2%2Fm%2Foutchain%2Fplayer%2Findex.html%3Fsongid%3D5408217&docType=1&docID=300000000$RwqOunTcpXjs&addonID=0b69e1b9517e44a4aee35d33ee021b55&packageID=817&nonce=m3rqxn';
|
'https://proxy.tencentsuite.com/openapi/proxy/v2/addon?uid=144115212008575217&creator=144115212008575217&redirect=https%3A%2F%2Fi.y.qq.com%2Fn2%2Fm%2Foutchain%2Fplayer%2Findex.html%3Fsongid%3D5408217&docType=1&docID=300000000$RwqOunTcpXjs&addonID=0b69e1b9517e44a4aee35d33ee021b55&packageID=817&nonce=m3rqxn';
|
||||||
|
|
||||||
export const IframeBubbleMenu = ({ editor }) => {
|
export const IframeBubbleMenu = ({ editor }) => {
|
||||||
const attrs = editor.getAttributes(Iframe.name);
|
const { width, height, url } = useAttributes(editor, Iframe.name, { width: 0, height: 0, url: '' });
|
||||||
const { width, height, url } = attrs;
|
|
||||||
const $form = useRef<FormApi>();
|
const $form = useRef<FormApi>();
|
||||||
const [visible, toggleVisible] = useToggle(false);
|
const [visible, toggleVisible] = useToggle(false);
|
||||||
|
|
||||||
|
@ -58,11 +58,14 @@ export const IframeBubbleMenu = ({ editor }) => {
|
||||||
[editor]
|
[editor]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const copyMe = useCallback(() => copyNode(Iframe.name, editor), [editor]);
|
||||||
|
const deleteMe = useCallback(() => deleteNode(Iframe.name, editor), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BubbleMenu
|
<BubbleMenu
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
pluginKey="link-bubble-menu"
|
pluginKey="iframe-bubble-menu"
|
||||||
shouldShow={() => editor.isActive(Iframe.name)}
|
shouldShow={() => editor.isActive(Iframe.name)}
|
||||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
>
|
>
|
||||||
|
@ -93,15 +96,9 @@ export const IframeBubbleMenu = ({ editor }) => {
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Space>
|
<Space spacing={4}>
|
||||||
<Tooltip content="复制">
|
<Tooltip content="复制">
|
||||||
<Button
|
<Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => copyNode(Iframe.name, editor)}
|
|
||||||
icon={<IconCopy />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="访问链接">
|
<Tooltip content="访问链接">
|
||||||
|
@ -121,13 +118,7 @@ export const IframeBubbleMenu = ({ editor }) => {
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Tooltip content="删除节点" hideOnClick>
|
<Tooltip content="删除节点" hideOnClick>
|
||||||
<Button
|
<Button onClick={deleteMe} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => deleteNode(Iframe.name, editor)}
|
|
||||||
icon={<IconDelete />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</BubbleMenu>
|
</BubbleMenu>
|
||||||
|
|
|
@ -3,10 +3,6 @@ import { Editor } from '@tiptap/core';
|
||||||
import { IframeBubbleMenu } from './bubble';
|
import { IframeBubbleMenu } from './bubble';
|
||||||
|
|
||||||
export const Iframe: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Iframe: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
if (!editor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IframeBubbleMenu editor={editor} />
|
<IframeBubbleMenu editor={editor} />
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { Space, Button } from '@douyinfe/semi-ui';
|
import { Space, Button } from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
IconAlignLeft,
|
IconAlignLeft,
|
||||||
|
@ -13,15 +13,58 @@ import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
||||||
import { Divider } from 'tiptap/divider';
|
import { Divider } from 'tiptap/divider';
|
||||||
import { Image } from 'tiptap/extensions/image';
|
import { Image } from 'tiptap/extensions/image';
|
||||||
import { getEditorContainerDOMSize, copyNode, deleteNode } from 'tiptap/prose-utils';
|
import { getEditorContainerDOMSize, copyNode, deleteNode } from 'tiptap/prose-utils';
|
||||||
|
import { useAttributes } from 'tiptap/hooks/use-attributes';
|
||||||
import { Size } from '../_components/size';
|
import { Size } from '../_components/size';
|
||||||
|
|
||||||
export const ImageBubbleMenu = ({ editor }) => {
|
export const ImageBubbleMenu = ({ editor }) => {
|
||||||
const attrs = editor.getAttributes(Image.name);
|
|
||||||
const { width: currentWidth, height: currentHeight } = attrs;
|
|
||||||
const { width: maxWidth } = getEditorContainerDOMSize(editor);
|
const { width: maxWidth } = getEditorContainerDOMSize(editor);
|
||||||
|
const { width: currentWidth, height: currentHeight } = useAttributes(editor, Image.name, { width: 0, height: 0 });
|
||||||
const [width, setWidth] = useState(currentWidth);
|
const [width, setWidth] = useState(currentWidth);
|
||||||
const [height, setHeight] = useState(currentHeight);
|
const [height, setHeight] = useState(currentHeight);
|
||||||
|
|
||||||
|
const copyMe = useCallback(() => copyNode(Image.name, editor), [editor]);
|
||||||
|
const deleteMe = useCallback(() => deleteNode(Image.name, editor), [editor]);
|
||||||
|
|
||||||
|
const alignLeft = useCallback(() => {
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.updateAttributes(Image.name, {
|
||||||
|
textAlign: 'left',
|
||||||
|
})
|
||||||
|
.setNodeSelection(editor.state.selection.from)
|
||||||
|
.focus()
|
||||||
|
.run();
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
const alignCenter = useCallback(() => {
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.updateAttributes(Image.name, {
|
||||||
|
textAlign: 'center',
|
||||||
|
})
|
||||||
|
.setNodeSelection(editor.state.selection.from)
|
||||||
|
.focus()
|
||||||
|
.run();
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
const alignRight = useCallback(() => {
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.updateAttributes(Image.name, {
|
||||||
|
textAlign: 'right',
|
||||||
|
})
|
||||||
|
.setNodeSelection(editor.state.selection.from)
|
||||||
|
.focus()
|
||||||
|
.run();
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
const updateSize = useCallback(
|
||||||
|
(size) => {
|
||||||
|
editor.chain().updateAttributes(Image.name, size).setNodeSelection(editor.state.selection.from).focus().run();
|
||||||
|
},
|
||||||
|
[editor]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setWidth(parseInt(currentWidth));
|
setWidth(parseInt(currentWidth));
|
||||||
setHeight(parseInt(currentHeight));
|
setHeight(parseInt(currentHeight));
|
||||||
|
@ -38,87 +81,24 @@ export const ImageBubbleMenu = ({ editor }) => {
|
||||||
}}
|
}}
|
||||||
matchRenderContainer={(node) => node && node.id === 'js-resizeable-container'}
|
matchRenderContainer={(node) => node && node.id === 'js-resizeable-container'}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space spacing={4}>
|
||||||
<Tooltip content="复制">
|
<Tooltip content="复制">
|
||||||
<Button
|
<Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => copyNode(Image.name, editor)}
|
|
||||||
icon={<IconCopy />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="左对齐">
|
<Tooltip content="左对齐">
|
||||||
<Button
|
<Button onClick={alignLeft} icon={<IconAlignLeft />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => {
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.updateAttributes(Image.name, {
|
|
||||||
textAlign: 'left',
|
|
||||||
})
|
|
||||||
.setNodeSelection(editor.state.selection.from)
|
|
||||||
.focus()
|
|
||||||
.run();
|
|
||||||
}}
|
|
||||||
icon={<IconAlignLeft />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="居中">
|
<Tooltip content="居中">
|
||||||
<Button
|
<Button onClick={alignCenter} icon={<IconAlignCenter />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => {
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.updateAttributes(Image.name, {
|
|
||||||
textAlign: 'center',
|
|
||||||
})
|
|
||||||
.setNodeSelection(editor.state.selection.from)
|
|
||||||
.focus()
|
|
||||||
.run();
|
|
||||||
}}
|
|
||||||
icon={<IconAlignCenter />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="右对齐">
|
<Tooltip content="右对齐">
|
||||||
<Button
|
<Button onClick={alignRight} icon={<IconAlignRight />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => {
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.updateAttributes(Image.name, {
|
|
||||||
textAlign: 'right',
|
|
||||||
})
|
|
||||||
.setNodeSelection(editor.state.selection.from)
|
|
||||||
.focus()
|
|
||||||
.run();
|
|
||||||
}}
|
|
||||||
icon={<IconAlignRight />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Size
|
<Size width={width} maxWidth={maxWidth} height={height} onOk={updateSize}>
|
||||||
width={width}
|
|
||||||
maxWidth={maxWidth}
|
|
||||||
height={height}
|
|
||||||
onOk={(size) => {
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.updateAttributes(Image.name, size)
|
|
||||||
.setNodeSelection(editor.state.selection.from)
|
|
||||||
.focus()
|
|
||||||
.run();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tooltip content="设置宽高">
|
<Tooltip content="设置宽高">
|
||||||
<Button icon={<IconLineHeight />} type="tertiary" theme="borderless" size="small" />
|
<Button icon={<IconLineHeight />} type="tertiary" theme="borderless" size="small" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -127,13 +107,7 @@ export const ImageBubbleMenu = ({ editor }) => {
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Tooltip content="删除" hideOnClick>
|
<Tooltip content="删除" hideOnClick>
|
||||||
<Button
|
<Button size="small" type="tertiary" theme="borderless" icon={<IconDelete />} onClick={deleteMe} />
|
||||||
size="small"
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
icon={<IconDelete />}
|
|
||||||
onClick={() => deleteNode(Image.name, editor)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</BubbleMenu>
|
</BubbleMenu>
|
||||||
|
|
|
@ -3,10 +3,6 @@ import { Editor } from '@tiptap/core';
|
||||||
import { ImageBubbleMenu } from './bubble';
|
import { ImageBubbleMenu } from './bubble';
|
||||||
|
|
||||||
export const Image: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Image: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
if (!editor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ImageBubbleMenu editor={editor} />
|
<ImageBubbleMenu editor={editor} />
|
||||||
|
|
|
@ -20,7 +20,9 @@ import { GridSelect } from 'components/grid-select';
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
import { useUser } from 'data/user';
|
import { useUser } from 'data/user';
|
||||||
import { createKeysLocalStorageLRUCache } from 'helpers/lru-cache';
|
import { createKeysLocalStorageLRUCache } from 'helpers/lru-cache';
|
||||||
import { isTitleActive, getEditorContainerDOMSize } from 'tiptap/prose-utils';
|
import { getEditorContainerDOMSize } from 'tiptap/prose-utils';
|
||||||
|
import { useActive } from 'tiptap/hooks/use-active';
|
||||||
|
import { Title } from 'tiptap/extensions/title';
|
||||||
import { createCountdown } from '../countdown/service';
|
import { createCountdown } from '../countdown/service';
|
||||||
|
|
||||||
const insertMenuLRUCache = createKeysLocalStorageLRUCache('TIPTAP_INSERT_MENU', 3);
|
const insertMenuLRUCache = createKeysLocalStorageLRUCache('TIPTAP_INSERT_MENU', 3);
|
||||||
|
@ -34,6 +36,7 @@ const COMMANDS = [
|
||||||
label: '表格',
|
label: '表格',
|
||||||
custom: (editor, runCommand) => (
|
custom: (editor, runCommand) => (
|
||||||
<Popover
|
<Popover
|
||||||
|
key="table"
|
||||||
showArrow
|
showArrow
|
||||||
position="rightTop"
|
position="rightTop"
|
||||||
zIndex={10000}
|
zIndex={10000}
|
||||||
|
@ -130,6 +133,7 @@ const COMMANDS = [
|
||||||
export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const [recentUsed, setRecentUsed] = useState([]);
|
const [recentUsed, setRecentUsed] = useState([]);
|
||||||
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
const [visible, toggleVisible] = useToggle(false);
|
const [visible, toggleVisible] = useToggle(false);
|
||||||
|
|
||||||
const renderedCommands = useMemo(
|
const renderedCommands = useMemo(
|
||||||
|
@ -194,7 +198,7 @@ export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Tooltip content="插入">
|
<Tooltip content="插入">
|
||||||
<Button type="tertiary" theme="borderless" icon={<IconPlus />} disabled={isTitleActive(editor)} />
|
<Button type="tertiary" theme="borderless" icon={<IconPlus />} disabled={isTitleActive} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
|
@ -3,17 +3,22 @@ import { Editor } from '@tiptap/core';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconItalic } from '@douyinfe/semi-icons';
|
import { IconItalic } from '@douyinfe/semi-icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { isTitleActive } from 'tiptap/prose-utils';
|
import { useActive } from 'tiptap/hooks/use-active';
|
||||||
|
import { Title } from 'tiptap/extensions/title';
|
||||||
|
import { Italic as ItalicExtension } from 'tiptap/extensions/italic';
|
||||||
|
|
||||||
export const Italic: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Italic: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
|
const isItalicActive = useActive(editor, ItalicExtension.name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="斜体">
|
<Tooltip content="斜体">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('italic') ? 'light' : 'borderless'}
|
theme={isItalicActive ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
icon={<IconItalic />}
|
icon={<IconItalic />}
|
||||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||||
disabled={isTitleActive(editor)}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,11 +6,11 @@ import { Divider } from 'tiptap/divider';
|
||||||
import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
||||||
import { Link } from 'tiptap/extensions/link';
|
import { Link } from 'tiptap/extensions/link';
|
||||||
import { isMarkActive, findMarkPosition } from 'tiptap/prose-utils';
|
import { isMarkActive, findMarkPosition } from 'tiptap/prose-utils';
|
||||||
|
import { useAttributes } from 'tiptap/hooks/use-attributes';
|
||||||
import { triggerOpenLinkSettingModal } from '../_event';
|
import { triggerOpenLinkSettingModal } from '../_event';
|
||||||
|
|
||||||
export const LinkBubbleMenu = ({ editor }) => {
|
export const LinkBubbleMenu = ({ editor }) => {
|
||||||
const attrs = editor.getAttributes(Link.name);
|
const { href, target } = useAttributes(editor, Link.name, { href: '', target: '' });
|
||||||
const { href, target } = attrs;
|
|
||||||
const [text, setText] = useState();
|
const [text, setText] = useState();
|
||||||
const [from, setFrom] = useState(-1);
|
const [from, setFrom] = useState(-1);
|
||||||
const [to, setTo] = useState(-1);
|
const [to, setTo] = useState(-1);
|
||||||
|
@ -69,7 +69,7 @@ export const LinkBubbleMenu = ({ editor }) => {
|
||||||
shouldShow={() => editor.isActive(Link.name)}
|
shouldShow={() => editor.isActive(Link.name)}
|
||||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space spacing={4}>
|
||||||
<Tooltip content="访问链接">
|
<Tooltip content="访问链接">
|
||||||
<Button size="small" type="tertiary" theme="borderless" icon={<IconExternalOpen />} onClick={visitLink} />
|
<Button size="small" type="tertiary" theme="borderless" icon={<IconExternalOpen />} onClick={visitLink} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -1,23 +1,30 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { IconLink } from 'components/icons';
|
import { IconLink } from 'components/icons';
|
||||||
import { isTitleActive } from 'tiptap/prose-utils';
|
import { useActive } from 'tiptap/hooks/use-active';
|
||||||
|
import { Title } from 'tiptap/extensions/title';
|
||||||
|
import { Link as LinkExtension } from 'tiptap/extensions/link';
|
||||||
import { createOrToggleLink } from './service';
|
import { createOrToggleLink } from './service';
|
||||||
import { LinkBubbleMenu } from './bubble';
|
import { LinkBubbleMenu } from './bubble';
|
||||||
import { LinkSettingModal } from './modal';
|
import { LinkSettingModal } from './modal';
|
||||||
|
|
||||||
export const Link: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Link: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
|
const isLinkActive = useActive(editor, LinkExtension.name);
|
||||||
|
|
||||||
|
const callLinkService = useCallback(() => createOrToggleLink(editor), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip content="插入链接">
|
<Tooltip content="插入链接">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('link') ? 'light' : 'borderless'}
|
theme={isLinkActive ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
icon={<IconLink />}
|
icon={<IconLink />}
|
||||||
onClick={() => createOrToggleLink(editor)}
|
onClick={callLinkService}
|
||||||
disabled={isTitleActive(editor)}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<LinkBubbleMenu editor={editor} />
|
<LinkBubbleMenu editor={editor} />
|
||||||
|
|
|
@ -13,6 +13,8 @@ export const LinkSettingModal: React.FC<IProps> = ({ editor }) => {
|
||||||
const [initialState, setInitialState] = useState({ text: '', href: '', from: -1, to: -1 });
|
const [initialState, setInitialState] = useState({ text: '', href: '', from: -1, to: -1 });
|
||||||
const [visible, toggleVisible] = useToggle(false);
|
const [visible, toggleVisible] = useToggle(false);
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => toggleVisible(false), [toggleVisible]);
|
||||||
|
|
||||||
const handleOk = useCallback(() => {
|
const handleOk = useCallback(() => {
|
||||||
$form.current.validate().then((values) => {
|
$form.current.validate().then((values) => {
|
||||||
if (!values.text) {
|
if (!values.text) {
|
||||||
|
@ -21,9 +23,6 @@ export const LinkSettingModal: React.FC<IProps> = ({ editor }) => {
|
||||||
|
|
||||||
const { from, to } = initialState;
|
const { from, to } = initialState;
|
||||||
const { view } = editor;
|
const { view } = editor;
|
||||||
|
|
||||||
console.log(from, to);
|
|
||||||
|
|
||||||
const schema = view.state.schema;
|
const schema = view.state.schema;
|
||||||
const node = schema.text(values.text, [schema.marks.link.create({ href: values.href })]);
|
const node = schema.text(values.text, [schema.marks.link.create({ href: values.href })]);
|
||||||
view.dispatch(view.state.tr.replaceRangeWith(from, to, node));
|
view.dispatch(view.state.tr.replaceRangeWith(from, to, node));
|
||||||
|
@ -46,7 +45,7 @@ export const LinkSettingModal: React.FC<IProps> = ({ editor }) => {
|
||||||
}, [editor, toggleVisible]);
|
}, [editor, toggleVisible]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal title="编辑链接" visible={visible} onOk={handleOk} onCancel={() => toggleVisible(false)} centered>
|
<Modal title="编辑链接" visible={visible} onOk={handleOk} onCancel={handleCancel} centered>
|
||||||
<Form initValues={initialState} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left">
|
<Form initValues={initialState} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left">
|
||||||
<Form.Input label="文本" field="text" placeholder="请输入文本"></Form.Input>
|
<Form.Input label="文本" field="text" placeholder="请输入文本"></Form.Input>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
|
|
|
@ -6,12 +6,12 @@ import { BubbleMenu } from 'tiptap/views/bubble-menu';
|
||||||
import { Mind } from 'tiptap/extensions/mind';
|
import { Mind } from 'tiptap/extensions/mind';
|
||||||
import { Divider } from 'tiptap/divider';
|
import { Divider } from 'tiptap/divider';
|
||||||
import { getEditorContainerDOMSize, copyNode, deleteNode } from 'tiptap/prose-utils';
|
import { getEditorContainerDOMSize, copyNode, deleteNode } from 'tiptap/prose-utils';
|
||||||
|
import { useAttributes } from 'tiptap/hooks/use-attributes';
|
||||||
import { Size } from '../_components/size';
|
import { Size } from '../_components/size';
|
||||||
|
|
||||||
export const MindBubbleMenu = ({ editor }) => {
|
export const MindBubbleMenu = ({ editor }) => {
|
||||||
const attrs = editor.getAttributes(Mind.name);
|
|
||||||
const { width, height } = attrs;
|
|
||||||
const { width: maxWidth } = getEditorContainerDOMSize(editor);
|
const { width: maxWidth } = getEditorContainerDOMSize(editor);
|
||||||
|
const { width, height } = useAttributes(editor, Mind.name, { width: 0, height: 0 });
|
||||||
|
|
||||||
const setSize = useCallback(
|
const setSize = useCallback(
|
||||||
(size) => {
|
(size) => {
|
||||||
|
@ -20,6 +20,9 @@ export const MindBubbleMenu = ({ editor }) => {
|
||||||
[editor]
|
[editor]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const copyMe = useCallback(() => copyNode(Mind.name, editor), [editor]);
|
||||||
|
const deleteMe = useCallback(() => deleteNode(Mind.name, editor), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BubbleMenu
|
<BubbleMenu
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu'}
|
||||||
|
@ -29,15 +32,9 @@ export const MindBubbleMenu = ({ editor }) => {
|
||||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
matchRenderContainer={(node) => node && node.id === 'js-resizeable-container'}
|
matchRenderContainer={(node) => node && node.id === 'js-resizeable-container'}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space spacing={4}>
|
||||||
<Tooltip content="复制">
|
<Tooltip content="复制">
|
||||||
<Button
|
<Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => copyNode(Mind.name, editor)}
|
|
||||||
icon={<IconCopy />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Size width={width} maxWidth={maxWidth} height={height} onOk={setSize}>
|
<Size width={width} maxWidth={maxWidth} height={height} onOk={setSize}>
|
||||||
|
@ -45,15 +42,11 @@ export const MindBubbleMenu = ({ editor }) => {
|
||||||
<Button icon={<IconLineHeight />} type="tertiary" theme="borderless" size="small" />
|
<Button icon={<IconLineHeight />} type="tertiary" theme="borderless" size="small" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Size>
|
</Size>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Tooltip content="删除节点" hideOnClick>
|
<Tooltip content="删除节点" hideOnClick>
|
||||||
<Button
|
<Button onClick={deleteMe} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => deleteNode(Mind.name, editor)}
|
|
||||||
icon={<IconDelete />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</BubbleMenu>
|
</BubbleMenu>
|
||||||
|
|
|
@ -3,10 +3,6 @@ import { Editor } from '@tiptap/core';
|
||||||
import { MindBubbleMenu } from './bubble';
|
import { MindBubbleMenu } from './bubble';
|
||||||
|
|
||||||
export const Mind: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Mind: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
if (!editor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MindBubbleMenu editor={editor} />
|
<MindBubbleMenu editor={editor} />
|
||||||
|
|
|
@ -1,23 +1,26 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconOrderedList } from 'components/icons';
|
import { IconOrderedList } from 'components/icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { isTitleActive } from 'tiptap/prose-utils';
|
import { useActive } from 'tiptap/hooks/use-active';
|
||||||
|
import { Title } from 'tiptap/extensions/title';
|
||||||
|
import { OrderedList as OrderedListExtension } from 'tiptap/extensions/ordered-list';
|
||||||
|
|
||||||
export const OrderedList: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const OrderedList: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
if (!editor) {
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
return null;
|
const isOrderedListActive = useActive(editor, OrderedListExtension.name);
|
||||||
}
|
|
||||||
|
const toggleOrderedList = useCallback(() => editor.chain().focus().toggleOrderedList().run(), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="有序列表">
|
<Tooltip content="有序列表">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('orderedList') ? 'light' : 'borderless'}
|
theme={isOrderedListActive ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
icon={<IconOrderedList />}
|
icon={<IconOrderedList />}
|
||||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
onClick={toggleOrderedList}
|
||||||
disabled={isTitleActive(editor)}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { Popover, Button, Typography, Input, Space } from '@douyinfe/semi-ui';
|
import { Popover, Button, Typography, Input, Space } from '@douyinfe/semi-ui';
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
@ -8,12 +8,18 @@ import { SearchNReplace } from 'tiptap/extensions/search';
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
const searchExtension = editor.extensionManager.extensions.find((ext) => ext.name === SearchNReplace.name);
|
const [currentIndex, setCurrentIndex] = useState(-1);
|
||||||
const currentIndex = searchExtension ? searchExtension.options.currentIndex : -1;
|
const [results, setResults] = useState([]);
|
||||||
const results = searchExtension ? searchExtension.options.results : [];
|
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
const [replaceValue, setReplaceValue] = useState('');
|
const [replaceValue, setReplaceValue] = useState('');
|
||||||
|
|
||||||
|
const onVisibleChange = useCallback((visible) => {
|
||||||
|
if (!visible) {
|
||||||
|
setSearchValue('');
|
||||||
|
setReplaceValue('');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editor && editor.commands && editor.commands.setSearchTerm) {
|
if (editor && editor.commands && editor.commands.setSearchTerm) {
|
||||||
editor.commands.setSearchTerm(searchValue);
|
editor.commands.setSearchTerm(searchValue);
|
||||||
|
@ -26,18 +32,33 @@ export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
}
|
}
|
||||||
}, [replaceValue, editor]);
|
}, [replaceValue, editor]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const searchExtension = editor.extensionManager.extensions.find((ext) => ext.name === SearchNReplace.name);
|
||||||
|
|
||||||
|
if (!searchExtension) return;
|
||||||
|
|
||||||
|
const listener = () => {
|
||||||
|
const currentIndex = searchExtension ? searchExtension.options.currentIndex : -1;
|
||||||
|
const results = searchExtension ? searchExtension.options.results : [];
|
||||||
|
setCurrentIndex(currentIndex);
|
||||||
|
setResults(results);
|
||||||
|
};
|
||||||
|
|
||||||
|
searchExtension.options.onChange = listener;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (!searchExtension) return;
|
||||||
|
delete searchExtension.options.onChange;
|
||||||
|
};
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
showArrow
|
showArrow
|
||||||
zIndex={10000}
|
zIndex={10000}
|
||||||
trigger="click"
|
trigger="click"
|
||||||
position="bottomRight"
|
position="bottomRight"
|
||||||
onVisibleChange={(visible) => {
|
onVisibleChange={onVisibleChange}
|
||||||
if (!visible) {
|
|
||||||
setSearchValue('');
|
|
||||||
setReplaceValue('');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
content={
|
content={
|
||||||
<div>
|
<div>
|
||||||
<div style={{ marginBottom: 12 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
@ -45,29 +66,29 @@ export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
<Input
|
<Input
|
||||||
autofocus
|
autofocus
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
onChange={(v) => setSearchValue(v)}
|
onChange={setSearchValue}
|
||||||
suffix={results.length ? `${currentIndex + 1}/${results.length}` : ''}
|
suffix={results.length ? `${currentIndex + 1}/${results.length}` : ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginBottom: 12 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<Text type="tertiary">替换为</Text>
|
<Text type="tertiary">替换为</Text>
|
||||||
<Input value={replaceValue} onChange={(v) => setReplaceValue(v)} />
|
<Input value={replaceValue} onChange={setReplaceValue} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Space>
|
<Space>
|
||||||
<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>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
|
@ -3,17 +3,22 @@ import { Editor } from '@tiptap/core';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconStrikeThrough } from '@douyinfe/semi-icons';
|
import { IconStrikeThrough } from '@douyinfe/semi-icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { isTitleActive } from 'tiptap/prose-utils';
|
import { useActive } from 'tiptap/hooks/use-active';
|
||||||
|
import { Title } from 'tiptap/extensions/title';
|
||||||
|
import { Strike as StrikeExtension } from 'tiptap/extensions/strike';
|
||||||
|
|
||||||
export const Strike: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Strike: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
|
const isStrikeActive = useActive(editor, StrikeExtension.name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="删除线">
|
<Tooltip content="删除线">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('strike') ? 'light' : 'borderless'}
|
theme={isStrikeActive ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
icon={<IconStrikeThrough />}
|
icon={<IconStrikeThrough />}
|
||||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||||
disabled={isTitleActive(editor)}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,17 +2,22 @@ import React from 'react';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconSub } from 'components/icons';
|
import { IconSub } from 'components/icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { isTitleActive } from 'tiptap/prose-utils';
|
import { useActive } from 'tiptap/hooks/use-active';
|
||||||
|
import { Title } from 'tiptap/extensions/title';
|
||||||
|
import { Subscript as SubscriptExtension } from 'tiptap/extensions/subscript';
|
||||||
|
|
||||||
export const Subscript: React.FC<{ editor: any }> = ({ editor }) => {
|
export const Subscript: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
|
const isSubscriptActive = useActive(editor, SubscriptExtension.name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="下标">
|
<Tooltip content="下标">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('subscript') ? 'light' : 'borderless'}
|
theme={isSubscriptActive ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
icon={<IconSub />}
|
icon={<IconSub />}
|
||||||
onClick={() => editor.chain().focus().toggleSubscript().run()}
|
onClick={() => editor.chain().focus().toggleSubscript().run()}
|
||||||
disabled={isTitleActive(editor)}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,17 +2,22 @@ import React from 'react';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconSup } from 'components/icons';
|
import { IconSup } from 'components/icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { isTitleActive } from 'tiptap/prose-utils';
|
import { useActive } from 'tiptap/hooks/use-active';
|
||||||
|
import { Title } from 'tiptap/extensions/title';
|
||||||
|
import { Superscript as SuperscriptExtension } from 'tiptap/extensions/superscript';
|
||||||
|
|
||||||
export const Superscript: React.FC<{ editor: any }> = ({ editor }) => {
|
export const Superscript: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
|
const isSuperscriptActive = useActive(editor, SuperscriptExtension.name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="上标">
|
<Tooltip content="上标">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('superscript') ? 'light' : 'borderless'}
|
theme={isSuperscriptActive ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
icon={<IconSup />}
|
icon={<IconSup />}
|
||||||
onClick={() => editor.chain().focus().toggleSuperscript().run()}
|
onClick={() => editor.chain().focus().toggleSuperscript().run()}
|
||||||
disabled={isTitleActive(editor)}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
import { Space, Button } from '@douyinfe/semi-ui';
|
import { Space, Button } from '@douyinfe/semi-ui';
|
||||||
import { IconCopy } from '@douyinfe/semi-icons';
|
import { IconCopy } from '@douyinfe/semi-icons';
|
||||||
import {
|
import {
|
||||||
|
@ -21,6 +22,20 @@ import { Table } from 'tiptap/extensions/table';
|
||||||
import { copyNode } from 'tiptap/prose-utils';
|
import { copyNode } from 'tiptap/prose-utils';
|
||||||
|
|
||||||
export const TableBubbleMenu = ({ editor }) => {
|
export const TableBubbleMenu = ({ editor }) => {
|
||||||
|
const copyMe = useCallback(() => copyNode(Table.name, editor), [editor]);
|
||||||
|
const deleteMe = useCallback(() => editor.chain().focus().deleteTable(), [editor]);
|
||||||
|
const addColumnBefore = useCallback(() => editor.chain().focus().addColumnBefore().run(), [editor]);
|
||||||
|
const addColumnAfter = useCallback(() => editor.chain().focus().addColumnAfter().run(), [editor]);
|
||||||
|
const deleteColumn = useCallback(() => editor.chain().focus().deleteColumn().run(), [editor]);
|
||||||
|
const addRowBefore = useCallback(() => editor.chain().focus().addRowBefore().run(), [editor]);
|
||||||
|
const addRowAfter = useCallback(() => editor.chain().focus().addRowAfter().run(), [editor]);
|
||||||
|
const deleteRow = useCallback(() => editor.chain().focus().deleteRow().run(), [editor]);
|
||||||
|
const toggleHeaderColumn = useCallback(() => editor.chain().focus().toggleHeaderColumn().run(), [editor]);
|
||||||
|
const toggleHeaderRow = useCallback(() => editor.chain().focus().toggleHeaderRow().run(), [editor]);
|
||||||
|
const toggleHeaderCell = useCallback(() => editor.chain().focus().toggleHeaderCell().run(), [editor]);
|
||||||
|
const mergeCells = useCallback(() => editor.chain().focus().mergeCells().run(), [editor]);
|
||||||
|
const splitCell = useCallback(() => editor.chain().focus().splitCell().run(), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BubbleMenu
|
<BubbleMenu
|
||||||
className={'bubble-menu bubble-menu-table'}
|
className={'bubble-menu bubble-menu-table'}
|
||||||
|
@ -32,22 +47,16 @@ export const TableBubbleMenu = ({ editor }) => {
|
||||||
}}
|
}}
|
||||||
matchRenderContainer={(node: HTMLElement) => node && node.tagName === 'TABLE'}
|
matchRenderContainer={(node: HTMLElement) => node && node.tagName === 'TABLE'}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space spacing={4}>
|
||||||
<Tooltip content="复制">
|
<Tooltip content="复制">
|
||||||
<Button
|
<Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => copyNode(Table.name, editor)}
|
|
||||||
icon={<IconCopy />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Tooltip content="向前插入一列">
|
<Tooltip content="向前插入一列">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => editor.chain().focus().addColumnBefore().run()}
|
onClick={addColumnBefore}
|
||||||
icon={<IconAddColumnBefore />}
|
icon={<IconAddColumnBefore />}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
theme="borderless"
|
theme="borderless"
|
||||||
|
@ -57,7 +66,7 @@ export const TableBubbleMenu = ({ editor }) => {
|
||||||
|
|
||||||
<Tooltip content="向后插入一列">
|
<Tooltip content="向后插入一列">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => editor.chain().focus().addColumnAfter().run()}
|
onClick={addColumnAfter}
|
||||||
icon={<IconAddColumnAfter />}
|
icon={<IconAddColumnAfter />}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
theme="borderless"
|
theme="borderless"
|
||||||
|
@ -65,45 +74,21 @@ export const TableBubbleMenu = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content="删除当前列" hideOnClick>
|
<Tooltip content="删除当前列" hideOnClick>
|
||||||
<Button
|
<Button onClick={deleteColumn} icon={<IconDeleteColumn />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => editor.chain().focus().deleteColumn().run()}
|
|
||||||
icon={<IconDeleteColumn />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Tooltip content="向前插入一行">
|
<Tooltip content="向前插入一行">
|
||||||
<Button
|
<Button onClick={addRowBefore} icon={<IconAddRowBefore />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => editor.chain().focus().addRowBefore().run()}
|
|
||||||
icon={<IconAddRowBefore />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="向后插入一行">
|
<Tooltip content="向后插入一行">
|
||||||
<Button
|
<Button onClick={addRowAfter} icon={<IconAddRowAfter />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => editor.chain().focus().addRowAfter().run()}
|
|
||||||
icon={<IconAddRowAfter />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="删除当前行" hideOnClick>
|
<Tooltip content="删除当前行" hideOnClick>
|
||||||
<Button
|
<Button onClick={deleteRow} icon={<IconDeleteRow />} type="tertiary" theme="borderless" size="small" />
|
||||||
onClick={() => editor.chain().focus().deleteRow().run()}
|
|
||||||
icon={<IconDeleteRow />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
@ -114,7 +99,7 @@ export const TableBubbleMenu = ({ editor }) => {
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
theme="borderless"
|
theme="borderless"
|
||||||
icon={<IconTableHeaderColumn />}
|
icon={<IconTableHeaderColumn />}
|
||||||
onClick={() => editor.chain().focus().toggleHeaderColumn().run()}
|
onClick={toggleHeaderColumn}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
@ -124,7 +109,7 @@ export const TableBubbleMenu = ({ editor }) => {
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
theme="borderless"
|
theme="borderless"
|
||||||
icon={<IconTableHeaderRow />}
|
icon={<IconTableHeaderRow />}
|
||||||
onClick={() => editor.chain().focus().toggleHeaderRow().run()}
|
onClick={toggleHeaderRow}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
@ -134,42 +119,24 @@ export const TableBubbleMenu = ({ editor }) => {
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
theme="borderless"
|
theme="borderless"
|
||||||
icon={<IconTableHeaderCell />}
|
icon={<IconTableHeaderCell />}
|
||||||
onClick={() => editor.chain().focus().toggleHeaderCell().run()}
|
onClick={toggleHeaderCell}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Tooltip content="合并单元格">
|
<Tooltip content="合并单元格">
|
||||||
<Button
|
<Button size="small" type="tertiary" theme="borderless" icon={<IconMergeCell />} onClick={mergeCells} />
|
||||||
size="small"
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
icon={<IconMergeCell />}
|
|
||||||
onClick={() => editor.chain().focus().mergeCells().run()}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="分离单元格">
|
<Tooltip content="分离单元格">
|
||||||
<Button
|
<Button size="small" type="tertiary" theme="borderless" icon={<IconSplitCell />} onClick={splitCell} />
|
||||||
size="small"
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
icon={<IconSplitCell />}
|
|
||||||
onClick={() => editor.chain().focus().splitCell().run()}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Tooltip content="删除表格" hideOnClick>
|
<Tooltip content="删除表格" hideOnClick>
|
||||||
<Button
|
<Button size="small" type="tertiary" theme="borderless" icon={<IconDeleteTable />} onClick={deleteMe} />
|
||||||
size="small"
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
icon={<IconDeleteTable />}
|
|
||||||
onClick={() => editor.chain().focus().deleteTable().run()}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</BubbleMenu>
|
</BubbleMenu>
|
||||||
|
|
|
@ -3,9 +3,5 @@ import { Editor } from '@tiptap/core';
|
||||||
import { TableBubbleMenu } from './bubble';
|
import { TableBubbleMenu } from './bubble';
|
||||||
|
|
||||||
export const Table: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Table: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
if (!editor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <TableBubbleMenu editor={editor} />;
|
return <TableBubbleMenu editor={editor} />;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,23 +1,26 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { IconTask } from 'components/icons';
|
import { IconTask } from 'components/icons';
|
||||||
import { isTitleActive } from 'tiptap/prose-utils';
|
import { useActive } from 'tiptap/hooks/use-active';
|
||||||
|
import { Title } from 'tiptap/extensions/title';
|
||||||
|
import { TaskList as TaskListExtension } from 'tiptap/extensions/task-list';
|
||||||
|
|
||||||
export const TaskList: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const TaskList: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
if (!editor) {
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
return null;
|
const isTaskListActive = useActive(editor, TaskListExtension.name);
|
||||||
}
|
|
||||||
|
const toggleTaskList = useCallback(() => editor.chain().focus().toggleTaskList().run(), [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="任务列表">
|
<Tooltip content="任务列表">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('taskList') ? 'light' : 'borderless'}
|
theme={isTaskListActive ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
icon={<IconTask />}
|
icon={<IconTask />}
|
||||||
onClick={() => editor.chain().focus().toggleTaskList().run()}
|
onClick={toggleTaskList}
|
||||||
disabled={isTitleActive(editor)}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,33 +1,47 @@
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconFont } from '@douyinfe/semi-icons';
|
import { IconFont } from '@douyinfe/semi-icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { isTitleActive } from 'tiptap/prose-utils';
|
import { useActive } from 'tiptap/hooks/use-active';
|
||||||
|
import { Title } from 'tiptap/extensions/title';
|
||||||
|
import { TextStyle } from 'tiptap/extensions/text-style';
|
||||||
|
import { useAttributes } from 'tiptap/hooks/use-attributes';
|
||||||
import { ColorPicker } from '../_components/color-picker';
|
import { ColorPicker } from '../_components/color-picker';
|
||||||
|
|
||||||
export const TextColor: React.FC<{ editor: Editor }> = ({ editor }) => {
|
type Color = { color: string };
|
||||||
const { color } = editor.getAttributes('textStyle');
|
|
||||||
|
|
||||||
return (
|
const FlexStyle = {
|
||||||
<ColorPicker
|
|
||||||
onSetColor={(color) => {
|
|
||||||
color ? editor.chain().focus().setColor(color).run() : editor.chain().focus().unsetColor().run();
|
|
||||||
}}
|
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
>
|
|
||||||
<Tooltip content="文本色">
|
|
||||||
<Button
|
|
||||||
theme={editor.isActive('textStyle') ? 'light' : 'borderless'}
|
|
||||||
type={'tertiary'}
|
|
||||||
icon={
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}}
|
} as React.CSSProperties;
|
||||||
>
|
|
||||||
|
export const TextColor: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
|
const isTextStyleActive = useActive(editor, TextStyle.name);
|
||||||
|
const color = useAttributes<Color, Color['color']>(
|
||||||
|
editor,
|
||||||
|
'textStyle',
|
||||||
|
{ color: 'transparent' },
|
||||||
|
(attrs) => attrs.color
|
||||||
|
);
|
||||||
|
|
||||||
|
const setColor = useCallback(
|
||||||
|
(color) => {
|
||||||
|
color ? editor.chain().focus().setColor(color).run() : editor.chain().focus().unsetColor().run();
|
||||||
|
},
|
||||||
|
[editor]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ColorPicker onSetColor={setColor} disabled={isTitleActive}>
|
||||||
|
<Tooltip content="文本色">
|
||||||
|
<Button
|
||||||
|
theme={isTextStyleActive ? 'light' : 'borderless'}
|
||||||
|
type={'tertiary'}
|
||||||
|
icon={
|
||||||
|
<div style={FlexStyle}>
|
||||||
<IconFont style={{ fontSize: '0.85em' }} />
|
<IconFont style={{ fontSize: '0.85em' }} />
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
|
@ -38,7 +52,7 @@ export const TextColor: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
disabled={isTitleActive(editor)}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ColorPicker>
|
</ColorPicker>
|
||||||
|
|
|
@ -3,9 +3,12 @@ import { Editor } from '@tiptap/core';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconUnderline } from '@douyinfe/semi-icons';
|
import { IconUnderline } from '@douyinfe/semi-icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { isTitleActive } from 'tiptap/prose-utils';
|
import { useActive } from 'tiptap/hooks/use-active';
|
||||||
|
import { Title } from 'tiptap/extensions/title';
|
||||||
|
|
||||||
export const Underline: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Underline: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="下划线">
|
<Tooltip content="下划线">
|
||||||
<Button
|
<Button
|
||||||
|
@ -13,7 +16,7 @@ export const Underline: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
icon={<IconUnderline />}
|
icon={<IconUnderline />}
|
||||||
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||||
disabled={isTitleActive(editor)}
|
disabled={isTitleActive}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue