mirror of https://github.com/fantasticit/think.git
refactor: refactor the tiptap code
This commit is contained in:
parent
0353d20dcf
commit
c94fe79804
|
@ -32,6 +32,7 @@
|
||||||
"@tiptap/extension-hard-break": "^2.0.0-beta.30",
|
"@tiptap/extension-hard-break": "^2.0.0-beta.30",
|
||||||
"@tiptap/extension-heading": "^2.0.0-beta.26",
|
"@tiptap/extension-heading": "^2.0.0-beta.26",
|
||||||
"@tiptap/extension-highlight": "^2.0.0-beta.33",
|
"@tiptap/extension-highlight": "^2.0.0-beta.33",
|
||||||
|
"@tiptap/extension-horizontal-rule": "^2.0.0-beta.31",
|
||||||
"@tiptap/extension-image": "^2.0.0-beta.25",
|
"@tiptap/extension-image": "^2.0.0-beta.25",
|
||||||
"@tiptap/extension-italic": "^2.0.0-beta.25",
|
"@tiptap/extension-italic": "^2.0.0-beta.25",
|
||||||
"@tiptap/extension-link": "^2.0.0-beta.36",
|
"@tiptap/extension-link": "^2.0.0-beta.36",
|
||||||
|
@ -51,6 +52,7 @@
|
||||||
"@tiptap/extension-text-style": "^2.0.0-beta.23",
|
"@tiptap/extension-text-style": "^2.0.0-beta.23",
|
||||||
"@tiptap/extension-underline": "^2.0.0-beta.23",
|
"@tiptap/extension-underline": "^2.0.0-beta.23",
|
||||||
"@tiptap/react": "^2.0.0-beta.107",
|
"@tiptap/react": "^2.0.0-beta.107",
|
||||||
|
"@traptitech/markdown-it-katex": "^3.5.0",
|
||||||
"axios": "^0.25.0",
|
"axios": "^0.25.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"copy-to-clipboard": "^3.3.1",
|
"copy-to-clipboard": "^3.3.1",
|
||||||
|
@ -59,6 +61,12 @@
|
||||||
"interactjs": "^1.10.11",
|
"interactjs": "^1.10.11",
|
||||||
"katex": "^0.15.2",
|
"katex": "^0.15.2",
|
||||||
"lowlight": "^2.5.0",
|
"lowlight": "^2.5.0",
|
||||||
|
"markdown-it": "^12.3.2",
|
||||||
|
"markdown-it-anchor": "^8.4.1",
|
||||||
|
"markdown-it-footnote": "^3.0.3",
|
||||||
|
"markdown-it-sub": "^1.0.0",
|
||||||
|
"markdown-it-sup": "^1.0.0",
|
||||||
|
"markdown-it-task-lists": "^2.1.1",
|
||||||
"marked": "^4.0.12",
|
"marked": "^4.0.12",
|
||||||
"next": "12.0.10",
|
"next": "12.0.10",
|
||||||
"prosemirror-markdown": "^1.7.0",
|
"prosemirror-markdown": "^1.7.0",
|
||||||
|
|
|
@ -12,7 +12,6 @@ import {
|
||||||
getProvider,
|
getProvider,
|
||||||
destoryProvider,
|
destoryProvider,
|
||||||
MenuBar,
|
MenuBar,
|
||||||
Toc,
|
|
||||||
} from 'components/tiptap';
|
} from 'components/tiptap';
|
||||||
import { DataRender } from 'components/data-render';
|
import { DataRender } from 'components/data-render';
|
||||||
import { joinUser } from 'components/document/collaboration';
|
import { joinUser } from 'components/document/collaboration';
|
||||||
|
|
|
@ -1,110 +0,0 @@
|
||||||
import { Document, TitledDocument, Title } from './extensions/title';
|
|
||||||
import Placeholder from '@tiptap/extension-placeholder';
|
|
||||||
import Paragraph from '@tiptap/extension-paragraph';
|
|
||||||
import Text from '@tiptap/extension-text';
|
|
||||||
import Strike from '@tiptap/extension-strike';
|
|
||||||
import Underline from '@tiptap/extension-underline';
|
|
||||||
import TextStyle from '@tiptap/extension-text-style';
|
|
||||||
import { Color } from '@tiptap/extension-color';
|
|
||||||
import Blockquote from '@tiptap/extension-blockquote';
|
|
||||||
import Bold from '@tiptap/extension-bold';
|
|
||||||
import Code from '@tiptap/extension-code';
|
|
||||||
import Highlight from '@tiptap/extension-highlight';
|
|
||||||
import TextAlign from '@tiptap/extension-text-align';
|
|
||||||
import Dropcursor from '@tiptap/extension-dropcursor';
|
|
||||||
import Gapcursor from '@tiptap/extension-gapcursor';
|
|
||||||
import HardBreak from '@tiptap/extension-hard-break';
|
|
||||||
import Heading from '@tiptap/extension-heading';
|
|
||||||
import Italic from '@tiptap/extension-italic';
|
|
||||||
import OrderedList from '@tiptap/extension-ordered-list';
|
|
||||||
import BulletList from '@tiptap/extension-bullet-list';
|
|
||||||
import ListItem from '@tiptap/extension-list-item';
|
|
||||||
import TaskList from '@tiptap/extension-task-list';
|
|
||||||
import TaskItem from '@tiptap/extension-task-item';
|
|
||||||
import { HorizontalRule } from './extensions/horizontal-rule';
|
|
||||||
import { BackgroundColor } from './extensions/background-color';
|
|
||||||
import { Link } from './extensions/link';
|
|
||||||
import { FontSize } from './extensions/font-size';
|
|
||||||
import { ColorHighlighter } from './extensions/color-highlight';
|
|
||||||
import { Indent } from './extensions/indent';
|
|
||||||
import { Div } from './extensions/div';
|
|
||||||
import { Banner } from './extensions/banner';
|
|
||||||
import { CodeBlock } from './extensions/code-block';
|
|
||||||
import { Iframe } from './extensions/iframe';
|
|
||||||
import { Mind } from './extensions/mind';
|
|
||||||
import { Image } from './extensions/image';
|
|
||||||
import { Status } from './extensions/status';
|
|
||||||
import { Paste } from './extensions/paste';
|
|
||||||
import { Table, TableRow, TableCell, TableHeader } from './extensions/table';
|
|
||||||
import { Toc } from './extensions/toc';
|
|
||||||
import { TrailingNode } from './extensions/trailing-node';
|
|
||||||
import { Attachment } from './extensions/attachment';
|
|
||||||
import { Katex } from './extensions/katex';
|
|
||||||
import { DocumentReference } from './extensions/documents/reference';
|
|
||||||
import { DocumentChildren } from './extensions/documents/children';
|
|
||||||
|
|
||||||
export { Document, TitledDocument };
|
|
||||||
|
|
||||||
export const BaseExtension = [
|
|
||||||
Placeholder.configure({
|
|
||||||
placeholder: ({ node }) => {
|
|
||||||
if (node.type.name === 'title') {
|
|
||||||
return '请输入标题';
|
|
||||||
}
|
|
||||||
return '请输入内容';
|
|
||||||
},
|
|
||||||
showOnlyWhenEditable: true,
|
|
||||||
}),
|
|
||||||
Title,
|
|
||||||
Paragraph,
|
|
||||||
Text,
|
|
||||||
Strike,
|
|
||||||
Underline,
|
|
||||||
TextStyle,
|
|
||||||
Color,
|
|
||||||
BackgroundColor,
|
|
||||||
Bold,
|
|
||||||
Code,
|
|
||||||
Dropcursor,
|
|
||||||
Gapcursor,
|
|
||||||
HardBreak,
|
|
||||||
Heading,
|
|
||||||
HorizontalRule,
|
|
||||||
Italic,
|
|
||||||
OrderedList,
|
|
||||||
BulletList,
|
|
||||||
ListItem,
|
|
||||||
TaskList,
|
|
||||||
TaskItem.configure({
|
|
||||||
nested: true,
|
|
||||||
}),
|
|
||||||
Highlight.configure({ multicolor: true }),
|
|
||||||
TextAlign.configure({
|
|
||||||
types: ['heading', 'paragraph', 'image'],
|
|
||||||
}),
|
|
||||||
Link.configure({ openOnClick: false }),
|
|
||||||
Blockquote,
|
|
||||||
FontSize,
|
|
||||||
ColorHighlighter,
|
|
||||||
Indent,
|
|
||||||
CodeBlock,
|
|
||||||
Div,
|
|
||||||
Banner,
|
|
||||||
Iframe,
|
|
||||||
Mind,
|
|
||||||
Image,
|
|
||||||
Status,
|
|
||||||
Paste,
|
|
||||||
Table.configure({
|
|
||||||
resizable: true,
|
|
||||||
}),
|
|
||||||
TableRow,
|
|
||||||
TableCell,
|
|
||||||
TableHeader,
|
|
||||||
Toc,
|
|
||||||
TrailingNode,
|
|
||||||
Attachment,
|
|
||||||
Katex,
|
|
||||||
DocumentReference,
|
|
||||||
DocumentChildren,
|
|
||||||
];
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { Attachment } from './extensions/attachment';
|
||||||
|
import { BackgroundColor } from './extensions/backgroundColor';
|
||||||
|
import { Banner } from './extensions/banner';
|
||||||
|
import { Blockquote } from './extensions/blockquote';
|
||||||
|
import { Bold } from './extensions/bold';
|
||||||
|
import { BulletList } from './extensions/bulletList';
|
||||||
|
import { Code } from './extensions/code';
|
||||||
|
import { CodeBlock } from './extensions/codeBlock';
|
||||||
|
import { Color } from './extensions/color';
|
||||||
|
import { ColorHighlighter } from './extensions/colorHighlighter';
|
||||||
|
import { DocumentChildren } from './extensions/documentChildren';
|
||||||
|
import { DocumentReference } from './extensions/documentReference';
|
||||||
|
import { Dropcursor } from './extensions/dropCursor';
|
||||||
|
import { FontSize } from './extensions/fontSize';
|
||||||
|
import { FootnoteDefinition } from './extensions/footnoteDefinition';
|
||||||
|
import { FootnoteReference } from './extensions/footnoteReference';
|
||||||
|
import { FootnotesSection } from './extensions/footnotesSection';
|
||||||
|
import { Gapcursor } from './extensions/gapCursor';
|
||||||
|
import { HardBreak } from './extensions/hardBreak';
|
||||||
|
import { Heading } from './extensions/heading';
|
||||||
|
import { HorizontalRule } from './extensions/horizontalRule';
|
||||||
|
import { HTMLMarks } from './extensions/htmlMarks';
|
||||||
|
import { Iframe } from './extensions/iframe';
|
||||||
|
import { Image } from './extensions/image';
|
||||||
|
import { Italic } from './extensions/italic';
|
||||||
|
import { Katex } from './extensions/katex';
|
||||||
|
import { Link } from './extensions/link';
|
||||||
|
import { ListItem } from './extensions/listItem';
|
||||||
|
import { Mind } from './extensions/mind';
|
||||||
|
import { OrderedList } from './extensions/orderedList';
|
||||||
|
import { Paragraph } from './extensions/paragraph';
|
||||||
|
import { PasteFile } from './extensions/pasteFile';
|
||||||
|
import { PasteMarkdown } from './extensions/pasteMarkdown';
|
||||||
|
import { Placeholder } from './extensions/placeholder';
|
||||||
|
import { Status } from './extensions/status';
|
||||||
|
import { Strike } from './extensions/strike';
|
||||||
|
import { Table } from './extensions/table';
|
||||||
|
import { TableCell } from './extensions/tableCell';
|
||||||
|
import { TableHeader } from './extensions/tableHeader';
|
||||||
|
import { TableRow } from './extensions/tableRow';
|
||||||
|
import { Text } from './extensions/text';
|
||||||
|
import { TextAlign } from './extensions/textAlign';
|
||||||
|
import { TextStyle } from './extensions/textStyle';
|
||||||
|
import { TaskItem } from './extensions/taskItem';
|
||||||
|
import { TaskList } from './extensions/taskList';
|
||||||
|
import { Title } from './extensions/title';
|
||||||
|
import { TrailingNode } from './extensions/trailingNode';
|
||||||
|
import { Underline } from './extensions/underline';
|
||||||
|
|
||||||
|
export const BaseKit = [
|
||||||
|
Attachment,
|
||||||
|
BackgroundColor,
|
||||||
|
Banner,
|
||||||
|
Blockquote,
|
||||||
|
Bold,
|
||||||
|
BulletList,
|
||||||
|
Code,
|
||||||
|
CodeBlock,
|
||||||
|
Color,
|
||||||
|
ColorHighlighter,
|
||||||
|
DocumentChildren,
|
||||||
|
DocumentReference,
|
||||||
|
Dropcursor,
|
||||||
|
FontSize,
|
||||||
|
FootnoteDefinition,
|
||||||
|
FootnoteReference,
|
||||||
|
FootnotesSection,
|
||||||
|
Gapcursor,
|
||||||
|
HardBreak,
|
||||||
|
Heading,
|
||||||
|
HorizontalRule,
|
||||||
|
...HTMLMarks,
|
||||||
|
Iframe,
|
||||||
|
Image,
|
||||||
|
Italic,
|
||||||
|
Katex,
|
||||||
|
Link,
|
||||||
|
ListItem,
|
||||||
|
Mind,
|
||||||
|
OrderedList,
|
||||||
|
Paragraph,
|
||||||
|
PasteFile,
|
||||||
|
PasteMarkdown,
|
||||||
|
Placeholder.configure({
|
||||||
|
placeholder: ({ node }) => {
|
||||||
|
if (node.type.name === 'title') {
|
||||||
|
return '请输入标题';
|
||||||
|
}
|
||||||
|
return '请输入内容';
|
||||||
|
},
|
||||||
|
showOnlyWhenEditable: true,
|
||||||
|
}),
|
||||||
|
Status,
|
||||||
|
Strike,
|
||||||
|
Table,
|
||||||
|
TableCell,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
Text,
|
||||||
|
TextAlign.configure({
|
||||||
|
types: ['heading', 'paragraph', 'image'],
|
||||||
|
}),
|
||||||
|
TextStyle,
|
||||||
|
TaskItem,
|
||||||
|
TaskList,
|
||||||
|
Title,
|
||||||
|
TrailingNode,
|
||||||
|
Underline,
|
||||||
|
];
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
||||||
|
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
||||||
|
import { IconDownload } from '@douyinfe/semi-icons';
|
||||||
|
import { download } from '../../services/download';
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
export const AttachmentWrapper = ({ node }) => {
|
||||||
|
const { name, url } = node.attrs;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeViewWrapper as="div">
|
||||||
|
<div className={styles.wrap}>
|
||||||
|
<span>{name}</span>
|
||||||
|
<span>
|
||||||
|
<Tooltip zIndex={10000} content="下载">
|
||||||
|
<Button
|
||||||
|
theme={'borderless'}
|
||||||
|
type="tertiary"
|
||||||
|
icon={<IconDownload />}
|
||||||
|
onClick={() => download(url, name)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<NodeViewContent></NodeViewContent>
|
||||||
|
</NodeViewWrapper>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
||||||
|
import { Banner as SemiBanner } from '@douyinfe/semi-ui';
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
export const BannerWrapper = ({ node }) => {
|
||||||
|
return (
|
||||||
|
<NodeViewWrapper id="js-bannber-container" className={styles.wrap}>
|
||||||
|
<SemiBanner
|
||||||
|
type={node.attrs.type}
|
||||||
|
description={<NodeViewContent />}
|
||||||
|
closeIcon={null}
|
||||||
|
fullMode={false}
|
||||||
|
/>
|
||||||
|
</NodeViewWrapper>
|
||||||
|
);
|
||||||
|
};
|
|
@ -8,7 +8,7 @@ import { lowlight } from 'lowlight';
|
||||||
import { copy } from 'helpers/copy';
|
import { copy } from 'helpers/copy';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
const Render = ({
|
export const CodeBlockWrapper = ({
|
||||||
editor,
|
editor,
|
||||||
node: {
|
node: {
|
||||||
attrs: { language: defaultLanguage },
|
attrs: { language: defaultLanguage },
|
||||||
|
@ -53,9 +53,3 @@ const Render = ({
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CodeBlock = CodeBlockLowlight.extend({
|
|
||||||
addNodeView() {
|
|
||||||
return ReactNodeViewRenderer(Render);
|
|
||||||
},
|
|
||||||
}).configure({ lowlight });
|
|
|
@ -1,15 +1,7 @@
|
||||||
import {
|
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
||||||
Node,
|
|
||||||
Command,
|
|
||||||
mergeAttributes,
|
|
||||||
textInputRule,
|
|
||||||
textblockTypeInputRule,
|
|
||||||
wrappingInputRule,
|
|
||||||
} from '@tiptap/core';
|
|
||||||
import { NodeViewWrapper, NodeViewContent, ReactNodeViewRenderer } from '@tiptap/react';
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Space, Popover, Tag, Input, Typography } from '@douyinfe/semi-ui';
|
import { Typography } from '@douyinfe/semi-ui';
|
||||||
import { useChildrenDocument } from 'data/document';
|
import { useChildrenDocument } from 'data/document';
|
||||||
import { DataRender } from 'components/data-render';
|
import { DataRender } from 'components/data-render';
|
||||||
import { Empty } from 'components/empty';
|
import { Empty } from 'components/empty';
|
||||||
|
@ -18,70 +10,7 @@ import styles from './index.module.scss';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
export const DocumentChildrenWrapper = () => {
|
||||||
interface Commands {
|
|
||||||
documentChildren: {
|
|
||||||
setDocumentChildren: () => Command;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DocumentChildrenInputRegex = /^documentChildren\$$/;
|
|
||||||
|
|
||||||
const DocumentChildrenExtension = Node.create({
|
|
||||||
name: 'documentChildren',
|
|
||||||
group: 'block',
|
|
||||||
defining: true,
|
|
||||||
draggable: true,
|
|
||||||
atom: true,
|
|
||||||
|
|
||||||
addAttributes() {
|
|
||||||
return {
|
|
||||||
color: {
|
|
||||||
default: 'grey',
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
parseHTML() {
|
|
||||||
return [{ tag: 'div[data-type=documentChildren]' }];
|
|
||||||
},
|
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
|
||||||
return [
|
|
||||||
'div',
|
|
||||||
mergeAttributes((this.options && this.options.HTMLAttributes) || {}, HTMLAttributes),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
addCommands() {
|
|
||||||
return {
|
|
||||||
setDocumentChildren:
|
|
||||||
() =>
|
|
||||||
({ commands }) => {
|
|
||||||
return commands.insertContent({
|
|
||||||
type: this.name,
|
|
||||||
attrs: {},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
addInputRules() {
|
|
||||||
return [
|
|
||||||
wrappingInputRule({
|
|
||||||
find: DocumentChildrenInputRegex,
|
|
||||||
type: this.type,
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const Render = () => {
|
|
||||||
const { pathname, query } = useRouter();
|
const { pathname, query } = useRouter();
|
||||||
const wikiId = query?.wikiId;
|
const wikiId = query?.wikiId;
|
||||||
const documentId = query?.documentId;
|
const documentId = query?.documentId;
|
||||||
|
@ -135,9 +64,3 @@ const Render = () => {
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentChildren = DocumentChildrenExtension.extend({
|
|
||||||
addNodeView() {
|
|
||||||
return ReactNodeViewRenderer(Render);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Node, Command, mergeAttributes, wrappingInputRule } from '@tiptap/core';
|
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
||||||
import { NodeViewWrapper, NodeViewContent, ReactNodeViewRenderer } from '@tiptap/react';
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Select } from '@douyinfe/semi-ui';
|
import { Select } from '@douyinfe/semi-ui';
|
||||||
|
@ -8,73 +7,7 @@ import { DataRender } from 'components/data-render';
|
||||||
import { IconDocument } from 'components/icons';
|
import { IconDocument } from 'components/icons';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
export const DocumentReferenceWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
interface Commands {
|
|
||||||
documentReference: {
|
|
||||||
setDocumentReference: () => Command;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DocumentReferenceInputRegex = /^documentReference\$$/;
|
|
||||||
|
|
||||||
const DocumentReferenceExtension = Node.create({
|
|
||||||
name: 'documentReference',
|
|
||||||
group: 'block',
|
|
||||||
defining: true,
|
|
||||||
draggable: true,
|
|
||||||
atom: true,
|
|
||||||
|
|
||||||
addAttributes() {
|
|
||||||
return {
|
|
||||||
wikiId: {
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
documentId: {
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
parseHTML() {
|
|
||||||
return [{ tag: 'div[data-type=documentReference]' }];
|
|
||||||
},
|
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
|
||||||
return [
|
|
||||||
'div',
|
|
||||||
mergeAttributes((this.options && this.options.HTMLAttributes) || {}, HTMLAttributes),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
addCommands() {
|
|
||||||
return {
|
|
||||||
setDocumentReference:
|
|
||||||
() =>
|
|
||||||
({ commands }) => {
|
|
||||||
return commands.insertContent({
|
|
||||||
type: this.name,
|
|
||||||
attrs: {},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
addInputRules() {
|
|
||||||
return [
|
|
||||||
wrappingInputRule({
|
|
||||||
find: DocumentReferenceInputRegex,
|
|
||||||
type: this.type,
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const Render = ({ editor, node, updateAttributes }) => {
|
|
||||||
const { pathname, query } = useRouter();
|
const { pathname, query } = useRouter();
|
||||||
const wikiIdFromUrl = query?.wikiId;
|
const wikiIdFromUrl = query?.wikiId;
|
||||||
const isShare = pathname.includes('share');
|
const isShare = pathname.includes('share');
|
||||||
|
@ -83,7 +16,7 @@ const Render = ({ editor, node, updateAttributes }) => {
|
||||||
const { data: tocs, loading, error } = useWikiTocs(isShare ? null : wikiIdFromUrl);
|
const { data: tocs, loading, error } = useWikiTocs(isShare ? null : wikiIdFromUrl);
|
||||||
|
|
||||||
const selectDoc = (str) => {
|
const selectDoc = (str) => {
|
||||||
const [wikiId, documentId, title] = str.split('/');
|
const [wikiId, title, documentId] = str.split('/');
|
||||||
updateAttributes({ wikiId, documentId, title });
|
updateAttributes({ wikiId, documentId, title });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -98,12 +31,13 @@ const Render = ({ editor, node, updateAttributes }) => {
|
||||||
<Select
|
<Select
|
||||||
placeholder="请选择文档"
|
placeholder="请选择文档"
|
||||||
onChange={(v) => selectDoc(v)}
|
onChange={(v) => selectDoc(v)}
|
||||||
{...(wikiId && documentId ? { value: `${wikiId}/${documentId}/${title}` } : {})}
|
{...(wikiId && documentId ? { value: `${wikiId}/${title}/${documentId}` } : {})}
|
||||||
>
|
>
|
||||||
{(tocs || []).map((toc) => (
|
{(tocs || []).map((toc) => (
|
||||||
<Select.Option
|
<Select.Option
|
||||||
label={`${toc.id}/${toc.title}`}
|
// FIXME: semi-design 抄 antd,抄的什么玩意!!!
|
||||||
value={`${toc.wikiId}/${toc.id}/${toc.title}`}
|
label={`${toc.title}/${toc.id}`}
|
||||||
|
value={`${toc.wikiId}/${toc.title}/${toc.id}`}
|
||||||
>
|
>
|
||||||
{toc.title}
|
{toc.title}
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
|
@ -129,9 +63,3 @@ const Render = ({ editor, node, updateAttributes }) => {
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentReference = DocumentReferenceExtension.extend({
|
|
||||||
addNodeView() {
|
|
||||||
return ReactNodeViewRenderer(Render);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
||||||
|
import { Input } from '@douyinfe/semi-ui';
|
||||||
|
import { Resizeable } from 'components/resizeable';
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
export const IframeWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
|
const isEditable = editor.isEditable;
|
||||||
|
const { url, width, height } = node.attrs;
|
||||||
|
|
||||||
|
const onResize = (size) => {
|
||||||
|
updateAttributes({ width: size.width, height: size.height });
|
||||||
|
};
|
||||||
|
const content = (
|
||||||
|
<NodeViewContent as="div" className={styles.wrap}>
|
||||||
|
{isEditable && (
|
||||||
|
<div className={styles.handlerWrap}>
|
||||||
|
<Input
|
||||||
|
placeholder={'输入外链地址'}
|
||||||
|
value={url}
|
||||||
|
onChange={(url) => updateAttributes({ url })}
|
||||||
|
></Input>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{url && (
|
||||||
|
<div className={styles.innerWrap} style={{ pointerEvents: !isEditable ? 'auto' : 'none' }}>
|
||||||
|
<iframe src={url}></iframe>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</NodeViewContent>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isEditable && !url) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeViewWrapper>
|
||||||
|
{isEditable ? (
|
||||||
|
<Resizeable height={height} width={width} onChange={onResize}>
|
||||||
|
{content}
|
||||||
|
</Resizeable>
|
||||||
|
) : (
|
||||||
|
<div style={{ width, height, maxWidth: '100%' }}>{content}</div>
|
||||||
|
)}
|
||||||
|
</NodeViewWrapper>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,8 +1,7 @@
|
||||||
import { Image as TImage } from '@tiptap/extension-image';
|
import { NodeViewWrapper } from '@tiptap/react';
|
||||||
import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react';
|
|
||||||
import { Resizeable } from 'components/resizeable';
|
import { Resizeable } from 'components/resizeable';
|
||||||
|
|
||||||
const Render = ({ editor, node, updateAttributes }) => {
|
export const ImageWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
const isEditable = editor.isEditable;
|
const isEditable = editor.isEditable;
|
||||||
const { src, alt, title, width, height, textAlign } = node.attrs;
|
const { src, alt, title, width, height, textAlign } = node.attrs;
|
||||||
|
|
||||||
|
@ -24,29 +23,3 @@ const Render = ({ editor, node, updateAttributes }) => {
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Image = TImage.extend({
|
|
||||||
draggable: true,
|
|
||||||
addAttributes() {
|
|
||||||
return {
|
|
||||||
src: {
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
alt: {
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
width: {
|
|
||||||
default: 'auto',
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
default: 'auto',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
addNodeView() {
|
|
||||||
return ReactNodeViewRenderer(Render);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,77 +1,13 @@
|
||||||
import { Node, Command, mergeAttributes, wrappingInputRule } from '@tiptap/core';
|
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
||||||
import { NodeViewWrapper, NodeViewContent, ReactNodeViewRenderer } from '@tiptap/react';
|
import { useMemo } from 'react';
|
||||||
import { Popover, TextArea, Typography, Space } from '@douyinfe/semi-ui';
|
import { Popover, TextArea, Typography, Space } from '@douyinfe/semi-ui';
|
||||||
import { IconHelpCircle } from '@douyinfe/semi-icons';
|
import { IconHelpCircle } from '@douyinfe/semi-icons';
|
||||||
import katex from 'katex';
|
import katex from 'katex';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
|
||||||
interface Commands {
|
|
||||||
katex: {
|
|
||||||
setKatex: () => Command;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
export const KatexInputRegex = /^\$\$(.+)?\$$/;
|
export const KatexWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
|
|
||||||
const KatexExtension = Node.create({
|
|
||||||
name: 'katex',
|
|
||||||
group: 'block',
|
|
||||||
defining: true,
|
|
||||||
draggable: true,
|
|
||||||
atom: true,
|
|
||||||
|
|
||||||
addAttributes() {
|
|
||||||
return {
|
|
||||||
text: {
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
parseHTML() {
|
|
||||||
return [{ tag: 'div[data-type=katex]' }];
|
|
||||||
},
|
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
|
||||||
return [
|
|
||||||
'div',
|
|
||||||
mergeAttributes((this.options && this.options.HTMLAttributes) || {}, HTMLAttributes),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
addCommands() {
|
|
||||||
return {
|
|
||||||
setKatex:
|
|
||||||
(options) =>
|
|
||||||
({ commands }) => {
|
|
||||||
return commands.insertContent({
|
|
||||||
type: this.name,
|
|
||||||
attrs: options,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
addInputRules() {
|
|
||||||
return [
|
|
||||||
wrappingInputRule({
|
|
||||||
find: KatexInputRegex,
|
|
||||||
type: this.type,
|
|
||||||
getAttributes: (match) => {
|
|
||||||
return { text: match[1] };
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const Render = ({ editor, node, updateAttributes }) => {
|
|
||||||
const isEditable = editor.isEditable;
|
const isEditable = editor.isEditable;
|
||||||
const { text } = node.attrs;
|
const { text } = node.attrs;
|
||||||
const formatText = useMemo(() => {
|
const formatText = useMemo(() => {
|
||||||
|
@ -123,9 +59,3 @@ const Render = ({ editor, node, updateAttributes }) => {
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Katex = KatexExtension.extend({
|
|
||||||
addNodeView() {
|
|
||||||
return ReactNodeViewRenderer(Render);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Node, mergeAttributes } from '@tiptap/core';
|
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
||||||
import { NodeViewWrapper, NodeViewContent, ReactNodeViewRenderer } from '@tiptap/react';
|
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { Button } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconMinus, IconPlus } from '@douyinfe/semi-icons';
|
import { IconMinus, IconPlus } from '@douyinfe/semi-icons';
|
||||||
|
@ -9,84 +8,7 @@ import deepEqual from 'deep-equal';
|
||||||
import jsMind from './jsmind.jsx';
|
import jsMind from './jsmind.jsx';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
const DEFAULT_MIND_DATA = {
|
export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
meta: {
|
|
||||||
name: 'jsMind',
|
|
||||||
author: 'think',
|
|
||||||
version: '0.2',
|
|
||||||
},
|
|
||||||
format: 'node_tree',
|
|
||||||
data: { id: 'root', topic: '中心节点', children: [] },
|
|
||||||
};
|
|
||||||
|
|
||||||
const MindNode = Node.create({
|
|
||||||
name: 'jsmind',
|
|
||||||
content: '',
|
|
||||||
marks: '',
|
|
||||||
group: 'block',
|
|
||||||
draggable: true,
|
|
||||||
atom: true,
|
|
||||||
|
|
||||||
addOptions() {
|
|
||||||
return {
|
|
||||||
HTMLAttributes: {
|
|
||||||
'data-type': 'jsmind',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
addAttributes() {
|
|
||||||
return {
|
|
||||||
width: {
|
|
||||||
default: '100%',
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
default: 240,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
default: DEFAULT_MIND_DATA,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
parseHTML() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
tag: 'div[data-type="jsmind"]',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
|
||||||
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
|
|
||||||
},
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
addCommands() {
|
|
||||||
return {
|
|
||||||
insertMind:
|
|
||||||
(options) =>
|
|
||||||
({ tr, commands, chain, editor }) => {
|
|
||||||
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: { data: DEFAULT_MIND_DATA },
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.run();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const Render = ({ editor, node, updateAttributes }) => {
|
|
||||||
const $container = useRef();
|
const $container = useRef();
|
||||||
const $mind = useRef<any>();
|
const $mind = useRef<any>();
|
||||||
const isEditable = editor.isEditable;
|
const isEditable = editor.isEditable;
|
||||||
|
@ -216,9 +138,3 @@ const Render = ({ editor, node, updateAttributes }) => {
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Mind = MindNode.extend({
|
|
||||||
addNodeView() {
|
|
||||||
return ReactNodeViewRenderer(Render);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -2,7 +2,7 @@ import ReactDOM from 'react-dom';
|
||||||
import { Space, Button, Tooltip } from '@douyinfe/semi-ui';
|
import { Space, Button, Tooltip } from '@douyinfe/semi-ui';
|
||||||
import { IconPlus, IconDelete } from '@douyinfe/semi-icons';
|
import { IconPlus, IconDelete } from '@douyinfe/semi-icons';
|
||||||
import { IconZoomOut, IconZoomIn } from 'components/icons';
|
import { IconZoomOut, IconZoomIn } from 'components/icons';
|
||||||
import { Divider } from '../../components/divider';
|
import { Divider } from '../divider';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
/*
|
/*
|
|
@ -1,61 +1,8 @@
|
||||||
import { Node, Command, mergeAttributes } from '@tiptap/core';
|
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
||||||
import { NodeViewWrapper, NodeViewContent, ReactNodeViewRenderer } from '@tiptap/react';
|
|
||||||
import { Space, Popover, Tag, Input } from '@douyinfe/semi-ui';
|
import { Space, Popover, Tag, Input } from '@douyinfe/semi-ui';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
export const StatusWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
interface Commands {
|
|
||||||
status: {
|
|
||||||
setStatus: () => Command;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const StatusExtension = Node.create({
|
|
||||||
name: 'status',
|
|
||||||
content: 'text*',
|
|
||||||
group: 'inline',
|
|
||||||
inline: true,
|
|
||||||
atom: true,
|
|
||||||
|
|
||||||
addAttributes() {
|
|
||||||
return {
|
|
||||||
color: {
|
|
||||||
default: 'grey',
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
parseHTML() {
|
|
||||||
return [{ tag: 'span[data-type=status]' }];
|
|
||||||
},
|
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
|
||||||
return [
|
|
||||||
'span',
|
|
||||||
mergeAttributes((this.options && this.options.HTMLAttributes) || {}, HTMLAttributes),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
addCommands() {
|
|
||||||
return {
|
|
||||||
setStatus:
|
|
||||||
(options) =>
|
|
||||||
({ commands }) => {
|
|
||||||
return commands.insertContent({
|
|
||||||
type: this.name,
|
|
||||||
attrs: options,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const Render = ({ editor, node, updateAttributes }) => {
|
|
||||||
const isEditable = editor.isEditable;
|
const isEditable = editor.isEditable;
|
||||||
const { color, text } = node.attrs;
|
const { color, text } = node.attrs;
|
||||||
const content = <Tag color={color}>{text || '设置状态'}</Tag>;
|
const content = <Tag color={color}>{text || '设置状态'}</Tag>;
|
||||||
|
@ -101,9 +48,3 @@ const Render = ({ editor, node, updateAttributes }) => {
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Status = StatusExtension.extend({
|
|
||||||
addNodeView() {
|
|
||||||
return ReactNodeViewRenderer(Render);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const PARSE_HTML_PRIORITY_LOWEST = 1;
|
||||||
|
export const PARSE_HTML_PRIORITY_DEFAULT = 50;
|
||||||
|
export const PARSE_HTML_PRIORITY_HIGHEST = 100;
|
||||||
|
export const EXTENSION_PRIORITY_LOWER = 75;
|
||||||
|
export const EXTENSION_PRIORITY_DEFAULT = 100;
|
||||||
|
export const EXTENSION_PRIORITY_HIGHEST = 200;
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { Node, mergeAttributes } from '@tiptap/core';
|
||||||
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
|
import { AttachmentWrapper } from '../components/attachment';
|
||||||
|
|
||||||
|
export const Attachment = Node.create({
|
||||||
|
name: 'attachment',
|
||||||
|
group: 'block',
|
||||||
|
draggable: true,
|
||||||
|
atom: true,
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
HTMLAttributes: {
|
||||||
|
class: 'attachment',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [{ tag: 'div[class=attachment]' }];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
|
||||||
|
},
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
name: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// @ts-ignore
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setAttachment:
|
||||||
|
(attrs) =>
|
||||||
|
({ chain }) => {
|
||||||
|
return chain().insertContent({ type: this.name, attrs }).run();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(AttachmentWrapper);
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,76 +0,0 @@
|
||||||
import { Node, mergeAttributes } from '@tiptap/core';
|
|
||||||
import { NodeViewWrapper, NodeViewContent, ReactNodeViewRenderer } from '@tiptap/react';
|
|
||||||
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
|
||||||
import { IconDownload } from '@douyinfe/semi-icons';
|
|
||||||
import { download } from '../../utils/download';
|
|
||||||
import styles from './index.module.scss';
|
|
||||||
|
|
||||||
const Render = ({ node }) => {
|
|
||||||
const { name, url } = node.attrs;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NodeViewWrapper as="div">
|
|
||||||
<div className={styles.wrap}>
|
|
||||||
<span>{name}</span>
|
|
||||||
<span>
|
|
||||||
<Tooltip zIndex={10000} content="下载">
|
|
||||||
<Button
|
|
||||||
theme={'borderless'}
|
|
||||||
type="tertiary"
|
|
||||||
icon={<IconDownload />}
|
|
||||||
onClick={() => download(url, name)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<NodeViewContent></NodeViewContent>
|
|
||||||
</NodeViewWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Attachment = Node.create({
|
|
||||||
name: 'attachment',
|
|
||||||
group: 'block',
|
|
||||||
draggable: true,
|
|
||||||
atom: true,
|
|
||||||
|
|
||||||
addOptions() {
|
|
||||||
return {
|
|
||||||
HTMLAttributes: {
|
|
||||||
class: 'attachment',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
parseHTML() {
|
|
||||||
return [{ tag: 'div[class=attachment]' }];
|
|
||||||
},
|
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
|
||||||
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
|
|
||||||
},
|
|
||||||
|
|
||||||
addAttributes() {
|
|
||||||
return {
|
|
||||||
name: {
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
url: {
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
// @ts-ignore
|
|
||||||
addCommands() {
|
|
||||||
return {
|
|
||||||
setAttachment:
|
|
||||||
(attrs) =>
|
|
||||||
({ chain }) => {
|
|
||||||
return chain().insertContent({ type: this.name, attrs }).run();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
addNodeView() {
|
|
||||||
return ReactNodeViewRenderer(Render);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -8,13 +8,7 @@ export type ColorOptions = {
|
||||||
declare module '@tiptap/core' {
|
declare module '@tiptap/core' {
|
||||||
interface Commands<ReturnType> {
|
interface Commands<ReturnType> {
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
/**
|
|
||||||
* Set the text color
|
|
||||||
*/
|
|
||||||
setBackgroundColor: (color: string) => ReturnType;
|
setBackgroundColor: (color: string) => ReturnType;
|
||||||
/**
|
|
||||||
* Unset the text color
|
|
||||||
*/
|
|
||||||
unsetBackgroundColor: () => ReturnType;
|
unsetBackgroundColor: () => ReturnType;
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
import { Node, Command, mergeAttributes } from '@tiptap/core';
|
import { Node, Command, mergeAttributes } from '@tiptap/core';
|
||||||
import { NodeViewWrapper, NodeViewContent, ReactNodeViewRenderer } from '@tiptap/react';
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
import { Banner as SemiBanner } from '@douyinfe/semi-ui';
|
import { BannerWrapper } from '../components/banner';
|
||||||
import styles from './index.module.scss';
|
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
declare module '@tiptap/core' {
|
||||||
interface Commands {
|
interface Commands {
|
||||||
|
@ -11,7 +10,7 @@ declare module '@tiptap/core' {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const BannerExtension = Node.create({
|
export const Banner = Node.create({
|
||||||
name: 'banner',
|
name: 'banner',
|
||||||
content: 'block*',
|
content: 'block*',
|
||||||
group: 'block',
|
group: 'block',
|
||||||
|
@ -57,23 +56,8 @@ const BannerExtension = Node.create({
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
const Render = ({ node }) => {
|
|
||||||
return (
|
|
||||||
<NodeViewWrapper id="js-bannber-container" className={styles.wrap}>
|
|
||||||
<SemiBanner
|
|
||||||
type={node.attrs.type}
|
|
||||||
description={<NodeViewContent />}
|
|
||||||
closeIcon={null}
|
|
||||||
fullMode={false}
|
|
||||||
/>
|
|
||||||
</NodeViewWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Banner = BannerExtension.extend({
|
|
||||||
addNodeView() {
|
addNodeView() {
|
||||||
return ReactNodeViewRenderer(Render);
|
return ReactNodeViewRenderer(BannerWrapper);
|
||||||
},
|
},
|
||||||
});
|
});
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { Blockquote as BuiltInBlockquote } from '@tiptap/extension-blockquote';
|
||||||
|
import { wrappingInputRule } from '@tiptap/core';
|
||||||
|
import { getParents } from '../services/dom';
|
||||||
|
import { getMarkdownSource } from '../services/markdownSourceMap';
|
||||||
|
|
||||||
|
export const Blockquote = BuiltInBlockquote.extend({
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
...this.parent?.(),
|
||||||
|
|
||||||
|
multiline: {
|
||||||
|
default: false,
|
||||||
|
parseHTML: (element) => {
|
||||||
|
const source = getMarkdownSource(element);
|
||||||
|
const parentsIncludeBlockquote = getParents(element).some(
|
||||||
|
(p) => p.nodeName.toLowerCase() === 'blockquote'
|
||||||
|
);
|
||||||
|
|
||||||
|
return source && !source.startsWith('>') && !parentsIncludeBlockquote;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addInputRules() {
|
||||||
|
const multilineInputRegex = /^\s*>>>\s$/gm;
|
||||||
|
|
||||||
|
return [
|
||||||
|
...this.parent?.(),
|
||||||
|
wrappingInputRule({
|
||||||
|
find: multilineInputRegex,
|
||||||
|
type: this.type,
|
||||||
|
getAttributes: () => ({ multiline: true }),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1 @@
|
||||||
|
export { Bold } from '@tiptap/extension-bold';
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { BulletList as BuiltInBulletList } from '@tiptap/extension-bullet-list';
|
||||||
|
import { getMarkdownSource } from '../services/markdownSourceMap';
|
||||||
|
|
||||||
|
export const BulletList = BuiltInBulletList.extend({
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
...this.parent?.(),
|
||||||
|
|
||||||
|
bullet: {
|
||||||
|
default: '*',
|
||||||
|
parseHTML(element) {
|
||||||
|
const bullet = getMarkdownSource(element)?.charAt(0);
|
||||||
|
|
||||||
|
return '*+-'.includes(bullet) ? bullet : '*';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,12 @@
|
||||||
|
import BuiltInCode from '@tiptap/extension-code';
|
||||||
|
import { EXTENSION_PRIORITY_LOWER } from '../constants';
|
||||||
|
|
||||||
|
export const Code = BuiltInCode.extend({
|
||||||
|
excludes: null,
|
||||||
|
/**
|
||||||
|
* Reduce the rendering priority of the code mark to
|
||||||
|
* ensure the bold, italic, and strikethrough marks
|
||||||
|
* are rendered first.
|
||||||
|
*/
|
||||||
|
priority: EXTENSION_PRIORITY_LOWER,
|
||||||
|
});
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
|
||||||
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
|
import { lowlight } from 'lowlight/lib/all';
|
||||||
|
import { CodeBlockWrapper } from '../components/codeBlock';
|
||||||
|
|
||||||
|
const extractLanguage = (element) => element.getAttribute('lang');
|
||||||
|
|
||||||
|
export const CodeBlock = CodeBlockLowlight.extend({
|
||||||
|
isolating: true,
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
language: {
|
||||||
|
default: null,
|
||||||
|
parseHTML: (element) => extractLanguage(element),
|
||||||
|
},
|
||||||
|
class: {
|
||||||
|
default: 'code highlight',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
'pre',
|
||||||
|
{
|
||||||
|
...HTMLAttributes,
|
||||||
|
class: `content-editor-code-block ${HTMLAttributes.class}`,
|
||||||
|
},
|
||||||
|
['code', {}, 0],
|
||||||
|
];
|
||||||
|
},
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(CodeBlockWrapper);
|
||||||
|
},
|
||||||
|
}).configure({
|
||||||
|
lowlight,
|
||||||
|
});
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { Color } from '@tiptap/extension-color';
|
||||||
|
|
||||||
|
export { Color };
|
|
@ -1,6 +1,6 @@
|
||||||
import { Extension } from '@tiptap/core';
|
import { Extension } from '@tiptap/core';
|
||||||
import { Plugin } from 'prosemirror-state';
|
import { Plugin } from 'prosemirror-state';
|
||||||
import findColors from '../utils/find-colors';
|
import { findColors } from '../services/color';
|
||||||
|
|
||||||
export const ColorHighlighter = Extension.create({
|
export const ColorHighlighter = Extension.create({
|
||||||
name: 'colorHighlighter',
|
name: 'colorHighlighter',
|
|
@ -1,39 +0,0 @@
|
||||||
import { Node, mergeAttributes } from '@tiptap/core';
|
|
||||||
|
|
||||||
export const Div = Node.create({
|
|
||||||
name: 'div',
|
|
||||||
addOptions() {
|
|
||||||
return {
|
|
||||||
HTMLAttributes: {},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
content: 'block*',
|
|
||||||
group: 'block',
|
|
||||||
defining: true,
|
|
||||||
parseHTML() {
|
|
||||||
return [{ tag: 'div' }];
|
|
||||||
},
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
|
||||||
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
|
||||||
},
|
|
||||||
// @ts-ignore
|
|
||||||
addCommands() {
|
|
||||||
return {
|
|
||||||
setDiv:
|
|
||||||
(attributes) =>
|
|
||||||
({ commands }) => {
|
|
||||||
return commands.wrapIn('div', attributes);
|
|
||||||
},
|
|
||||||
toggleDiv:
|
|
||||||
(attributes) =>
|
|
||||||
({ commands }) => {
|
|
||||||
return commands.toggleWrap('div', attributes);
|
|
||||||
},
|
|
||||||
unsetDiv:
|
|
||||||
(attributes) =>
|
|
||||||
({ commands }) => {
|
|
||||||
return commands.lift('div');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -0,0 +1 @@
|
||||||
|
export { Document } from '@tiptap/extension-document';
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { Node, Command, mergeAttributes, wrappingInputRule } from '@tiptap/core';
|
||||||
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
|
import { DocumentChildrenWrapper } from '../components/documentChildren';
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands {
|
||||||
|
documentChildren: {
|
||||||
|
setDocumentChildren: () => Command;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DocumentChildrenInputRegex = /^documentChildren\$$/;
|
||||||
|
|
||||||
|
export const DocumentChildren = Node.create({
|
||||||
|
name: 'documentChildren',
|
||||||
|
group: 'block',
|
||||||
|
defining: true,
|
||||||
|
draggable: true,
|
||||||
|
atom: true,
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
color: {
|
||||||
|
default: 'grey',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [{ tag: 'div[data-type=documentChildren]' }];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
'div',
|
||||||
|
mergeAttributes((this.options && this.options.HTMLAttributes) || {}, HTMLAttributes),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setDocumentChildren:
|
||||||
|
() =>
|
||||||
|
({ commands }) => {
|
||||||
|
return commands.insertContent({
|
||||||
|
type: this.name,
|
||||||
|
attrs: {},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addInputRules() {
|
||||||
|
return [
|
||||||
|
wrappingInputRule({
|
||||||
|
find: DocumentChildrenInputRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(DocumentChildrenWrapper);
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { Node, Command, mergeAttributes, wrappingInputRule } from '@tiptap/core';
|
||||||
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
|
import { DocumentReferenceWrapper } from '../components/documentReference';
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands {
|
||||||
|
documentReference: {
|
||||||
|
setDocumentReference: () => Command;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DocumentReferenceInputRegex = /^documentReference\$$/;
|
||||||
|
|
||||||
|
export const DocumentReference = Node.create({
|
||||||
|
name: 'documentReference',
|
||||||
|
group: 'block',
|
||||||
|
defining: true,
|
||||||
|
draggable: true,
|
||||||
|
atom: true,
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
wikiId: {
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
documentId: {
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [{ tag: 'div[data-type=documentReference]' }];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
'div',
|
||||||
|
mergeAttributes((this.options && this.options.HTMLAttributes) || {}, HTMLAttributes),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setDocumentReference:
|
||||||
|
() =>
|
||||||
|
({ commands }) => {
|
||||||
|
return commands.insertContent({
|
||||||
|
type: this.name,
|
||||||
|
attrs: {},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addInputRules() {
|
||||||
|
return [
|
||||||
|
wrappingInputRule({
|
||||||
|
find: DocumentReferenceInputRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(DocumentReferenceWrapper);
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1 @@
|
||||||
|
export { Dropcursor } from '@tiptap/extension-dropcursor';
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { mergeAttributes, Node } from '@tiptap/core';
|
||||||
|
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
|
||||||
|
|
||||||
|
export const FootnoteDefinition = Node.create({
|
||||||
|
name: 'footnoteDefinition',
|
||||||
|
|
||||||
|
content: 'paragraph',
|
||||||
|
|
||||||
|
group: 'block',
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{ tag: 'section.footnotes li' },
|
||||||
|
{ tag: '.footnote-backref', priority: PARSE_HTML_PRIORITY_HIGHEST, ignore: true },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return ['li', mergeAttributes(HTMLAttributes), 0];
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { Node, mergeAttributes } from '@tiptap/core';
|
||||||
|
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
|
||||||
|
|
||||||
|
export const FootnoteReference = Node.create({
|
||||||
|
name: 'footnoteReference',
|
||||||
|
|
||||||
|
inline: true,
|
||||||
|
|
||||||
|
group: 'inline',
|
||||||
|
|
||||||
|
atom: true,
|
||||||
|
|
||||||
|
draggable: true,
|
||||||
|
|
||||||
|
selectable: true,
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
footnoteId: {
|
||||||
|
default: null,
|
||||||
|
parseHTML: (element) => element.querySelector('a').getAttribute('id'),
|
||||||
|
},
|
||||||
|
footnoteNumber: {
|
||||||
|
default: null,
|
||||||
|
parseHTML: (element) => element.textContent,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [{ tag: 'sup.footnote-ref', priority: PARSE_HTML_PRIORITY_HIGHEST }];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes: { footnoteNumber, footnoteId, ...HTMLAttributes } }) {
|
||||||
|
return ['sup', mergeAttributes(HTMLAttributes), footnoteNumber];
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { mergeAttributes, Node } from '@tiptap/core';
|
||||||
|
|
||||||
|
export const FootnotesSection = Node.create({
|
||||||
|
name: 'footnotesSection',
|
||||||
|
|
||||||
|
content: 'footnoteDefinition+',
|
||||||
|
|
||||||
|
group: 'block',
|
||||||
|
|
||||||
|
isolating: true,
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [{ tag: 'section.footnotes > ol' }];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return ['ol', mergeAttributes(HTMLAttributes, { class: 'footnotes gl-font-sm' }), 0];
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1 @@
|
||||||
|
export { Gapcursor } from '@tiptap/extension-gapcursor';
|
|
@ -0,0 +1,3 @@
|
||||||
|
import HardBreak from '@tiptap/extension-hard-break';
|
||||||
|
|
||||||
|
export { HardBreak };
|
|
@ -0,0 +1 @@
|
||||||
|
export { Heading } from '@tiptap/extension-heading';
|
|
@ -8,9 +8,6 @@ export interface HorizontalRuleOptions {
|
||||||
declare module '@tiptap/core' {
|
declare module '@tiptap/core' {
|
||||||
interface Commands<ReturnType> {
|
interface Commands<ReturnType> {
|
||||||
horizontalRule: {
|
horizontalRule: {
|
||||||
/**
|
|
||||||
* Add a horizontal rule
|
|
||||||
*/
|
|
||||||
setHorizontalRule: () => ReturnType;
|
setHorizontalRule: () => ReturnType;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -41,34 +38,30 @@ export const HorizontalRule = Node.create<HorizontalRuleOptions>({
|
||||||
setHorizontalRule:
|
setHorizontalRule:
|
||||||
() =>
|
() =>
|
||||||
({ chain }) => {
|
({ chain }) => {
|
||||||
return (
|
return chain()
|
||||||
chain()
|
.insertContent({ type: this.name })
|
||||||
.insertContent({ type: this.name })
|
.command(({ tr, dispatch }) => {
|
||||||
// set cursor after horizontal rule
|
if (dispatch) {
|
||||||
.command(({ tr, dispatch }) => {
|
const { $to } = tr.selection;
|
||||||
if (dispatch) {
|
const posAfter = $to.end();
|
||||||
const { $to } = tr.selection;
|
|
||||||
const posAfter = $to.end();
|
|
||||||
|
|
||||||
if ($to.nodeAfter) {
|
if ($to.nodeAfter) {
|
||||||
tr.setSelection(TextSelection.create(tr.doc, $to.pos));
|
tr.setSelection(TextSelection.create(tr.doc, $to.pos));
|
||||||
} else {
|
} else {
|
||||||
// add node after horizontal rule if it’s the end of the document
|
const node = $to.parent.type.contentMatch.defaultType?.create();
|
||||||
const node = $to.parent.type.contentMatch.defaultType?.create();
|
|
||||||
|
|
||||||
if (node) {
|
if (node) {
|
||||||
tr.insert(posAfter, node);
|
tr.insert(posAfter, node);
|
||||||
tr.setSelection(TextSelection.create(tr.doc, posAfter));
|
tr.setSelection(TextSelection.create(tr.doc, posAfter));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tr.scrollIntoView();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
tr.scrollIntoView();
|
||||||
})
|
}
|
||||||
.run()
|
|
||||||
);
|
return true;
|
||||||
|
})
|
||||||
|
.run();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { Mark, mergeAttributes, markInputRule } from '@tiptap/core';
|
||||||
|
import { PARSE_HTML_PRIORITY_LOWEST } from '../constants';
|
||||||
|
import { markInputRegex, extractMarkAttributesFromMatch } from '../services/markUtils';
|
||||||
|
|
||||||
|
const marks = [
|
||||||
|
'ins',
|
||||||
|
'abbr',
|
||||||
|
'bdo',
|
||||||
|
'cite',
|
||||||
|
'dfn',
|
||||||
|
'mark',
|
||||||
|
'small',
|
||||||
|
'span',
|
||||||
|
'time',
|
||||||
|
'kbd',
|
||||||
|
'q',
|
||||||
|
'samp',
|
||||||
|
'var',
|
||||||
|
'ruby',
|
||||||
|
'rp',
|
||||||
|
'rt',
|
||||||
|
];
|
||||||
|
|
||||||
|
const attrs = {
|
||||||
|
time: ['datetime'],
|
||||||
|
abbr: ['title'],
|
||||||
|
span: ['dir'],
|
||||||
|
bdo: ['dir'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HTMLMarks = marks.map((name) =>
|
||||||
|
Mark.create({
|
||||||
|
name,
|
||||||
|
inclusive: false,
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
HTMLAttributes: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addAttributes() {
|
||||||
|
return (attrs[name] || []).reduce(
|
||||||
|
(acc, attr) => ({
|
||||||
|
...acc,
|
||||||
|
[attr]: {
|
||||||
|
default: null,
|
||||||
|
parseHTML: (element) => element.getAttribute(attr),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [{ tag: name, priority: PARSE_HTML_PRIORITY_LOWEST }];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return [name, mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||||
|
},
|
||||||
|
|
||||||
|
addInputRules() {
|
||||||
|
return [
|
||||||
|
markInputRule({
|
||||||
|
find: markInputRegex(name),
|
||||||
|
type: this.type,
|
||||||
|
getAttributes: extractMarkAttributesFromMatch,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { Node, mergeAttributes } from '@tiptap/core';
|
||||||
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
|
import { IframeWrapper } from '../components/iframe';
|
||||||
|
|
||||||
|
export const Iframe = Node.create({
|
||||||
|
name: 'external-iframe',
|
||||||
|
content: '',
|
||||||
|
marks: '',
|
||||||
|
group: 'block',
|
||||||
|
draggable: true,
|
||||||
|
atom: true,
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
HTMLAttributes: {
|
||||||
|
'data-type': 'external-iframe',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
width: {
|
||||||
|
default: '100%',
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
default: 54,
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: 'iframe[data-type="external-iframe"]',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return ['iframe', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
|
||||||
|
},
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
insertIframe:
|
||||||
|
(options) =>
|
||||||
|
({ tr, commands, chain, editor }) => {
|
||||||
|
if (tr.selection?.node?.type?.name == this.name) {
|
||||||
|
return commands.updateAttributes(this.name, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url } = options || {};
|
||||||
|
const { selection } = editor.state;
|
||||||
|
const pos = selection.$head;
|
||||||
|
|
||||||
|
return chain()
|
||||||
|
.insertContentAt(pos.before(), [
|
||||||
|
{
|
||||||
|
type: this.name,
|
||||||
|
attrs: { url },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.run();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(IframeWrapper);
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,123 +0,0 @@
|
||||||
import { Node, mergeAttributes } from '@tiptap/core';
|
|
||||||
import { NodeViewWrapper, NodeViewContent, ReactNodeViewRenderer } from '@tiptap/react';
|
|
||||||
import { Input } from '@douyinfe/semi-ui';
|
|
||||||
import { Resizeable } from 'components/resizeable';
|
|
||||||
import styles from './index.module.scss';
|
|
||||||
|
|
||||||
const IframeNode = Node.create({
|
|
||||||
name: 'external-iframe',
|
|
||||||
content: '',
|
|
||||||
marks: '',
|
|
||||||
group: 'block',
|
|
||||||
draggable: true,
|
|
||||||
atom: true,
|
|
||||||
|
|
||||||
addOptions() {
|
|
||||||
return {
|
|
||||||
HTMLAttributes: {
|
|
||||||
'data-type': 'external-iframe',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
addAttributes() {
|
|
||||||
return {
|
|
||||||
width: {
|
|
||||||
default: '100%',
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
default: 54,
|
|
||||||
},
|
|
||||||
url: {
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
parseHTML() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
tag: 'iframe[data-type="external-iframe"]',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
|
||||||
return ['iframe', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
|
|
||||||
},
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
addCommands() {
|
|
||||||
return {
|
|
||||||
insertIframe:
|
|
||||||
(options) =>
|
|
||||||
({ tr, commands, chain, editor }) => {
|
|
||||||
if (tr.selection?.node?.type?.name == this.name) {
|
|
||||||
return commands.updateAttributes(this.name, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { url } = options || {};
|
|
||||||
const { selection } = editor.state;
|
|
||||||
const pos = selection.$head;
|
|
||||||
|
|
||||||
return chain()
|
|
||||||
.insertContentAt(pos.before(), [
|
|
||||||
{
|
|
||||||
type: this.name,
|
|
||||||
attrs: { url },
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.run();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const Render = ({ editor, node, updateAttributes }) => {
|
|
||||||
const isEditable = editor.isEditable;
|
|
||||||
const { url, width, height } = node.attrs;
|
|
||||||
|
|
||||||
const onResize = (size) => {
|
|
||||||
updateAttributes({ width: size.width, height: size.height });
|
|
||||||
};
|
|
||||||
const content = (
|
|
||||||
<NodeViewContent as="div" className={styles.wrap}>
|
|
||||||
{isEditable && (
|
|
||||||
<div className={styles.handlerWrap}>
|
|
||||||
<Input
|
|
||||||
placeholder={'输入外链地址'}
|
|
||||||
value={url}
|
|
||||||
onChange={(url) => updateAttributes({ url })}
|
|
||||||
></Input>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{url && (
|
|
||||||
<div className={styles.innerWrap} style={{ pointerEvents: !isEditable ? 'auto' : 'none' }}>
|
|
||||||
<iframe src={url}></iframe>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</NodeViewContent>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isEditable && !url) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NodeViewWrapper>
|
|
||||||
{isEditable ? (
|
|
||||||
<Resizeable height={height} width={width} onChange={onResize}>
|
|
||||||
{content}
|
|
||||||
</Resizeable>
|
|
||||||
) : (
|
|
||||||
<div style={{ width, height, maxWidth: '100%' }}>{content}</div>
|
|
||||||
)}
|
|
||||||
</NodeViewWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Iframe = IframeNode.extend({
|
|
||||||
addNodeView() {
|
|
||||||
return ReactNodeViewRenderer(Render);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { Image as BuiltInImage } from '@tiptap/extension-image';
|
||||||
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
|
import { ImageWrapper } from '../components/image';
|
||||||
|
|
||||||
|
const resolveImageEl = (element) =>
|
||||||
|
element.nodeName === 'IMG' ? element : element.querySelector('img');
|
||||||
|
|
||||||
|
export const Image = BuiltInImage.extend({
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
...this.parent?.(),
|
||||||
|
inline: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
...this.parent?.(),
|
||||||
|
src: {
|
||||||
|
default: null,
|
||||||
|
parseHTML: (element) => {
|
||||||
|
const img = resolveImageEl(element);
|
||||||
|
|
||||||
|
return img.dataset.src || img.getAttribute('src');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
alt: {
|
||||||
|
default: null,
|
||||||
|
parseHTML: (element) => {
|
||||||
|
const img = resolveImageEl(element);
|
||||||
|
|
||||||
|
return img.getAttribute('alt');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
default: 'auto',
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
default: 'auto',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(ImageWrapper);
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,164 +0,0 @@
|
||||||
import { Command, Extension } from '@tiptap/core';
|
|
||||||
import { sinkListItem, liftListItem } from 'prosemirror-schema-list';
|
|
||||||
import { TextSelection, AllSelection, Transaction } from 'prosemirror-state';
|
|
||||||
import { clamp } from '../utils/shared';
|
|
||||||
import { isListActive } from '../utils/active';
|
|
||||||
import { getNodeType } from '../utils/type';
|
|
||||||
import { isListNode } from '../utils/node';
|
|
||||||
|
|
||||||
type IndentOptions = {
|
|
||||||
types: string[];
|
|
||||||
indentLevels: number[];
|
|
||||||
defaultIndentLevel: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
|
||||||
interface Commands {
|
|
||||||
indent: {
|
|
||||||
indent: () => Command;
|
|
||||||
outdent: () => Command;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum IndentProps {
|
|
||||||
min = 0,
|
|
||||||
max = 210,
|
|
||||||
more = 30,
|
|
||||||
less = -30,
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNodeIndentMarkup(tr: Transaction, pos: number, delta: number): Transaction {
|
|
||||||
if (!tr.doc) return tr;
|
|
||||||
|
|
||||||
const node = tr.doc.nodeAt(pos);
|
|
||||||
if (!node) return tr;
|
|
||||||
|
|
||||||
const minIndent = IndentProps.min;
|
|
||||||
const maxIndent = IndentProps.max;
|
|
||||||
|
|
||||||
const indent = clamp((node.attrs.indent || 0) + delta, minIndent, maxIndent);
|
|
||||||
|
|
||||||
if (indent === node.attrs.indent) return tr;
|
|
||||||
|
|
||||||
const nodeAttrs = {
|
|
||||||
...node.attrs,
|
|
||||||
indent,
|
|
||||||
};
|
|
||||||
|
|
||||||
return tr.setNodeMarkup(pos, node.type, nodeAttrs, node.marks);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateIndentLevel(tr: Transaction, delta: number): Transaction {
|
|
||||||
const { doc, selection } = tr;
|
|
||||||
|
|
||||||
if (!doc || !selection) return tr;
|
|
||||||
|
|
||||||
if (!(selection instanceof TextSelection || selection instanceof AllSelection)) {
|
|
||||||
return tr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { from, to } = selection;
|
|
||||||
|
|
||||||
doc.nodesBetween(from, to, (node, pos) => {
|
|
||||||
const nodeType = node.type;
|
|
||||||
|
|
||||||
if (nodeType.name === 'paragraph' || nodeType.name === 'heading') {
|
|
||||||
tr = setNodeIndentMarkup(tr, pos, delta);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (isListNode(node)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
return tr;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Indent = Extension.create<IndentOptions>({
|
|
||||||
name: 'indent',
|
|
||||||
|
|
||||||
addOptions() {
|
|
||||||
return {
|
|
||||||
types: ['heading', 'paragraph'],
|
|
||||||
indentLevels: [0, 30, 60, 90, 120, 150, 180, 210],
|
|
||||||
defaultIndentLevel: 0,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
addGlobalAttributes() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
types: this.options.types,
|
|
||||||
attributes: {
|
|
||||||
indent: {
|
|
||||||
default: this.options.defaultIndentLevel,
|
|
||||||
renderHTML: (attributes) => ({
|
|
||||||
style: `margin-left: ${attributes.indent}px!important;`,
|
|
||||||
}),
|
|
||||||
parseHTML: (element) =>
|
|
||||||
parseInt(element.style.marginLeft) || this.options.defaultIndentLevel,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
addCommands() {
|
|
||||||
return {
|
|
||||||
indent:
|
|
||||||
() =>
|
|
||||||
({ tr, state, dispatch }) => {
|
|
||||||
if (isListActive(this.editor)) {
|
|
||||||
const name = this.editor.can().liftListItem('taskItem') ? 'taskItem' : 'listItem';
|
|
||||||
const type = getNodeType(name, state.schema);
|
|
||||||
return sinkListItem(type)(state, dispatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { selection } = state;
|
|
||||||
tr = tr.setSelection(selection);
|
|
||||||
tr = updateIndentLevel(tr, IndentProps.more);
|
|
||||||
|
|
||||||
if (tr.docChanged) {
|
|
||||||
dispatch && dispatch(tr);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
outdent:
|
|
||||||
() =>
|
|
||||||
({ tr, state, dispatch }) => {
|
|
||||||
if (isListActive(this.editor)) {
|
|
||||||
const name = this.editor.can().liftListItem('taskItem') ? 'taskItem' : 'listItem';
|
|
||||||
const type = getNodeType(name, state.schema);
|
|
||||||
return liftListItem(type)(state, dispatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { selection } = state;
|
|
||||||
tr = tr.setSelection(selection);
|
|
||||||
tr = updateIndentLevel(tr, IndentProps.less);
|
|
||||||
|
|
||||||
if (tr.docChanged) {
|
|
||||||
dispatch && dispatch(tr);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
addKeyboardShortcuts() {
|
|
||||||
return {
|
|
||||||
'Tab': () => {
|
|
||||||
return this.editor.commands.indent();
|
|
||||||
},
|
|
||||||
'Shift-Tab': () => {
|
|
||||||
return this.editor.commands.outdent();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -0,0 +1 @@
|
||||||
|
export { Italic } from '@tiptap/extension-italic';
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { Node, Command, mergeAttributes, wrappingInputRule } from '@tiptap/core';
|
||||||
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
|
import { KatexWrapper } from '../components/katex';
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands {
|
||||||
|
katex: {
|
||||||
|
setKatex: () => Command;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KatexInputRegex = /^\$\$(.+)?\$\$$/;
|
||||||
|
|
||||||
|
export const Katex = Node.create({
|
||||||
|
name: 'katex',
|
||||||
|
group: 'block',
|
||||||
|
defining: true,
|
||||||
|
draggable: true,
|
||||||
|
selectable: true,
|
||||||
|
atom: true,
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
text: {
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [{ tag: 'div[data-type=katex]' }];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
'div',
|
||||||
|
mergeAttributes((this.options && this.options.HTMLAttributes) || {}, HTMLAttributes),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setKatex:
|
||||||
|
(options) =>
|
||||||
|
({ commands }) => {
|
||||||
|
return commands.insertContent({
|
||||||
|
type: this.name,
|
||||||
|
attrs: options,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addInputRules() {
|
||||||
|
return [
|
||||||
|
wrappingInputRule({
|
||||||
|
find: KatexInputRegex,
|
||||||
|
type: this.type,
|
||||||
|
getAttributes: (match) => {
|
||||||
|
return { text: match[1] };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(KatexWrapper);
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { markInputRule } from '@tiptap/core';
|
||||||
|
import { Link as BuiltInLink } from '@tiptap/extension-link';
|
||||||
|
|
||||||
|
const extractHrefFromMatch = (match) => {
|
||||||
|
return { href: match.groups.href };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const extractHrefFromMarkdownLink = (match) => {
|
||||||
|
/**
|
||||||
|
* Removes the last capture group from the match to satisfy
|
||||||
|
* tiptap markInputRule expectation of having the content as
|
||||||
|
* the last capture group in the match.
|
||||||
|
*
|
||||||
|
* https://github.com/ueberdosis/tiptap/blob/%40tiptap/core%402.0.0-beta.75/packages/core/src/inputRules/markInputRule.ts#L11
|
||||||
|
*/
|
||||||
|
match.pop();
|
||||||
|
return extractHrefFromMatch(match);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Link = BuiltInLink.extend({
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
...this.parent?.(),
|
||||||
|
openOnClick: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addInputRules() {
|
||||||
|
const markdownLinkSyntaxInputRuleRegExp = /(?:^|\s)\[([\w|\s|-]+)\]\((?<href>.+?)\)$/gm;
|
||||||
|
const urlSyntaxRegExp = /(?:^|\s)(?<href>(?:https?:\/\/|www\.)[\S]+)(?:\s|\n)$/gim;
|
||||||
|
|
||||||
|
return [
|
||||||
|
markInputRule({
|
||||||
|
find: markdownLinkSyntaxInputRuleRegExp,
|
||||||
|
type: this.type,
|
||||||
|
getAttributes: extractHrefFromMarkdownLink,
|
||||||
|
}),
|
||||||
|
markInputRule({
|
||||||
|
find: urlSyntaxRegExp,
|
||||||
|
type: this.type,
|
||||||
|
getAttributes: extractHrefFromMatch,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
...this.parent?.(),
|
||||||
|
href: {
|
||||||
|
default: null,
|
||||||
|
parseHTML: (element) => element.getAttribute('href'),
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
title: null,
|
||||||
|
parseHTML: (element) => element.getAttribute('title'),
|
||||||
|
},
|
||||||
|
canonicalSrc: {
|
||||||
|
default: null,
|
||||||
|
parseHTML: (element) => element.dataset.canonicalSrc,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,3 +0,0 @@
|
||||||
import { Link as TLink } from '@tiptap/extension-link';
|
|
||||||
|
|
||||||
export const Link = TLink.extend({});
|
|
|
@ -0,0 +1 @@
|
||||||
|
export { ListItem } from '@tiptap/extension-list-item';
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { Node, mergeAttributes } from '@tiptap/core';
|
||||||
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
|
import { MindWrapper } from '../components/mind';
|
||||||
|
|
||||||
|
const DEFAULT_MIND_DATA = {
|
||||||
|
meta: {
|
||||||
|
name: 'jsMind',
|
||||||
|
author: 'think',
|
||||||
|
version: '0.2',
|
||||||
|
},
|
||||||
|
format: 'node_tree',
|
||||||
|
data: { id: 'root', topic: '中心节点', children: [] },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Mind = Node.create({
|
||||||
|
name: 'jsmind',
|
||||||
|
content: '',
|
||||||
|
marks: '',
|
||||||
|
group: 'block',
|
||||||
|
draggable: true,
|
||||||
|
atom: true,
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
HTMLAttributes: {
|
||||||
|
'data-type': 'jsmind',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
width: {
|
||||||
|
default: '100%',
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
default: 240,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
default: DEFAULT_MIND_DATA,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: 'div[data-type="jsmind"]',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
|
||||||
|
},
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
insertMind:
|
||||||
|
(options) =>
|
||||||
|
({ tr, commands, chain, editor }) => {
|
||||||
|
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: { data: DEFAULT_MIND_DATA },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.run();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(MindWrapper);
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { OrderedList as BuiltInOrderedList } from '@tiptap/extension-ordered-list';
|
||||||
|
import { getMarkdownSource } from '../services/markdownSourceMap';
|
||||||
|
|
||||||
|
export const OrderedList = BuiltInOrderedList.extend({
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
...this.parent?.(),
|
||||||
|
|
||||||
|
parens: {
|
||||||
|
default: false,
|
||||||
|
parseHTML: (element) => /^[0-9]+\)/.test(getMarkdownSource(element)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1 @@
|
||||||
|
export { Paragraph } from '@tiptap/extension-paragraph';
|
|
@ -1,198 +0,0 @@
|
||||||
import { Plugin, EditorState } from 'prosemirror-state';
|
|
||||||
import { Extension } from '@tiptap/core';
|
|
||||||
// @ts-ignore
|
|
||||||
import { lowlight } from 'lowlight';
|
|
||||||
import { uploadFile } from 'services/file';
|
|
||||||
import { Attachment } from './attachment';
|
|
||||||
import { Image } from './image';
|
|
||||||
import { markdownSerializer } from '../markdown';
|
|
||||||
|
|
||||||
const isMarkActive =
|
|
||||||
(type) =>
|
|
||||||
(state: EditorState): boolean => {
|
|
||||||
if (!type) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { from, $from, to, empty } = state.selection;
|
|
||||||
|
|
||||||
return empty
|
|
||||||
? type.isInSet(state.storedMarks || $from.marks())
|
|
||||||
: state.doc.rangeHasMark(from, to, type);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function isInCode(state: EditorState): boolean {
|
|
||||||
if (state.schema.nodes.codeBlock) {
|
|
||||||
const $head = state.selection.$head;
|
|
||||||
for (let d = $head.depth; d > 0; d--) {
|
|
||||||
if ($head.node(d).type === state.schema.nodes.codeBlock) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return isMarkActive(state.schema.marks.code)(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
const LANGUAGES = lowlight.listLanguages().reduce((a, language) => {
|
|
||||||
a[language] = language;
|
|
||||||
return a;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
export const acceptedMimes = {
|
|
||||||
image: ['image/jpeg', 'image/png', 'image/gif', 'image/jpg'],
|
|
||||||
};
|
|
||||||
|
|
||||||
function isMarkdown(text: string): boolean {
|
|
||||||
// code-ish
|
|
||||||
const fences = text.match(/^```/gm);
|
|
||||||
if (fences && fences.length > 1) return true;
|
|
||||||
|
|
||||||
// link-ish
|
|
||||||
if (text.match(/\[[^]+\]\(https?:\/\/\S+\)/gm)) return true;
|
|
||||||
if (text.match(/\[[^]+\]\(\/\S+\)/gm)) return true;
|
|
||||||
|
|
||||||
// heading-ish
|
|
||||||
if (text.match(/^#{1,6}\s+\S+/gm)) return true;
|
|
||||||
|
|
||||||
// list-ish
|
|
||||||
const listItems = text.match(/^[\d-*].?\s\S+/gm);
|
|
||||||
if (listItems && listItems.length > 1) return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePastedMarkdown(text: string): string {
|
|
||||||
const CHECKBOX_REGEX = /^\s?(\[(X|\s|_|-)\]\s(.*)?)/gim;
|
|
||||||
|
|
||||||
while (text.match(CHECKBOX_REGEX)) {
|
|
||||||
text = text.replace(CHECKBOX_REGEX, (match) => `- ${match.trim()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Paste = Extension.create({
|
|
||||||
addProseMirrorPlugins() {
|
|
||||||
return [
|
|
||||||
new Plugin({
|
|
||||||
props: {
|
|
||||||
// @ts-ignore
|
|
||||||
handlePaste: async (view, event: ClipboardEvent) => {
|
|
||||||
if (view.props.editable && !view.props.editable(view.state)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!event.clipboardData) return false;
|
|
||||||
|
|
||||||
const file = event.clipboardData.files[0];
|
|
||||||
const text = event.clipboardData.getData('text/plain');
|
|
||||||
const html = event.clipboardData.getData('text/html');
|
|
||||||
const vscode = event.clipboardData.getData('vscode-editor-data');
|
|
||||||
|
|
||||||
if (file) {
|
|
||||||
event.preventDefault();
|
|
||||||
const url = await uploadFile(file);
|
|
||||||
let node = null;
|
|
||||||
if (acceptedMimes.image.includes(file?.type)) {
|
|
||||||
node = view.props.state.schema.nodes[Image.name].create({
|
|
||||||
src: url,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
node = view.props.state.schema.nodes[Attachment.name].create({
|
|
||||||
url,
|
|
||||||
name: file.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const transaction = view.state.tr.replaceSelectionWith(node);
|
|
||||||
view.dispatch(transaction);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 粘贴代码
|
|
||||||
if (isInCode(view.state)) {
|
|
||||||
event.preventDefault();
|
|
||||||
view.dispatch(view.state.tr.insertText(text));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const vscodeMeta = vscode ? JSON.parse(vscode) : undefined;
|
|
||||||
const pasteCodeLanguage = vscodeMeta?.mode;
|
|
||||||
|
|
||||||
// if (pasteCodeLanguage && pasteCodeLanguage !== "markdown") {
|
|
||||||
// event.preventDefault();
|
|
||||||
// view.dispatch(
|
|
||||||
// view.state.tr.replaceSelectionWith(
|
|
||||||
// view.state.schema.nodes.codeBlock.create({
|
|
||||||
// language: Object.keys(LANGUAGES).includes(vscodeMeta.mode)
|
|
||||||
// ? vscodeMeta.mode
|
|
||||||
// : null,
|
|
||||||
// })
|
|
||||||
// )
|
|
||||||
// );
|
|
||||||
// view.dispatch(view.state.tr.insertText(text));
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 处理 markdown
|
|
||||||
if (isMarkdown(text) || html.length === 0 || pasteCodeLanguage === 'markdown') {
|
|
||||||
event.preventDefault();
|
|
||||||
const paste = markdownSerializer.deserialize({
|
|
||||||
schema: view.props.state.schema,
|
|
||||||
content: normalizePastedMarkdown(text),
|
|
||||||
});
|
|
||||||
const transaction = view.state.tr.replaceSelectionWith(paste);
|
|
||||||
view.dispatch(transaction);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
// @ts-ignore
|
|
||||||
handleDrop: async (view, event: any) => {
|
|
||||||
if (view.props.editable && !view.props.editable(view.state)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasFiles = event.dataTransfer.files.length > 0;
|
|
||||||
if (!hasFiles) return false;
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const files = Array.from(event.dataTransfer.files);
|
|
||||||
files.forEach(async (file: any) => {
|
|
||||||
if (!file) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const url = await uploadFile(file);
|
|
||||||
let node = null;
|
|
||||||
if (acceptedMimes.image.includes(file?.type)) {
|
|
||||||
node = view.props.state.schema.nodes[Image.name].create({
|
|
||||||
src: url,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
node = view.props.state.schema.nodes[Attachment.name].create({
|
|
||||||
url,
|
|
||||||
name: file.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const transaction = view.state.tr.replaceSelectionWith(node);
|
|
||||||
view.dispatch(transaction);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
clipboardTextSerializer: (slice) => {
|
|
||||||
const doc = this.editor.schema.topNodeType.createAndFill(undefined, slice.content);
|
|
||||||
if (!doc) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const content = markdownSerializer.serialize({
|
|
||||||
schema: this.editor.schema,
|
|
||||||
document: doc,
|
|
||||||
});
|
|
||||||
|
|
||||||
return content;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { Plugin } from 'prosemirror-state';
|
||||||
|
import { Extension } from '@tiptap/core';
|
||||||
|
import { uploadFile } from 'services/file';
|
||||||
|
import { Attachment } from './attachment';
|
||||||
|
import { Image } from './image';
|
||||||
|
|
||||||
|
export const acceptedMimes = {
|
||||||
|
image: ['image/jpeg', 'image/png', 'image/gif', 'image/jpg'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PasteFile = Extension.create({
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
props: {
|
||||||
|
// @ts-ignore
|
||||||
|
handlePaste: async (view, event: ClipboardEvent) => {
|
||||||
|
if (view.props.editable && !view.props.editable(view.state)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!event.clipboardData) return false;
|
||||||
|
|
||||||
|
const file = event.clipboardData.files[0];
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
event.preventDefault();
|
||||||
|
const url = await uploadFile(file);
|
||||||
|
let node = null;
|
||||||
|
if (acceptedMimes.image.includes(file?.type)) {
|
||||||
|
node = view.props.state.schema.nodes[Image.name].create({
|
||||||
|
src: url,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
node = view.props.state.schema.nodes[Attachment.name].create({
|
||||||
|
url,
|
||||||
|
name: file.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const transaction = view.state.tr.replaceSelectionWith(node);
|
||||||
|
view.dispatch(transaction);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
// @ts-ignore
|
||||||
|
handleDrop: async (view, event: any) => {
|
||||||
|
if (view.props.editable && !view.props.editable(view.state)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFiles = event.dataTransfer.files.length > 0;
|
||||||
|
if (!hasFiles) return false;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const files = Array.from(event.dataTransfer.files);
|
||||||
|
files.forEach(async (file: any) => {
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = await uploadFile(file);
|
||||||
|
let node = null;
|
||||||
|
if (acceptedMimes.image.includes(file?.type)) {
|
||||||
|
node = view.props.state.schema.nodes[Image.name].create({
|
||||||
|
src: url,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
node = view.props.state.schema.nodes[Attachment.name].create({
|
||||||
|
url,
|
||||||
|
name: file.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const transaction = view.state.tr.replaceSelectionWith(node);
|
||||||
|
view.dispatch(transaction);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { Extension } from '@tiptap/core';
|
||||||
|
import { Plugin, PluginKey } from 'prosemirror-state';
|
||||||
|
import { markdownSerializer } from '../services/serializer';
|
||||||
|
import { EXTENSION_PRIORITY_HIGHEST } from '../constants';
|
||||||
|
|
||||||
|
const TEXT_FORMAT = 'text/plain';
|
||||||
|
const HTML_FORMAT = 'text/html';
|
||||||
|
const VS_CODE_FORMAT = 'vscode-editor-data';
|
||||||
|
|
||||||
|
export const PasteMarkdown = Extension.create({
|
||||||
|
name: 'pasteMarkdown',
|
||||||
|
priority: EXTENSION_PRIORITY_HIGHEST,
|
||||||
|
// @ts-ignore
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
pasteMarkdown: (markdown) => () => {
|
||||||
|
const { editor } = this;
|
||||||
|
const { state, view } = editor;
|
||||||
|
const { tr, selection } = state;
|
||||||
|
|
||||||
|
const document = markdownSerializer.deserialize({
|
||||||
|
schema: view.props.state.schema,
|
||||||
|
content: markdown,
|
||||||
|
});
|
||||||
|
|
||||||
|
// tr.replaceWith(selection.from - 1, selection.to, document.content);
|
||||||
|
// view.dispatch(tr);
|
||||||
|
const transaction = view.state.tr.replaceSelectionWith(document);
|
||||||
|
view.dispatch(transaction);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
key: new PluginKey('pasteMarkdown'),
|
||||||
|
props: {
|
||||||
|
handlePaste: (_, event) => {
|
||||||
|
const { clipboardData } = event;
|
||||||
|
const content = clipboardData.getData(TEXT_FORMAT);
|
||||||
|
const hasHTML = clipboardData.types.some((type) => type === HTML_FORMAT);
|
||||||
|
const hasVsCode = clipboardData.types.some((type) => type === VS_CODE_FORMAT);
|
||||||
|
const vsCodeMeta = hasVsCode ? JSON.parse(clipboardData.getData(VS_CODE_FORMAT)) : {};
|
||||||
|
const language = vsCodeMeta.mode;
|
||||||
|
|
||||||
|
if (!content || (hasHTML && !hasVsCode) || (hasVsCode && language !== 'markdown')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
this.editor.commands.pasteMarkdown(content);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
clipboardTextSerializer: (slice) => {
|
||||||
|
const doc = this.editor.schema.topNodeType.createAndFill(undefined, slice.content);
|
||||||
|
if (!doc) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const content = markdownSerializer.serialize({
|
||||||
|
schema: this.editor.schema,
|
||||||
|
content: doc,
|
||||||
|
});
|
||||||
|
|
||||||
|
return content;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,3 @@
|
||||||
|
import Placeholder from '@tiptap/extension-placeholder';
|
||||||
|
|
||||||
|
export { Placeholder };
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { Node, Command, mergeAttributes } from '@tiptap/core';
|
||||||
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
|
import { StatusWrapper } from '../components/status';
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands {
|
||||||
|
status: {
|
||||||
|
setStatus: () => Command;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Status = Node.create({
|
||||||
|
name: 'status',
|
||||||
|
content: 'text*',
|
||||||
|
group: 'inline',
|
||||||
|
inline: true,
|
||||||
|
atom: true,
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
color: {
|
||||||
|
default: 'grey',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [{ tag: 'span[data-type=status]' }];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
'span',
|
||||||
|
mergeAttributes((this.options && this.options.HTMLAttributes) || {}, HTMLAttributes),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setStatus:
|
||||||
|
(options) =>
|
||||||
|
({ commands }) => {
|
||||||
|
return commands.insertContent({
|
||||||
|
type: this.name,
|
||||||
|
attrs: options,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(StatusWrapper);
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1 @@
|
||||||
|
export { Strike } from '@tiptap/extension-strike';
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { Table as BuiltInTable } from '@tiptap/extension-table';
|
||||||
|
|
||||||
|
export const Table = BuiltInTable.configure({
|
||||||
|
resizable: true,
|
||||||
|
});
|
|
@ -1,6 +0,0 @@
|
||||||
import Table from '@tiptap/extension-table';
|
|
||||||
import TableRow from '@tiptap/extension-table-row';
|
|
||||||
import TableCell from '@tiptap/extension-table-cell';
|
|
||||||
import TableHeader from '@tiptap/extension-table-header';
|
|
||||||
|
|
||||||
export { Table, TableRow, TableCell, TableHeader };
|
|
|
@ -0,0 +1 @@
|
||||||
|
export { TableCell } from '@tiptap/extension-table-cell';
|
|
@ -0,0 +1 @@
|
||||||
|
export { TableHeader } from '@tiptap/extension-table-header';
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { TableRow as BuiltInTableRow } from '@tiptap/extension-table-row';
|
||||||
|
|
||||||
|
export const TableRow = BuiltInTableRow.extend({
|
||||||
|
allowGapCursor: false,
|
||||||
|
});
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { TaskItem as BuiltInTaskItem } from '@tiptap/extension-task-item';
|
||||||
|
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
|
||||||
|
|
||||||
|
export const TaskItem = BuiltInTaskItem.extend({
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
nested: true,
|
||||||
|
HTMLAttributes: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
checked: {
|
||||||
|
default: false,
|
||||||
|
parseHTML: (element) => {
|
||||||
|
const checkbox = element.querySelector('input[type=checkbox].task-list-item-checkbox');
|
||||||
|
// @ts-ignore
|
||||||
|
return checkbox?.checked;
|
||||||
|
},
|
||||||
|
renderHTML: (attributes) => ({
|
||||||
|
'data-checked': attributes.checked,
|
||||||
|
}),
|
||||||
|
keepOnSplit: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: 'li.task-list-item',
|
||||||
|
priority: PARSE_HTML_PRIORITY_HIGHEST,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { mergeAttributes } from '@tiptap/core';
|
||||||
|
import { TaskList as BuiltInTaskList } from '@tiptap/extension-task-list';
|
||||||
|
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
|
||||||
|
import { getMarkdownSource } from '../services/markdownSourceMap';
|
||||||
|
|
||||||
|
export const TaskList = BuiltInTaskList.extend({
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
numeric: {
|
||||||
|
default: false,
|
||||||
|
parseHTML: (element) => element.tagName.toLowerCase() === 'ol',
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
default: 1,
|
||||||
|
parseHTML: (element) =>
|
||||||
|
element.hasAttribute('start') ? parseInt(element.getAttribute('start') || '', 10) : 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
parens: {
|
||||||
|
default: false,
|
||||||
|
parseHTML: (element) => /^[0-9]+\)/.test(getMarkdownSource(element)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: '.task-list',
|
||||||
|
priority: PARSE_HTML_PRIORITY_HIGHEST,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes: { numeric, ...HTMLAttributes } }) {
|
||||||
|
return [numeric ? 'ol' : 'ul', mergeAttributes(HTMLAttributes, { 'data-type': 'taskList' }), 0];
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1 @@
|
||||||
|
export { Text } from '@tiptap/extension-text';
|
|
@ -0,0 +1,3 @@
|
||||||
|
import TextAlign from '@tiptap/extension-text-align';
|
||||||
|
|
||||||
|
export { TextAlign };
|
|
@ -0,0 +1,3 @@
|
||||||
|
import TextStyle from '@tiptap/extension-text-style';
|
||||||
|
|
||||||
|
export { TextStyle };
|
|
@ -1,7 +1,6 @@
|
||||||
import { Node, mergeAttributes } from '@tiptap/core';
|
import { Node, mergeAttributes } from '@tiptap/core';
|
||||||
import Document from '@tiptap/extension-document';
|
|
||||||
|
|
||||||
const Title = Node.create({
|
export const Title = Node.create({
|
||||||
name: 'title',
|
name: 'title',
|
||||||
group: 'block',
|
group: 'block',
|
||||||
content: 'text*',
|
content: 'text*',
|
||||||
|
@ -26,9 +25,3 @@ const Title = Node.create({
|
||||||
return ['h1', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
return ['h1', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const TitledDocument = Document.extend({
|
|
||||||
content: 'title block+',
|
|
||||||
});
|
|
||||||
|
|
||||||
export { Document, Title, TitledDocument };
|
|
||||||
|
|
|
@ -1,107 +0,0 @@
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
|
||||||
import { Command, Node, mergeAttributes } from '@tiptap/core';
|
|
||||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
|
||||||
interface Commands {
|
|
||||||
tableOfContents: {
|
|
||||||
setToc: () => Command;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const Component = ({ editor }) => {
|
|
||||||
const [items, setItems] = useState([]);
|
|
||||||
|
|
||||||
const handleUpdate = useCallback(() => {
|
|
||||||
const headings = [];
|
|
||||||
const transaction = editor.state.tr;
|
|
||||||
|
|
||||||
editor.state.doc.descendants((node, pos) => {
|
|
||||||
if (node.type.name === 'heading') {
|
|
||||||
const id = `heading-${headings.length + 1}`;
|
|
||||||
|
|
||||||
if (node.attrs.id !== id) {
|
|
||||||
transaction.setNodeMarkup(pos, undefined, {
|
|
||||||
...node.attrs,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
headings.push({
|
|
||||||
level: node.attrs.level,
|
|
||||||
text: node.textContent,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
transaction.setMeta('addToHistory', false);
|
|
||||||
transaction.setMeta('preventUpdate', true);
|
|
||||||
|
|
||||||
editor.view.dispatch(transaction);
|
|
||||||
|
|
||||||
setItems(headings);
|
|
||||||
}, [editor]);
|
|
||||||
|
|
||||||
useEffect(handleUpdate, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!editor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.on('update', handleUpdate);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
editor.off('update', handleUpdate);
|
|
||||||
};
|
|
||||||
}, [editor]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Toc = Node.create({
|
|
||||||
name: 'tableOfContents',
|
|
||||||
group: 'block',
|
|
||||||
atom: true,
|
|
||||||
|
|
||||||
parseHTML() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
tag: 'toc',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
|
||||||
return ['toc', mergeAttributes(HTMLAttributes)];
|
|
||||||
},
|
|
||||||
|
|
||||||
addNodeView() {
|
|
||||||
return ReactNodeViewRenderer(Component);
|
|
||||||
},
|
|
||||||
|
|
||||||
addGlobalAttributes() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
types: ['heading'],
|
|
||||||
attributes: {
|
|
||||||
id: {
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
addCommands() {
|
|
||||||
return {
|
|
||||||
setToc:
|
|
||||||
() =>
|
|
||||||
({ chain }) => {
|
|
||||||
return chain().insertContent({ type: this.name }).run();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
import Underline from '@tiptap/extension-underline';
|
||||||
|
|
||||||
|
export { Underline };
|
|
@ -1,17 +1,22 @@
|
||||||
import { BaseExtension, Document, TitledDocument as DocumentWithTitle } from './base-kit';
|
|
||||||
import { HocuspocusProvider } from '@hocuspocus/provider';
|
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||||
import Collaboration from '@tiptap/extension-collaboration';
|
import Collaboration from '@tiptap/extension-collaboration';
|
||||||
import CollaborationCursor from '@tiptap/extension-collaboration-cursor';
|
import CollaborationCursor from '@tiptap/extension-collaboration-cursor';
|
||||||
import { getRandomColor } from 'helpers/color';
|
import { getRandomColor } from 'helpers/color';
|
||||||
|
import { Document } from './extensions/document';
|
||||||
|
import { BaseKit } from './basekit';
|
||||||
|
|
||||||
export { getSchema } from '@tiptap/core';
|
export { getSchema } from '@tiptap/core';
|
||||||
export * from './menubar';
|
export * from './menubar';
|
||||||
export * from './provider';
|
export * from './provider';
|
||||||
export * from './skeleton';
|
export * from './skeleton';
|
||||||
export * from './toc';
|
|
||||||
|
|
||||||
export { Document, DocumentWithTitle };
|
export const DocumentWithTitle = Document.extend({
|
||||||
export const DEFAULT_EXTENSION = [...BaseExtension];
|
content: 'title block+',
|
||||||
|
});
|
||||||
|
|
||||||
|
export { Document };
|
||||||
|
export const DEFAULT_EXTENSION = [...BaseKit];
|
||||||
|
|
||||||
export const getCollaborationExtension = (provider: HocuspocusProvider) => {
|
export const getCollaborationExtension = (provider: HocuspocusProvider) => {
|
||||||
return Collaboration.configure({
|
return Collaboration.configure({
|
||||||
document: provider.document,
|
document: provider.document,
|
|
@ -1 +0,0 @@
|
||||||
export * from './serializer';
|
|
|
@ -1,246 +0,0 @@
|
||||||
import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model';
|
|
||||||
import { marked } from 'marked';
|
|
||||||
import { sanitize } from 'dompurify';
|
|
||||||
import {
|
|
||||||
MarkdownSerializer as ProseMirrorMarkdownSerializer,
|
|
||||||
defaultMarkdownSerializer,
|
|
||||||
} from 'prosemirror-markdown';
|
|
||||||
import { Document, TitledDocument, Title } from '../extensions/title';
|
|
||||||
import Placeholder from '@tiptap/extension-placeholder';
|
|
||||||
import Paragraph from '@tiptap/extension-paragraph';
|
|
||||||
import Text from '@tiptap/extension-text';
|
|
||||||
import Strike from '@tiptap/extension-strike';
|
|
||||||
import Underline from '@tiptap/extension-underline';
|
|
||||||
import TextStyle from '@tiptap/extension-text-style';
|
|
||||||
import { Color } from '@tiptap/extension-color';
|
|
||||||
import Blockquote from '@tiptap/extension-blockquote';
|
|
||||||
import Bold from '@tiptap/extension-bold';
|
|
||||||
import Code from '@tiptap/extension-code';
|
|
||||||
import Highlight from '@tiptap/extension-highlight';
|
|
||||||
import TextAlign from '@tiptap/extension-text-align';
|
|
||||||
import Dropcursor from '@tiptap/extension-dropcursor';
|
|
||||||
import Gapcursor from '@tiptap/extension-gapcursor';
|
|
||||||
import HardBreak from '@tiptap/extension-hard-break';
|
|
||||||
import Heading from '@tiptap/extension-heading';
|
|
||||||
import Italic from '@tiptap/extension-italic';
|
|
||||||
import OrderedList from '@tiptap/extension-ordered-list';
|
|
||||||
import BulletList from '@tiptap/extension-bullet-list';
|
|
||||||
import ListItem from '@tiptap/extension-list-item';
|
|
||||||
import TaskList from '@tiptap/extension-task-list';
|
|
||||||
import TaskItem from '@tiptap/extension-task-item';
|
|
||||||
import { BackgroundColor } from '../extensions/background-color';
|
|
||||||
import { Link } from '../extensions/link';
|
|
||||||
import { FontSize } from '../extensions/font-size';
|
|
||||||
import { ColorHighlighter } from '../extensions/color-highlight';
|
|
||||||
import { Indent } from '../extensions/indent';
|
|
||||||
import { Div } from '../extensions/div';
|
|
||||||
import { Banner } from '../extensions/banner';
|
|
||||||
import { CodeBlock } from '../extensions/code-block';
|
|
||||||
import { Iframe } from '../extensions/iframe';
|
|
||||||
import { Mind } from '../extensions/mind';
|
|
||||||
import { Katex } from '../extensions/katex';
|
|
||||||
import { Image } from '../extensions/image';
|
|
||||||
import { HorizontalRule } from '../extensions/horizontal-rule';
|
|
||||||
import { Table, TableCell, TableHeader, TableRow } from '../extensions/table';
|
|
||||||
import { DocumentChildren } from '../extensions/documents/children';
|
|
||||||
|
|
||||||
import {
|
|
||||||
isPlainURL,
|
|
||||||
renderHardBreak,
|
|
||||||
renderTable,
|
|
||||||
renderTableCell,
|
|
||||||
renderTableRow,
|
|
||||||
openTag,
|
|
||||||
closeTag,
|
|
||||||
renderOrderedList,
|
|
||||||
renderImage,
|
|
||||||
renderPlayable,
|
|
||||||
renderHTMLNode,
|
|
||||||
renderContent,
|
|
||||||
} from './helpers';
|
|
||||||
|
|
||||||
const defaultSerializerConfig = {
|
|
||||||
marks: {
|
|
||||||
[Bold.name]: defaultMarkdownSerializer.marks.strong,
|
|
||||||
[Italic.name]: {
|
|
||||||
open: '_',
|
|
||||||
close: '_',
|
|
||||||
mixable: true,
|
|
||||||
expelEnclosingWhitespace: true,
|
|
||||||
},
|
|
||||||
[Code.name]: defaultMarkdownSerializer.marks.code,
|
|
||||||
// [Subscript.name]: { open: "<sub>", close: "</sub>", mixable: true },
|
|
||||||
// [Superscript.name]: { open: "<sup>", close: "</sup>", mixable: true },
|
|
||||||
// [InlineDiff.name]: {
|
|
||||||
// mixable: true,
|
|
||||||
// open(state, mark) {
|
|
||||||
// return mark.attrs.type === "addition" ? "{+" : "{-";
|
|
||||||
// },
|
|
||||||
// close(state, mark) {
|
|
||||||
// return mark.attrs.type === "addition" ? "+}" : "-}";
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
[Link.name]: {
|
|
||||||
open(state, mark, parent, index) {
|
|
||||||
return isPlainURL(mark, parent, index, 1) ? '<' : '[';
|
|
||||||
},
|
|
||||||
close(state, mark, parent, index) {
|
|
||||||
const href = mark.attrs.canonicalSrc || mark.attrs.href;
|
|
||||||
|
|
||||||
return isPlainURL(mark, parent, index, -1)
|
|
||||||
? '>'
|
|
||||||
: `](${state.esc(href)}${mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ''})`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[Strike.name]: {
|
|
||||||
open: '~~',
|
|
||||||
close: '~~',
|
|
||||||
mixable: true,
|
|
||||||
expelEnclosingWhitespace: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
nodes: {
|
|
||||||
// [Audio.name]: renderPlayable,
|
|
||||||
[Blockquote.name]: (state, node) => {
|
|
||||||
if (node.attrs.multiline) {
|
|
||||||
state.write('>>>');
|
|
||||||
state.ensureNewLine();
|
|
||||||
state.renderContent(node);
|
|
||||||
state.ensureNewLine();
|
|
||||||
state.write('>>>');
|
|
||||||
state.closeBlock(node);
|
|
||||||
} else {
|
|
||||||
state.wrapBlock('> ', null, node, () => state.renderContent(node));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[BulletList.name]: defaultMarkdownSerializer.nodes.bullet_list,
|
|
||||||
[CodeBlock.name]: (state, node) => {
|
|
||||||
state.write(`\`\`\`${node.attrs.language || ''}\n`);
|
|
||||||
state.text(node.textContent, false);
|
|
||||||
state.ensureNewLine();
|
|
||||||
state.write('```');
|
|
||||||
state.closeBlock(node);
|
|
||||||
},
|
|
||||||
[Katex.name]: (state, node) => {
|
|
||||||
state.ensureNewLine();
|
|
||||||
state.write(`\$\$${node.attrs.text || ''}\$`);
|
|
||||||
state.closeBlock(node);
|
|
||||||
},
|
|
||||||
[DocumentChildren.name]: (state, node) => {
|
|
||||||
state.ensureNewLine();
|
|
||||||
state.write(`documentChildren$`);
|
|
||||||
state.closeBlock(node);
|
|
||||||
},
|
|
||||||
[Mind.name]: (state, node) => {
|
|
||||||
state.ensureNewLine();
|
|
||||||
state.write(`mind$`);
|
|
||||||
state.closeBlock(node);
|
|
||||||
},
|
|
||||||
// [DescriptionList.name]: renderHTMLNode("dl", true),
|
|
||||||
// [DescriptionItem.name]: (state, node, parent, index) => {
|
|
||||||
// if (index === 1) state.ensureNewLine();
|
|
||||||
// renderHTMLNode(node.attrs.isTerm ? "dt" : "dd")(state, node);
|
|
||||||
// if (index === parent.childCount - 1) state.ensureNewLine();
|
|
||||||
// },
|
|
||||||
// [Details.name]: renderHTMLNode("details", true),
|
|
||||||
// [DetailsContent.name]: (state, node, parent, index) => {
|
|
||||||
// if (!index) renderHTMLNode("summary")(state, node);
|
|
||||||
// else {
|
|
||||||
// if (index === 1) state.ensureNewLine();
|
|
||||||
// renderContent(state, node);
|
|
||||||
// if (index === parent.childCount - 1) state.ensureNewLine();
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// [Emoji.name]: (state, node) => {
|
|
||||||
// const { name } = node.attrs;
|
|
||||||
|
|
||||||
// state.write(`:${name}:`);
|
|
||||||
// },
|
|
||||||
// [FootnoteDefinition.name]: (state, node) => {
|
|
||||||
// state.renderInline(node);
|
|
||||||
// },
|
|
||||||
// [FootnoteReference.name]: (state, node) => {
|
|
||||||
// state.write(`[^${node.attrs.footnoteNumber}]`);
|
|
||||||
// },
|
|
||||||
// [FootnotesSection.name]: (state, node) => {
|
|
||||||
// state.renderList(node, "", (index) => `[^${index + 1}]: `);
|
|
||||||
// },
|
|
||||||
// [Frontmatter.name]: (state, node) => {
|
|
||||||
// const { language } = node.attrs;
|
|
||||||
// const syntax = {
|
|
||||||
// toml: "+++",
|
|
||||||
// json: ";;;",
|
|
||||||
// yaml: "---",
|
|
||||||
// }[language];
|
|
||||||
|
|
||||||
// state.write(`${syntax}\n`);
|
|
||||||
// state.text(node.textContent, false);
|
|
||||||
// state.ensureNewLine();
|
|
||||||
// state.write(syntax);
|
|
||||||
// state.closeBlock(node);
|
|
||||||
// },
|
|
||||||
[Title.name]: renderHTMLNode('div', true, true),
|
|
||||||
// [FigureCaption.name]: renderHTMLNode("figcaption"),
|
|
||||||
[HardBreak.name]: renderHardBreak,
|
|
||||||
[Heading.name]: defaultMarkdownSerializer.nodes.heading,
|
|
||||||
[HorizontalRule.name]: defaultMarkdownSerializer.nodes.horizontal_rule,
|
|
||||||
[Image.name]: renderImage,
|
|
||||||
[ListItem.name]: defaultMarkdownSerializer.nodes.list_item,
|
|
||||||
[OrderedList.name]: renderOrderedList,
|
|
||||||
[Paragraph.name]: defaultMarkdownSerializer.nodes.paragraph,
|
|
||||||
// [Reference.name]: (state, node) => {
|
|
||||||
// state.write(node.attrs.originalText || node.attrs.text);
|
|
||||||
// },
|
|
||||||
// [TableOfContents.name]: (state, node) => {
|
|
||||||
// state.write("[[_TOC_]]");
|
|
||||||
// state.closeBlock(node);
|
|
||||||
// },
|
|
||||||
[Table.name]: renderTable,
|
|
||||||
[TableCell.name]: renderTableCell,
|
|
||||||
[TableHeader.name]: renderTableCell,
|
|
||||||
[TableRow.name]: renderTableRow,
|
|
||||||
[TaskItem.name]: (state, node) => {
|
|
||||||
state.write(`[${node.attrs.checked ? 'x' : ' '}] `);
|
|
||||||
state.renderContent(node);
|
|
||||||
},
|
|
||||||
[TaskList.name]: (state, node) => {
|
|
||||||
if (node.attrs.numeric) renderOrderedList(state, node);
|
|
||||||
else defaultMarkdownSerializer.nodes.bullet_list(state, node);
|
|
||||||
},
|
|
||||||
[Text.name]: defaultMarkdownSerializer.nodes.text,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderMarkdown = (rawMarkdown) => {
|
|
||||||
return sanitize(marked(rawMarkdown), {});
|
|
||||||
};
|
|
||||||
|
|
||||||
const createMarkdownSerializer = () => ({
|
|
||||||
deserialize: ({ schema, content }) => {
|
|
||||||
const html = renderMarkdown(content);
|
|
||||||
if (!html) return null;
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const { body } = parser.parseFromString(html, 'text/html');
|
|
||||||
body.append(document.createComment(content));
|
|
||||||
const state = ProseMirrorDOMParser.fromSchema(schema).parse(body);
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
|
|
||||||
serialize: ({ schema, document }) => {
|
|
||||||
// const proseMirrorDocument = schema.nodeFromJSON(content);
|
|
||||||
const serializer = new ProseMirrorMarkdownSerializer(
|
|
||||||
{
|
|
||||||
...defaultSerializerConfig.nodes,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...defaultSerializerConfig.marks,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return serializer.serialize(document, {
|
|
||||||
tightLists: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const markdownSerializer = createMarkdownSerializer();
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
IconAlignRight,
|
IconAlignRight,
|
||||||
IconAlignJustify,
|
IconAlignJustify,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import { isTitleActive } from '../utils/active';
|
import { isTitleActive } from './utils/active';
|
||||||
|
|
||||||
export const AlignMenu = ({ editor }) => {
|
export const AlignMenu = ({ editor }) => {
|
||||||
const current = (() => {
|
const current = (() => {
|
||||||
|
|
|
@ -6,10 +6,10 @@ import {
|
||||||
IconClear,
|
IconClear,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import { BubbleMenu } from '../components/bubble-menu';
|
import { BubbleMenu } from './components/bubble-menu';
|
||||||
import { Divider } from '../components/divider';
|
import { Divider } from '../components/divider';
|
||||||
import { Banner } from '../extensions/banner';
|
import { Banner } from '../extensions/banner';
|
||||||
import { deleteNode } from '../utils/delete';
|
import { deleteNode } from './utils/delete';
|
||||||
|
|
||||||
export const BannerBubbleMenu = ({ editor }) => {
|
export const BannerBubbleMenu = ({ editor }) => {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -10,8 +10,8 @@ import { Iframe } from '../extensions/iframe';
|
||||||
import { Mind } from '../extensions/mind';
|
import { Mind } from '../extensions/mind';
|
||||||
import { Table } from '../extensions/table';
|
import { Table } from '../extensions/table';
|
||||||
import { Katex } from '../extensions/katex';
|
import { Katex } from '../extensions/katex';
|
||||||
import { DocumentReference } from '../extensions/documents/reference';
|
import { DocumentReference } from '../extensions/documentReference';
|
||||||
import { DocumentChildren } from '../extensions/documents/children';
|
import { DocumentChildren } from '../extensions/documentChildren';
|
||||||
import { BaseMenu } from './base-menu';
|
import { BaseMenu } from './base-menu';
|
||||||
|
|
||||||
const OTHER_BUBBLE_MENU_TYPES = [
|
const OTHER_BUBBLE_MENU_TYPES = [
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
||||||
import { IconQuote, IconCheckboxIndeterminate, IconLink } from '@douyinfe/semi-icons';
|
import { IconQuote, IconCheckboxIndeterminate, IconLink } from '@douyinfe/semi-icons';
|
||||||
import { isTitleActive } from '../utils/active';
|
import { isTitleActive } from './utils/active';
|
||||||
import { Emoji } from './components/emoji';
|
import { Emoji } from './components/emoji';
|
||||||
|
|
||||||
export const BaseInsertMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
export const BaseInsertMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
IconUnderline,
|
IconUnderline,
|
||||||
IconCode,
|
IconCode,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import { isTitleActive } from '../utils/active';
|
import { isTitleActive } from './utils/active';
|
||||||
import { ColorMenu } from './color';
|
import { ColorMenu } from './color';
|
||||||
|
|
||||||
export const BaseMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
export const BaseMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
||||||
import { IconFont, IconMark } from '@douyinfe/semi-icons';
|
import { IconFont, IconMark } from '@douyinfe/semi-icons';
|
||||||
import { isTitleActive } from '../utils/active';
|
import { isTitleActive } from './utils/active';
|
||||||
import { Color } from '../components/color';
|
import { Color } from './components/color';
|
||||||
|
|
||||||
export const ColorMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
export const ColorMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
const { color, backgroundColor } = editor.getAttributes('textStyle');
|
const { color, backgroundColor } = editor.getAttributes('textStyle');
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
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 '../../utils/active';
|
import { isTitleActive } from '../utils/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];
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
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 '../../utils/active';
|
import { isTitleActive } from '../utils/active';
|
||||||
|
|
||||||
const getCurrentCaretTitle = (editor) => {
|
const getCurrentCaretTitle = (editor) => {
|
||||||
if (editor.isActive('heading', { level: 1 })) return 1;
|
if (editor.isActive('heading', { level: 1 })) return 1;
|
||||||
|
|
|
@ -8,10 +8,10 @@ import {
|
||||||
IconDelete,
|
IconDelete,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import { Upload } from 'components/upload';
|
import { Upload } from 'components/upload';
|
||||||
import { BubbleMenu } from '../components/bubble-menu';
|
import { BubbleMenu } from './components/bubble-menu';
|
||||||
import { Divider } from '../components/divider';
|
import { Divider } from '../components/divider';
|
||||||
import { Image } from '../extensions/image';
|
import { Image } from '../extensions/image';
|
||||||
import { getImageOriginSize } from '../utils/image';
|
import { getImageOriginSize } from './utils/image';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Space, Button, Tooltip, Input } from '@douyinfe/semi-ui';
|
import { Space, Button, Tooltip, Input } from '@douyinfe/semi-ui';
|
||||||
import { IconExternalOpen, IconUnlink, IconTickCircle } from '@douyinfe/semi-icons';
|
import { IconExternalOpen, IconUnlink, IconTickCircle } from '@douyinfe/semi-icons';
|
||||||
import { BubbleMenu } from '../components/bubble-menu';
|
import { BubbleMenu } from './components/bubble-menu';
|
||||||
import { Link } from '../extensions/link';
|
import { Link } from '../extensions/link';
|
||||||
|
|
||||||
export const LinkBubbleMenu = ({ editor }) => {
|
export const LinkBubbleMenu = ({ editor }) => {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
||||||
import { IconList, IconOrderedList, IconIndentLeft, IconIndentRight } from '@douyinfe/semi-icons';
|
import { IconList, IconOrderedList, IconIndentLeft, IconIndentRight } from '@douyinfe/semi-icons';
|
||||||
import { IconTask } from 'components/icons';
|
import { IconTask } from 'components/icons';
|
||||||
import { isTitleActive } from '../utils/active';
|
import { isTitleActive } from './utils/active';
|
||||||
|
|
||||||
export const ListMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
export const ListMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue