mirror of https://github.com/fantasticit/think.git
feat: improve editor
parent
c8c370e0cb
commit
cc30e00984
|
@ -51,6 +51,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",
|
||||||
|
"@tiptap/suggestion": "^2.0.0-beta.90",
|
||||||
"@traptitech/markdown-it-katex": "^3.5.0",
|
"@traptitech/markdown-it-katex": "^3.5.0",
|
||||||
"axios": "^0.25.0",
|
"axios": "^0.25.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
|
@ -62,6 +63,8 @@
|
||||||
"lowlight": "^2.5.0",
|
"lowlight": "^2.5.0",
|
||||||
"markdown-it": "^12.3.2",
|
"markdown-it": "^12.3.2",
|
||||||
"markdown-it-anchor": "^8.4.1",
|
"markdown-it-anchor": "^8.4.1",
|
||||||
|
"markdown-it-container": "^3.0.0",
|
||||||
|
"markdown-it-emoji": "^2.0.0",
|
||||||
"markdown-it-footnote": "^3.0.3",
|
"markdown-it-footnote": "^3.0.3",
|
||||||
"markdown-it-sub": "^1.0.0",
|
"markdown-it-sub": "^1.0.0",
|
||||||
"markdown-it-sup": "^1.0.0",
|
"markdown-it-sup": "^1.0.0",
|
||||||
|
@ -69,11 +72,13 @@
|
||||||
"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",
|
||||||
|
"prosemirror-utils": "^0.9.6",
|
||||||
"prosemirror-view": "^1.23.6",
|
"prosemirror-view": "^1.23.6",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-split-pane": "^0.1.92",
|
"react-split-pane": "^0.1.92",
|
||||||
|
"scroll-into-view-if-needed": "^2.2.29",
|
||||||
"swr": "^1.2.0",
|
"swr": "^1.2.0",
|
||||||
"tippy.js": "^6.3.7"
|
"tippy.js": "^6.3.7"
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,12 +13,12 @@ export const DocumentContent: React.FC<IProps> = ({ document }) => {
|
||||||
const c = safeJSONParse(document.content);
|
const c = safeJSONParse(document.content);
|
||||||
let json = c.default || c;
|
let json = c.default || c;
|
||||||
|
|
||||||
if (json && json.content) {
|
// if (json && json.content) {
|
||||||
json = {
|
// json = {
|
||||||
type: 'doc',
|
// type: 'doc',
|
||||||
content: json.content.slice(1),
|
// content: json.content.slice(1),
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
editable: false,
|
editable: false,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useMemo, useEffect } from 'react';
|
import React, { useMemo, useEffect, useRef } from 'react';
|
||||||
import { useEditor, EditorContent } from '@tiptap/react';
|
import { useEditor, EditorContent } from '@tiptap/react';
|
||||||
import { Layout } from '@douyinfe/semi-ui';
|
import { Layout } from '@douyinfe/semi-ui';
|
||||||
import { ILoginUser } from '@think/domains';
|
import { IDocument, ILoginUser } from '@think/domains';
|
||||||
import { useToggle } from 'hooks/useToggle';
|
import { useToggle } from 'hooks/useToggle';
|
||||||
import {
|
import {
|
||||||
DEFAULT_EXTENSION,
|
DEFAULT_EXTENSION,
|
||||||
|
@ -13,6 +13,7 @@ import {
|
||||||
} 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';
|
||||||
|
import { CreateUser } from './user';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
const { Content } = Layout;
|
const { Content } = Layout;
|
||||||
|
@ -20,11 +21,13 @@ const { Content } = Layout;
|
||||||
interface IProps {
|
interface IProps {
|
||||||
user: ILoginUser;
|
user: ILoginUser;
|
||||||
documentId: string;
|
documentId: string;
|
||||||
|
document: IDocument;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Editor: React.FC<IProps> = ({ user, documentId }) => {
|
export const Editor: React.FC<IProps> = ({ user, documentId, document }) => {
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
|
const $ref = useRef();
|
||||||
const provider = useMemo(() => {
|
const provider = useMemo(() => {
|
||||||
return getProvider({
|
return getProvider({
|
||||||
targetId: documentId,
|
targetId: documentId,
|
||||||
|
@ -68,7 +71,15 @@ export const Editor: React.FC<IProps> = ({ user, documentId }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Content className={styles.editorWrap}>
|
<Content className={styles.editorWrap}>
|
||||||
<EditorContent editor={editor} />
|
<div id="js-reader-container">
|
||||||
|
<EditorContent editor={editor} />
|
||||||
|
</div>
|
||||||
|
<CreateUser
|
||||||
|
document={document}
|
||||||
|
container={() =>
|
||||||
|
window.document.querySelector('#js-reader-container .ProseMirror .title')
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Content>
|
</Content>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -122,10 +122,12 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Seo title={document.title} />
|
<Seo title={document.title} />
|
||||||
<Editor key={document.id} user={user} documentId={document.id} />
|
<Editor
|
||||||
<div style={{ marginBottom: 24 }}>
|
key={document.id}
|
||||||
<CreateUser document={document} />
|
user={user}
|
||||||
</div>
|
documentId={document.id}
|
||||||
|
document={document}
|
||||||
|
/>
|
||||||
<div className={styles.commentWrap}>
|
<div className={styles.commentWrap}>
|
||||||
<CommentEditor documentId={document.id} />
|
<CommentEditor documentId={document.id} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -131,11 +131,15 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
|
||||||
style={{ fontSize }}
|
style={{ fontSize }}
|
||||||
id="js-share-document-editor-container"
|
id="js-share-document-editor-container"
|
||||||
>
|
>
|
||||||
<Title style={{ fontSize: '2.4em' }}>{data.title}</Title>
|
|
||||||
<div style={{ margin: '24px 0' }}>
|
|
||||||
<CreateUser document={data} />
|
|
||||||
</div>
|
|
||||||
<DocumentContent document={data} />
|
<DocumentContent document={data} />
|
||||||
|
<CreateUser
|
||||||
|
document={data}
|
||||||
|
container={() =>
|
||||||
|
window.document.querySelector(
|
||||||
|
'#js-share-document-editor-container .ProseMirror .title'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<BackTop
|
<BackTop
|
||||||
target={() =>
|
target={() =>
|
||||||
|
|
|
@ -1,15 +1,26 @@
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { Space, Typography, Avatar } from '@douyinfe/semi-ui';
|
import { Space, Typography, Avatar } from '@douyinfe/semi-ui';
|
||||||
import { IconUser } from '@douyinfe/semi-icons';
|
import { IconUser } from '@douyinfe/semi-icons';
|
||||||
import { IDocument } from '@think/domains';
|
import { IDocument } from '@think/domains';
|
||||||
import { LocaleTime } from 'components/locale-time';
|
import { LocaleTime } from 'components/locale-time';
|
||||||
|
|
||||||
const { Text } = Typography;
|
export const CreateUser: React.FC<{ document: IDocument; container: () => HTMLElement }> = ({
|
||||||
|
document,
|
||||||
export const CreateUser: React.FC<{ document: IDocument }> = ({ document }) => {
|
container = null,
|
||||||
|
}) => {
|
||||||
if (!document.createUser) return null;
|
if (!document.createUser) return null;
|
||||||
|
|
||||||
return (
|
const content = (
|
||||||
<Text type="tertiary" size="small">
|
<div
|
||||||
|
style={{
|
||||||
|
borderTop: '1px solid var(--semi-color-border)',
|
||||||
|
marginTop: 24,
|
||||||
|
padding: '16px 0',
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 'normal',
|
||||||
|
color: 'var(--semi-color-text-0)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Space>
|
<Space>
|
||||||
<Avatar size="extra-extra-small" src={document.createUser && document.createUser.avatar}>
|
<Avatar size="extra-extra-small" src={document.createUser && document.createUser.avatar}>
|
||||||
<IconUser />
|
<IconUser />
|
||||||
|
@ -27,6 +38,11 @@ export const CreateUser: React.FC<{ document: IDocument }> = ({ document }) => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
</Text>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const el = container && container();
|
||||||
|
|
||||||
|
if (!el) return content;
|
||||||
|
return createPortal(content, el);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Icon } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
export const IconSearchReplace: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
style={style}
|
||||||
|
svg={
|
||||||
|
<svg width="16" height="16" viewBox="0 0 256 256" role="presentation">
|
||||||
|
<g fill="currentColor" fill-rule="evenodd">
|
||||||
|
<g fill-rule="nonzero">
|
||||||
|
<path d="M191.527 15c18.302 0 33.173 14.688 33.469 32.919l.004.554v65.424c0 5.523-4.477 10-10 10-5.43 0-9.848-4.327-9.996-9.72l-.004-.28V48.473c0-7.338-5.865-13.305-13.163-13.47l-.31-.003H64.473c-7.338 0-13.305 5.865-13.47 13.163l-.003.31v159.054c0 7.338 5.865 13.305 13.163 13.47l.31.003H99c5.523 0 10 4.477 10 10 0 5.43-4.327 9.848-9.72 9.996L99 241H64.473c-18.302 0-33.173-14.688-33.469-32.919l-.004-.554V48.473C31 30.17 45.688 15.3 63.919 15.004l.554-.004h127.054Z"></path>
|
||||||
|
<path d="M147.385 150.885c-17.964 17.964-17.964 47.09 0 65.054s47.09 17.964 65.054 0c17.964-17.965 17.964-47.09 0-65.054-17.965-17.964-47.09-17.964-65.054 0Zm14.142 14.142c10.154-10.154 26.616-10.154 36.77 0 10.153 10.154 10.153 26.616 0 36.77-10.154 10.153-26.616 10.153-36.77 0-10.154-10.154-10.154-26.616 0-36.77Z"></path>
|
||||||
|
<path d="M234.545 241.752c-3.839 3.84-10.023 3.904-13.941.196l-.2-.196-21.921-21.92c-3.905-3.905-3.905-10.237 0-14.142 3.839-3.84 10.023-3.904 13.941-.195l.2.195 21.921 21.92c3.905 3.905 3.905 10.237 0 14.142Z"></path>
|
||||||
|
</g>
|
||||||
|
<path d="M92 71h72c5.523 0 10 4.477 10 10s-4.477 10-10 10H92c-5.523 0-10-4.477-10-10s4.477-10 10-10ZM92 125h26c5.523 0 10 4.477 10 10s-4.477 10-10 10H92c-5.523 0-10-4.477-10-10s4.477-10 10-10Z"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -34,3 +34,4 @@ export * from './IconSplitCell';
|
||||||
export * from './IconAttachment';
|
export * from './IconAttachment';
|
||||||
export * from './IconMath';
|
export * from './IconMath';
|
||||||
export * from './IconSearch';
|
export * from './IconSearch';
|
||||||
|
export * from './IconSearchReplace';
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { ColorHighlighter } from './extensions/colorHighlighter';
|
||||||
import { DocumentChildren } from './extensions/documentChildren';
|
import { DocumentChildren } from './extensions/documentChildren';
|
||||||
import { DocumentReference } from './extensions/documentReference';
|
import { DocumentReference } from './extensions/documentReference';
|
||||||
import { Dropcursor } from './extensions/dropCursor';
|
import { Dropcursor } from './extensions/dropCursor';
|
||||||
|
import { Emoji } from './extensions/emoji';
|
||||||
import { FontSize } from './extensions/fontSize';
|
import { FontSize } from './extensions/fontSize';
|
||||||
import { FootnoteDefinition } from './extensions/footnoteDefinition';
|
import { FootnoteDefinition } from './extensions/footnoteDefinition';
|
||||||
import { FootnoteReference } from './extensions/footnoteReference';
|
import { FootnoteReference } from './extensions/footnoteReference';
|
||||||
|
@ -33,6 +34,7 @@ import { Paragraph } from './extensions/paragraph';
|
||||||
import { PasteFile } from './extensions/pasteFile';
|
import { PasteFile } from './extensions/pasteFile';
|
||||||
import { PasteMarkdown } from './extensions/pasteMarkdown';
|
import { PasteMarkdown } from './extensions/pasteMarkdown';
|
||||||
import { Placeholder } from './extensions/placeholder';
|
import { Placeholder } from './extensions/placeholder';
|
||||||
|
import { SearchNReplace } from './extensions/search';
|
||||||
import { Status } from './extensions/status';
|
import { Status } from './extensions/status';
|
||||||
import { Strike } from './extensions/strike';
|
import { Strike } from './extensions/strike';
|
||||||
import { Table } from './extensions/table';
|
import { Table } from './extensions/table';
|
||||||
|
@ -62,6 +64,7 @@ export const BaseKit = [
|
||||||
DocumentChildren,
|
DocumentChildren,
|
||||||
DocumentReference,
|
DocumentReference,
|
||||||
Dropcursor,
|
Dropcursor,
|
||||||
|
Emoji,
|
||||||
FontSize,
|
FontSize,
|
||||||
FootnoteDefinition,
|
FootnoteDefinition,
|
||||||
FootnoteReference,
|
FootnoteReference,
|
||||||
|
@ -83,15 +86,8 @@ export const BaseKit = [
|
||||||
Paragraph,
|
Paragraph,
|
||||||
PasteFile,
|
PasteFile,
|
||||||
PasteMarkdown,
|
PasteMarkdown,
|
||||||
Placeholder.configure({
|
Placeholder,
|
||||||
placeholder: ({ node }) => {
|
SearchNReplace,
|
||||||
if (node.type.name === 'title') {
|
|
||||||
return '请输入标题';
|
|
||||||
}
|
|
||||||
return '请输入内容';
|
|
||||||
},
|
|
||||||
showOnlyWhenEditable: true,
|
|
||||||
}),
|
|
||||||
Status,
|
Status,
|
||||||
Strike,
|
Strike,
|
||||||
Table,
|
Table,
|
||||||
|
@ -99,9 +95,7 @@ export const BaseKit = [
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
Text,
|
Text,
|
||||||
TextAlign.configure({
|
TextAlign,
|
||||||
types: ['heading', 'paragraph', 'image'],
|
|
||||||
}),
|
|
||||||
TextStyle,
|
TextStyle,
|
||||||
TaskItem,
|
TaskItem,
|
||||||
TaskList,
|
TaskList,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
||||||
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconDownload } from '@douyinfe/semi-icons';
|
import { IconDownload } from '@douyinfe/semi-icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { download } from '../../services/download';
|
import { download } from '../../services/download';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
@ -12,7 +13,7 @@ export const AttachmentWrapper = ({ node }) => {
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<span>{name}</span>
|
<span>{name}</span>
|
||||||
<span>
|
<span>
|
||||||
<Tooltip zIndex={10000} content="下载">
|
<Tooltip content="下载">
|
||||||
<Button
|
<Button
|
||||||
theme={'borderless'}
|
theme={'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
||||||
import { Button, Select, Tooltip } from '@douyinfe/semi-ui';
|
import { Button, Select, Tooltip } from '@douyinfe/semi-ui';
|
||||||
import { IconCopy } from '@douyinfe/semi-icons';
|
import { IconCopy } from '@douyinfe/semi-icons';
|
||||||
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
|
|
||||||
// @ts-ignore
|
|
||||||
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';
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,38 @@
|
||||||
|
.items {
|
||||||
|
max-height: 50vh;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0.2rem;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--semi-color-text-0);
|
||||||
|
border-radius: var(--semi-border-radius-medium);
|
||||||
|
background-color: var(--semi-color-bg-0);
|
||||||
|
border: 1px solid var(--semi-color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--semi-color-info);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-selected {
|
||||||
|
border-color: var(--semi-color-info);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
import React, { useState, useEffect, forwardRef, useImperativeHandle, useRef } from 'react';
|
||||||
|
import cls from 'classnames';
|
||||||
|
import scrollIntoView from 'scroll-into-view-if-needed';
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
items: any[];
|
||||||
|
command: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmojiList: React.FC<IProps> = forwardRef((props, ref) => {
|
||||||
|
const $container = useRef<HTMLDivElement>();
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
|
||||||
|
const selectItem = (index) => {
|
||||||
|
const item = props.items[index];
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
props.command(item);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const upHandler = () => {
|
||||||
|
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
const downHandler = () => {
|
||||||
|
setSelectedIndex((selectedIndex + 1) % props.items.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
const enterHandler = () => {
|
||||||
|
selectItem(selectedIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => setSelectedIndex(0), [props.items]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = $container.current.querySelector(`button:nth-of-type(${selectedIndex + 1})`);
|
||||||
|
el && scrollIntoView(el, { behavior: 'smooth', scrollMode: 'if-needed' });
|
||||||
|
}, [selectedIndex]);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
onKeyDown: ({ event }) => {
|
||||||
|
if (event.key === 'ArrowUp') {
|
||||||
|
upHandler();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowDown') {
|
||||||
|
downHandler();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
enterHandler();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.items}>
|
||||||
|
<div ref={$container}>
|
||||||
|
{props.items.map((item, index) => (
|
||||||
|
<button
|
||||||
|
className={cls(styles.item, index === selectedIndex ? styles['is-selected'] : '')}
|
||||||
|
key={index}
|
||||||
|
onClick={() => selectItem(index)}
|
||||||
|
>
|
||||||
|
{item.fallbackImage ? <img src={item.fallbackImage} /> : item.emoji}:{item.name}:
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
|
@ -21,7 +21,7 @@ export const KatexWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
const content = text ? (
|
const content = text ? (
|
||||||
<span contentEditable={false} dangerouslySetInnerHTML={{ __html: formatText }}></span>
|
<span contentEditable={false} dangerouslySetInnerHTML={{ __html: formatText }}></span>
|
||||||
) : (
|
) : (
|
||||||
<span contentEditable={false}>请输入公式</span>
|
<span contentEditable={false}>点击输入公式</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -32,7 +32,7 @@ export const KatexWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
content={
|
content={
|
||||||
<div style={{ width: 320 }}>
|
<div style={{ width: 320 }}>
|
||||||
<TextArea
|
<TextArea
|
||||||
autofocus
|
autoFocus
|
||||||
placeholder="输入公式"
|
placeholder="输入公式"
|
||||||
autosize
|
autosize
|
||||||
rows={3}
|
rows={3}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { Space, Button, Tooltip } from '@douyinfe/semi-ui';
|
import { Space, Button } 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 { Tooltip } from 'components/tooltip';
|
||||||
import { Divider } from '../divider';
|
import { Divider } from '../divider';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import styles from './index.module.scss';
|
||||||
export const StatusWrapper = ({ editor, node, updateAttributes }) => {
|
export const StatusWrapper = ({ 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>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeViewWrapper as="span" className={styles.wrap}>
|
<NodeViewWrapper as="span" className={styles.wrap}>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Node, Command, mergeAttributes } from '@tiptap/core';
|
import { Node, Command, mergeAttributes, wrappingInputRule } from '@tiptap/core';
|
||||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
import { BannerWrapper } from '../components/banner';
|
import { BannerWrapper } from '../components/banner';
|
||||||
|
import { typesAvailable } from '../services/markdown/markdownBanner';
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
declare module '@tiptap/core' {
|
||||||
interface Commands {
|
interface Commands {
|
||||||
|
@ -12,33 +13,53 @@ declare module '@tiptap/core' {
|
||||||
|
|
||||||
export const Banner = Node.create({
|
export const Banner = Node.create({
|
||||||
name: 'banner',
|
name: 'banner',
|
||||||
content: 'block*',
|
content: 'paragraph+',
|
||||||
group: 'block',
|
group: 'block',
|
||||||
defining: true,
|
defining: true,
|
||||||
draggable: true,
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
types: typesAvailable,
|
||||||
|
HTMLAttributes: {
|
||||||
|
class: 'banner',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
type: {
|
type: {
|
||||||
default: 'info',
|
default: 'info',
|
||||||
|
rendered: false,
|
||||||
|
parseHTML: (element) => element.getAttribute('data-banner'),
|
||||||
|
renderHTML: (attributes) => {
|
||||||
|
return {
|
||||||
|
'data-banner': attributes.type,
|
||||||
|
'class': `banner banner-${attributes.type}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [{ tag: 'div' }];
|
return [
|
||||||
|
{
|
||||||
|
tag: 'div',
|
||||||
|
},
|
||||||
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
renderHTML({ node, HTMLAttributes }) {
|
||||||
return [
|
const { class: classy } = this.options.HTMLAttributes;
|
||||||
'div',
|
|
||||||
{ class: 'banner' },
|
const attributes = {
|
||||||
[
|
...this.options.HTMLAttributes,
|
||||||
'div',
|
'data-callout': node.attrs.type,
|
||||||
mergeAttributes((this.options && this.options.HTMLAttributes) || {}, HTMLAttributes),
|
'class': `${classy} ${classy}-${node.attrs.type}`,
|
||||||
0,
|
};
|
||||||
],
|
|
||||||
];
|
return ['div', mergeAttributes(attributes, HTMLAttributes), 0];
|
||||||
},
|
},
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -57,6 +78,18 @@ export const Banner = Node.create({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addInputRules() {
|
||||||
|
return [
|
||||||
|
wrappingInputRule({
|
||||||
|
find: /^:::([\dA-Za-z]*) $/,
|
||||||
|
type: this.type,
|
||||||
|
getAttributes: (match) => {
|
||||||
|
return { type: match[1] };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
addNodeView() {
|
addNodeView() {
|
||||||
return ReactNodeViewRenderer(BannerWrapper);
|
return ReactNodeViewRenderer(BannerWrapper);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Blockquote as BuiltInBlockquote } from '@tiptap/extension-blockquote';
|
import { Blockquote as BuiltInBlockquote } from '@tiptap/extension-blockquote';
|
||||||
import { wrappingInputRule } from '@tiptap/core';
|
import { wrappingInputRule } from '@tiptap/core';
|
||||||
import { getParents } from '../services/dom';
|
import { getParents } from '../services/dom';
|
||||||
import { getMarkdownSource } from '../services/markdownSourceMap';
|
import { getMarkdownSource } from '../services/markdown/markdownSourceMap';
|
||||||
|
|
||||||
export const Blockquote = BuiltInBlockquote.extend({
|
export const Blockquote = BuiltInBlockquote.extend({
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { BulletList as BuiltInBulletList } from '@tiptap/extension-bullet-list';
|
import { BulletList as BuiltInBulletList } from '@tiptap/extension-bullet-list';
|
||||||
import { getMarkdownSource } from '../services/markdownSourceMap';
|
import { getMarkdownSource } from '../services/markdown/markdownSourceMap';
|
||||||
|
import { listInputRule } from '../services/listInputRule';
|
||||||
|
|
||||||
export const BulletList = BuiltInBulletList.extend({
|
export const BulletList = BuiltInBulletList.extend({
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
|
@ -16,4 +17,8 @@ export const BulletList = BuiltInBulletList.extend({
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addInputRules() {
|
||||||
|
return [listInputRule(/^\s*([-+*])\s([^\s[])$/, this.type)];
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { Node } from '@tiptap/core';
|
||||||
|
import { ReactRenderer } from '@tiptap/react';
|
||||||
|
import { PluginKey } from 'prosemirror-state';
|
||||||
|
import Suggestion from '@tiptap/suggestion';
|
||||||
|
import tippy from 'tippy.js';
|
||||||
|
import { EmojiList } from '../components/emojiList';
|
||||||
|
import { emojiSearch, emojisToName } from '../components/emojiList/emojis';
|
||||||
|
|
||||||
|
export const EmojiPluginKey = new PluginKey('emoji');
|
||||||
|
export { emojisToName };
|
||||||
|
export const Emoji = Node.create({
|
||||||
|
name: 'emoji',
|
||||||
|
content: 'text*',
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
HTMLAttributes: {},
|
||||||
|
suggestion: {
|
||||||
|
char: ':',
|
||||||
|
pluginKey: EmojiPluginKey,
|
||||||
|
command: ({ editor, range, props }) => {
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.insertContentAt(range, props.emoji + ' ')
|
||||||
|
.run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
emoji:
|
||||||
|
(emojiObject) =>
|
||||||
|
({ commands }) => {
|
||||||
|
return commands.insertContent(emojiObject.emoji + ' ');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return [
|
||||||
|
Suggestion({
|
||||||
|
editor: this.editor,
|
||||||
|
...this.options.suggestion,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
}).configure({
|
||||||
|
suggestion: {
|
||||||
|
items: ({ query }) => {
|
||||||
|
return emojiSearch(query);
|
||||||
|
},
|
||||||
|
render: () => {
|
||||||
|
let component;
|
||||||
|
let popup;
|
||||||
|
|
||||||
|
return {
|
||||||
|
onStart: (props) => {
|
||||||
|
component = new ReactRenderer(EmojiList, {
|
||||||
|
props,
|
||||||
|
editor: props.editor,
|
||||||
|
});
|
||||||
|
|
||||||
|
popup = tippy('body', {
|
||||||
|
getReferenceClientRect: props.clientRect,
|
||||||
|
appendTo: () => document.body,
|
||||||
|
content: component.element,
|
||||||
|
showOnCreate: true,
|
||||||
|
interactive: true,
|
||||||
|
trigger: 'manual',
|
||||||
|
placement: 'bottom-start',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onUpdate(props) {
|
||||||
|
component.updateProps(props);
|
||||||
|
popup[0].setProps({
|
||||||
|
getReferenceClientRect: props.clientRect,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onKeyDown(props) {
|
||||||
|
if (props.event.key === 'Escape') {
|
||||||
|
popup[0].hide();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return component.ref?.onKeyDown(props);
|
||||||
|
},
|
||||||
|
|
||||||
|
onExit() {
|
||||||
|
popup[0].destroy();
|
||||||
|
component.destroy();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,7 +1,7 @@
|
||||||
import { Command, Extension } from '@tiptap/core';
|
import { Command, Extension } from '@tiptap/core';
|
||||||
import { sinkListItem, liftListItem } from 'prosemirror-schema-list';
|
import { sinkListItem, liftListItem } from 'prosemirror-schema-list';
|
||||||
import { TextSelection, AllSelection, Transaction } from 'prosemirror-state';
|
import { TextSelection, AllSelection, Transaction } from 'prosemirror-state';
|
||||||
import { isListActive } from '../services/active';
|
import { isListActive } from '../services/isActive';
|
||||||
import { clamp } from '../services/clamp';
|
import { clamp } from '../services/clamp';
|
||||||
import { getNodeType } from '../services/type';
|
import { getNodeType } from '../services/type';
|
||||||
import { isListNode } from '../services/node';
|
import { isListNode } from '../services/node';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Node, Command, mergeAttributes, wrappingInputRule } from '@tiptap/core';
|
import { Node, Command, mergeAttributes, nodeInputRule } from '@tiptap/core';
|
||||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
import { KatexWrapper } from '../components/katex';
|
import { KatexWrapper } from '../components/katex';
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ export const Katex = Node.create({
|
||||||
|
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return [
|
return [
|
||||||
wrappingInputRule({
|
nodeInputRule({
|
||||||
find: KatexInputRegex,
|
find: KatexInputRegex,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
getAttributes: (match) => {
|
getAttributes: (match) => {
|
||||||
|
|
|
@ -59,4 +59,8 @@ export const Link = BuiltInLink.extend({
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
}).configure({
|
||||||
|
openOnClick: false,
|
||||||
|
linkOnPaste: true,
|
||||||
|
autolink: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Node, mergeAttributes } from '@tiptap/core';
|
import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core';
|
||||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
import { MindWrapper } from '../components/mind';
|
import { MindWrapper } from '../components/mind';
|
||||||
|
|
||||||
|
@ -81,4 +81,16 @@ export const Mind = Node.create({
|
||||||
addNodeView() {
|
addNodeView() {
|
||||||
return ReactNodeViewRenderer(MindWrapper);
|
return ReactNodeViewRenderer(MindWrapper);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addInputRules() {
|
||||||
|
return [
|
||||||
|
nodeInputRule({
|
||||||
|
find: /^\$mind $/,
|
||||||
|
type: this.type,
|
||||||
|
getAttributes: (match) => {
|
||||||
|
return { type: match[1] };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { OrderedList as BuiltInOrderedList } from '@tiptap/extension-ordered-list';
|
import { OrderedList as BuiltInOrderedList } from '@tiptap/extension-ordered-list';
|
||||||
import { getMarkdownSource } from '../services/markdownSourceMap';
|
import { getMarkdownSource } from '../services/markdown/markdownSourceMap';
|
||||||
|
|
||||||
export const OrderedList = BuiltInOrderedList.extend({
|
export const OrderedList = BuiltInOrderedList.extend({
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
|
|
|
@ -1,56 +1,133 @@
|
||||||
import { Extension } from '@tiptap/core';
|
import { Extension } from '@tiptap/core';
|
||||||
import { Plugin, PluginKey } from 'prosemirror-state';
|
import { Plugin, PluginKey, EditorState } from 'prosemirror-state';
|
||||||
import { markdownSerializer } from '../services/serializer';
|
// @ts-ignore
|
||||||
|
import { lowlight } from 'lowlight';
|
||||||
|
import { markdownSerializer } from '../services/markdown';
|
||||||
import { EXTENSION_PRIORITY_HIGHEST } from '../constants';
|
import { EXTENSION_PRIORITY_HIGHEST } from '../constants';
|
||||||
|
|
||||||
const TEXT_FORMAT = 'text/plain';
|
const isMarkActive =
|
||||||
const HTML_FORMAT = 'text/html';
|
(type) =>
|
||||||
const VS_CODE_FORMAT = 'vscode-editor-data';
|
(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 PasteMarkdown = Extension.create({
|
export const PasteMarkdown = Extension.create({
|
||||||
name: 'pasteMarkdown',
|
name: 'pasteMarkdown',
|
||||||
priority: EXTENSION_PRIORITY_HIGHEST,
|
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() {
|
addProseMirrorPlugins() {
|
||||||
return [
|
return [
|
||||||
new Plugin({
|
new Plugin({
|
||||||
key: new PluginKey('pasteMarkdown'),
|
key: new PluginKey('pasteMarkdown'),
|
||||||
props: {
|
props: {
|
||||||
handlePaste: (_, event) => {
|
// @ts-ignore
|
||||||
const { clipboardData } = event;
|
handlePaste: async (view, event: ClipboardEvent) => {
|
||||||
const content = clipboardData.getData(TEXT_FORMAT);
|
if (view.props.editable && !view.props.editable(view.state)) {
|
||||||
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!event.clipboardData) return false;
|
||||||
|
|
||||||
// @ts-ignore
|
const text = event.clipboardData.getData('text/plain');
|
||||||
this.editor.commands.pasteMarkdown(content);
|
const html = event.clipboardData.getData('text/html');
|
||||||
return true;
|
const vscode = event.clipboardData.getData('vscode-editor-data');
|
||||||
|
|
||||||
|
// 粘贴代码
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
// @ts-ignore
|
||||||
|
const transaction = view.state.tr.replaceSelectionWith(paste);
|
||||||
|
view.dispatch(transaction);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
clipboardTextSerializer: (slice) => {
|
clipboardTextSerializer: (slice) => {
|
||||||
const doc = this.editor.schema.topNodeType.createAndFill(undefined, slice.content);
|
const doc = this.editor.schema.topNodeType.createAndFill(undefined, slice.content);
|
||||||
|
|
|
@ -1,3 +1,12 @@
|
||||||
import Placeholder from '@tiptap/extension-placeholder';
|
import BuiltInPlaceholder from '@tiptap/extension-placeholder';
|
||||||
|
|
||||||
export { Placeholder };
|
export const Placeholder = BuiltInPlaceholder.configure({
|
||||||
|
placeholder: ({ node }) => {
|
||||||
|
if (node.type.name === 'title') {
|
||||||
|
return '请输入标题';
|
||||||
|
}
|
||||||
|
return '请输入内容';
|
||||||
|
},
|
||||||
|
showOnlyCurrent: false,
|
||||||
|
showOnlyWhenEditable: true,
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,362 @@
|
||||||
|
import { Extension } from '@tiptap/core';
|
||||||
|
import { Decoration, DecorationSet } from 'prosemirror-view';
|
||||||
|
import { EditorState, Plugin, PluginKey } from 'prosemirror-state';
|
||||||
|
import { Node as ProsemirrorNode } from 'prosemirror-model';
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
search: {
|
||||||
|
/**
|
||||||
|
* @description Set search term in extension.
|
||||||
|
*/
|
||||||
|
setSearchTerm: (searchTerm: string) => ReturnType;
|
||||||
|
/**
|
||||||
|
* @description Set replace term in extension.
|
||||||
|
*/
|
||||||
|
setReplaceTerm: (replaceTerm: string) => ReturnType;
|
||||||
|
/**
|
||||||
|
* @description Replace first instance of search result with given replace term.
|
||||||
|
*/
|
||||||
|
replace: () => ReturnType;
|
||||||
|
/**
|
||||||
|
* @description Replace all instances of search result with given replace term.
|
||||||
|
*/
|
||||||
|
replaceAll: () => ReturnType;
|
||||||
|
goToNextSearchResult: () => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Result {
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchOptions {
|
||||||
|
searchTerm: string;
|
||||||
|
replaceTerm: string;
|
||||||
|
results: Result[];
|
||||||
|
currentIndex: number;
|
||||||
|
searchResultClass: string;
|
||||||
|
searchResultCurrentClass: string;
|
||||||
|
caseSensitive: boolean;
|
||||||
|
disableRegex: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TextNodesWithPosition {
|
||||||
|
text: string;
|
||||||
|
pos: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateView = (state: EditorState<any>, dispatch: any) => dispatch(state.tr);
|
||||||
|
|
||||||
|
const regex = (s: string, disableRegex: boolean, caseSensitive: boolean): RegExp => {
|
||||||
|
return RegExp(
|
||||||
|
disableRegex ? s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') : s,
|
||||||
|
caseSensitive ? 'gu' : 'gui'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function processSearches(
|
||||||
|
doc: ProsemirrorNode,
|
||||||
|
searchTerm: RegExp,
|
||||||
|
searchResultClass: string
|
||||||
|
): { decorationsToReturn: any[]; results: Result[] } {
|
||||||
|
const decorations: Decoration[] = [];
|
||||||
|
let textNodesWithPosition: TextNodesWithPosition[] = [];
|
||||||
|
const results: Result[] = [];
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
if (!searchTerm) return { decorationsToReturn: [], results: [] };
|
||||||
|
|
||||||
|
doc?.descendants((node, pos) => {
|
||||||
|
if (node.isText) {
|
||||||
|
if (textNodesWithPosition[index]) {
|
||||||
|
textNodesWithPosition[index] = {
|
||||||
|
text: textNodesWithPosition[index].text + node.text,
|
||||||
|
pos: textNodesWithPosition[index].pos,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
textNodesWithPosition[index] = {
|
||||||
|
text: `${node.text}`,
|
||||||
|
pos,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
textNodesWithPosition = textNodesWithPosition.filter(Boolean);
|
||||||
|
|
||||||
|
for (let i = 0; i < textNodesWithPosition.length; i += 1) {
|
||||||
|
const { text, pos } = textNodesWithPosition[i];
|
||||||
|
|
||||||
|
const matches = [...text.matchAll(searchTerm)];
|
||||||
|
|
||||||
|
for (let j = 0; j < matches.length; j += 1) {
|
||||||
|
const m = matches[j];
|
||||||
|
|
||||||
|
if (m[0] === '') break;
|
||||||
|
|
||||||
|
if (m.index !== undefined) {
|
||||||
|
results.push({
|
||||||
|
from: pos + m.index,
|
||||||
|
to: pos + m.index + m[0].length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < results.length; i += 1) {
|
||||||
|
const r = results[i];
|
||||||
|
decorations.push(Decoration.inline(r.from, r.to, { class: searchResultClass }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
decorationsToReturn: decorations,
|
||||||
|
results,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const replace = (replaceTerm: string, results: Result[], { state, dispatch }: any) => {
|
||||||
|
const firstResult = results[0];
|
||||||
|
|
||||||
|
if (!firstResult) return;
|
||||||
|
|
||||||
|
const { from, to } = results[0];
|
||||||
|
|
||||||
|
if (dispatch) dispatch(state.tr.insertText(replaceTerm, from, to));
|
||||||
|
};
|
||||||
|
|
||||||
|
const rebaseNextResult = (
|
||||||
|
replaceTerm: string,
|
||||||
|
index: number,
|
||||||
|
lastOffset: number,
|
||||||
|
results: Result[]
|
||||||
|
): [number, Result[]] | null => {
|
||||||
|
const nextIndex = index + 1;
|
||||||
|
|
||||||
|
if (!results[nextIndex]) return null;
|
||||||
|
|
||||||
|
const { from: currentFrom, to: currentTo } = results[index];
|
||||||
|
|
||||||
|
const offset = currentTo - currentFrom - replaceTerm.length + lastOffset;
|
||||||
|
|
||||||
|
const { from, to } = results[nextIndex];
|
||||||
|
|
||||||
|
results[nextIndex] = {
|
||||||
|
to: to - offset,
|
||||||
|
from: from - offset,
|
||||||
|
};
|
||||||
|
|
||||||
|
return [offset, results];
|
||||||
|
};
|
||||||
|
|
||||||
|
const replaceAll = (replaceTerm: string, results: Result[], { tr, dispatch }: any) => {
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
let ourResults = results.slice();
|
||||||
|
|
||||||
|
if (!ourResults.length) return false;
|
||||||
|
|
||||||
|
for (let i = 0; i < ourResults.length; i += 1) {
|
||||||
|
const { from, to } = ourResults[i];
|
||||||
|
|
||||||
|
tr.insertText(replaceTerm, from, to);
|
||||||
|
|
||||||
|
const rebaseNextResultResponse = rebaseNextResult(replaceTerm, i, offset, ourResults);
|
||||||
|
|
||||||
|
if (rebaseNextResultResponse) {
|
||||||
|
offset = rebaseNextResultResponse[0];
|
||||||
|
ourResults = rebaseNextResultResponse[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(tr);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const gotoSearchResult = ({ view, tr, searchResults, searchResultCurrentClass, gotoIndex }) => {
|
||||||
|
const result = searchResults[gotoIndex];
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
let transaction = tr.setMeta('directDecoration', {
|
||||||
|
fromPos: result.from,
|
||||||
|
toPos: result.to,
|
||||||
|
attrs: { class: searchResultCurrentClass },
|
||||||
|
});
|
||||||
|
view?.dispatch(transaction);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const el = window.document.querySelector(`.${searchResultCurrentClass}`);
|
||||||
|
if (el) {
|
||||||
|
el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' });
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
export const SearchNReplace = Extension.create<SearchOptions>({
|
||||||
|
name: 'search',
|
||||||
|
|
||||||
|
defaultOptions: {
|
||||||
|
searchTerm: '',
|
||||||
|
replaceTerm: '',
|
||||||
|
results: [],
|
||||||
|
currentIndex: 0,
|
||||||
|
searchResultClass: 'search-result',
|
||||||
|
searchResultCurrentClass: 'search-result-current',
|
||||||
|
caseSensitive: false,
|
||||||
|
disableRegex: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setSearchTerm:
|
||||||
|
(searchTerm: string) =>
|
||||||
|
({ state, dispatch }) => {
|
||||||
|
this.options.searchTerm = searchTerm;
|
||||||
|
this.options.results = [];
|
||||||
|
this.options.currentIndex = 0;
|
||||||
|
|
||||||
|
updateView(state, dispatch);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
setReplaceTerm:
|
||||||
|
(replaceTerm: string) =>
|
||||||
|
({ state, dispatch }) => {
|
||||||
|
this.options.replaceTerm = replaceTerm;
|
||||||
|
|
||||||
|
updateView(state, dispatch);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
replace:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) => {
|
||||||
|
const { replaceTerm, results, currentIndex } = this.options;
|
||||||
|
const currentResult = results[currentIndex];
|
||||||
|
|
||||||
|
if (currentResult) {
|
||||||
|
replace(replaceTerm, [currentResult], { state, dispatch });
|
||||||
|
this.options.results.splice(currentIndex, 1);
|
||||||
|
} else {
|
||||||
|
replace(replaceTerm, results, { state, dispatch });
|
||||||
|
|
||||||
|
this.options.results.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateView(state, dispatch);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
replaceAll:
|
||||||
|
() =>
|
||||||
|
({ state, tr, dispatch }) => {
|
||||||
|
const { replaceTerm, results } = this.options;
|
||||||
|
|
||||||
|
replaceAll(replaceTerm, results, { tr, dispatch });
|
||||||
|
|
||||||
|
this.options.results = [];
|
||||||
|
|
||||||
|
updateView(state, dispatch);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
goToPrevSearchResult:
|
||||||
|
() =>
|
||||||
|
({ view, tr }) => {
|
||||||
|
const { currentIndex, results, searchResultCurrentClass } = this.options;
|
||||||
|
const nextIndex = (currentIndex + results.length - 1) % results.length;
|
||||||
|
this.options.currentIndex = nextIndex;
|
||||||
|
|
||||||
|
return gotoSearchResult({
|
||||||
|
view,
|
||||||
|
tr,
|
||||||
|
searchResults: results,
|
||||||
|
searchResultCurrentClass,
|
||||||
|
gotoIndex: nextIndex,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
goToNextSearchResult:
|
||||||
|
() =>
|
||||||
|
({ view, tr }) => {
|
||||||
|
const { currentIndex, results, searchResultCurrentClass } = this.options;
|
||||||
|
const nextIndex = (currentIndex + 1) % results.length;
|
||||||
|
this.options.currentIndex = nextIndex;
|
||||||
|
|
||||||
|
return gotoSearchResult({
|
||||||
|
view,
|
||||||
|
tr,
|
||||||
|
searchResults: results,
|
||||||
|
searchResultCurrentClass,
|
||||||
|
gotoIndex: nextIndex,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
|
const extensionThis = this;
|
||||||
|
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
key: new PluginKey('search'),
|
||||||
|
state: {
|
||||||
|
init() {
|
||||||
|
return DecorationSet.empty;
|
||||||
|
},
|
||||||
|
apply(ctx) {
|
||||||
|
const { doc, docChanged } = ctx;
|
||||||
|
|
||||||
|
const {
|
||||||
|
searchTerm,
|
||||||
|
searchResultClass,
|
||||||
|
searchResultCurrentClass,
|
||||||
|
disableRegex,
|
||||||
|
caseSensitive,
|
||||||
|
} = extensionThis.options;
|
||||||
|
|
||||||
|
if (docChanged || searchTerm) {
|
||||||
|
const { decorationsToReturn, results } = processSearches(
|
||||||
|
doc,
|
||||||
|
regex(searchTerm, disableRegex, caseSensitive),
|
||||||
|
searchResultClass
|
||||||
|
);
|
||||||
|
extensionThis.options.results = results;
|
||||||
|
|
||||||
|
if (ctx.getMeta('directDecoration')) {
|
||||||
|
const { fromPos, toPos, attrs } = ctx.getMeta('directDecoration');
|
||||||
|
decorationsToReturn.push(Decoration.inline(fromPos, toPos, attrs));
|
||||||
|
} else {
|
||||||
|
if (results.length) {
|
||||||
|
decorationsToReturn[0] = Decoration.inline(results[0].from, results[0].to, {
|
||||||
|
class: searchResultCurrentClass,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DecorationSet.create(doc, decorationsToReturn);
|
||||||
|
}
|
||||||
|
return DecorationSet.empty;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
decorations(state) {
|
||||||
|
return this.getState(state);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,7 +1,10 @@
|
||||||
|
import { wrappingInputRule } from '@tiptap/core';
|
||||||
import { TaskItem as BuiltInTaskItem } from '@tiptap/extension-task-item';
|
import { TaskItem as BuiltInTaskItem } from '@tiptap/extension-task-item';
|
||||||
|
import { Plugin } from 'prosemirror-state';
|
||||||
|
import { findParentNodeClosestToPos } from 'prosemirror-utils';
|
||||||
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
|
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
|
||||||
|
|
||||||
export const TaskItem = BuiltInTaskItem.extend({
|
const CustomTaskItem = BuiltInTaskItem.extend({
|
||||||
addOptions() {
|
addOptions() {
|
||||||
return {
|
return {
|
||||||
nested: true,
|
nested: true,
|
||||||
|
@ -34,4 +37,49 @@ export const TaskItem = BuiltInTaskItem.extend({
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addInputRules() {
|
||||||
|
return [
|
||||||
|
...this.parent(),
|
||||||
|
wrappingInputRule({
|
||||||
|
find: /^\s*([-+*])\s(\[(x|X| ?)\])\s$/,
|
||||||
|
type: this.type,
|
||||||
|
getAttributes: (match) => ({
|
||||||
|
checked: 'xX'.includes(match[match.length - 1]),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
props: {
|
||||||
|
// @ts-ignore
|
||||||
|
handleClick: (view, pos, event) => {
|
||||||
|
const state = view.state;
|
||||||
|
const schema = state.schema;
|
||||||
|
|
||||||
|
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY });
|
||||||
|
const position = state.doc.resolve(coordinates.pos);
|
||||||
|
const parentList = findParentNodeClosestToPos(position, function (node) {
|
||||||
|
return node.type === schema.nodes.taskItem || node.type === schema.nodes.listItem;
|
||||||
|
});
|
||||||
|
// @ts-ignore
|
||||||
|
const isListClicked = event.target.tagName.toLowerCase() === 'li';
|
||||||
|
if (!isListClicked || !parentList || parentList.node.type !== schema.nodes.taskItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tr = state.tr;
|
||||||
|
tr.setNodeMarkup(parentList.pos, schema.nodes.taskItem, {
|
||||||
|
checked: !parentList.node.attrs.checked,
|
||||||
|
});
|
||||||
|
view.dispatch(tr);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const TaskItem = CustomTaskItem.configure({ nested: true });
|
||||||
|
|
|
@ -1,32 +1,12 @@
|
||||||
import { mergeAttributes } from '@tiptap/core';
|
import { mergeAttributes } from '@tiptap/core';
|
||||||
import { TaskList as BuiltInTaskList } from '@tiptap/extension-task-list';
|
import { TaskList as BuiltInTaskList } from '@tiptap/extension-task-list';
|
||||||
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
|
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
|
||||||
import { getMarkdownSource } from '../services/markdownSourceMap';
|
|
||||||
|
|
||||||
export const TaskList = BuiltInTaskList.extend({
|
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() {
|
parseHTML() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
tag: '.task-list',
|
tag: 'ul.task-list',
|
||||||
priority: PARSE_HTML_PRIORITY_HIGHEST,
|
priority: PARSE_HTML_PRIORITY_HIGHEST,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
import TextAlign from '@tiptap/extension-text-align';
|
import BuiltInTextAlign from '@tiptap/extension-text-align';
|
||||||
|
|
||||||
export { TextAlign };
|
export const TextAlign = BuiltInTextAlign.configure({
|
||||||
|
types: ['heading', 'paragraph', 'image'],
|
||||||
|
});
|
||||||
|
|
|
@ -2,8 +2,12 @@ import { Node, mergeAttributes } from '@tiptap/core';
|
||||||
|
|
||||||
export const Title = Node.create({
|
export const Title = Node.create({
|
||||||
name: 'title',
|
name: 'title',
|
||||||
group: 'block',
|
content: 'inline*',
|
||||||
content: 'text*',
|
selectable: true,
|
||||||
|
defining: true,
|
||||||
|
inline: false,
|
||||||
|
group: 'basic',
|
||||||
|
allowGapCursor: true,
|
||||||
|
|
||||||
addOptions() {
|
addOptions() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,17 +1,23 @@
|
||||||
import { Extension } from '@tiptap/core';
|
import { Extension } from '@tiptap/core';
|
||||||
import { PluginKey, Plugin } from 'prosemirror-state';
|
import { PluginKey, Plugin } from 'prosemirror-state';
|
||||||
|
|
||||||
// @ts-ignore
|
/**
|
||||||
|
* @param {object} args Arguments as deconstructable object
|
||||||
|
* @param {Array | object} args.types possible types
|
||||||
|
* @param {object} args.node node to check
|
||||||
|
*/
|
||||||
function nodeEqualsType({ types, node }) {
|
function nodeEqualsType({ types, node }) {
|
||||||
return (Array.isArray(types) && types.includes(node.type)) || node.type === types;
|
return (Array.isArray(types) && types.includes(node.type)) || node.type === types;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TrailingNodeOptions {
|
/**
|
||||||
node: string;
|
* Extension based on:
|
||||||
notAfter: string[];
|
* - https://github.com/ueberdosis/tiptap/tree/main/demos/src/Experiments/TrailingNode
|
||||||
}
|
* - https://github.com/ueberdosis/tiptap/blob/v1/packages/tiptap-extensions/src/extensions/TrailingNode.js
|
||||||
|
* - https://github.com/remirror/remirror/blob/e0f1bec4a1e8073ce8f5500d62193e52321155b9/packages/prosemirror-trailing-node/src/trailing-node-plugin.ts
|
||||||
|
*/
|
||||||
|
|
||||||
export const TrailingNode = Extension.create<TrailingNodeOptions>({
|
export const TrailingNode = Extension.create({
|
||||||
name: 'trailingNode',
|
name: 'trailingNode',
|
||||||
|
|
||||||
addOptions() {
|
addOptions() {
|
||||||
|
@ -27,10 +33,14 @@ export const TrailingNode = Extension.create<TrailingNodeOptions>({
|
||||||
.map(([, value]) => value)
|
.map(([, value]) => value)
|
||||||
.filter((node) => this.options.notAfter.includes(node.name));
|
.filter((node) => this.options.notAfter.includes(node.name));
|
||||||
|
|
||||||
|
const isEditable = this.editor.isEditable;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
new Plugin({
|
new Plugin({
|
||||||
key: plugin,
|
key: plugin,
|
||||||
appendTransaction: (_, __, state) => {
|
appendTransaction: (_, __, state) => {
|
||||||
|
if (!isEditable) return;
|
||||||
|
|
||||||
const { doc, tr, schema } = state;
|
const { doc, tr, schema } = state;
|
||||||
const shouldInsertNodeAtEnd = plugin.getState(state);
|
const shouldInsertNodeAtEnd = plugin.getState(state);
|
||||||
const endPosition = doc.content.size;
|
const endPosition = doc.content.size;
|
||||||
|
@ -44,17 +54,18 @@ export const TrailingNode = Extension.create<TrailingNodeOptions>({
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
init: (_, state) => {
|
init: (_, state) => {
|
||||||
|
if (!isEditable) return false;
|
||||||
const lastNode = state.tr.doc.lastChild;
|
const lastNode = state.tr.doc.lastChild;
|
||||||
|
|
||||||
return !nodeEqualsType({ node: lastNode, types: disabledNodes });
|
return !nodeEqualsType({ node: lastNode, types: disabledNodes });
|
||||||
},
|
},
|
||||||
apply: (tr, value) => {
|
apply: (tr, value) => {
|
||||||
|
if (!isEditable) return value;
|
||||||
|
|
||||||
if (!tr.docChanged) {
|
if (!tr.docChanged) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastNode = tr.doc.lastChild;
|
const lastNode = tr.doc.lastChild;
|
||||||
|
|
||||||
return !nodeEqualsType({ node: lastNode, types: disabledNodes });
|
return !nodeEqualsType({ node: lastNode, types: disabledNodes });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
IconAlignRight,
|
IconAlignRight,
|
||||||
IconAlignJustify,
|
IconAlignJustify,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import { isTitleActive } from '../services/active';
|
import { isTitleActive } from '../services/isActive';
|
||||||
|
|
||||||
export const AlignMenu = ({ editor }) => {
|
export const AlignMenu = ({ editor }) => {
|
||||||
const current = (() => {
|
const current = (() => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Space, Button, Tooltip } from '@douyinfe/semi-ui';
|
import { Space, Button } from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
IconDelete,
|
IconDelete,
|
||||||
IconTickCircle,
|
IconTickCircle,
|
||||||
|
@ -6,22 +6,23 @@ import {
|
||||||
IconClear,
|
IconClear,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
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 '../services//delete';
|
import { deleteNode } from '../services/deleteNode';
|
||||||
|
|
||||||
export const BannerBubbleMenu = ({ editor }) => {
|
export const BannerBubbleMenu = ({ editor }) => {
|
||||||
return (
|
return (
|
||||||
<BubbleMenu
|
<BubbleMenu
|
||||||
className={'bubble-menu'}
|
className={'bubble-menu js-bubble-menu-banner'}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
pluginKey="banner-bubble-menu"
|
pluginKey="banner-bubble-menu"
|
||||||
shouldShow={() => editor.isActive(Banner.name)}
|
shouldShow={() => editor.isActive(Banner.name)}
|
||||||
matchRenderContainer={(node) => node && node.id === 'js-bannber-container'}
|
matchRenderContainer={(node) => node && node.id === 'js-bannber-container'}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space>
|
||||||
<Tooltip content="信息" zIndex={10000}>
|
<Tooltip content="信息">
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
@ -39,7 +40,7 @@ export const BannerBubbleMenu = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="警告" zIndex={10000}>
|
<Tooltip content="警告">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor
|
editor
|
||||||
|
@ -57,7 +58,7 @@ export const BannerBubbleMenu = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="危险" zIndex={10000}>
|
<Tooltip content="危险">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor
|
editor
|
||||||
|
@ -75,7 +76,7 @@ export const BannerBubbleMenu = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="成功" zIndex={10000}>
|
<Tooltip content="成功">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor
|
editor
|
||||||
|
@ -95,7 +96,7 @@ export const BannerBubbleMenu = ({ editor }) => {
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Tooltip content="删除" zIndex={10000}>
|
<Tooltip content="删除" hideOnClick>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { Link } from '../extensions/link';
|
||||||
import { Attachment } from '../extensions/attachment';
|
import { Attachment } from '../extensions/attachment';
|
||||||
import { Image } from '../extensions/image';
|
import { Image } from '../extensions/image';
|
||||||
import { Banner } from '../extensions/banner';
|
import { Banner } from '../extensions/banner';
|
||||||
|
import { HorizontalRule } from '../extensions/horizontalRule';
|
||||||
import { Iframe } from '../extensions/iframe';
|
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';
|
||||||
|
@ -26,6 +27,7 @@ const OTHER_BUBBLE_MENU_TYPES = [
|
||||||
DocumentReference.name,
|
DocumentReference.name,
|
||||||
DocumentChildren.name,
|
DocumentChildren.name,
|
||||||
Katex.name,
|
Katex.name,
|
||||||
|
HorizontalRule.name,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const BaseBubbleMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const BaseBubbleMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconQuote, IconCheckboxIndeterminate, IconLink } from '@douyinfe/semi-icons';
|
import { IconQuote, IconCheckboxIndeterminate, IconLink } from '@douyinfe/semi-icons';
|
||||||
import { isTitleActive } from '../services/active';
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { isTitleActive } from '../services/isActive';
|
||||||
import { Emoji } from './components/emoji';
|
import { Emoji } from './components/emoji';
|
||||||
|
import { Search } from './search';
|
||||||
|
|
||||||
export const BaseInsertMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
export const BaseInsertMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
|
@ -13,7 +15,7 @@ export const BaseInsertMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
<>
|
<>
|
||||||
<Emoji editor={editor} />
|
<Emoji editor={editor} />
|
||||||
|
|
||||||
<Tooltip zIndex={10000} content="插入链接">
|
<Tooltip content="插入链接">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('link') ? 'light' : 'borderless'}
|
theme={editor.isActive('link') ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
@ -23,7 +25,7 @@ export const BaseInsertMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip zIndex={10000} content="插入引用">
|
<Tooltip content="插入引用">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('blockquote') ? 'light' : 'borderless'}
|
theme={editor.isActive('blockquote') ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
@ -34,7 +36,7 @@ export const BaseInsertMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip zIndex={10000} content="插入分割线">
|
<Tooltip content="插入分割线">
|
||||||
<Button
|
<Button
|
||||||
theme={'borderless'}
|
theme={'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
@ -43,6 +45,8 @@ export const BaseInsertMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
disabled={isTitleActive(editor)}
|
disabled={isTitleActive(editor)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
<Search editor={editor} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
IconBold,
|
IconBold,
|
||||||
IconItalic,
|
IconItalic,
|
||||||
|
@ -7,7 +7,8 @@ import {
|
||||||
IconUnderline,
|
IconUnderline,
|
||||||
IconCode,
|
IconCode,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import { isTitleActive } from '../services/active';
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { isTitleActive } from '../services/isActive';
|
||||||
import { ColorMenu } from './color';
|
import { ColorMenu } from './color';
|
||||||
|
|
||||||
export const BaseMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
export const BaseMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
|
@ -17,7 +18,7 @@ export const BaseMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip zIndex={10000} content="粗体">
|
<Tooltip content="粗体">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('bold') ? 'light' : 'borderless'}
|
theme={editor.isActive('bold') ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
@ -27,7 +28,7 @@ export const BaseMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip zIndex={10000} content="斜体">
|
<Tooltip content="斜体">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('italic') ? 'light' : 'borderless'}
|
theme={editor.isActive('italic') ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
@ -37,7 +38,7 @@ export const BaseMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip zIndex={10000} content="下划线">
|
<Tooltip content="下划线">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('underline') ? 'light' : 'borderless'}
|
theme={editor.isActive('underline') ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
@ -47,7 +48,7 @@ export const BaseMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip zIndex={10000} content="删除线">
|
<Tooltip content="删除线">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('strike') ? 'light' : 'borderless'}
|
theme={editor.isActive('strike') ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
@ -57,7 +58,7 @@ export const BaseMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip zIndex={10000} content="行内代码">
|
<Tooltip content="行内代码">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('code') ? 'light' : 'borderless'}
|
theme={editor.isActive('code') ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconFont, IconMark } from '@douyinfe/semi-icons';
|
import { IconFont, IconMark } from '@douyinfe/semi-icons';
|
||||||
import { isTitleActive } from '../services/active';
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { isTitleActive } from '../services/isActive';
|
||||||
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 }) => {
|
||||||
|
@ -19,7 +20,7 @@ export const ColorMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
}}
|
}}
|
||||||
disabled={isTitleActive(editor)}
|
disabled={isTitleActive(editor)}
|
||||||
>
|
>
|
||||||
<Tooltip zIndex={10000} content="文本色">
|
<Tooltip content="文本色">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('textStyle') ? 'light' : 'borderless'}
|
theme={editor.isActive('textStyle') ? 'light' : 'borderless'}
|
||||||
type={'tertiary'}
|
type={'tertiary'}
|
||||||
|
@ -51,7 +52,7 @@ export const ColorMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
}}
|
}}
|
||||||
disabled={isTitleActive(editor)}
|
disabled={isTitleActive(editor)}
|
||||||
>
|
>
|
||||||
<Tooltip zIndex={10000} content="背景色">
|
<Tooltip content="背景色">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('textStyle') ? 'light' : 'borderless'}
|
theme={editor.isActive('textStyle') ? 'light' : 'borderless'}
|
||||||
type={'tertiary'}
|
type={'tertiary'}
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
.wrap {
|
.wrap {
|
||||||
height: 300px;
|
height: 300px;
|
||||||
padding: 14px;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sectionWrap {
|
|
||||||
}
|
|
||||||
|
|
||||||
.listWrap {
|
.listWrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Popover, Button, Tooltip, Typography } from '@douyinfe/semi-ui';
|
import { Popover, Button, Typography } from '@douyinfe/semi-ui';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { IconEmoji } from 'components/icons';
|
import { IconEmoji } from 'components/icons';
|
||||||
import { EXPRESSIONES, GESTURES } from './constants';
|
import { EXPRESSIONES, GESTURES } from './constants';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
@ -52,7 +53,7 @@ export const Emoji = ({ editor }) => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<Tooltip zIndex={10000} content="插入表情">
|
<Tooltip content="插入表情">
|
||||||
<Button theme={'borderless'} type="tertiary" icon={<IconEmoji />} />
|
<Button theme={'borderless'} type="tertiary" icon={<IconEmoji />} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -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 '../../services/active';
|
import { isTitleActive } from '../../services/isActive';
|
||||||
|
|
||||||
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 '../../services/active';
|
import { isTitleActive } from '../../services/isActive';
|
||||||
|
|
||||||
const getCurrentCaretTitle = (editor) => {
|
const getCurrentCaretTitle = (editor) => {
|
||||||
if (editor.isActive('heading', { level: 1 })) return 1;
|
if (editor.isActive('heading', { level: 1 })) return 1;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Space, Button, Tooltip, InputNumber, Typography } from '@douyinfe/semi-ui';
|
import { Space, Button, InputNumber, Typography } from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
IconAlignLeft,
|
IconAlignLeft,
|
||||||
IconAlignCenter,
|
IconAlignCenter,
|
||||||
|
@ -7,6 +7,7 @@ import {
|
||||||
IconUpload,
|
IconUpload,
|
||||||
IconDelete,
|
IconDelete,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
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';
|
||||||
|
@ -38,7 +39,7 @@ export const ImageBubbleMenu = ({ editor }) => {
|
||||||
matchRenderContainer={(node) => node && node.id === 'js-resizeable-container'}
|
matchRenderContainer={(node) => node && node.id === 'js-resizeable-container'}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space>
|
||||||
<Tooltip content="左对齐" zIndex={10000}>
|
<Tooltip content="左对齐">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor
|
editor
|
||||||
|
@ -56,7 +57,7 @@ export const ImageBubbleMenu = ({ editor }) => {
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content="居中" zIndex={10000}>
|
<Tooltip content="居中">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor
|
editor
|
||||||
|
@ -74,7 +75,7 @@ export const ImageBubbleMenu = ({ editor }) => {
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content="右对齐" zIndex={10000}>
|
<Tooltip content="右对齐">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor
|
editor
|
||||||
|
@ -148,12 +149,12 @@ export const ImageBubbleMenu = ({ editor }) => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{() => (
|
{() => (
|
||||||
<Tooltip content="上传图片" zIndex={10000}>
|
<Tooltip content="上传图片">
|
||||||
<Button size="small" type="tertiary" theme="borderless" icon={<IconUpload />} />
|
<Button size="small" type="tertiary" theme="borderless" icon={<IconUpload />} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</Upload>
|
</Upload>
|
||||||
<Tooltip content="删除" zIndex={10000}>
|
<Tooltip content="删除" hideOnClick>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Space, Button, Tooltip, Input } from '@douyinfe/semi-ui';
|
import { Space, Button, Input } from '@douyinfe/semi-ui';
|
||||||
import { IconExternalOpen, IconUnlink, IconTickCircle } from '@douyinfe/semi-icons';
|
import { IconExternalOpen, IconUnlink, IconTickCircle } from '@douyinfe/semi-icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { BubbleMenu } from './components/bubble-menu';
|
import { BubbleMenu } from './components/bubble-menu';
|
||||||
import { Link } from '../extensions/link';
|
import { Link } from '../extensions/link';
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ export const LinkBubbleMenu = ({ editor }) => {
|
||||||
setUrl(url);
|
setUrl(url);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tooltip content="设置链接" zIndex={10000}>
|
<Tooltip content="设置链接">
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
@ -50,7 +51,7 @@ export const LinkBubbleMenu = ({ editor }) => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content="去除链接" zIndex={10000}>
|
<Tooltip content="去除链接">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor.chain().unsetLink().run();
|
editor.chain().unsetLink().run();
|
||||||
|
@ -61,7 +62,7 @@ export const LinkBubbleMenu = ({ editor }) => {
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content="访问链接" zIndex={10000}>
|
<Tooltip content="访问链接">
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import { IconList, IconOrderedList, IconIndentLeft, IconIndentRight } from '@douyinfe/semi-icons';
|
import { IconList, IconOrderedList, IconIndentLeft, IconIndentRight } from '@douyinfe/semi-icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { IconTask } from 'components/icons';
|
import { IconTask } from 'components/icons';
|
||||||
import { isTitleActive } from '../services/active';
|
import { isTitleActive } from '../services/isActive';
|
||||||
|
|
||||||
export const ListMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
export const ListMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
|
@ -11,7 +12,7 @@ export const ListMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip zIndex={10000} content="无序列表">
|
<Tooltip content="无序列表">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('bulletList') ? 'light' : 'borderless'}
|
theme={editor.isActive('bulletList') ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
@ -21,7 +22,7 @@ export const ListMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip zIndex={10000} content="有序列表">
|
<Tooltip content="有序列表">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('orderedList') ? 'light' : 'borderless'}
|
theme={editor.isActive('orderedList') ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
@ -31,7 +32,7 @@ export const ListMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip zIndex={10000} content="任务列表">
|
<Tooltip content="任务列表">
|
||||||
<Button
|
<Button
|
||||||
theme={editor.isActive('taskList') ? 'light' : 'borderless'}
|
theme={editor.isActive('taskList') ? 'light' : 'borderless'}
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
@ -41,7 +42,7 @@ export const ListMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip zIndex={10000} content="增加缩进">
|
<Tooltip content="增加缩进">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor.chain().focus().indent().run();
|
editor.chain().focus().indent().run();
|
||||||
|
@ -53,7 +54,7 @@ export const ListMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip zIndex={10000} content="减少缩进">
|
<Tooltip content="减少缩进">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor.chain().focus().outdent().run();
|
editor.chain().focus().outdent().run();
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, Tooltip, Dropdown, Popover } from '@douyinfe/semi-ui';
|
import { Button, Dropdown, Popover } from '@douyinfe/semi-ui';
|
||||||
import { IconPlus } from '@douyinfe/semi-icons';
|
import { IconPlus } from '@douyinfe/semi-icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { Upload } from 'components/upload';
|
import { Upload } from 'components/upload';
|
||||||
import {
|
import {
|
||||||
IconDocument,
|
IconDocument,
|
||||||
|
@ -15,7 +16,7 @@ import {
|
||||||
IconMath,
|
IconMath,
|
||||||
} from 'components/icons';
|
} from 'components/icons';
|
||||||
import { GridSelect } from 'components/grid-select';
|
import { GridSelect } from 'components/grid-select';
|
||||||
import { isTitleActive } from '../services/active';
|
import { isTitleActive } from '../services/isActive';
|
||||||
import { getImageOriginSize } from '../services/image';
|
import { getImageOriginSize } from '../services/image';
|
||||||
|
|
||||||
export const MediaInsertMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
export const MediaInsertMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
|
@ -117,7 +118,7 @@ export const MediaInsertMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Tooltip content="插入" zIndex={10000}>
|
<Tooltip content="插入">
|
||||||
<Button
|
<Button
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
theme="borderless"
|
theme="borderless"
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Popover, Button, Typography, Input, Space } from '@douyinfe/semi-ui';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { IconSearchReplace } from 'components/icons';
|
||||||
|
import { SearchNReplace } from '../../extensions/search';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
export const Search = ({ editor }) => {
|
||||||
|
const searchExtension = editor.extensionManager.extensions.find(
|
||||||
|
(ext) => ext.name === SearchNReplace.name
|
||||||
|
);
|
||||||
|
const currentIndex = searchExtension ? searchExtension.options.currentIndex : -1;
|
||||||
|
const results = searchExtension ? searchExtension.options.results : [];
|
||||||
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
const [replaceValue, setReplaceValue] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
editor?.commands?.setSearchTerm(searchValue);
|
||||||
|
}, [searchValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
editor?.commands?.setReplaceTerm(replaceValue);
|
||||||
|
}, [replaceValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
showArrow
|
||||||
|
zIndex={10000}
|
||||||
|
trigger="click"
|
||||||
|
position="bottomRight"
|
||||||
|
onVisibleChange={(visible) => {
|
||||||
|
if (!visible) {
|
||||||
|
setSearchValue('');
|
||||||
|
setReplaceValue('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
content={
|
||||||
|
<div>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<Text type="tertiary">查找</Text>
|
||||||
|
<Input
|
||||||
|
autofocus
|
||||||
|
value={searchValue}
|
||||||
|
onChange={(v) => setSearchValue(v)}
|
||||||
|
suffix={results.length ? `${currentIndex + 1}/${results.length}` : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<Text type="tertiary">替换为</Text>
|
||||||
|
<Input value={replaceValue} onChange={(v) => setReplaceValue(v)} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Space>
|
||||||
|
<Button disabled={!results.length} onClick={() => editor.commands.replaceAll()}>
|
||||||
|
全部替换
|
||||||
|
</Button>
|
||||||
|
<Button disabled={!results.length} onClick={() => editor.commands.replace()}>
|
||||||
|
替换
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={!results.length}
|
||||||
|
onClick={() => editor.commands.goToPrevSearchResult()}
|
||||||
|
>
|
||||||
|
上一个
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={!results.length}
|
||||||
|
onClick={() => editor.commands.goToNextSearchResult()}
|
||||||
|
>
|
||||||
|
下一个
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<Tooltip content="查找替换">
|
||||||
|
<Button theme={'borderless'} type="tertiary" icon={<IconSearchReplace />} />
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,4 +1,4 @@
|
||||||
import { Space, Button, Tooltip } from '@douyinfe/semi-ui';
|
import { Space, Button } from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
IconAddColumnBefore,
|
IconAddColumnBefore,
|
||||||
IconAddColumnAfter,
|
IconAddColumnAfter,
|
||||||
|
@ -10,6 +10,7 @@ import {
|
||||||
IconSplitCell,
|
IconSplitCell,
|
||||||
IconDeleteTable,
|
IconDeleteTable,
|
||||||
} from 'components/icons';
|
} from 'components/icons';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { BubbleMenu } from './components/bubble-menu';
|
import { BubbleMenu } from './components/bubble-menu';
|
||||||
import { Table } from '../extensions/table';
|
import { Table } from '../extensions/table';
|
||||||
|
|
||||||
|
@ -28,7 +29,7 @@ export const TableBubbleMenu = ({ editor }) => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space>
|
||||||
<Tooltip content="向前插入一列" zIndex={10000}>
|
<Tooltip content="向前插入一列">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => editor.chain().focus().addColumnBefore().run()}
|
onClick={() => editor.chain().focus().addColumnBefore().run()}
|
||||||
icon={<IconAddColumnBefore />}
|
icon={<IconAddColumnBefore />}
|
||||||
|
@ -38,7 +39,7 @@ export const TableBubbleMenu = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="向后插入一列" zIndex={10000}>
|
<Tooltip content="向后插入一列">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => editor.chain().focus().addColumnAfter().run()}
|
onClick={() => editor.chain().focus().addColumnAfter().run()}
|
||||||
icon={<IconAddColumnAfter />}
|
icon={<IconAddColumnAfter />}
|
||||||
|
@ -47,7 +48,7 @@ export const TableBubbleMenu = ({ editor }) => {
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content="删除当前列" zIndex={10000}>
|
<Tooltip content="删除当前列">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => editor.chain().focus().deleteColumn().run()}
|
onClick={() => editor.chain().focus().deleteColumn().run()}
|
||||||
icon={<IconDeleteColumn />}
|
icon={<IconDeleteColumn />}
|
||||||
|
@ -57,7 +58,7 @@ export const TableBubbleMenu = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="向前插入一行" zIndex={10000}>
|
<Tooltip content="向前插入一行">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => editor.chain().focus().addRowBefore().run()}
|
onClick={() => editor.chain().focus().addRowBefore().run()}
|
||||||
icon={<IconAddRowBefore />}
|
icon={<IconAddRowBefore />}
|
||||||
|
@ -67,7 +68,7 @@ export const TableBubbleMenu = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="向后插入一行" zIndex={10000}>
|
<Tooltip content="向后插入一行">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => editor.chain().focus().addRowAfter().run()}
|
onClick={() => editor.chain().focus().addRowAfter().run()}
|
||||||
icon={<IconAddRowAfter />}
|
icon={<IconAddRowAfter />}
|
||||||
|
@ -77,7 +78,7 @@ export const TableBubbleMenu = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="删除当前行" zIndex={10000}>
|
<Tooltip content="删除当前行">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => editor.chain().focus().deleteRow().run()}
|
onClick={() => editor.chain().focus().deleteRow().run()}
|
||||||
icon={<IconDeleteRow />}
|
icon={<IconDeleteRow />}
|
||||||
|
@ -87,7 +88,7 @@ export const TableBubbleMenu = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="合并单元格" zIndex={10000}>
|
<Tooltip content="合并单元格">
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
@ -97,7 +98,7 @@ export const TableBubbleMenu = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="分离单元格" zIndex={10000}>
|
<Tooltip content="分离单元格">
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
@ -107,7 +108,7 @@ export const TableBubbleMenu = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="删除表格" zIndex={10000}>
|
<Tooltip content="删除表格" hideOnClick>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { InputRule, wrappingInputRule } from '@tiptap/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapping input handler that will append the content of the last match
|
||||||
|
*
|
||||||
|
* @param {RegExp} find find param for the wrapping input rule
|
||||||
|
* @param {object} type Node Type object
|
||||||
|
* @param {*} getAttributes handler to get the attributes
|
||||||
|
*/
|
||||||
|
export function listInputRule(find, type, getAttributes = null) {
|
||||||
|
const handler = ({ state, range, match }) => {
|
||||||
|
const wrap = wrappingInputRule({ find, type, getAttributes });
|
||||||
|
// @ts-ignore
|
||||||
|
wrap.handler({ state, range, match });
|
||||||
|
// Insert the first character after bullet if there is one
|
||||||
|
if (match.length >= 3) {
|
||||||
|
state.tr.insertText(match[2]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return new InputRule({ find, handler });
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import markdownit from 'markdown-it';
|
||||||
|
import sub from 'markdown-it-sub';
|
||||||
|
import sup from 'markdown-it-sup';
|
||||||
|
import footnote from 'markdown-it-footnote';
|
||||||
|
import anchor from 'markdown-it-anchor';
|
||||||
|
import tasklist from 'markdown-it-task-lists';
|
||||||
|
import emoji from 'markdown-it-emoji';
|
||||||
|
import katex from '@traptitech/markdown-it-katex';
|
||||||
|
import splitMixedLists from './markedownSplitMixedList';
|
||||||
|
import markdownUnderline from './markdownUnderline';
|
||||||
|
import markdownBanner from './markdownBanner';
|
||||||
|
|
||||||
|
export const markdown = markdownit('commonmark', { html: false, breaks: false })
|
||||||
|
.enable('strikethrough')
|
||||||
|
.use(sub)
|
||||||
|
.use(sup)
|
||||||
|
.use(footnote)
|
||||||
|
.use(anchor)
|
||||||
|
.use(tasklist, { enable: true })
|
||||||
|
.use(splitMixedLists)
|
||||||
|
.use(markdownUnderline)
|
||||||
|
.use(markdownBanner)
|
||||||
|
.use(emoji)
|
||||||
|
.use(katex);
|
||||||
|
|
||||||
|
export * from './serializer';
|
|
@ -0,0 +1,29 @@
|
||||||
|
import container from 'markdown-it-container';
|
||||||
|
|
||||||
|
export const typesAvailable = ['info', 'warning', 'danger', 'success'];
|
||||||
|
|
||||||
|
const buildRender = (type) => (tokens, idx, options, env, slf) => {
|
||||||
|
const tag = tokens[idx];
|
||||||
|
|
||||||
|
// add attributes to the opening tag
|
||||||
|
if (tag.nesting === 1) {
|
||||||
|
tag.attrSet('data-banner', type);
|
||||||
|
tag.attrJoin('class', `banner banner-${type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return slf.renderToken(tokens, idx, options, env, slf);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} md Markdown object
|
||||||
|
*/
|
||||||
|
export default function markdownBanner(md) {
|
||||||
|
// create a custom container to each callout type
|
||||||
|
typesAvailable.forEach((type) => {
|
||||||
|
md.use(container, type, {
|
||||||
|
render: buildRender(type),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return md;
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
export default function markdownUnderlines(md) {
|
||||||
|
md.inline.ruler2.after('emphasis', 'underline', (state) => {
|
||||||
|
const tokens = state.tokens;
|
||||||
|
|
||||||
|
for (let i = tokens.length - 1; i > 0; i--) {
|
||||||
|
const token = tokens[i];
|
||||||
|
|
||||||
|
if (token.markup === '__') {
|
||||||
|
if (token.type === 'strong_open') {
|
||||||
|
tokens[i].tag = 'u';
|
||||||
|
tokens[i].type = 'u_open';
|
||||||
|
}
|
||||||
|
if (token.type === 'strong_close') {
|
||||||
|
tokens[i].tag = 'u';
|
||||||
|
tokens[i].type = 'u_close';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
/**
|
||||||
|
* @param {object} md Markdown object
|
||||||
|
*/
|
||||||
|
export default function splitMixedLists(md) {
|
||||||
|
md.core.ruler.after('github-task-lists', 'split-mixed-task-lists', (state) => {
|
||||||
|
const tokens = state.tokens;
|
||||||
|
|
||||||
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
|
const token = tokens[i];
|
||||||
|
if (token.attrGet('class') !== 'contains-task-list') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const firstChild = tokens[i + 1];
|
||||||
|
const startsWithTask = firstChild.attrGet('class') === 'task-list-item';
|
||||||
|
if (!startsWithTask) {
|
||||||
|
token.attrs.splice(token.attrIndex('class'));
|
||||||
|
if (token.attrs.length === 0) {
|
||||||
|
token.attrs = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const splitBefore = findChildOf(tokens, i, (child) => {
|
||||||
|
return child.nesting === 1 && child.attrGet('class') !== firstChild.attrGet('class');
|
||||||
|
});
|
||||||
|
if (splitBefore > i) {
|
||||||
|
splitListAt(tokens, splitBefore, state.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array} tokens - all the tokens in the doc
|
||||||
|
* @param {number} index - index into the tokens array where to split
|
||||||
|
* @param {object} TokenConstructor - constructor provided by Markdown-it
|
||||||
|
*/
|
||||||
|
function splitListAt(tokens, index, TokenConstructor) {
|
||||||
|
const closeList = new TokenConstructor('bullet_list_close', 'ul', -1);
|
||||||
|
closeList.block = true;
|
||||||
|
const openList = new TokenConstructor('bullet_list_open', 'ul', 1);
|
||||||
|
openList.attrSet('class', 'contains-task-list');
|
||||||
|
openList.block = true;
|
||||||
|
tokens.splice(index, 0, closeList, openList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array} tokens - all the tokens in the doc
|
||||||
|
* @param {number} parentIndex - index of the parent in the tokens array
|
||||||
|
* @param {Function} predicate - test function returned child needs to pass
|
||||||
|
*/
|
||||||
|
function findChildOf(tokens, parentIndex, predicate) {
|
||||||
|
const searchLevel = tokens[parentIndex].level + 1;
|
||||||
|
for (let i = parentIndex + 1; i < tokens.length; i++) {
|
||||||
|
const token = tokens[i];
|
||||||
|
if (token.level < searchLevel) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (token.level === searchLevel && predicate(tokens[i])) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
|
@ -4,41 +4,41 @@ import {
|
||||||
MarkdownSerializer as ProseMirrorMarkdownSerializer,
|
MarkdownSerializer as ProseMirrorMarkdownSerializer,
|
||||||
defaultMarkdownSerializer,
|
defaultMarkdownSerializer,
|
||||||
} from 'prosemirror-markdown';
|
} from 'prosemirror-markdown';
|
||||||
import { marked } from './marked';
|
import { markdown } from '.';
|
||||||
import { Attachment } from '../extensions/attachment';
|
import { Attachment } from '../../extensions/attachment';
|
||||||
import { Banner } from '../extensions/banner';
|
import { Banner } from '../../extensions/banner';
|
||||||
import { Blockquote } from '../extensions/blockquote';
|
import { Blockquote } from '../../extensions/blockquote';
|
||||||
import { Bold } from '../extensions/bold';
|
import { Bold } from '../../extensions/bold';
|
||||||
import { BulletList } from '../extensions/bulletList';
|
import { BulletList } from '../../extensions/bulletList';
|
||||||
import { Code } from '../extensions/code';
|
import { Code } from '../../extensions/code';
|
||||||
import { CodeBlock } from '../extensions/codeBlock';
|
import { CodeBlock } from '../../extensions/codeBlock';
|
||||||
import { DocumentChildren } from '../extensions/documentChildren';
|
import { DocumentChildren } from '../../extensions/documentChildren';
|
||||||
import { DocumentReference } from '../extensions/documentReference';
|
import { DocumentReference } from '../../extensions/documentReference';
|
||||||
import { FootnoteDefinition } from '../extensions/footnoteDefinition';
|
import { FootnoteDefinition } from '../../extensions/footnoteDefinition';
|
||||||
import { FootnoteReference } from '../extensions/footnoteReference';
|
import { FootnoteReference } from '../../extensions/footnoteReference';
|
||||||
import { FootnotesSection } from '../extensions/footnotesSection';
|
import { FootnotesSection } from '../../extensions/footnotesSection';
|
||||||
import { HardBreak } from '../extensions/hardBreak';
|
import { HardBreak } from '../../extensions/hardBreak';
|
||||||
import { Heading } from '../extensions/heading';
|
import { Heading } from '../../extensions/heading';
|
||||||
import { HorizontalRule } from '../extensions/horizontalRule';
|
import { HorizontalRule } from '../../extensions/horizontalRule';
|
||||||
import { HTMLMarks } from '../extensions/htmlMarks';
|
import { HTMLMarks } from '../../extensions/htmlMarks';
|
||||||
import { Iframe } from '../extensions/iframe';
|
import { Iframe } from '../../extensions/iframe';
|
||||||
import { Image } from '../extensions/image';
|
import { Image } from '../../extensions/image';
|
||||||
import { Italic } from '../extensions/italic';
|
import { Italic } from '../../extensions/italic';
|
||||||
import { Katex } from '../extensions/katex';
|
import { Katex } from '../../extensions/katex';
|
||||||
import { Link } from '../extensions/link';
|
import { Link } from '../../extensions/link';
|
||||||
import { ListItem } from '../extensions/listItem';
|
import { ListItem } from '../../extensions/listItem';
|
||||||
import { Mind } from '../extensions/mind';
|
import { Mind } from '../../extensions/mind';
|
||||||
import { OrderedList } from '../extensions/orderedList';
|
import { OrderedList } from '../../extensions/orderedList';
|
||||||
import { Paragraph } from '../extensions/paragraph';
|
import { Paragraph } from '../../extensions/paragraph';
|
||||||
import { Strike } from '../extensions/strike';
|
import { Strike } from '../../extensions/strike';
|
||||||
import { Table } from '../extensions/table';
|
import { Table } from '../../extensions/table';
|
||||||
import { TableCell } from '../extensions/tableCell';
|
import { TableCell } from '../../extensions/tableCell';
|
||||||
import { TableHeader } from '../extensions/tableHeader';
|
import { TableHeader } from '../../extensions/tableHeader';
|
||||||
import { TableRow } from '../extensions/tableRow';
|
import { TableRow } from '../../extensions/tableRow';
|
||||||
import { Text } from '../extensions/text';
|
import { Text } from '../../extensions/text';
|
||||||
import { TaskItem } from '../extensions/taskItem';
|
import { TaskItem } from '../../extensions/taskItem';
|
||||||
import { TaskList } from '../extensions/taskList';
|
import { TaskList } from '../../extensions/taskList';
|
||||||
import { Title } from '../extensions/title';
|
import { Title } from '../../extensions/title';
|
||||||
import {
|
import {
|
||||||
isPlainURL,
|
isPlainURL,
|
||||||
renderHardBreak,
|
renderHardBreak,
|
||||||
|
@ -96,8 +96,11 @@ const defaultSerializerConfig = {
|
||||||
state.closeBlock(node);
|
state.closeBlock(node);
|
||||||
},
|
},
|
||||||
[Banner.name]: (state, node) => {
|
[Banner.name]: (state, node) => {
|
||||||
|
state.write(`:::${node.attrs.type || 'info'}\n`);
|
||||||
state.ensureNewLine();
|
state.ensureNewLine();
|
||||||
state.write(`banner$`);
|
state.renderContent(node);
|
||||||
|
state.ensureNewLine();
|
||||||
|
state.write(':::');
|
||||||
state.closeBlock(node);
|
state.closeBlock(node);
|
||||||
},
|
},
|
||||||
[Blockquote.name]: (state, node) => {
|
[Blockquote.name]: (state, node) => {
|
||||||
|
@ -151,8 +154,10 @@ const defaultSerializerConfig = {
|
||||||
},
|
},
|
||||||
[ListItem.name]: defaultMarkdownSerializer.nodes.list_item,
|
[ListItem.name]: defaultMarkdownSerializer.nodes.list_item,
|
||||||
[Mind.name]: (state, node) => {
|
[Mind.name]: (state, node) => {
|
||||||
|
state.write(`$mind\n`);
|
||||||
|
state.ensureNewLine();
|
||||||
|
state.renderContent(node);
|
||||||
state.ensureNewLine();
|
state.ensureNewLine();
|
||||||
state.write(`mind$`);
|
|
||||||
state.closeBlock(node);
|
state.closeBlock(node);
|
||||||
},
|
},
|
||||||
[OrderedList.name]: renderOrderedList,
|
[OrderedList.name]: renderOrderedList,
|
||||||
|
@ -166,16 +171,22 @@ const defaultSerializerConfig = {
|
||||||
state.renderContent(node);
|
state.renderContent(node);
|
||||||
},
|
},
|
||||||
[TaskList.name]: (state, node) => {
|
[TaskList.name]: (state, node) => {
|
||||||
if (node.attrs.numeric) renderOrderedList(state, node);
|
state.renderList(node, ' ', () => (node.attrs.bullet || '*') + ' ');
|
||||||
else defaultMarkdownSerializer.nodes.bullet_list(state, node);
|
|
||||||
},
|
},
|
||||||
[Text.name]: defaultMarkdownSerializer.nodes.text,
|
[Text.name]: defaultMarkdownSerializer.nodes.text,
|
||||||
[Title.name]: renderHTMLNode('h1', true, true, { class: 'title' }),
|
[Title.name]: (state, node) => {
|
||||||
|
if (!node.textContent) return;
|
||||||
|
|
||||||
|
state.write(`# `);
|
||||||
|
state.text(node.textContent, false);
|
||||||
|
state.ensureNewLine();
|
||||||
|
state.closeBlock(node);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderMarkdown = (rawMarkdown) => {
|
const renderMarkdown = (rawMarkdown) => {
|
||||||
return sanitize(marked.render(rawMarkdown), {});
|
return sanitize(markdown.render(rawMarkdown), {});
|
||||||
};
|
};
|
||||||
|
|
||||||
const createMarkdownSerializer = () => ({
|
const createMarkdownSerializer = () => ({
|
|
@ -1,4 +1,3 @@
|
||||||
// @ts-ignore
|
|
||||||
const uniq = (arr: string[]) => [...new Set(arr)];
|
const uniq = (arr: string[]) => [...new Set(arr)];
|
||||||
|
|
||||||
function isString(value) {
|
function isString(value) {
|
|
@ -1,15 +0,0 @@
|
||||||
import markdownit from 'markdown-it';
|
|
||||||
import sub from 'markdown-it-sub';
|
|
||||||
import sup from 'markdown-it-sup';
|
|
||||||
import footnote from 'markdown-it-footnote';
|
|
||||||
import anchor from 'markdown-it-anchor';
|
|
||||||
import tasklist from 'markdown-it-task-lists';
|
|
||||||
import katex from '@traptitech/markdown-it-katex';
|
|
||||||
|
|
||||||
export const marked = markdownit()
|
|
||||||
.use(sub)
|
|
||||||
.use(sup)
|
|
||||||
.use(footnote)
|
|
||||||
.use(anchor)
|
|
||||||
.use(tasklist)
|
|
||||||
.use(katex);
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Tooltip as SemiTooltip } from '@douyinfe/semi-ui';
|
||||||
|
import { useToggle } from 'hooks/useToggle';
|
||||||
|
|
||||||
|
let id = 0;
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
content: React.ReactNode;
|
||||||
|
hideOnClick?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tooltip: React.FC<IProps> = ({ content, hideOnClick = false, children }) => {
|
||||||
|
const [visible, toggleVisible] = useToggle(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SemiTooltip visible={visible} content={content} zIndex={10000} trigger={'custom'}>
|
||||||
|
<span
|
||||||
|
onMouseEnter={() => {
|
||||||
|
toggleVisible(true);
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
toggleVisible(false);
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
hideOnClick && toggleVisible(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
</SemiTooltip>
|
||||||
|
);
|
||||||
|
};
|
|
@ -198,3 +198,18 @@ a {
|
||||||
.react-resizable-handle-s {
|
.react-resizable-handle-s {
|
||||||
cursor: ns-resize;
|
cursor: ns-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.react-tooltip {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
background-color: rgba(var(--semi-grey-7), 1) !important;
|
||||||
|
color: var(--semi-color-bg-0) !important;
|
||||||
|
border-radius: var(--semi-border-radius-medium) !important;
|
||||||
|
padding: 6px 8px !important;
|
||||||
|
z-index: 10000 !important;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border-top-color: rgba(var(--semi-grey-7), 1) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
.is-empty::before {
|
.is-empty::before {
|
||||||
content: attr(data-placeholder);
|
content: attr(data-placeholder);
|
||||||
float: left;
|
float: left;
|
||||||
color: #ced4da;
|
color: #aaa;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
@ -99,6 +99,7 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--semi-color-text-0);
|
color: var(--semi-color-text-0);
|
||||||
margin: 10px 0 22px;
|
margin: 10px 0 22px;
|
||||||
|
border-bottom: 1px solid var(--semi-color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
@ -275,6 +276,14 @@
|
||||||
width: 4px;
|
width: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-result {
|
||||||
|
background: rgb(255, 217, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-current {
|
||||||
|
background: rgb(255, 0, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.resize-cursor {
|
.resize-cursor {
|
||||||
|
|
|
@ -85,6 +85,7 @@ importers:
|
||||||
'@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
|
||||||
|
'@tiptap/suggestion': ^2.0.0-beta.90
|
||||||
'@traptitech/markdown-it-katex': ^3.5.0
|
'@traptitech/markdown-it-katex': ^3.5.0
|
||||||
'@types/node': 17.0.13
|
'@types/node': 17.0.13
|
||||||
'@types/react': 17.0.38
|
'@types/react': 17.0.38
|
||||||
|
@ -98,6 +99,8 @@ importers:
|
||||||
lowlight: ^2.5.0
|
lowlight: ^2.5.0
|
||||||
markdown-it: ^12.3.2
|
markdown-it: ^12.3.2
|
||||||
markdown-it-anchor: ^8.4.1
|
markdown-it-anchor: ^8.4.1
|
||||||
|
markdown-it-container: ^3.0.0
|
||||||
|
markdown-it-emoji: ^2.0.0
|
||||||
markdown-it-footnote: ^3.0.3
|
markdown-it-footnote: ^3.0.3
|
||||||
markdown-it-sub: ^1.0.0
|
markdown-it-sub: ^1.0.0
|
||||||
markdown-it-sup: ^1.0.0
|
markdown-it-sup: ^1.0.0
|
||||||
|
@ -105,11 +108,13 @@ importers:
|
||||||
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
|
||||||
|
prosemirror-utils: ^0.9.6
|
||||||
prosemirror-view: ^1.23.6
|
prosemirror-view: ^1.23.6
|
||||||
react: 17.0.2
|
react: 17.0.2
|
||||||
react-dom: 17.0.2
|
react-dom: 17.0.2
|
||||||
react-helmet: ^6.1.0
|
react-helmet: ^6.1.0
|
||||||
react-split-pane: ^0.1.92
|
react-split-pane: ^0.1.92
|
||||||
|
scroll-into-view-if-needed: ^2.2.29
|
||||||
swr: ^1.2.0
|
swr: ^1.2.0
|
||||||
tippy.js: ^6.3.7
|
tippy.js: ^6.3.7
|
||||||
tsconfig-paths-webpack-plugin: ^3.5.2
|
tsconfig-paths-webpack-plugin: ^3.5.2
|
||||||
|
@ -157,6 +162,7 @@ importers:
|
||||||
'@tiptap/extension-text-style': 2.0.0-beta.23_@tiptap+core@2.0.0-beta.171
|
'@tiptap/extension-text-style': 2.0.0-beta.23_@tiptap+core@2.0.0-beta.171
|
||||||
'@tiptap/extension-underline': 2.0.0-beta.23_@tiptap+core@2.0.0-beta.171
|
'@tiptap/extension-underline': 2.0.0-beta.23_@tiptap+core@2.0.0-beta.171
|
||||||
'@tiptap/react': 2.0.0-beta.107_a3fcdb91535fe17b69dfabaa94f3bb3d
|
'@tiptap/react': 2.0.0-beta.107_a3fcdb91535fe17b69dfabaa94f3bb3d
|
||||||
|
'@tiptap/suggestion': 2.0.0-beta.90_@tiptap+core@2.0.0-beta.171
|
||||||
'@traptitech/markdown-it-katex': 3.5.0
|
'@traptitech/markdown-it-katex': 3.5.0
|
||||||
axios: 0.25.0
|
axios: 0.25.0
|
||||||
classnames: 2.3.1
|
classnames: 2.3.1
|
||||||
|
@ -168,6 +174,8 @@ importers:
|
||||||
lowlight: 2.5.0
|
lowlight: 2.5.0
|
||||||
markdown-it: 12.3.2
|
markdown-it: 12.3.2
|
||||||
markdown-it-anchor: 8.4.1_markdown-it@12.3.2
|
markdown-it-anchor: 8.4.1_markdown-it@12.3.2
|
||||||
|
markdown-it-container: 3.0.0
|
||||||
|
markdown-it-emoji: 2.0.0
|
||||||
markdown-it-footnote: 3.0.3
|
markdown-it-footnote: 3.0.3
|
||||||
markdown-it-sub: 1.0.0
|
markdown-it-sub: 1.0.0
|
||||||
markdown-it-sup: 1.0.0
|
markdown-it-sup: 1.0.0
|
||||||
|
@ -175,11 +183,13 @@ importers:
|
||||||
marked: 4.0.12
|
marked: 4.0.12
|
||||||
next: 12.0.10_react-dom@17.0.2+react@17.0.2
|
next: 12.0.10_react-dom@17.0.2+react@17.0.2
|
||||||
prosemirror-markdown: 1.7.0
|
prosemirror-markdown: 1.7.0
|
||||||
|
prosemirror-utils: 0.9.6
|
||||||
prosemirror-view: 1.23.6
|
prosemirror-view: 1.23.6
|
||||||
react: 17.0.2
|
react: 17.0.2
|
||||||
react-dom: 17.0.2_react@17.0.2
|
react-dom: 17.0.2_react@17.0.2
|
||||||
react-helmet: 6.1.0_react@17.0.2
|
react-helmet: 6.1.0_react@17.0.2
|
||||||
react-split-pane: 0.1.92_react-dom@17.0.2+react@17.0.2
|
react-split-pane: 0.1.92_react-dom@17.0.2+react@17.0.2
|
||||||
|
scroll-into-view-if-needed: 2.2.29
|
||||||
swr: 1.2.0_react@17.0.2
|
swr: 1.2.0_react@17.0.2
|
||||||
tippy.js: 6.3.7
|
tippy.js: 6.3.7
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
@ -792,7 +802,7 @@ packages:
|
||||||
date-fns-tz: 1.2.2_date-fns@2.28.0
|
date-fns-tz: 1.2.2_date-fns@2.28.0
|
||||||
lodash: 4.17.21
|
lodash: 4.17.21
|
||||||
memoize-one: 5.2.1
|
memoize-one: 5.2.1
|
||||||
scroll-into-view-if-needed: 2.2.28
|
scroll-into-view-if-needed: 2.2.29
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@douyinfe/semi-icons/2.3.1_react@17.0.2:
|
/@douyinfe/semi-icons/2.3.1_react@17.0.2:
|
||||||
|
@ -860,7 +870,7 @@ packages:
|
||||||
react-sortable-hoc: 1.11.0_react-dom@17.0.2+react@17.0.2
|
react-sortable-hoc: 1.11.0_react-dom@17.0.2+react@17.0.2
|
||||||
react-window: 1.8.6_react-dom@17.0.2+react@17.0.2
|
react-window: 1.8.6_react-dom@17.0.2+react@17.0.2
|
||||||
resize-observer-polyfill: 1.5.1
|
resize-observer-polyfill: 1.5.1
|
||||||
scroll-into-view-if-needed: 2.2.28
|
scroll-into-view-if-needed: 2.2.29
|
||||||
utility-types: 3.10.0
|
utility-types: 3.10.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
@ -1914,6 +1924,17 @@ packages:
|
||||||
react-dom: 17.0.2_react@17.0.2
|
react-dom: 17.0.2_react@17.0.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@tiptap/suggestion/2.0.0-beta.90_@tiptap+core@2.0.0-beta.171:
|
||||||
|
resolution: {integrity: sha512-L5PPYRatY/75uJJRQx2o/Ce+gzcOkmd81TwLjio9sADV3bRf4DO4WYcQy0AtGe6uNSz78DTL0SUVw4204VjoBw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@tiptap/core': ^2.0.0-beta.1
|
||||||
|
dependencies:
|
||||||
|
'@tiptap/core': 2.0.0-beta.171
|
||||||
|
prosemirror-model: 1.16.1
|
||||||
|
prosemirror-state: 1.3.4
|
||||||
|
prosemirror-view: 1.23.6
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@tootallnate/once/1.1.2:
|
/@tootallnate/once/1.1.2:
|
||||||
resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==}
|
resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
@ -5958,6 +5979,14 @@ packages:
|
||||||
markdown-it: 12.3.2
|
markdown-it: 12.3.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/markdown-it-container/3.0.0:
|
||||||
|
resolution: {integrity: sha512-y6oKTq4BB9OQuY/KLfk/O3ysFhB3IMYoIWhGJEidXt1NQFocFK2sA2t0NYZAMyMShAGL6x5OPIbrmXPIqaN9rw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/markdown-it-emoji/2.0.0:
|
||||||
|
resolution: {integrity: sha512-39j7/9vP/CPCKbEI44oV8yoPJTpvfeReTn/COgRhSpNrjWF3PfP/JUxxB0hxV6ynOY8KH8Y8aX9NMDdo6z+6YQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/markdown-it-footnote/3.0.3:
|
/markdown-it-footnote/3.0.3:
|
||||||
resolution: {integrity: sha512-YZMSuCGVZAjzKMn+xqIco9d1cLGxbELHZ9do/TSYVzraooV8ypsppKNmUJ0fVH5ljkCInQAtFpm8Rb3eXSrt5w==}
|
resolution: {integrity: sha512-YZMSuCGVZAjzKMn+xqIco9d1cLGxbELHZ9do/TSYVzraooV8ypsppKNmUJ0fVH5ljkCInQAtFpm8Rb3eXSrt5w==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -6811,6 +6840,14 @@ packages:
|
||||||
prosemirror-model: 1.16.1
|
prosemirror-model: 1.16.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/prosemirror-utils/0.9.6:
|
||||||
|
resolution: {integrity: sha512-UC+j9hQQ1POYfMc5p7UFxBTptRiGPR7Kkmbl3jVvU8VgQbkI89tR/GK+3QYC8n+VvBZrtAoCrJItNhWSxX3slA==}
|
||||||
|
peerDependencies:
|
||||||
|
prosemirror-model: ^1.0.0
|
||||||
|
prosemirror-state: ^1.0.1
|
||||||
|
prosemirror-tables: ^0.9.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/prosemirror-view/1.23.6:
|
/prosemirror-view/1.23.6:
|
||||||
resolution: {integrity: sha512-B4DAzriNpI/AVoW0Lu6SVfX00jZZQxOVwdBQEjWlRbCdT9V0pvk4GQJ3JTFaib+b6BcPdRZ3MjWXz2xvV1rblA==}
|
resolution: {integrity: sha512-B4DAzriNpI/AVoW0Lu6SVfX00jZZQxOVwdBQEjWlRbCdT9V0pvk4GQJ3JTFaib+b6BcPdRZ3MjWXz2xvV1rblA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -7274,8 +7311,8 @@ packages:
|
||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
ajv-keywords: 3.5.2_ajv@6.12.6
|
ajv-keywords: 3.5.2_ajv@6.12.6
|
||||||
|
|
||||||
/scroll-into-view-if-needed/2.2.28:
|
/scroll-into-view-if-needed/2.2.29:
|
||||||
resolution: {integrity: sha512-8LuxJSuFVc92+0AdNv4QOxRL4Abeo1DgLnGNkn1XlaujPH/3cCFz3QI60r2VNu4obJJROzgnIUw5TKQkZvZI1w==}
|
resolution: {integrity: sha512-hxpAR6AN+Gh53AdAimHM6C8oTN1ppwVZITihix+WqalywBeFcQ6LdQP5ABNl26nX8GTEL7VT+b8lKpdqq65wXg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
compute-scroll-into-view: 1.0.17
|
compute-scroll-into-view: 1.0.17
|
||||||
dev: false
|
dev: false
|
||||||
|
|
Loading…
Reference in New Issue