feat: now we can insert countdown in editor

This commit is contained in:
fantasticit 2022-04-03 13:10:14 +08:00
parent dadc08c39a
commit c18a327b80
14 changed files with 268 additions and 2 deletions

View File

@ -74,6 +74,7 @@
"prosemirror-utils": "^0.9.6", "prosemirror-utils": "^0.9.6",
"prosemirror-view": "^1.23.6", "prosemirror-view": "^1.23.6",
"react": "17.0.2", "react": "17.0.2",
"react-countdown": "^2.3.2",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-pdf": "^5.7.2", "react-pdf": "^5.7.2",

View File

@ -8,6 +8,7 @@ import { Code } from './extensions/code';
import { CodeBlock } from './extensions/code-block'; import { CodeBlock } from './extensions/code-block';
import { Color } from './extensions/color'; import { Color } from './extensions/color';
import { ColorHighlighter } from './extensions/color-highlighter'; import { ColorHighlighter } from './extensions/color-highlighter';
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 { Dropcursor } from './extensions/dropcursor'; import { Dropcursor } from './extensions/dropcursor';
@ -63,6 +64,7 @@ export const BaseKit = [
CodeBlock, CodeBlock,
Color, Color,
ColorHighlighter, ColorHighlighter,
Countdown,
DocumentChildren, DocumentChildren,
DocumentReference, DocumentReference,
Dropcursor, Dropcursor,

View File

@ -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<ReturnType> {
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);
},
});

View File

@ -18,6 +18,9 @@ import { LinkBubbleMenu } from './menus/link';
import { IframeBubbleMenu } from './menus/iframe'; import { IframeBubbleMenu } from './menus/iframe';
import { TableBubbleMenu } from './menus/table'; import { TableBubbleMenu } from './menus/table';
import { CountdownBubbleMenu } from './menus/countdown';
import { CountdownSettingModal } from './menus/countdown-setting';
export const MenuBar: React.FC<{ editor: any }> = ({ editor }) => { export const MenuBar: React.FC<{ editor: any }> = ({ editor }) => {
if (!editor) { if (!editor) {
return null; return null;
@ -80,6 +83,9 @@ export const MenuBar: React.FC<{ editor: any }> = ({ editor }) => {
<IframeBubbleMenu editor={editor} /> <IframeBubbleMenu editor={editor} />
<BannerBubbleMenu editor={editor} /> <BannerBubbleMenu editor={editor} />
<TableBubbleMenu editor={editor} /> <TableBubbleMenu editor={editor} />
<CountdownBubbleMenu editor={editor} />
<CountdownSettingModal editor={editor} />
</div> </div>
); );
}; };

View File

@ -17,6 +17,7 @@ import { TaskItem } from '../extensions/task-item';
import { Katex } from '../extensions/katex'; import { Katex } from '../extensions/katex';
import { DocumentReference } from '../extensions/document-reference'; import { DocumentReference } from '../extensions/document-reference';
import { DocumentChildren } from '../extensions/document-children'; import { DocumentChildren } from '../extensions/document-children';
import { Countdown } from '../extensions/countdown';
import { BaseMenu } from './base-menu'; import { BaseMenu } from './base-menu';
const OTHER_BUBBLE_MENU_TYPES = [ const OTHER_BUBBLE_MENU_TYPES = [
@ -36,6 +37,7 @@ const OTHER_BUBBLE_MENU_TYPES = [
DocumentChildren.name, DocumentChildren.name,
Katex.name, Katex.name,
HorizontalRule.name, HorizontalRule.name,
Countdown.name,
]; ];
export const BaseBubbleMenu: React.FC<{ editor: Editor }> = ({ editor }) => { export const BaseBubbleMenu: React.FC<{ editor: Editor }> = ({ editor }) => {

View File

@ -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<IProps> = ({ editor, children }) => {
const $form = useRef<FormApi>();
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 (
<Modal title="倒计时" visible={visible} onOk={handleOk} onCancel={() => toggleVisible(false)}>
<Form initValues={initialState} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left">
<Form.Input labelWidth={72} label="标题" field="title" required />
<Form.DatePicker labelWidth={72} style={{ width: '100%' }} label="截止日期" field="date" type="dateTime" />
</Form>
</Modal>
);
};

View File

@ -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<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>
);
};

View File

@ -0,0 +1,8 @@
import { EventEmitter } from 'helpers/event-emitter';
export const event = new EventEmitter();
export const OPEN_COUNT_SETTING_MODAL = 'OPEN_COUNT_SETTING_MODAL';
export const triggerOpenCountSettingModal = (data) => {
event.emit(OPEN_COUNT_SETTING_MODAL, data);
};

View File

@ -111,7 +111,7 @@ export const IframeBubbleMenu = ({ editor }) => {
<Divider /> <Divider />
<Tooltip content="删除节点"> <Tooltip content="删除节点" hideOnClick>
<Button onClick={deleteNode} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" /> <Button onClick={deleteNode} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
</Tooltip> </Tooltip>
</Space> </Space>

View File

@ -16,7 +16,10 @@ import {
IconMath, IconMath,
} 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 { isTitleActive } from '../services/is-active';
import { event, OPEN_COUNT_SETTING_MODAL, triggerOpenCountSettingModal } from './event';
import { CountdownSettingModal } from './countdown-setting';
export const MediaInsertMenu: React.FC<{ editor: Editor }> = ({ editor }) => { export const MediaInsertMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
if (!editor) { if (!editor) {
@ -36,6 +39,10 @@ export const MediaInsertMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
<IconCodeBlock /> <IconCodeBlock />
</Dropdown.Item> */} </Dropdown.Item> */}
<Dropdown.Item onClick={() => triggerOpenCountSettingModal(null)}>
<IconCodeBlock />
</Dropdown.Item>
<Popover <Popover
showArrow showArrow
position="rightTop" position="rightTop"

View File

@ -51,12 +51,14 @@
.node-attachment, .node-attachment,
.node-iframe, .node-iframe,
.node-mind, .node-mind,
.node-banner { .node-banner,
.node-countdown {
margin-top: 0.75em; margin-top: 0.75em;
} }
.node-attachment, .node-attachment,
.node-banner, .node-banner,
.node-countdown,
.node-iframe, .node-iframe,
.node-katex, .node-katex,
.node-mind, .node-mind,

View File

@ -0,0 +1,13 @@
.wrap {
height: 96px;
overflow: hidden;
line-height: 0;
background-color: var(--semi-color-fill-0);
border: 1px solid var(--semi-color-border);
border-radius: var(--border-radius);
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}

View File

@ -0,0 +1,20 @@
import { NodeViewWrapper } from '@tiptap/react';
import cls from 'classnames';
import { Typography } from '@douyinfe/semi-ui';
import Countdown from 'react-countdown';
import styles from './index.module.scss';
const { Text } = Typography;
export const CountdownWrapper = ({ editor, node }) => {
const { title, date } = node.attrs;
return (
<NodeViewWrapper>
<div className={cls(styles.wrap, 'render-wrapper')}>
<Text style={{ marginBottom: 12 }}>{title}</Text>
<Countdown date={date}></Countdown>
</div>
</NodeViewWrapper>
);
};

View File

@ -102,6 +102,7 @@ importers:
prosemirror-utils: ^0.9.6 prosemirror-utils: ^0.9.6
prosemirror-view: ^1.23.6 prosemirror-view: ^1.23.6
react: 17.0.2 react: 17.0.2
react-countdown: ^2.3.2
react-dom: 17.0.2 react-dom: 17.0.2
react-helmet: ^6.1.0 react-helmet: ^6.1.0
react-pdf: ^5.7.2 react-pdf: ^5.7.2
@ -181,6 +182,7 @@ importers:
prosemirror-utils: 0.9.6_prosemirror-tables@1.1.1 prosemirror-utils: 0.9.6_prosemirror-tables@1.1.1
prosemirror-view: 1.23.6 prosemirror-view: 1.23.6
react: 17.0.2 react: 17.0.2
react-countdown: 2.3.2_react-dom@17.0.2+react@17.0.2
react-dom: 17.0.2_react@17.0.2 react-dom: 17.0.2_react@17.0.2
react-helmet: 6.1.0_react@17.0.2 react-helmet: 6.1.0_react@17.0.2
react-pdf: 5.7.2_react-dom@17.0.2+react@17.0.2 react-pdf: 5.7.2_react-dom@17.0.2+react@17.0.2
@ -6658,6 +6660,17 @@ packages:
unpipe: 1.0.0 unpipe: 1.0.0
dev: false dev: false
/react-countdown/2.3.2_react-dom@17.0.2+react@17.0.2:
resolution: {integrity: sha512-Q4SADotHtgOxNWhDdvgupmKVL0pMB9DvoFcxv5AzjsxVhzOVxnttMbAywgqeOdruwEAmnPhOhNv/awAgkwru2w==}
peerDependencies:
react: '>= 15'
react-dom: '>= 15'
dependencies:
prop-types: 15.8.1
react: 17.0.2
react-dom: 17.0.2_react@17.0.2
dev: false
/react-dom/17.0.2_react@17.0.2: /react-dom/17.0.2_react@17.0.2:
resolution: {integrity: sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==} resolution: {integrity: sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==}
peerDependencies: peerDependencies: