mirror of https://github.com/fantasticit/think.git
feat: now we can insert countdown in editor
parent
dadc08c39a
commit
c18a327b80
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 { 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 }) => {
|
|||
<IframeBubbleMenu editor={editor} />
|
||||
<BannerBubbleMenu editor={editor} />
|
||||
<TableBubbleMenu editor={editor} />
|
||||
|
||||
<CountdownBubbleMenu editor={editor} />
|
||||
<CountdownSettingModal editor={editor} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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 />
|
||||
|
||||
<Tooltip content="删除节点">
|
||||
<Tooltip content="删除节点" hideOnClick>
|
||||
<Button onClick={deleteNode} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
|
|
|
@ -16,7 +16,10 @@ import {
|
|||
IconMath,
|
||||
} from 'components/icons';
|
||||
import { GridSelect } from 'components/grid-select';
|
||||
|
||||
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 }) => {
|
||||
if (!editor) {
|
||||
|
@ -36,6 +39,10 @@ export const MediaInsertMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
|
|||
<IconCodeBlock /> 目录
|
||||
</Dropdown.Item> */}
|
||||
|
||||
<Dropdown.Item onClick={() => triggerOpenCountSettingModal(null)}>
|
||||
<IconCodeBlock /> 倒计时
|
||||
</Dropdown.Item>
|
||||
|
||||
<Popover
|
||||
showArrow
|
||||
position="rightTop"
|
||||
|
|
|
@ -51,12 +51,14 @@
|
|||
.node-attachment,
|
||||
.node-iframe,
|
||||
.node-mind,
|
||||
.node-banner {
|
||||
.node-banner,
|
||||
.node-countdown {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
.node-attachment,
|
||||
.node-banner,
|
||||
.node-countdown,
|
||||
.node-iframe,
|
||||
.node-katex,
|
||||
.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-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
|
||||
|
@ -181,6 +182,7 @@ importers:
|
|||
prosemirror-utils: 0.9.6_prosemirror-tables@1.1.1
|
||||
prosemirror-view: 1.23.6
|
||||
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-helmet: 6.1.0_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
|
||||
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:
|
||||
resolution: {integrity: sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==}
|
||||
peerDependencies:
|
||||
|
|
Loading…
Reference in New Issue