mirror of https://github.com/fantasticit/think.git
feat: now we can insert countdown in editor
This commit is contained in:
parent
dadc08c39a
commit
c18a327b80
|
@ -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",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
},
|
||||||
|
});
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue