mirror of https://github.com/fantasticit/think.git
feat: refactor menu, improve link, add countdown
This commit is contained in:
parent
c18a327b80
commit
6fbcb4f127
|
@ -0,0 +1,16 @@
|
||||||
|
import { Icon } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
export const IconCountdown: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
style={style}
|
||||||
|
svg={
|
||||||
|
<svg width="16" height="16" viewBox="0 0 1024 1024" role="presentation">
|
||||||
|
<path d="M896 170.666667H128a42.666667 42.666667 0 0 1 0-85.333334h768a42.666667 42.666667 0 0 1 0 85.333334zM896 938.666667H128a42.666667 42.666667 0 0 1 0-85.333334h768a42.666667 42.666667 0 0 1 0 85.333334z"></path>
|
||||||
|
<path d="M512 554.666667a298.666667 298.666667 0 0 1-298.666667-298.666667V128a42.666667 42.666667 0 0 1 42.666667-42.666667h512a42.666667 42.666667 0 0 1 42.666667 42.666667v128a298.666667 298.666667 0 0 1-298.666667 298.666667zM298.666667 170.666667v85.333333a213.333333 213.333333 0 0 0 426.666666 0V170.666667z"></path>
|
||||||
|
<path d="M768 938.666667H256a42.666667 42.666667 0 0 1-42.666667-42.666667v-128a298.666667 298.666667 0 0 1 597.333334 0v128a42.666667 42.666667 0 0 1-42.666667 42.666667zM298.666667 853.333333h426.666666v-85.333333a213.333333 213.333333 0 0 0-426.666666 0z"></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -48,3 +48,4 @@ export * from './IconTableHeaderCell';
|
||||||
export * from './IconSub';
|
export * from './IconSub';
|
||||||
export * from './IconSup';
|
export * from './IconSup';
|
||||||
export * from './IconGlobe';
|
export * from './IconGlobe';
|
||||||
|
export * from './IconCountdown';
|
||||||
|
|
|
@ -13,7 +13,6 @@ import { DocumentChildren } from './extensions/document-children';
|
||||||
import { DocumentReference } from './extensions/document-reference';
|
import { DocumentReference } from './extensions/document-reference';
|
||||||
import { Dropcursor } from './extensions/dropcursor';
|
import { Dropcursor } from './extensions/dropcursor';
|
||||||
import { Emoji } from './extensions/emoji';
|
import { Emoji } from './extensions/emoji';
|
||||||
import { EvokeMenu } from './extensions/evoke-menu';
|
|
||||||
import { Focus } from './extensions/focus';
|
import { Focus } from './extensions/focus';
|
||||||
import { FontSize } from './extensions/font-size';
|
import { FontSize } from './extensions/font-size';
|
||||||
import { Gapcursor } from './extensions/gapcursor';
|
import { Gapcursor } from './extensions/gapcursor';
|
||||||
|
@ -33,6 +32,7 @@ import { Mind } from './extensions/mind';
|
||||||
import { OrderedList } from './extensions/ordered-list';
|
import { OrderedList } from './extensions/ordered-list';
|
||||||
import { Paragraph } from './extensions/paragraph';
|
import { Paragraph } from './extensions/paragraph';
|
||||||
import { Placeholder } from './extensions/placeholder';
|
import { Placeholder } from './extensions/placeholder';
|
||||||
|
import { QuickInsert } from './extensions/quick-insert';
|
||||||
import { SearchNReplace } from './extensions/search';
|
import { SearchNReplace } from './extensions/search';
|
||||||
import { SelectionExtension } from './extensions/selection';
|
import { SelectionExtension } from './extensions/selection';
|
||||||
import { Status } from './extensions/status';
|
import { Status } from './extensions/status';
|
||||||
|
@ -69,7 +69,6 @@ export const BaseKit = [
|
||||||
DocumentReference,
|
DocumentReference,
|
||||||
Dropcursor,
|
Dropcursor,
|
||||||
Emoji,
|
Emoji,
|
||||||
EvokeMenu,
|
|
||||||
Focus,
|
Focus,
|
||||||
FontSize,
|
FontSize,
|
||||||
Gapcursor,
|
Gapcursor,
|
||||||
|
@ -89,6 +88,7 @@ export const BaseKit = [
|
||||||
OrderedList,
|
OrderedList,
|
||||||
Paragraph,
|
Paragraph,
|
||||||
Placeholder,
|
Placeholder,
|
||||||
|
QuickInsert,
|
||||||
SearchNReplace,
|
SearchNReplace,
|
||||||
SelectionExtension,
|
SelectionExtension,
|
||||||
Status,
|
Status,
|
||||||
|
|
|
@ -30,7 +30,7 @@ export const Countdown = Node.create({
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
title: {
|
title: {
|
||||||
default: '倒计时',
|
default: '倒计时⏰',
|
||||||
parseHTML: getDatasetAttribute('title'),
|
parseHTML: getDatasetAttribute('title'),
|
||||||
},
|
},
|
||||||
date: {
|
date: {
|
||||||
|
|
|
@ -5,19 +5,19 @@ import { Decoration, DecorationSet } from 'prosemirror-view';
|
||||||
import Suggestion from '@tiptap/suggestion';
|
import Suggestion from '@tiptap/suggestion';
|
||||||
import tippy from 'tippy.js';
|
import tippy from 'tippy.js';
|
||||||
import { MenuList } from '../wrappers/menu-list';
|
import { MenuList } from '../wrappers/menu-list';
|
||||||
import { EVOKE_MENU_ITEMS } from '../menus/evoke-menu';
|
import { QUICK_INSERT_ITEMS } from '../menus/quick-insert';
|
||||||
|
|
||||||
export const EvokeMenuPluginKey = new PluginKey('evokeMenu');
|
export const QuickInsertPluginKey = new PluginKey('quickInsert');
|
||||||
|
|
||||||
export const EvokeMenu = Node.create({
|
export const QuickInsert = Node.create({
|
||||||
name: 'evokeMenu',
|
name: 'quickInsert',
|
||||||
|
|
||||||
addOptions() {
|
addOptions() {
|
||||||
return {
|
return {
|
||||||
HTMLAttributes: {},
|
HTMLAttributes: {},
|
||||||
suggestion: {
|
suggestion: {
|
||||||
char: '/',
|
char: '/',
|
||||||
pluginKey: EvokeMenuPluginKey,
|
pluginKey: QuickInsertPluginKey,
|
||||||
command: ({ editor, range, props }) => {
|
command: ({ editor, range, props }) => {
|
||||||
const { state, dispatch } = editor.view;
|
const { state, dispatch } = editor.view;
|
||||||
const $from = state.selection.$from;
|
const $from = state.selection.$from;
|
||||||
|
@ -81,7 +81,7 @@ export const EvokeMenu = Node.create({
|
||||||
}).configure({
|
}).configure({
|
||||||
suggestion: {
|
suggestion: {
|
||||||
items: ({ query }) => {
|
items: ({ query }) => {
|
||||||
return EVOKE_MENU_ITEMS.filter((command) => command.key.startsWith(query));
|
return QUICK_INSERT_ITEMS.filter((command) => command.key.startsWith(query));
|
||||||
},
|
},
|
||||||
render: () => {
|
render: () => {
|
||||||
let component;
|
let component;
|
|
@ -23,6 +23,7 @@ declare module '@tiptap/core' {
|
||||||
* @description Replace all instances of search result with given replace term.
|
* @description Replace all instances of search result with given replace term.
|
||||||
*/
|
*/
|
||||||
replaceAll: () => ReturnType;
|
replaceAll: () => ReturnType;
|
||||||
|
goToPrevSearchResult: () => void;
|
||||||
goToNextSearchResult: () => void;
|
goToNextSearchResult: () => void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,41 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Space, Button } from '@douyinfe/semi-ui';
|
import { Space } from '@douyinfe/semi-ui';
|
||||||
import { IconUndo, IconRedo } from '@douyinfe/semi-icons';
|
|
||||||
import { Tooltip } from 'components/tooltip';
|
|
||||||
import { IconClear } from 'components/icons';
|
|
||||||
import { Divider } from './divider';
|
import { Divider } from './divider';
|
||||||
import { MediaInsertMenu } from './menus/media-insert';
|
|
||||||
import { Paragraph } from './menus/paragraph';
|
|
||||||
import { FontSize } from './menus/font-size';
|
|
||||||
import { BaseMenu } from './menus/base-menu';
|
|
||||||
import { AlignMenu } from './menus/align';
|
|
||||||
import { ListMenu } from './menus/list';
|
|
||||||
import { BaseInsertMenu } from './menus/base-insert';
|
|
||||||
import { BaseBubbleMenu } from './menus/base-bubble-menu';
|
|
||||||
import { ImageBubbleMenu } from './menus/image';
|
|
||||||
import { BannerBubbleMenu } from './menus/banner';
|
|
||||||
import { LinkBubbleMenu } from './menus/link';
|
|
||||||
import { IframeBubbleMenu } from './menus/iframe';
|
|
||||||
import { TableBubbleMenu } from './menus/table';
|
|
||||||
|
|
||||||
import { CountdownBubbleMenu } from './menus/countdown';
|
import { Insert } from './menus/insert';
|
||||||
import { CountdownSettingModal } from './menus/countdown-setting';
|
import { Undo } from './menus/undo';
|
||||||
|
import { Redo } from './menus/redo';
|
||||||
|
import { CleadrNodeAndMarks } from './menus/clear-node-and-marks';
|
||||||
|
|
||||||
|
import { Heading } from './menus/heading';
|
||||||
|
import { FontSize } from './menus/fontsize';
|
||||||
|
import { Bold } from './menus/bold';
|
||||||
|
import { Italic } from './menus/italic';
|
||||||
|
import { Underline } from './menus/underline';
|
||||||
|
import { Strike } from './menus/strike';
|
||||||
|
import { Code } from './menus/code';
|
||||||
|
import { Superscript } from './menus/superscript';
|
||||||
|
import { Subscript } from './menus/subscript';
|
||||||
|
import { TextColor } from './menus/text-color';
|
||||||
|
import { BackgroundColor } from './menus/background-color';
|
||||||
|
|
||||||
|
import { Align } from './menus/align';
|
||||||
|
|
||||||
|
import { BulletList } from './menus/bullet-list';
|
||||||
|
import { OrderedList } from './menus/ordered-list';
|
||||||
|
import { TaskList } from './menus/task-list';
|
||||||
|
import { Ident } from './menus/ident';
|
||||||
|
|
||||||
|
import { Emoji } from './menus/emoji';
|
||||||
|
import { Link } from './menus/link';
|
||||||
|
import { Blockquote } from './menus/blockquote';
|
||||||
|
import { HorizontalRule } from './menus/horizontal-rule';
|
||||||
|
import { Search } from './menus/search';
|
||||||
|
|
||||||
|
import { Countdonw } from './menus/countdown';
|
||||||
|
import { Image } from './menus/image';
|
||||||
|
import { Iframe } from './menus/iframe';
|
||||||
|
import { Table } from './menus/table';
|
||||||
|
|
||||||
export const MenuBar: React.FC<{ editor: any }> = ({ editor }) => {
|
export const MenuBar: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
|
@ -29,63 +45,52 @@ export const MenuBar: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Space spacing={2}>
|
<Space spacing={2}>
|
||||||
<MediaInsertMenu editor={editor} />
|
<Insert editor={editor} />
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<Tooltip content="撤销">
|
|
||||||
<Button
|
|
||||||
onClick={() => editor.chain().focus().undo().run()}
|
|
||||||
icon={<IconUndo />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="重做">
|
<Undo editor={editor} />
|
||||||
<Button
|
<Redo editor={editor} />
|
||||||
onClick={() => editor.chain().focus().redo().run()}
|
<CleadrNodeAndMarks editor={editor} />
|
||||||
icon={<IconRedo />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="清除格式">
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
editor.chain().focus().unsetAllMarks().run();
|
|
||||||
editor.chain().focus().clearNodes().run();
|
|
||||||
}}
|
|
||||||
icon={<IconClear />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<Paragraph editor={editor} />
|
|
||||||
|
<Heading editor={editor} />
|
||||||
<FontSize editor={editor} />
|
<FontSize editor={editor} />
|
||||||
<BaseMenu editor={editor} />
|
<Bold editor={editor} />
|
||||||
|
<Italic editor={editor} />
|
||||||
|
<Underline editor={editor} />
|
||||||
|
<Strike editor={editor} />
|
||||||
|
<Code editor={editor} />
|
||||||
|
<Superscript editor={editor} />
|
||||||
|
<Subscript editor={editor} />
|
||||||
|
<TextColor editor={editor} />
|
||||||
|
<BackgroundColor editor={editor} />
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<AlignMenu editor={editor} />
|
|
||||||
|
<Align editor={editor} />
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<ListMenu editor={editor} />
|
|
||||||
|
<BulletList editor={editor} />
|
||||||
|
<OrderedList editor={editor} />
|
||||||
|
<TaskList editor={editor} />
|
||||||
|
<Ident editor={editor} />
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<BaseInsertMenu editor={editor} />
|
|
||||||
|
<Emoji editor={editor} />
|
||||||
|
<Link editor={editor} />
|
||||||
|
<Blockquote editor={editor} />
|
||||||
|
<HorizontalRule editor={editor} />
|
||||||
|
<Search editor={editor} />
|
||||||
|
|
||||||
|
<Countdonw editor={editor} />
|
||||||
|
<Image editor={editor} />
|
||||||
|
<Iframe editor={editor} />
|
||||||
|
<Table editor={editor} />
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<BaseBubbleMenu editor={editor} />
|
|
||||||
<ImageBubbleMenu editor={editor} />
|
|
||||||
<LinkBubbleMenu editor={editor} />
|
|
||||||
<IframeBubbleMenu editor={editor} />
|
|
||||||
<BannerBubbleMenu editor={editor} />
|
|
||||||
<TableBubbleMenu editor={editor} />
|
|
||||||
|
|
||||||
<CountdownBubbleMenu editor={editor} />
|
|
||||||
<CountdownSettingModal editor={editor} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -98,38 +103,23 @@ export const CommentMenuBar: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Space spacing={2}>
|
<Space spacing={2}>
|
||||||
<Tooltip content="撤销">
|
<Undo editor={editor} />
|
||||||
<Button
|
<Redo editor={editor} />
|
||||||
onClick={() => editor.chain().focus().undo().run()}
|
<CleadrNodeAndMarks editor={editor} />
|
||||||
icon={<IconUndo />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="重做">
|
|
||||||
<Button
|
|
||||||
onClick={() => editor.chain().focus().redo().run()}
|
|
||||||
icon={<IconRedo />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="清除格式">
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
editor.chain().focus().unsetAllMarks().run();
|
|
||||||
editor.chain().focus().clearNodes().run();
|
|
||||||
}}
|
|
||||||
icon={<IconClear />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<BaseMenu editor={editor} />
|
|
||||||
|
<Heading editor={editor} />
|
||||||
|
<FontSize editor={editor} />
|
||||||
|
<Bold editor={editor} />
|
||||||
|
<Italic editor={editor} />
|
||||||
|
<Underline editor={editor} />
|
||||||
|
<Strike editor={editor} />
|
||||||
|
<Code editor={editor} />
|
||||||
|
<Superscript editor={editor} />
|
||||||
|
<Subscript editor={editor} />
|
||||||
|
<TextColor editor={editor} />
|
||||||
|
<BackgroundColor editor={editor} />
|
||||||
</Space>
|
</Space>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,3 +6,8 @@ export const OPEN_COUNT_SETTING_MODAL = 'OPEN_COUNT_SETTING_MODAL';
|
||||||
export const triggerOpenCountSettingModal = (data) => {
|
export const triggerOpenCountSettingModal = (data) => {
|
||||||
event.emit(OPEN_COUNT_SETTING_MODAL, data);
|
event.emit(OPEN_COUNT_SETTING_MODAL, data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const OPEN_LINK_SETTING_MODAL = 'OPEN_LINK_SETTING_MODAL';
|
||||||
|
export const triggerOpenLinkSettingModal = (data) => {
|
||||||
|
event.emit(OPEN_LINK_SETTING_MODAL, data);
|
||||||
|
};
|
|
@ -1,9 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
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 '../services/is-active';
|
import { isTitleActive } from '../../services/is-active';
|
||||||
|
|
||||||
export const AlignMenu = ({ editor }) => {
|
export const Align: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
const current = (() => {
|
const current = (() => {
|
||||||
if (editor.isActive({ textAlign: 'center' })) {
|
if (editor.isActive({ textAlign: 'center' })) {
|
||||||
return <IconAlignCenter />;
|
return <IconAlignCenter />;
|
|
@ -0,0 +1,46 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconMark } from '@douyinfe/semi-icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { isTitleActive } from '../../services/is-active';
|
||||||
|
import { ColorPicker } from '../_components/color-picker';
|
||||||
|
|
||||||
|
export const BackgroundColor: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
const { backgroundColor } = editor.getAttributes('textStyle');
|
||||||
|
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ColorPicker
|
||||||
|
onSetColor={(color) => {
|
||||||
|
color
|
||||||
|
? editor.chain().focus().setBackgroundColor(color).run()
|
||||||
|
: editor.chain().focus().unsetBackgroundColor().run();
|
||||||
|
}}
|
||||||
|
disabled={isTitleActive(editor)}
|
||||||
|
>
|
||||||
|
<Tooltip content="背景色">
|
||||||
|
<Button
|
||||||
|
theme={editor.isActive('textStyle') ? 'light' : 'borderless'}
|
||||||
|
type={'tertiary'}
|
||||||
|
icon={
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconMark />
|
||||||
|
<span style={{ backgroundColor, width: 12, height: 2 }}></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
disabled={isTitleActive(editor)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</ColorPicker>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,105 +0,0 @@
|
||||||
import { Space, Button } from '@douyinfe/semi-ui';
|
|
||||||
import { IconDelete, IconTickCircle, IconAlertTriangle, IconClear, IconInfoCircle } from '@douyinfe/semi-icons';
|
|
||||||
import { Tooltip } from 'components/tooltip';
|
|
||||||
import { BubbleMenu } from '../views/bubble-menu';
|
|
||||||
import { Divider } from '../divider';
|
|
||||||
import { Banner } from '../extensions/banner';
|
|
||||||
import { deleteNode } from '../services/delete-node';
|
|
||||||
|
|
||||||
export const BannerBubbleMenu = ({ editor }) => {
|
|
||||||
return (
|
|
||||||
<BubbleMenu
|
|
||||||
className={'bubble-menu js-bubble-menu-banner'}
|
|
||||||
editor={editor}
|
|
||||||
pluginKey="banner-bubble-menu"
|
|
||||||
shouldShow={() => editor.isActive(Banner.name)}
|
|
||||||
matchRenderContainer={(node) => node && node.id === 'js-bannber-container'}
|
|
||||||
>
|
|
||||||
<Space>
|
|
||||||
<Tooltip content="信息">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
icon={<IconInfoCircle style={{ color: 'var(--semi-color-info)' }} />}
|
|
||||||
onClick={() => {
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.updateAttributes(Banner.name, {
|
|
||||||
type: 'info',
|
|
||||||
})
|
|
||||||
.focus()
|
|
||||||
.run();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="警告">
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.updateAttributes(Banner.name, {
|
|
||||||
type: 'warning',
|
|
||||||
})
|
|
||||||
.focus()
|
|
||||||
.run();
|
|
||||||
}}
|
|
||||||
icon={<IconAlertTriangle style={{ color: 'var(--semi-color-warning)' }} />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="危险">
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.updateAttributes(Banner.name, {
|
|
||||||
type: 'danger',
|
|
||||||
})
|
|
||||||
.focus()
|
|
||||||
.run();
|
|
||||||
}}
|
|
||||||
icon={<IconClear style={{ color: 'var(--semi-color-danger)' }} />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="成功">
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.updateAttributes(Banner.name, {
|
|
||||||
type: 'success',
|
|
||||||
})
|
|
||||||
.focus()
|
|
||||||
.run();
|
|
||||||
}}
|
|
||||||
icon={<IconTickCircle style={{ color: 'var(--semi-color-success)' }} />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<Tooltip content="删除" hideOnClick>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
icon={<IconDelete />}
|
|
||||||
onClick={() => deleteNode('banner', editor)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Space>
|
|
||||||
</BubbleMenu>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,62 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { BubbleMenu, Editor } from '@tiptap/react';
|
|
||||||
import { Space } from '@douyinfe/semi-ui';
|
|
||||||
import { Title } from '../extensions/title';
|
|
||||||
import { Link } from '../extensions/link';
|
|
||||||
import { Attachment } from '../extensions/attachment';
|
|
||||||
import { Image } from '../extensions/image';
|
|
||||||
import { Banner } from '../extensions/banner';
|
|
||||||
import { CodeBlock } from '../extensions/code-block';
|
|
||||||
import { Status } from '../extensions/status';
|
|
||||||
import { HorizontalRule } from '../extensions/horizontal-rule';
|
|
||||||
import { Iframe } from '../extensions/iframe';
|
|
||||||
import { Mind } from '../extensions/mind';
|
|
||||||
import { Table } from '../extensions/table';
|
|
||||||
import { TaskList } from '../extensions/task-list';
|
|
||||||
import { TaskItem } from '../extensions/task-item';
|
|
||||||
import { Katex } from '../extensions/katex';
|
|
||||||
import { DocumentReference } from '../extensions/document-reference';
|
|
||||||
import { DocumentChildren } from '../extensions/document-children';
|
|
||||||
import { Countdown } from '../extensions/countdown';
|
|
||||||
import { BaseMenu } from './base-menu';
|
|
||||||
|
|
||||||
const OTHER_BUBBLE_MENU_TYPES = [
|
|
||||||
Title.name,
|
|
||||||
Link.name,
|
|
||||||
Attachment.name,
|
|
||||||
Image.name,
|
|
||||||
Banner.name,
|
|
||||||
CodeBlock.name,
|
|
||||||
Status.name,
|
|
||||||
Iframe.name,
|
|
||||||
Mind.name,
|
|
||||||
Table.name,
|
|
||||||
TaskList.name,
|
|
||||||
TaskItem.name,
|
|
||||||
DocumentReference.name,
|
|
||||||
DocumentChildren.name,
|
|
||||||
Katex.name,
|
|
||||||
HorizontalRule.name,
|
|
||||||
Countdown.name,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const BaseBubbleMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
|
|
||||||
if (!editor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BubbleMenu
|
|
||||||
editor={editor}
|
|
||||||
className={'bubble-menu'}
|
|
||||||
pluginKey="base-bubble-menu"
|
|
||||||
shouldShow={() =>
|
|
||||||
!editor.state.selection.empty && OTHER_BUBBLE_MENU_TYPES.every((type) => !editor.isActive(type))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Space>
|
|
||||||
<BaseMenu editor={editor} />
|
|
||||||
</Space>
|
|
||||||
</BubbleMenu>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,52 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
|
||||||
import { Tooltip } from 'components/tooltip';
|
|
||||||
import { IconQuote, IconLink, IconHorizontalRule } from 'components/icons';
|
|
||||||
import { isTitleActive } from '../services/is-active';
|
|
||||||
import { Emoji } from './emoji';
|
|
||||||
import { Search } from './search';
|
|
||||||
|
|
||||||
export const BaseInsertMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
|
||||||
if (!editor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Emoji editor={editor} />
|
|
||||||
|
|
||||||
<Tooltip content="插入链接">
|
|
||||||
<Button
|
|
||||||
theme={editor.isActive('link') ? 'light' : 'borderless'}
|
|
||||||
type="tertiary"
|
|
||||||
icon={<IconLink />}
|
|
||||||
onClick={() => editor.chain().focus().toggleLink().run()}
|
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="插入引用">
|
|
||||||
<Button
|
|
||||||
theme={editor.isActive('blockquote') ? 'light' : 'borderless'}
|
|
||||||
type="tertiary"
|
|
||||||
icon={<IconQuote />}
|
|
||||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
|
||||||
className={editor.isActive('blockquote') ? 'is-active' : ''}
|
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="插入分割线">
|
|
||||||
<Button
|
|
||||||
theme={'borderless'}
|
|
||||||
type="tertiary"
|
|
||||||
icon={<IconHorizontalRule />}
|
|
||||||
onClick={() => editor.chain().focus().setHorizontalRule().run()}
|
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Search editor={editor} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,89 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
|
||||||
import { IconBold, IconItalic, IconStrikeThrough, IconUnderline, IconCode } from '@douyinfe/semi-icons';
|
|
||||||
import { IconSup, IconSub } from 'components/icons';
|
|
||||||
import { Tooltip } from 'components/tooltip';
|
|
||||||
import { isTitleActive } from '../services/is-active';
|
|
||||||
import { ColorMenu } from './color';
|
|
||||||
|
|
||||||
export const BaseMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
|
||||||
if (!editor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Tooltip content="粗体">
|
|
||||||
<Button
|
|
||||||
theme={editor.isActive('bold') ? 'light' : 'borderless'}
|
|
||||||
type="tertiary"
|
|
||||||
icon={<IconBold />}
|
|
||||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="斜体">
|
|
||||||
<Button
|
|
||||||
theme={editor.isActive('italic') ? 'light' : 'borderless'}
|
|
||||||
type="tertiary"
|
|
||||||
icon={<IconItalic />}
|
|
||||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="下划线">
|
|
||||||
<Button
|
|
||||||
theme={editor.isActive('underline') ? 'light' : 'borderless'}
|
|
||||||
type="tertiary"
|
|
||||||
icon={<IconUnderline />}
|
|
||||||
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="删除线">
|
|
||||||
<Button
|
|
||||||
theme={editor.isActive('strike') ? 'light' : 'borderless'}
|
|
||||||
type="tertiary"
|
|
||||||
icon={<IconStrikeThrough />}
|
|
||||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="行内代码">
|
|
||||||
<Button
|
|
||||||
theme={editor.isActive('code') ? 'light' : 'borderless'}
|
|
||||||
type="tertiary"
|
|
||||||
icon={<IconCode />}
|
|
||||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="上标">
|
|
||||||
<Button
|
|
||||||
theme={editor.isActive('superscript') ? 'light' : 'borderless'}
|
|
||||||
type="tertiary"
|
|
||||||
icon={<IconSup />}
|
|
||||||
onClick={() => editor.chain().focus().toggleSuperscript().run()}
|
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="下标">
|
|
||||||
<Button
|
|
||||||
theme={editor.isActive('subscript') ? 'light' : 'borderless'}
|
|
||||||
type="tertiary"
|
|
||||||
icon={<IconSub />}
|
|
||||||
onClick={() => editor.chain().focus().toggleSubscript().run()}
|
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<ColorMenu editor={editor} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { IconQuote } from 'components/icons';
|
||||||
|
import { isTitleActive } from '../../services/is-active';
|
||||||
|
|
||||||
|
export const Blockquote: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content="插入引用">
|
||||||
|
<Button
|
||||||
|
theme={editor.isActive('blockquote') ? 'light' : 'borderless'}
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconQuote />}
|
||||||
|
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||||
|
className={editor.isActive('blockquote') ? 'is-active' : ''}
|
||||||
|
disabled={isTitleActive(editor)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,24 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconBold } from '@douyinfe/semi-icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { isTitleActive } from '../../services/is-active';
|
||||||
|
|
||||||
|
export const Bold: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content="粗体">
|
||||||
|
<Button
|
||||||
|
theme={editor.isActive('bold') ? 'light' : 'borderless'}
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconBold />}
|
||||||
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||||
|
disabled={isTitleActive(editor)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,24 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconList } from 'components/icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { isTitleActive } from '../../services/is-active';
|
||||||
|
|
||||||
|
export const BulletList: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content="无序列表">
|
||||||
|
<Button
|
||||||
|
theme={editor.isActive('bulletList') ? 'light' : 'borderless'}
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconList />}
|
||||||
|
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||||
|
disabled={isTitleActive(editor)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconClear } from 'components/icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
|
||||||
|
export const CleadrNodeAndMarks: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content="清除格式">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
editor.chain().focus().unsetAllMarks().run();
|
||||||
|
editor.chain().focus().clearNodes().run();
|
||||||
|
}}
|
||||||
|
icon={<IconClear />}
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,24 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconCode } from '@douyinfe/semi-icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { isTitleActive } from '../../services/is-active';
|
||||||
|
|
||||||
|
export const Code: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content="行内代码">
|
||||||
|
<Button
|
||||||
|
theme={editor.isActive('code') ? 'light' : 'borderless'}
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconCode />}
|
||||||
|
onClick={() => editor.chain().focus().toggleCode().run()}
|
||||||
|
disabled={isTitleActive(editor)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,80 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
|
||||||
import { IconFont, IconMark } from '@douyinfe/semi-icons';
|
|
||||||
import { Tooltip } from 'components/tooltip';
|
|
||||||
import { isTitleActive } from '../services/is-active';
|
|
||||||
import { ColorPicker } from './color-picker';
|
|
||||||
|
|
||||||
export const ColorMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
|
||||||
const { color, backgroundColor } = editor.getAttributes('textStyle');
|
|
||||||
|
|
||||||
if (!editor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ColorPicker
|
|
||||||
onSetColor={(color) => {
|
|
||||||
color ? editor.chain().focus().setColor(color).run() : editor.chain().focus().unsetColor(color).run();
|
|
||||||
}}
|
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
>
|
|
||||||
<Tooltip content="文本色">
|
|
||||||
<Button
|
|
||||||
theme={editor.isActive('textStyle') ? 'light' : 'borderless'}
|
|
||||||
type={'tertiary'}
|
|
||||||
icon={
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconFont style={{ fontSize: '0.85em' }} />
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
width: 12,
|
|
||||||
height: 2,
|
|
||||||
backgroundColor: color,
|
|
||||||
}}
|
|
||||||
></span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</ColorPicker>
|
|
||||||
|
|
||||||
<ColorPicker
|
|
||||||
onSetColor={(color) => {
|
|
||||||
color
|
|
||||||
? editor.chain().focus().setBackgroundColor(color).run()
|
|
||||||
: editor.chain().focus().unsetBackgroundColor().run();
|
|
||||||
}}
|
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
>
|
|
||||||
<Tooltip content="背景色">
|
|
||||||
<Button
|
|
||||||
theme={editor.isActive('textStyle') ? 'light' : 'borderless'}
|
|
||||||
type={'tertiary'}
|
|
||||||
icon={
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconMark />
|
|
||||||
<span style={{ backgroundColor, width: 12, height: 2 }}></span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</ColorPicker>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,66 +0,0 @@
|
||||||
import { useCallback, useRef } from 'react';
|
|
||||||
import { Space, Button, Modal, Form, Typography } from '@douyinfe/semi-ui';
|
|
||||||
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
|
|
||||||
import { IconEdit, IconExternalOpen, IconLineHeight, IconDelete } from '@douyinfe/semi-icons';
|
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
|
||||||
import { Tooltip } from 'components/tooltip';
|
|
||||||
import { BubbleMenu } from '../views/bubble-menu';
|
|
||||||
import { Countdown } from '../extensions/countdown';
|
|
||||||
import { Divider } from '../divider';
|
|
||||||
import { event, triggerOpenCountSettingModal } from './event';
|
|
||||||
|
|
||||||
export const CountdownBubbleMenu = ({ editor }) => {
|
|
||||||
const attrs = editor.getAttributes(Countdown.name);
|
|
||||||
const $form = useRef<FormApi>();
|
|
||||||
// const [visible, toggleVisible] = useToggle(false);
|
|
||||||
|
|
||||||
// const useExample = useCallback(() => {
|
|
||||||
// $form.current.setValue('url', EXAMPLE_LINK);
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
// const handleCancel = useCallback(() => {
|
|
||||||
// toggleVisible(false);
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
// const handleOk = useCallback(() => {
|
|
||||||
// $form.current.validate().then((values) => {
|
|
||||||
// editor
|
|
||||||
// .chain()
|
|
||||||
// .updateAttributes(Countdown.name, {
|
|
||||||
// url: values.url,
|
|
||||||
// })
|
|
||||||
// .setNodeSelection(editor.state.selection.from)
|
|
||||||
// .focus()
|
|
||||||
// .run();
|
|
||||||
// toggleVisible(false);
|
|
||||||
// });
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
const openEditLinkModal = useCallback(() => {
|
|
||||||
triggerOpenCountSettingModal(attrs);
|
|
||||||
}, [attrs]);
|
|
||||||
|
|
||||||
const deleteNode = useCallback(() => editor.chain().deleteSelection().run(), [editor]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BubbleMenu
|
|
||||||
className={'bubble-menu'}
|
|
||||||
editor={editor}
|
|
||||||
pluginKey="countdonw-bubble-menu"
|
|
||||||
shouldShow={() => editor.isActive(Countdown.name)}
|
|
||||||
tippyOptions={{ maxWidth: 456 }}
|
|
||||||
>
|
|
||||||
<Space>
|
|
||||||
<Tooltip content="编辑">
|
|
||||||
<Button size="small" type="tertiary" theme="borderless" icon={<IconEdit />} onClick={openEditLinkModal} />
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<Tooltip content="删除节点" hideOnClick>
|
|
||||||
<Button onClick={deleteNode} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
|
||||||
</Tooltip>
|
|
||||||
</Space>
|
|
||||||
</BubbleMenu>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { Space, Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconEdit, IconDelete } from '@douyinfe/semi-icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { BubbleMenu } from '../../views/bubble-menu';
|
||||||
|
import { Countdown } from '../../extensions/countdown';
|
||||||
|
import { Divider } from '../../divider';
|
||||||
|
import { triggerOpenCountSettingModal } from '../_event';
|
||||||
|
|
||||||
|
export const CountdownBubbleMenu = ({ editor }) => {
|
||||||
|
const attrs = editor.getAttributes(Countdown.name);
|
||||||
|
|
||||||
|
const openEditLinkModal = useCallback(() => {
|
||||||
|
triggerOpenCountSettingModal(attrs);
|
||||||
|
}, [attrs]);
|
||||||
|
|
||||||
|
const deleteNode = useCallback(() => editor.chain().deleteSelection().run(), [editor]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BubbleMenu
|
||||||
|
className={'bubble-menu'}
|
||||||
|
editor={editor}
|
||||||
|
pluginKey="countdonw-bubble-menu"
|
||||||
|
shouldShow={() => editor.isActive(Countdown.name)}
|
||||||
|
tippyOptions={{ maxWidth: 456 }}
|
||||||
|
>
|
||||||
|
<Space>
|
||||||
|
<Tooltip content="编辑">
|
||||||
|
<Button size="small" type="tertiary" theme="borderless" icon={<IconEdit />} onClick={openEditLinkModal} />
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Tooltip content="删除节点" hideOnClick>
|
||||||
|
<Button onClick={deleteNode} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
</BubbleMenu>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,17 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { CountdownBubbleMenu } from './bubble';
|
||||||
|
import { CountdownSettingModal } from './modal';
|
||||||
|
|
||||||
|
export const Countdonw: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CountdownBubbleMenu editor={editor} />
|
||||||
|
<CountdownSettingModal editor={editor} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -3,13 +3,13 @@ import { Form, Modal } from '@douyinfe/semi-ui';
|
||||||
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
|
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
import { event, OPEN_COUNT_SETTING_MODAL } from './event';
|
import { event, OPEN_COUNT_SETTING_MODAL } from '../_event';
|
||||||
|
|
||||||
type IProps = { editor: Editor };
|
type IProps = { editor: Editor };
|
||||||
|
|
||||||
export const CountdownSettingModal: React.FC<IProps> = ({ editor, children }) => {
|
export const CountdownSettingModal: React.FC<IProps> = ({ editor }) => {
|
||||||
const $form = useRef<FormApi>();
|
const $form = useRef<FormApi>();
|
||||||
const [initialState, setInitialState] = useState({ date: Date.now() });
|
const [initialState, setInitialState] = useState({ title: '倒计时⏰', date: Date.now() });
|
||||||
const [visible, toggleVisible] = useToggle(false);
|
const [visible, toggleVisible] = useToggle(false);
|
||||||
|
|
||||||
const handleOk = useCallback(() => {
|
const handleOk = useCallback(() => {
|
||||||
|
@ -33,7 +33,7 @@ export const CountdownSettingModal: React.FC<IProps> = ({ editor, children }) =>
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal title="倒计时" visible={visible} onOk={handleOk} onCancel={() => toggleVisible(false)}>
|
<Modal centered title="倒计时" visible={visible} onOk={handleOk} onCancel={() => toggleVisible(false)}>
|
||||||
<Form initValues={initialState} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left">
|
<Form initValues={initialState} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left">
|
||||||
<Form.Input labelWidth={72} label="标题" field="title" required />
|
<Form.Input labelWidth={72} label="标题" field="title" required />
|
||||||
<Form.DatePicker labelWidth={72} style={{ width: '100%' }} label="截止日期" field="date" type="dateTime" />
|
<Form.DatePicker labelWidth={72} style={{ width: '100%' }} label="截止日期" field="date" type="dateTime" />
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { triggerOpenCountSettingModal } from '../_event';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新建倒计时
|
||||||
|
* @param editor
|
||||||
|
*/
|
||||||
|
export const createCountdown = (editor: Editor) => {
|
||||||
|
triggerOpenCountSettingModal(null);
|
||||||
|
};
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
import { Popover, Button, Typography } from '@douyinfe/semi-ui';
|
import { Popover, Button, Typography } from '@douyinfe/semi-ui';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { IconEmoji } from 'components/icons';
|
import { IconEmoji } from 'components/icons';
|
||||||
|
@ -18,7 +19,7 @@ const LIST = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Emoji = ({ editor }) => {
|
export const Emoji: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
const setEmoji = useCallback((emoji) => {
|
const setEmoji = useCallback((emoji) => {
|
||||||
return () => {
|
return () => {
|
||||||
const transaction = editor.state.tr.insertText(emoji);
|
const transaction = editor.state.tr.insertText(emoji);
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Select } from '@douyinfe/semi-ui';
|
import { Select } from '@douyinfe/semi-ui';
|
||||||
import { isTitleActive } from '../services/is-active';
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { isTitleActive } from '../../services/is-active';
|
||||||
|
|
||||||
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 = ({ editor }) => {
|
export const FontSize: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
const currentFontSizePx = editor.getAttributes('textStyle').fontSize || '16px';
|
const currentFontSizePx = editor.getAttributes('textStyle').fontSize || '16px';
|
||||||
const currentFontSize = +currentFontSizePx.replace('px', '');
|
const currentFontSize = +currentFontSizePx.replace('px', '');
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
import { Select } from '@douyinfe/semi-ui';
|
import { Select } from '@douyinfe/semi-ui';
|
||||||
import { isTitleActive } from '../services/is-active';
|
import { isTitleActive } from '../../services/is-active';
|
||||||
|
|
||||||
const getCurrentCaretTitle = (editor) => {
|
const getCurrentCaretTitle = (editor) => {
|
||||||
if (editor.isActive('heading', { level: 1 })) return 1;
|
if (editor.isActive('heading', { level: 1 })) return 1;
|
||||||
|
@ -12,7 +13,7 @@ const getCurrentCaretTitle = (editor) => {
|
||||||
return 'paragraph';
|
return 'paragraph';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Paragraph = ({ editor }) => {
|
export const Heading: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
const toggle = useCallback((level) => {
|
const toggle = useCallback((level) => {
|
||||||
if (level === 'paragraph') {
|
if (level === 'paragraph') {
|
||||||
editor.chain().focus().setParagraph().run();
|
editor.chain().focus().setParagraph().run();
|
|
@ -0,0 +1,24 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { IconHorizontalRule } from 'components/icons';
|
||||||
|
import { isTitleActive } from '../../services/is-active';
|
||||||
|
|
||||||
|
export const HorizontalRule: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content="插入分割线">
|
||||||
|
<Button
|
||||||
|
theme={'borderless'}
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconHorizontalRule />}
|
||||||
|
onClick={() => editor.chain().focus().setHorizontalRule().run()}
|
||||||
|
disabled={isTitleActive(editor)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,42 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconIndentLeft, IconIndentRight } from '@douyinfe/semi-icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { isTitleActive } from '../../services/is-active';
|
||||||
|
|
||||||
|
export const Ident: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip content="增加缩进">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
// @ts-ignore
|
||||||
|
editor.chain().focus().indent().run();
|
||||||
|
}}
|
||||||
|
icon={<IconIndentRight />}
|
||||||
|
theme={'borderless'}
|
||||||
|
type="tertiary"
|
||||||
|
disabled={isTitleActive(editor)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content="减少缩进">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
// @ts-ignore
|
||||||
|
editor.chain().focus().outdent().run();
|
||||||
|
}}
|
||||||
|
icon={<IconIndentLeft />}
|
||||||
|
theme={'borderless'}
|
||||||
|
type="tertiary"
|
||||||
|
disabled={isTitleActive(editor)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -4,10 +4,10 @@ import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
|
||||||
import { IconEdit, IconExternalOpen, IconLineHeight, IconDelete } from '@douyinfe/semi-icons';
|
import { IconEdit, IconExternalOpen, IconLineHeight, IconDelete } from '@douyinfe/semi-icons';
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { BubbleMenu } from '../views/bubble-menu';
|
import { BubbleMenu } from '../../views/bubble-menu';
|
||||||
import { Iframe } from '../extensions/iframe';
|
import { Iframe } from '../../extensions/iframe';
|
||||||
import { Divider } from '../divider';
|
import { Divider } from '../../divider';
|
||||||
import { Size } from './size';
|
import { Size } from '../_components/size';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { IframeBubbleMenu } from './bubble';
|
||||||
|
|
||||||
|
export const Iframe: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IframeBubbleMenu editor={editor} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -2,10 +2,10 @@ import React, { useEffect, useState } from 'react';
|
||||||
import { Space, Button } from '@douyinfe/semi-ui';
|
import { Space, Button } from '@douyinfe/semi-ui';
|
||||||
import { IconAlignLeft, IconAlignCenter, IconAlignRight, IconLineHeight, IconDelete } from '@douyinfe/semi-icons';
|
import { IconAlignLeft, IconAlignCenter, IconAlignRight, IconLineHeight, IconDelete } from '@douyinfe/semi-icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { BubbleMenu } from '../views/bubble-menu';
|
import { BubbleMenu } from '../../views/bubble-menu';
|
||||||
import { Divider } from '../divider';
|
import { Divider } from '../../divider';
|
||||||
import { Image } from '../extensions/image';
|
import { Image } from '../../extensions/image';
|
||||||
import { Size } from './size';
|
import { Size } from '../_components/size';
|
||||||
|
|
||||||
export const ImageBubbleMenu = ({ editor }) => {
|
export const ImageBubbleMenu = ({ editor }) => {
|
||||||
const attrs = editor.getAttributes(Image.name);
|
const attrs = editor.getAttributes(Image.name);
|
|
@ -0,0 +1,15 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { ImageBubbleMenu } from './bubble';
|
||||||
|
|
||||||
|
export const Image: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ImageBubbleMenu editor={editor} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -14,14 +14,13 @@ import {
|
||||||
IconInfo,
|
IconInfo,
|
||||||
IconAttachment,
|
IconAttachment,
|
||||||
IconMath,
|
IconMath,
|
||||||
|
IconCountdown,
|
||||||
} from 'components/icons';
|
} from 'components/icons';
|
||||||
import { GridSelect } from 'components/grid-select';
|
import { GridSelect } from 'components/grid-select';
|
||||||
|
import { isTitleActive } from '../../services/is-active';
|
||||||
|
import { createCountdown } from '../countdown/service';
|
||||||
|
|
||||||
import { isTitleActive } from '../services/is-active';
|
export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
import { event, OPEN_COUNT_SETTING_MODAL, triggerOpenCountSettingModal } from './event';
|
|
||||||
import { CountdownSettingModal } from './countdown-setting';
|
|
||||||
|
|
||||||
export const MediaInsertMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
|
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -35,14 +34,6 @@ export const MediaInsertMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
<Dropdown.Menu>
|
<Dropdown.Menu>
|
||||||
<Dropdown.Title>通用</Dropdown.Title>
|
<Dropdown.Title>通用</Dropdown.Title>
|
||||||
|
|
||||||
{/* <Dropdown.Item onClick={() => editor.chain().focus().setToc().run()}>
|
|
||||||
<IconCodeBlock /> 目录
|
|
||||||
</Dropdown.Item> */}
|
|
||||||
|
|
||||||
<Dropdown.Item onClick={() => triggerOpenCountSettingModal(null)}>
|
|
||||||
<IconCodeBlock /> 倒计时
|
|
||||||
</Dropdown.Item>
|
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
showArrow
|
showArrow
|
||||||
position="rightTop"
|
position="rightTop"
|
||||||
|
@ -76,6 +67,10 @@ export const MediaInsertMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
附件
|
附件
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
|
|
||||||
|
<Dropdown.Item onClick={() => createCountdown(editor)}>
|
||||||
|
<IconCountdown /> 倒计时
|
||||||
|
</Dropdown.Item>
|
||||||
|
|
||||||
<Dropdown.Item onClick={() => editor.chain().focus().setIframe({ url: '' }).run()}>
|
<Dropdown.Item onClick={() => editor.chain().focus().setIframe({ url: '' }).run()}>
|
||||||
<IconLink /> 外链
|
<IconLink /> 外链
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
|
@ -0,0 +1,24 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconItalic } from '@douyinfe/semi-icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { isTitleActive } from '../../services/is-active';
|
||||||
|
|
||||||
|
export const Italic: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content="斜体">
|
||||||
|
<Button
|
||||||
|
theme={editor.isActive('italic') ? 'light' : 'borderless'}
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconItalic />}
|
||||||
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||||
|
disabled={isTitleActive(editor)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,126 +0,0 @@
|
||||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
|
||||||
import { Space, Button, Modal, Form, Toast } from '@douyinfe/semi-ui';
|
|
||||||
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
|
|
||||||
import { IconExternalOpen, IconUnlink, IconEdit } from '@douyinfe/semi-icons';
|
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
|
||||||
import { Tooltip } from 'components/tooltip';
|
|
||||||
import { Divider } from '../divider';
|
|
||||||
import { BubbleMenu } from '../views/bubble-menu';
|
|
||||||
import { Link } from '../extensions/link';
|
|
||||||
import { isMarkActive } from '../services/is-active';
|
|
||||||
import { findMarkPosition } from '../services/find-position';
|
|
||||||
import { isValidURL } from '../services/valid-url';
|
|
||||||
|
|
||||||
export const LinkBubbleMenu = ({ editor }) => {
|
|
||||||
const attrs = editor.getAttributes(Link.name);
|
|
||||||
const { href, target } = attrs;
|
|
||||||
const isLinkActive = editor.isActive(Link.name);
|
|
||||||
const $form = useRef<FormApi>();
|
|
||||||
const [visible, toggleVisible] = useToggle(false);
|
|
||||||
const [text, setText] = useState();
|
|
||||||
|
|
||||||
const handleCancel = useCallback(() => {
|
|
||||||
toggleVisible(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleOk = useCallback(() => {
|
|
||||||
$form.current.validate().then((values) => {
|
|
||||||
if (!values.href) {
|
|
||||||
Toast.error('请输入有效链接');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!values.text) {
|
|
||||||
values.text = values.href;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values.text !== text && values.text) {
|
|
||||||
editor.chain().extendMarkRange(Link.name).run();
|
|
||||||
const { view } = editor;
|
|
||||||
const schema = view.state.schema;
|
|
||||||
const node = schema.text(values.text, [schema.marks.link.create({ href: values.href })]);
|
|
||||||
view.dispatch(view.state.tr.replaceSelectionWith(node, true).scrollIntoView());
|
|
||||||
} else {
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.extendMarkRange(Link.name)
|
|
||||||
.updateAttributes(Link.name, {
|
|
||||||
href: values.href,
|
|
||||||
})
|
|
||||||
.focus()
|
|
||||||
.run();
|
|
||||||
}
|
|
||||||
toggleVisible(false);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const visitLink = useCallback(() => {
|
|
||||||
window.open(href, target);
|
|
||||||
}, [href, target]);
|
|
||||||
|
|
||||||
const openEditLinkModal = useCallback(() => {
|
|
||||||
toggleVisible(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const unsetLink = useCallback(() => editor.chain().extendMarkRange(Link.name).unsetLink().run(), [editor]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isLinkActive) return;
|
|
||||||
|
|
||||||
const { state } = editor;
|
|
||||||
const isInLink = isMarkActive(state.schema.marks.link)(state);
|
|
||||||
|
|
||||||
if (!isInLink) return;
|
|
||||||
|
|
||||||
const { $head } = editor.state.selection;
|
|
||||||
const marks = $head.marks();
|
|
||||||
if (!marks.length) return;
|
|
||||||
|
|
||||||
const mark = marks[0];
|
|
||||||
const node = $head.node($head.depth);
|
|
||||||
const startPosOfThisLine = $head.pos - $head.parentOffset;
|
|
||||||
const endPosOfThisLine = startPosOfThisLine + node.content.size;
|
|
||||||
const { start, end } = findMarkPosition(state, mark, startPosOfThisLine, endPosOfThisLine);
|
|
||||||
const text = state.doc.textBetween(start, end);
|
|
||||||
setText(text);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BubbleMenu
|
|
||||||
className={'bubble-menu'}
|
|
||||||
editor={editor}
|
|
||||||
pluginKey="link-bubble-menu"
|
|
||||||
shouldShow={() => editor.isActive(Link.name)}
|
|
||||||
tippyOptions={{ maxWidth: 456 }}
|
|
||||||
>
|
|
||||||
<Modal title="编辑链接" visible={visible} onOk={handleOk} onCancel={handleCancel} centered>
|
|
||||||
<Form initValues={{ text, href }} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left">
|
|
||||||
<Form.Input label="文本" field="text" placeholder="请输入文本"></Form.Input>
|
|
||||||
<Form.Input
|
|
||||||
autofocus
|
|
||||||
label="链接"
|
|
||||||
field="href"
|
|
||||||
placeholder="请输入外链地址"
|
|
||||||
rules={[{ validator: (_, value) => isValidURL(value), message: '请输入有效链接' }]}
|
|
||||||
></Form.Input>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Space>
|
|
||||||
<Tooltip content="访问链接">
|
|
||||||
<Button size="small" type="tertiary" theme="borderless" icon={<IconExternalOpen />} onClick={visitLink} />
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="编辑链接">
|
|
||||||
<Button size="small" type="tertiary" theme="borderless" icon={<IconEdit />} onClick={openEditLinkModal} />
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<Tooltip content="去除链接" hideOnClick>
|
|
||||||
<Button onClick={unsetLink} icon={<IconUnlink />} type="tertiary" theme="borderless" size="small" />
|
|
||||||
</Tooltip>
|
|
||||||
</Space>
|
|
||||||
</BubbleMenu>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||||
|
import { Space, Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconExternalOpen, IconUnlink, IconEdit } from '@douyinfe/semi-icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { Divider } from '../../divider';
|
||||||
|
import { BubbleMenu } from '../../views/bubble-menu';
|
||||||
|
import { Link } from '../../extensions/link';
|
||||||
|
import { isMarkActive } from '../../services/is-active';
|
||||||
|
import { findMarkPosition } from '../../services/find-position';
|
||||||
|
import { triggerOpenLinkSettingModal } from '../_event';
|
||||||
|
|
||||||
|
export const LinkBubbleMenu = ({ editor }) => {
|
||||||
|
const attrs = editor.getAttributes(Link.name);
|
||||||
|
const { href, target } = attrs;
|
||||||
|
const isLinkActive = editor.isActive(Link.name);
|
||||||
|
const [text, setText] = useState();
|
||||||
|
const [from, setFrom] = useState(-1);
|
||||||
|
const [to, setTo] = useState(-1);
|
||||||
|
|
||||||
|
const visitLink = useCallback(() => {
|
||||||
|
window.open(href, target);
|
||||||
|
}, [href, target]);
|
||||||
|
|
||||||
|
const openEditLinkModal = useCallback(() => {
|
||||||
|
triggerOpenLinkSettingModal({ href, text, from, to });
|
||||||
|
}, [href, text, from, to]);
|
||||||
|
|
||||||
|
const unsetLink = useCallback(() => editor.chain().extendMarkRange(Link.name).unsetLink().run(), [editor]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLinkActive) return;
|
||||||
|
|
||||||
|
const { state } = editor;
|
||||||
|
const isInLink = isMarkActive(state.schema.marks.link)(state);
|
||||||
|
|
||||||
|
if (!isInLink) return;
|
||||||
|
|
||||||
|
const { $head } = editor.state.selection;
|
||||||
|
const marks = $head.marks();
|
||||||
|
if (!marks.length) return;
|
||||||
|
|
||||||
|
const mark = marks[0];
|
||||||
|
const node = $head.node($head.depth);
|
||||||
|
const startPosOfThisLine = $head.pos - (($head.nodeBefore && $head.nodeBefore.nodeSize) || 0);
|
||||||
|
const endPosOfThisLine = $head.nodeAfter
|
||||||
|
? startPosOfThisLine + $head.nodeAfter.nodeSize
|
||||||
|
: $head.pos - $head.parentOffset + node.content.size;
|
||||||
|
|
||||||
|
const { start, end } = findMarkPosition(state, mark, startPosOfThisLine, endPosOfThisLine);
|
||||||
|
const text = state.doc.textBetween(start, end);
|
||||||
|
setText(text);
|
||||||
|
setFrom(start);
|
||||||
|
setTo(end);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BubbleMenu
|
||||||
|
className={'bubble-menu'}
|
||||||
|
editor={editor}
|
||||||
|
pluginKey="link-bubble-menu"
|
||||||
|
shouldShow={() => editor.isActive(Link.name)}
|
||||||
|
tippyOptions={{ maxWidth: 456 }}
|
||||||
|
>
|
||||||
|
<Space>
|
||||||
|
<Tooltip content="访问链接">
|
||||||
|
<Button size="small" type="tertiary" theme="borderless" icon={<IconExternalOpen />} onClick={visitLink} />
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content="编辑链接">
|
||||||
|
<Button size="small" type="tertiary" theme="borderless" icon={<IconEdit />} onClick={openEditLinkModal} />
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Tooltip content="去除链接" hideOnClick>
|
||||||
|
<Button onClick={unsetLink} icon={<IconUnlink />} type="tertiary" theme="borderless" size="small" />
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
</BubbleMenu>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,31 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { IconLink } from 'components/icons';
|
||||||
|
import { isTitleActive } from '../../services/is-active';
|
||||||
|
import { createOrToggleLink } from './service';
|
||||||
|
import { LinkBubbleMenu } from './bubble';
|
||||||
|
import { LinkSettingModal } from './modal';
|
||||||
|
|
||||||
|
export const Link: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip content="插入链接">
|
||||||
|
<Button
|
||||||
|
theme={editor.isActive('link') ? 'light' : 'borderless'}
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconLink />}
|
||||||
|
onClick={() => createOrToggleLink(editor)}
|
||||||
|
disabled={isTitleActive(editor)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<LinkBubbleMenu editor={editor} />
|
||||||
|
<LinkSettingModal editor={editor} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { Form, Modal } from '@douyinfe/semi-ui';
|
||||||
|
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
|
import { isValidURL } from '../../services/valid-url';
|
||||||
|
import { event, OPEN_LINK_SETTING_MODAL } from '../_event';
|
||||||
|
|
||||||
|
type IProps = { editor: Editor };
|
||||||
|
|
||||||
|
export const LinkSettingModal: React.FC<IProps> = ({ editor }) => {
|
||||||
|
const $form = useRef<FormApi>();
|
||||||
|
const [initialState, setInitialState] = useState({ text: '', href: '', from: -1, to: -1 });
|
||||||
|
const [visible, toggleVisible] = useToggle(false);
|
||||||
|
|
||||||
|
const handleOk = useCallback(() => {
|
||||||
|
$form.current.validate().then((values) => {
|
||||||
|
if (!values.text) {
|
||||||
|
values.text = values.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { from, to } = initialState;
|
||||||
|
const { view } = editor;
|
||||||
|
const schema = view.state.schema;
|
||||||
|
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.scrollIntoView());
|
||||||
|
toggleVisible(false);
|
||||||
|
});
|
||||||
|
}, [initialState]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (data) => {
|
||||||
|
toggleVisible(true);
|
||||||
|
data && setInitialState(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
event.on(OPEN_LINK_SETTING_MODAL, handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
event.off(OPEN_LINK_SETTING_MODAL, handler);
|
||||||
|
};
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal title="编辑链接" visible={visible} onOk={handleOk} onCancel={() => toggleVisible(false)} centered>
|
||||||
|
<Form initValues={initialState} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left">
|
||||||
|
<Form.Input label="文本" field="text" placeholder="请输入文本"></Form.Input>
|
||||||
|
<Form.Input
|
||||||
|
autofocus
|
||||||
|
label="链接"
|
||||||
|
field="href"
|
||||||
|
placeholder="请输入外链地址"
|
||||||
|
rules={[{ validator: (_, value) => isValidURL(value), message: '请输入有效链接' }]}
|
||||||
|
></Form.Input>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { isMarkActive } from '../../services/is-active';
|
||||||
|
import { triggerOpenLinkSettingModal } from '../_event';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新建或重设链接
|
||||||
|
* @param editor
|
||||||
|
*/
|
||||||
|
export const createOrToggleLink = (editor: Editor) => {
|
||||||
|
const { state } = editor;
|
||||||
|
const isInLink = isMarkActive(state.schema.marks.link)(state);
|
||||||
|
|
||||||
|
if (!isInLink) {
|
||||||
|
const selection = state.selection;
|
||||||
|
const text = state.doc.textBetween(selection.from, selection.to);
|
||||||
|
triggerOpenLinkSettingModal({ text, href: '', from: selection.from, to: selection.to });
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
editor.chain().focus().toggleLink().run();
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,71 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
|
||||||
import { IconIndentLeft, IconIndentRight } from '@douyinfe/semi-icons';
|
|
||||||
import { IconOrderedList, IconList } from 'components/icons';
|
|
||||||
import { Tooltip } from 'components/tooltip';
|
|
||||||
import { IconTask } from 'components/icons';
|
|
||||||
import { isTitleActive } from '../services/is-active';
|
|
||||||
|
|
||||||
export const ListMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
|
||||||
if (!editor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Tooltip content="无序列表">
|
|
||||||
<Button
|
|
||||||
theme={editor.isActive('bulletList') ? 'light' : 'borderless'}
|
|
||||||
type="tertiary"
|
|
||||||
icon={<IconList />}
|
|
||||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="有序列表">
|
|
||||||
<Button
|
|
||||||
theme={editor.isActive('orderedList') ? 'light' : 'borderless'}
|
|
||||||
type="tertiary"
|
|
||||||
icon={<IconOrderedList />}
|
|
||||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="任务列表">
|
|
||||||
<Button
|
|
||||||
theme={editor.isActive('taskList') ? 'light' : 'borderless'}
|
|
||||||
type="tertiary"
|
|
||||||
icon={<IconTask />}
|
|
||||||
onClick={() => editor.chain().focus().toggleTaskList().run()}
|
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="增加缩进">
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
editor.chain().focus().indent().run();
|
|
||||||
}}
|
|
||||||
icon={<IconIndentRight />}
|
|
||||||
theme={'borderless'}
|
|
||||||
type="tertiary"
|
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="减少缩进">
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
editor.chain().focus().outdent().run();
|
|
||||||
}}
|
|
||||||
icon={<IconIndentLeft />}
|
|
||||||
theme={'borderless'}
|
|
||||||
type="tertiary"
|
|
||||||
disabled={isTitleActive(editor)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconOrderedList } from 'components/icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { isTitleActive } from '../../services/is-active';
|
||||||
|
|
||||||
|
export const OrderedList: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content="有序列表">
|
||||||
|
<Button
|
||||||
|
theme={editor.isActive('orderedList') ? 'light' : 'borderless'}
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconOrderedList />}
|
||||||
|
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||||
|
disabled={isTitleActive(editor)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
|
@ -18,9 +18,12 @@ import {
|
||||||
IconInfo,
|
IconInfo,
|
||||||
IconAttachment,
|
IconAttachment,
|
||||||
IconMath,
|
IconMath,
|
||||||
|
IconCountdown,
|
||||||
} from 'components/icons';
|
} from 'components/icons';
|
||||||
|
import { createCountdown } from './countdown/service';
|
||||||
|
import { createOrToggleLink } from './link/service';
|
||||||
|
|
||||||
export const EVOKE_MENU_ITEMS = [
|
export const QUICK_INSERT_ITEMS = [
|
||||||
{
|
{
|
||||||
key: '标题1',
|
key: '标题1',
|
||||||
label: (
|
label: (
|
||||||
|
@ -95,7 +98,7 @@ export const EVOKE_MENU_ITEMS = [
|
||||||
链接
|
链接
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
command: (editor: Editor) => editor.chain().focus().toggleLink({ href: '' }).run(),
|
command: (editor: Editor) => createOrToggleLink(editor),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -164,6 +167,17 @@ export const EVOKE_MENU_ITEMS = [
|
||||||
command: (editor: Editor) => editor.chain().focus().setAttachment().run(),
|
command: (editor: Editor) => editor.chain().focus().setAttachment().run(),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
key: '倒计时',
|
||||||
|
label: () => (
|
||||||
|
<Space>
|
||||||
|
<IconCountdown />
|
||||||
|
倒计时
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
command: (editor: Editor) => createCountdown(editor),
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
key: '外链',
|
key: '外链',
|
||||||
label: (
|
label: (
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconRedo } from '@douyinfe/semi-icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
|
||||||
|
export const Redo: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content="撤销">
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().redo().run()}
|
||||||
|
icon={<IconRedo />}
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,12 +1,13 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { 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 { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { IconSearchReplace } from 'components/icons';
|
import { IconSearchReplace } from 'components/icons';
|
||||||
import { SearchNReplace } from '../../extensions/search';
|
import { SearchNReplace } from '../../extensions/search';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
export const Search = ({ editor }) => {
|
export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
const searchExtension = editor.extensionManager.extensions.find((ext) => ext.name === SearchNReplace.name);
|
const searchExtension = editor.extensionManager.extensions.find((ext) => ext.name === SearchNReplace.name);
|
||||||
const currentIndex = searchExtension ? searchExtension.options.currentIndex : -1;
|
const currentIndex = searchExtension ? searchExtension.options.currentIndex : -1;
|
||||||
const results = searchExtension ? searchExtension.options.results : [];
|
const results = searchExtension ? searchExtension.options.results : [];
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconStrikeThrough } from '@douyinfe/semi-icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { isTitleActive } from '../../services/is-active';
|
||||||
|
|
||||||
|
export const Strike: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content="删除线">
|
||||||
|
<Button
|
||||||
|
theme={editor.isActive('strike') ? 'light' : 'borderless'}
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconStrikeThrough />}
|
||||||
|
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||||
|
disabled={isTitleActive(editor)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,23 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconSub } from 'components/icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { isTitleActive } from '../../services/is-active';
|
||||||
|
|
||||||
|
export const Subscript: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content="下标">
|
||||||
|
<Button
|
||||||
|
theme={editor.isActive('subscript') ? 'light' : 'borderless'}
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconSub />}
|
||||||
|
onClick={() => editor.chain().focus().toggleSubscript().run()}
|
||||||
|
disabled={isTitleActive(editor)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,23 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconSup } from 'components/icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { isTitleActive } from '../../services/is-active';
|
||||||
|
|
||||||
|
export const Superscript: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content="上标">
|
||||||
|
<Button
|
||||||
|
theme={editor.isActive('superscript') ? 'light' : 'borderless'}
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconSup />}
|
||||||
|
onClick={() => editor.chain().focus().toggleSuperscript().run()}
|
||||||
|
disabled={isTitleActive(editor)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
|
@ -14,9 +14,9 @@ import {
|
||||||
IconTableHeaderCell,
|
IconTableHeaderCell,
|
||||||
} from 'components/icons';
|
} from 'components/icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { Divider } from '../divider';
|
import { Divider } from '../../divider';
|
||||||
import { BubbleMenu } from '../views/bubble-menu';
|
import { BubbleMenu } from '../../views/bubble-menu';
|
||||||
import { Table } from '../extensions/table';
|
import { Table } from '../../extensions/table';
|
||||||
|
|
||||||
export const TableBubbleMenu = ({ editor }) => {
|
export const TableBubbleMenu = ({ editor }) => {
|
||||||
return (
|
return (
|
|
@ -0,0 +1,11 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { TableBubbleMenu } from './bubble';
|
||||||
|
|
||||||
|
export const Table: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <TableBubbleMenu editor={editor} />;
|
||||||
|
};
|
|
@ -0,0 +1,24 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { IconTask } from 'components/icons';
|
||||||
|
import { isTitleActive } from '../../services/is-active';
|
||||||
|
|
||||||
|
export const TaskList: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content="任务列表">
|
||||||
|
<Button
|
||||||
|
theme={editor.isActive('taskList') ? 'light' : 'borderless'}
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconTask />}
|
||||||
|
onClick={() => editor.chain().focus().toggleTaskList().run()}
|
||||||
|
disabled={isTitleActive(editor)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,50 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconFont } from '@douyinfe/semi-icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { isTitleActive } from '../../services/is-active';
|
||||||
|
import { ColorPicker } from '../_components/color-picker';
|
||||||
|
|
||||||
|
export const TextColor: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
const { color } = editor.getAttributes('textStyle');
|
||||||
|
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconFont style={{ fontSize: '0.85em' }} />
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
width: 12,
|
||||||
|
height: 2,
|
||||||
|
backgroundColor: color,
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
disabled={isTitleActive(editor)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</ColorPicker>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,24 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconUnderline } from '@douyinfe/semi-icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { isTitleActive } from '../../services/is-active';
|
||||||
|
|
||||||
|
export const Underline: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content="下划线">
|
||||||
|
<Button
|
||||||
|
theme={editor.isActive('underline') ? 'light' : 'borderless'}
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconUnderline />}
|
||||||
|
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||||
|
disabled={isTitleActive(editor)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Editor } from '@tiptap/core';
|
||||||
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
|
import { IconUndo } from '@douyinfe/semi-icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
|
||||||
|
export const Undo: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content="撤销">
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().undo().run()}
|
||||||
|
icon={<IconUndo />}
|
||||||
|
type="tertiary"
|
||||||
|
theme="borderless"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Node } from './node';
|
||||||
|
|
||||||
|
export class Countdown extends Node {
|
||||||
|
type = 'countdown';
|
||||||
|
|
||||||
|
matching() {
|
||||||
|
return this.DOMNode.nodeName === 'DIV' && this.DOMNode.classList.contains('countdown');
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import { Heading } from './nodes/heading';
|
||||||
import { Image } from './nodes/image';
|
import { Image } from './nodes/image';
|
||||||
import { HorizontalRule } from './nodes/horizontal-rule';
|
import { HorizontalRule } from './nodes/horizontal-rule';
|
||||||
import { Blockquote } from './nodes/blockquote';
|
import { Blockquote } from './nodes/blockquote';
|
||||||
|
import { Countdown } from './nodes/countdown';
|
||||||
// 文本
|
// 文本
|
||||||
import { Title } from './nodes/title';
|
import { Title } from './nodes/title';
|
||||||
import { Katex } from './nodes/katex';
|
import { Katex } from './nodes/katex';
|
||||||
|
@ -52,6 +53,7 @@ export class Renderer {
|
||||||
|
|
||||||
this.nodes = [
|
this.nodes = [
|
||||||
Attachment,
|
Attachment,
|
||||||
|
Countdown,
|
||||||
Banner,
|
Banner,
|
||||||
Iframe,
|
Iframe,
|
||||||
Status,
|
Status,
|
||||||
|
|
|
@ -13,11 +13,12 @@ import { markdownItTable } from './markdownTable';
|
||||||
import { createMarkdownContainer } from './markdownItContainer';
|
import { createMarkdownContainer } from './markdownItContainer';
|
||||||
|
|
||||||
const markdownAttachment = createMarkdownContainer('attachment');
|
const markdownAttachment = createMarkdownContainer('attachment');
|
||||||
const markdownIframe = createMarkdownContainer('iframe');
|
|
||||||
const markdownStatus = createMarkdownContainer('status');
|
const markdownStatus = createMarkdownContainer('status');
|
||||||
const markdownMind = createMarkdownContainer('mind');
|
const markdownCountdown = createMarkdownContainer('countdown');
|
||||||
const markdownDocumentReference = createMarkdownContainer('documentReference');
|
const markdownDocumentReference = createMarkdownContainer('documentReference');
|
||||||
const markdownDocumentChildren = createMarkdownContainer('documentChildren');
|
const markdownDocumentChildren = createMarkdownContainer('documentChildren');
|
||||||
|
const markdownIframe = createMarkdownContainer('iframe');
|
||||||
|
const markdownMind = createMarkdownContainer('mind');
|
||||||
|
|
||||||
const markdown = markdownit('commonmark')
|
const markdown = markdownit('commonmark')
|
||||||
.enable('strikethrough')
|
.enable('strikethrough')
|
||||||
|
@ -33,6 +34,7 @@ const markdown = markdownit('commonmark')
|
||||||
// 以下为自定义节点
|
// 以下为自定义节点
|
||||||
.use(markdownBanner)
|
.use(markdownBanner)
|
||||||
.use(markdownAttachment)
|
.use(markdownAttachment)
|
||||||
|
.use(markdownCountdown)
|
||||||
.use(markdownIframe)
|
.use(markdownIframe)
|
||||||
.use(markdownStatus)
|
.use(markdownStatus)
|
||||||
.use(markdownMind)
|
.use(markdownMind)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { Bold } from '../../../extensions/bold';
|
||||||
import { BulletList } from '../../../extensions/bullet-list';
|
import { BulletList } from '../../../extensions/bullet-list';
|
||||||
import { Code } from '../../../extensions/code';
|
import { Code } from '../../../extensions/code';
|
||||||
import { CodeBlock } from '../../../extensions/code-block';
|
import { CodeBlock } from '../../../extensions/code-block';
|
||||||
|
import { Countdown } from '../../../extensions/countdown';
|
||||||
import { DocumentChildren } from '../../../extensions/document-children';
|
import { DocumentChildren } from '../../../extensions/document-children';
|
||||||
import { DocumentReference } from '../../../extensions/document-reference';
|
import { DocumentReference } from '../../../extensions/document-reference';
|
||||||
import { HardBreak } from '../../../extensions/hard-break';
|
import { HardBreak } from '../../../extensions/hard-break';
|
||||||
|
@ -118,6 +119,7 @@ const SerializerConfig = {
|
||||||
state.write('```');
|
state.write('```');
|
||||||
state.closeBlock(node);
|
state.closeBlock(node);
|
||||||
},
|
},
|
||||||
|
[Countdown.name]: renderCustomContainer('countdown'),
|
||||||
[DocumentChildren.name]: renderCustomContainer('documentChildren'),
|
[DocumentChildren.name]: renderCustomContainer('documentChildren'),
|
||||||
[DocumentReference.name]: renderCustomContainer('documentReference'),
|
[DocumentReference.name]: renderCustomContainer('documentReference'),
|
||||||
[HardBreak.name]: renderHardBreak,
|
[HardBreak.name]: renderHardBreak,
|
||||||
|
|
|
@ -1,10 +1,31 @@
|
||||||
import { NodeViewWrapper } from '@tiptap/react';
|
import { NodeViewWrapper } from '@tiptap/react';
|
||||||
import cls from 'classnames';
|
import cls from 'classnames';
|
||||||
import { Typography } from '@douyinfe/semi-ui';
|
import { Typography, Space } from '@douyinfe/semi-ui';
|
||||||
import Countdown from 'react-countdown';
|
import Countdown from 'react-countdown';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text, Title } = Typography;
|
||||||
|
|
||||||
|
const renderer = (props) => {
|
||||||
|
const { completed, formatted } = props;
|
||||||
|
const { days, hours, minutes, seconds } = formatted;
|
||||||
|
|
||||||
|
if (completed) {
|
||||||
|
return <Title heading={4}>已结束</Title>;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Space align="baseline">
|
||||||
|
<Title heading={2}>{days}</Title>
|
||||||
|
<Text size="small" style={{ transform: `translateY(-2px)` }}>
|
||||||
|
/ 天
|
||||||
|
</Text>
|
||||||
|
<Title heading={3}>
|
||||||
|
{hours}:{minutes}:{seconds}
|
||||||
|
</Title>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const CountdownWrapper = ({ editor, node }) => {
|
export const CountdownWrapper = ({ editor, node }) => {
|
||||||
const { title, date } = node.attrs;
|
const { title, date } = node.attrs;
|
||||||
|
@ -12,8 +33,8 @@ export const CountdownWrapper = ({ editor, node }) => {
|
||||||
return (
|
return (
|
||||||
<NodeViewWrapper>
|
<NodeViewWrapper>
|
||||||
<div className={cls(styles.wrap, 'render-wrapper')}>
|
<div className={cls(styles.wrap, 'render-wrapper')}>
|
||||||
<Text style={{ marginBottom: 12 }}>{title}</Text>
|
<Text>{title}</Text>
|
||||||
<Countdown date={date}></Countdown>
|
<Countdown date={date} renderer={renderer}></Countdown>
|
||||||
</div>
|
</div>
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue