diff --git a/packages/client/package.json b/packages/client/package.json index 14b36b66..3f12c981 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -74,6 +74,7 @@ "prosemirror-utils": "^0.9.6", "prosemirror-view": "^1.23.6", "react": "17.0.2", + "react-countdown": "^2.3.2", "react-dom": "17.0.2", "react-helmet": "^6.1.0", "react-pdf": "^5.7.2", diff --git a/packages/client/src/tiptap/basekit.tsx b/packages/client/src/tiptap/basekit.tsx index 0c8531b7..582c6987 100644 --- a/packages/client/src/tiptap/basekit.tsx +++ b/packages/client/src/tiptap/basekit.tsx @@ -8,6 +8,7 @@ import { Code } from './extensions/code'; import { CodeBlock } from './extensions/code-block'; import { Color } from './extensions/color'; import { ColorHighlighter } from './extensions/color-highlighter'; +import { Countdown } from './extensions/countdown'; import { DocumentChildren } from './extensions/document-children'; import { DocumentReference } from './extensions/document-reference'; import { Dropcursor } from './extensions/dropcursor'; @@ -63,6 +64,7 @@ export const BaseKit = [ CodeBlock, Color, ColorHighlighter, + Countdown, DocumentChildren, DocumentReference, Dropcursor, diff --git a/packages/client/src/tiptap/extensions/countdown.ts b/packages/client/src/tiptap/extensions/countdown.ts new file mode 100644 index 00000000..a508984b --- /dev/null +++ b/packages/client/src/tiptap/extensions/countdown.ts @@ -0,0 +1,83 @@ +import { Node, mergeAttributes } from '@tiptap/core'; +import { ReactNodeViewRenderer } from '@tiptap/react'; +import { CountdownWrapper } from '../wrappers/countdown'; +import { getDatasetAttribute } from '../services/dataset'; + +declare module '@tiptap/core' { + interface Commands { + countdown: { + setCountdown: (attrs) => ReturnType; + }; + } +} + +export const Countdown = Node.create({ + name: 'countdown', + content: '', + marks: '', + group: 'block', + selectable: true, + atom: true, + + addOptions() { + return { + HTMLAttributes: { + class: 'countdown', + }, + }; + }, + + addAttributes() { + return { + title: { + default: '倒计时', + parseHTML: getDatasetAttribute('title'), + }, + date: { + default: Date.now().valueOf() + 60 * 1000, + parseHTML: getDatasetAttribute('date'), + }, + }; + }, + + parseHTML() { + return [ + { + tag: 'div', + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]; + }, + + addCommands() { + return { + setCountdown: + (options) => + ({ tr, commands, chain, editor }) => { + // @ts-ignore + if (tr.selection?.node?.type?.name == this.name) { + return commands.updateAttributes(this.name, options); + } + + const { selection } = editor.state; + const pos = selection.$head; + + return chain() + .insertContentAt(pos.before(), [ + { + type: this.name, + attrs: options, + }, + ]) + .run(); + }, + }; + }, + + addNodeView() { + return ReactNodeViewRenderer(CountdownWrapper); + }, +}); diff --git a/packages/client/src/tiptap/menubar.tsx b/packages/client/src/tiptap/menubar.tsx index 2de6181e..1f164c42 100644 --- a/packages/client/src/tiptap/menubar.tsx +++ b/packages/client/src/tiptap/menubar.tsx @@ -18,6 +18,9 @@ import { LinkBubbleMenu } from './menus/link'; import { IframeBubbleMenu } from './menus/iframe'; import { TableBubbleMenu } from './menus/table'; +import { CountdownBubbleMenu } from './menus/countdown'; +import { CountdownSettingModal } from './menus/countdown-setting'; + export const MenuBar: React.FC<{ editor: any }> = ({ editor }) => { if (!editor) { return null; @@ -80,6 +83,9 @@ export const MenuBar: React.FC<{ editor: any }> = ({ editor }) => { + + + ); }; diff --git a/packages/client/src/tiptap/menus/base-bubble-menu.tsx b/packages/client/src/tiptap/menus/base-bubble-menu.tsx index 9135ae88..01e331c0 100644 --- a/packages/client/src/tiptap/menus/base-bubble-menu.tsx +++ b/packages/client/src/tiptap/menus/base-bubble-menu.tsx @@ -17,6 +17,7 @@ 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 = [ @@ -36,6 +37,7 @@ const OTHER_BUBBLE_MENU_TYPES = [ DocumentChildren.name, Katex.name, HorizontalRule.name, + Countdown.name, ]; export const BaseBubbleMenu: React.FC<{ editor: Editor }> = ({ editor }) => { diff --git a/packages/client/src/tiptap/menus/countdown-setting.tsx b/packages/client/src/tiptap/menus/countdown-setting.tsx new file mode 100644 index 00000000..c216f3b6 --- /dev/null +++ b/packages/client/src/tiptap/menus/countdown-setting.tsx @@ -0,0 +1,43 @@ +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 { event, OPEN_COUNT_SETTING_MODAL } from './event'; + +type IProps = { editor: Editor }; + +export const CountdownSettingModal: React.FC = ({ editor, children }) => { + const $form = useRef(); + const [initialState, setInitialState] = useState({ date: Date.now() }); + const [visible, toggleVisible] = useToggle(false); + + const handleOk = useCallback(() => { + $form.current.validate().then((values) => { + editor.chain().focus().setCountdown({ title: values.title, date: values.date.valueOf() }).run(); + toggleVisible(false); + }); + }, []); + + useEffect(() => { + const handler = (data) => { + toggleVisible(true); + data && setInitialState(data); + }; + + event.on(OPEN_COUNT_SETTING_MODAL, handler); + + return () => { + event.off(OPEN_COUNT_SETTING_MODAL, handler); + }; + }, []); + + return ( + toggleVisible(false)}> +
($form.current = formApi)} labelPosition="left"> + + + +
+ ); +}; diff --git a/packages/client/src/tiptap/menus/countdown.tsx b/packages/client/src/tiptap/menus/countdown.tsx new file mode 100644 index 00000000..5a28f904 --- /dev/null +++ b/packages/client/src/tiptap/menus/countdown.tsx @@ -0,0 +1,66 @@ +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(); + // 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 ( + editor.isActive(Countdown.name)} + tippyOptions={{ maxWidth: 456 }} + > + + +