mirror of https://github.com/fantasticit/think.git
feat: improve tiptap
This commit is contained in:
parent
cc30e00984
commit
1835f8504b
|
@ -72,6 +72,7 @@
|
|||
"marked": "^4.0.12",
|
||||
"next": "12.0.10",
|
||||
"prosemirror-markdown": "^1.7.0",
|
||||
"prosemirror-tables": "^1.1.1",
|
||||
"prosemirror-utils": "^0.9.6",
|
||||
"prosemirror-view": "^1.23.6",
|
||||
"react": "17.0.2",
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import { Icon } from '@douyinfe/semi-ui';
|
||||
|
||||
export const IconHorizontalRule: 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="none" fill-rule="evenodd">
|
||||
<rect fill="currentColor" x="17" y="116" width="223" height="24" rx="12"></rect>
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
import { Icon } from '@douyinfe/semi-ui';
|
||||
|
||||
export const IconList: 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="M215 118c5.523 0 10 4.477 10 10 0 5.43-4.327 9.848-9.72 9.996L215 138H96c-5.523 0-10-4.477-10-10 0-5.43 4.327-9.848 9.72-9.996L96 118h119ZM215 35c5.523 0 10 4.477 10 10 0 5.43-4.327 9.848-9.72 9.996L215 55H96c-5.523 0-10-4.477-10-10 0-5.43 4.327-9.848 9.72-9.996L96 35h119ZM215 201c5.523 0 10 4.477 10 10 0 5.43-4.327 9.848-9.72 9.996L215 221H96c-5.523 0-10-4.477-10-10 0-5.43 4.327-9.848 9.72-9.996L96 201h119Z"></path>
|
||||
</g>
|
||||
<circle cx="44" cy="45" r="16"></circle>
|
||||
<circle cx="44" cy="128" r="16"></circle>
|
||||
<circle cx="44" cy="211" r="16"></circle>
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
import { Icon } from '@douyinfe/semi-ui';
|
||||
|
||||
export const IconOrderedList: 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="nonzero">
|
||||
<path d="M215 118c5.523 0 10 4.477 10 10 0 5.43-4.327 9.848-9.72 9.996L215 138H101c-5.523 0-10-4.477-10-10 0-5.43 4.327-9.848 9.72-9.996L101 118h114ZM215 35c5.523 0 10 4.477 10 10 0 5.43-4.327 9.848-9.72 9.996L215 55H101c-5.523 0-10-4.477-10-10 0-5.43 4.327-9.848 9.72-9.996L101 35h114ZM215 201c5.523 0 10 4.477 10 10 0 5.43-4.327 9.848-9.72 9.996L215 221H101c-5.523 0-10-4.477-10-10 0-5.43 4.327-9.848 9.72-9.996L101 201h114ZM65.988 190.061l-.739.79-1.88 2.061-4.394 4.885-6.435 7.205h13.852a9 9 0 0 1 8.996 8.736l.004.265a9 9 0 0 1-8.735 8.996l-.265.004H32.5c-7.762 0-11.883-9.167-6.732-14.973l15.666-17.621 6.446-7.197 3.323-3.665 1.424-1.54.623-.653.408-.407.042-.039c1.203-1.106 1.855-2.536 1.855-4.05 0-3.128-2.87-5.855-6.637-5.855-3.681 0-6.508 2.607-6.632 5.647l-.004.208a9 9 0 1 1-18 0c0-13.281 11.13-23.855 24.636-23.855 13.507 0 24.637 10.574 24.637 23.855 0 6.497-2.703 12.572-7.355 17.003l-.212.2ZM43.206 25.413c5.694-5.302 14.934-1.363 15.13 6.337l.003.25v73a9 9 0 0 1-17.996.265L40.34 105V52.583c-3.556 2.568-8.513 2.208-11.653-.96l-.198-.207a9 9 0 0 1 .247-12.522l.206-.198 14.265-13.283Z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
import { Icon } from '@douyinfe/semi-ui';
|
||||
|
||||
export const IconQuote: 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="none" fill-rule="evenodd">
|
||||
<g fill="currentColor">
|
||||
<path d="M36 143h44c6.627 0 12 5.373 12 12v44c0 6.627-5.373 12-12 12H36c-6.627 0-12-5.373-12-12v-44c0-6.627 5.373-12 12-12Z"></path>
|
||||
<path
|
||||
d="M84.144 53.257c5.502-.485 10.355 3.582 10.84 9.084.476 5.408-3.447 10.19-8.806 10.81l-.279.029c-23.384 2.06-41.533 21.544-41.894 45.105L44 119v41.723c0 5.523-4.477 10-10 10-5.43 0-9.848-4.326-9.996-9.72l-.004-.28V119c0-34.269 26.22-62.755 60.144-65.743Z"
|
||||
fill-rule="nonzero"
|
||||
></path>
|
||||
<path d="M43.272 120.769 38.56 142.58a9.567 9.567 0 0 0 13.93 10.42l11.401-6.216a2.015 2.015 0 0 0-.964-3.784h-2.933c-8.837 0-16-7.164-16-16v-6.155a.365.365 0 0 0-.722-.077Z"></path>
|
||||
</g>
|
||||
<g fill="currentColor">
|
||||
<path d="M176 143h44c6.627 0 12 5.373 12 12v44c0 6.627-5.373 12-12 12h-44c-6.627 0-12-5.373-12-12v-44c0-6.627 5.373-12 12-12Z"></path>
|
||||
<path
|
||||
d="M224.144 53.257c5.502-.485 10.355 3.582 10.84 9.084.476 5.408-3.447 10.19-8.806 10.81l-.279.029c-23.384 2.06-41.533 21.544-41.894 45.105L184 119v41.723c0 5.523-4.477 10-10 10-5.43 0-9.848-4.326-9.996-9.72l-.004-.28V119c0-34.269 26.22-62.755 60.144-65.743Z"
|
||||
fill-rule="nonzero"
|
||||
></path>
|
||||
<path d="m183.272 120.769-4.712 21.812a9.567 9.567 0 0 0 13.93 10.42l11.401-6.216a2.015 2.015 0 0 0-.964-3.784h-2.933c-8.837 0-16-7.164-16-16v-6.155a.365.365 0 0 0-.722-.077Z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -5,12 +5,19 @@ export const IconTask: React.FC<{ style?: React.CSSProperties }> = ({ style = {}
|
|||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" role="presentation">
|
||||
<path
|
||||
d="M7.5 6h9A1.5 1.5 0 0118 7.5v9a1.5 1.5 0 01-1.5 1.5h-9A1.5 1.5 0 016 16.5v-9A1.5 1.5 0 017.5 6zm3.072 8.838l.143.154a.5.5 0 00.769-.042l.13-.175 3.733-5.045a.8.8 0 00-.11-1.064.665.665 0 00-.984.118l-3.243 4.387-1.315-1.422a.663.663 0 00-.99 0 .801.801 0 000 1.07l1.867 2.019z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
></path>
|
||||
<svg width="16" height="16" viewBox="0 0 256 256" role="presentation">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path
|
||||
d="M169.002 89.757c3.945-3.865 10.276-3.8 14.141.145 3.795 3.873 3.801 10.047.067 13.928l-.212.213-63.285 62a10 10 0 0 1-13.776.209l-.219-.208-32.715-32.041c-3.946-3.865-4.012-10.196-.147-14.142 3.794-3.873 9.966-4.007 13.924-.354l.217.207 25.717 25.187 56.288-55.144Z"
|
||||
fill="currentColor"
|
||||
fill-rule="nonzero"
|
||||
></path>
|
||||
<path
|
||||
d="M201 23c17.673 0 32 14.327 32 32v146c0 17.673-14.327 32-32 32H55c-17.673 0-32-14.327-32-32V55c0-17.673 14.327-32 32-32h146Zm0 20H55c-6.525 0-11.834 5.209-11.996 11.695L43 55v146c0 6.525 5.209 11.834 11.695 11.996L55 213h146c6.525 0 11.834-5.209 11.996-11.695L213 201V55c0-6.525-5.209-11.834-11.695-11.996L201 43Z"
|
||||
fill="currentColor"
|
||||
fill-rule="nonzero"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -35,3 +35,7 @@ export * from './IconAttachment';
|
|||
export * from './IconMath';
|
||||
export * from './IconSearch';
|
||||
export * from './IconSearchReplace';
|
||||
export * from './IconQuote';
|
||||
export * from './IconHorizontalRule';
|
||||
export * from './IconOrderedList';
|
||||
export * from './IconList';
|
||||
|
|
|
@ -12,6 +12,7 @@ import { DocumentChildren } from './extensions/documentChildren';
|
|||
import { DocumentReference } from './extensions/documentReference';
|
||||
import { Dropcursor } from './extensions/dropCursor';
|
||||
import { Emoji } from './extensions/emoji';
|
||||
import { EvokeMenu } from './extensions/evokeMenu';
|
||||
import { FontSize } from './extensions/fontSize';
|
||||
import { FootnoteDefinition } from './extensions/footnoteDefinition';
|
||||
import { FootnoteReference } from './extensions/footnoteReference';
|
||||
|
@ -65,6 +66,7 @@ export const BaseKit = [
|
|||
DocumentReference,
|
||||
Dropcursor,
|
||||
Emoji,
|
||||
EvokeMenu,
|
||||
FontSize,
|
||||
FootnoteDefinition,
|
||||
FootnoteReference,
|
||||
|
|
|
@ -3,6 +3,19 @@
|
|||
padding-top: 12px;
|
||||
border-top: 1px solid var(--semi-color-border);
|
||||
|
||||
&.isEditable {
|
||||
&:hover {
|
||||
outline: 1px solid var(--semi-color-link);
|
||||
}
|
||||
|
||||
.itemWrap {
|
||||
&:hover {
|
||||
color: var(--semi-color-text-1);
|
||||
border-color: var(--semi-color-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.itemWrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Link from 'next/link';
|
||||
import cls from 'classnames';
|
||||
import { Typography } from '@douyinfe/semi-ui';
|
||||
import { useChildrenDocument } from 'data/document';
|
||||
import { DataRender } from 'components/data-render';
|
||||
|
@ -10,7 +11,8 @@ import styles from './index.module.scss';
|
|||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const DocumentChildrenWrapper = () => {
|
||||
export const DocumentChildrenWrapper = ({ editor }) => {
|
||||
const isEditable = editor.isEditable;
|
||||
const { pathname, query } = useRouter();
|
||||
const wikiId = query?.wikiId;
|
||||
const documentId = query?.documentId;
|
||||
|
@ -18,7 +20,7 @@ export const DocumentChildrenWrapper = () => {
|
|||
const { data: documents, loading, error } = useChildrenDocument({ wikiId, documentId, isShare });
|
||||
|
||||
return (
|
||||
<NodeViewWrapper as="div" className={styles.wrap}>
|
||||
<NodeViewWrapper as="div" className={cls(styles.wrap, isEditable && styles.isEditable)}>
|
||||
<div>
|
||||
<div>
|
||||
<Text type="tertiary">子文档</Text>
|
||||
|
|
|
@ -1,6 +1,19 @@
|
|||
.wrap {
|
||||
margin: 8px 0;
|
||||
|
||||
&.isEditable {
|
||||
&:hover {
|
||||
outline: 1px solid var(--semi-color-link);
|
||||
}
|
||||
|
||||
.itemWrap {
|
||||
&:hover {
|
||||
color: var(--semi-color-text-1);
|
||||
border-color: var(--semi-color-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.itemWrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Link from 'next/link';
|
||||
import cls from 'classnames';
|
||||
import { Select } from '@douyinfe/semi-ui';
|
||||
import { useWikiTocs } from 'data/wiki';
|
||||
import { DataRender } from 'components/data-render';
|
||||
|
@ -21,7 +22,7 @@ export const DocumentReferenceWrapper = ({ editor, node, updateAttributes }) =>
|
|||
};
|
||||
|
||||
return (
|
||||
<NodeViewWrapper as="div" className={styles.wrap}>
|
||||
<NodeViewWrapper as="div" className={cls(styles.wrap, isEditable && styles.isEditable)}>
|
||||
<div>
|
||||
{isEditable && (
|
||||
<DataRender
|
||||
|
|
|
@ -4,7 +4,7 @@ import scrollIntoView from 'scroll-into-view-if-needed';
|
|||
import styles from './index.module.scss';
|
||||
|
||||
interface IProps {
|
||||
items: any[];
|
||||
items: Array<{ name: string; emoji: string; fallbackImage?: string }>;
|
||||
command: any;
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,7 @@ export const EmojiList: React.FC<IProps> = forwardRef((props, ref) => {
|
|||
useEffect(() => setSelectedIndex(0), [props.items]);
|
||||
|
||||
useEffect(() => {
|
||||
if (Number.isNaN(selectedIndex + 1)) return;
|
||||
const el = $container.current.querySelector(`button:nth-of-type(${selectedIndex + 1})`);
|
||||
el && scrollIntoView(el, { behavior: 'smooth', scrollMode: 'if-needed' });
|
||||
}, [selectedIndex]);
|
||||
|
@ -63,15 +64,19 @@ export const EmojiList: React.FC<IProps> = forwardRef((props, ref) => {
|
|||
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>
|
||||
))}
|
||||
{props.items.length ? (
|
||||
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 className={styles.item}>没有找到结果</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
.items {
|
||||
width: 160px;
|
||||
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,85 @@
|
|||
import { Editor } from '@tiptap/core';
|
||||
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 {
|
||||
editor: Editor;
|
||||
items: Array<{ label: React.ReactNode | ((editor: Editor) => React.ReactNode) }>;
|
||||
command: any;
|
||||
}
|
||||
|
||||
export const MenuList: 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(() => {
|
||||
if (Number.isNaN(selectedIndex + 1)) return;
|
||||
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.length ? (
|
||||
props.items.map((item, index) => (
|
||||
<button
|
||||
className={cls(styles.item, index === selectedIndex ? styles['is-selected'] : '')}
|
||||
key={index}
|
||||
onClick={() => selectItem(index)}
|
||||
>
|
||||
{typeof item.label === 'function' ? item.label(props.editor) : item.label}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className={styles.item}>没有找到结果</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
||||
import { Space, Popover, Tag, Input } from '@douyinfe/semi-ui';
|
||||
|
||||
export const TableWrapper = ({ editor, node, updateAttributes }) => {
|
||||
const isEditable = editor.isEditable;
|
||||
const { color, text } = node.attrs;
|
||||
const content = <Tag color={color}>{text || '点击设置状态'}</Tag>;
|
||||
|
||||
console.log(node.attrs);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper as="div">
|
||||
<table>
|
||||
<NodeViewContent></NodeViewContent>
|
||||
</table>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
|
@ -15,8 +15,8 @@ export const DocumentChildrenInputRegex = /^documentChildren\$$/;
|
|||
export const DocumentChildren = Node.create({
|
||||
name: 'documentChildren',
|
||||
group: 'block',
|
||||
defining: true,
|
||||
draggable: true,
|
||||
selectable: true,
|
||||
atom: true,
|
||||
|
||||
addAttributes() {
|
||||
|
|
|
@ -15,9 +15,9 @@ export const DocumentReferenceInputRegex = /^documentReference\$$/;
|
|||
export const DocumentReference = Node.create({
|
||||
name: 'documentReference',
|
||||
group: 'block',
|
||||
defining: true,
|
||||
draggable: true,
|
||||
atom: true,
|
||||
selectable: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Node } from '@tiptap/core';
|
||||
import { Node, findParentNode } from '@tiptap/core';
|
||||
import { ReactRenderer } from '@tiptap/react';
|
||||
import { PluginKey } from 'prosemirror-state';
|
||||
import { Plugin, PluginKey } from 'prosemirror-state';
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view';
|
||||
import Suggestion from '@tiptap/suggestion';
|
||||
import tippy from 'tippy.js';
|
||||
import { EmojiList } from '../components/emojiList';
|
||||
|
@ -41,11 +42,48 @@ export const Emoji = Node.create({
|
|||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const { editor } = this;
|
||||
|
||||
return [
|
||||
Suggestion({
|
||||
editor: this.editor,
|
||||
...this.options.suggestion,
|
||||
}),
|
||||
|
||||
new Plugin({
|
||||
key: new PluginKey('emojiPlaceholder'),
|
||||
props: {
|
||||
decorations: (state) => {
|
||||
if (!editor.isEditable) return;
|
||||
|
||||
const parent = findParentNode((node) => node.type.name === 'paragraph')(
|
||||
state.selection
|
||||
);
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const decorations: Decoration[] = [];
|
||||
const isEmpty = parent && parent.node.content.size === 0;
|
||||
const isSlash = parent && parent.node.textContent === ':';
|
||||
const isTopLevel = state.selection.$from.depth === 1;
|
||||
|
||||
if (isTopLevel) {
|
||||
if (isSlash) {
|
||||
decorations.push(
|
||||
Decoration.node(parent.pos, parent.pos + parent.node.nodeSize, {
|
||||
'class': 'placeholder',
|
||||
'data-placeholder': ` 继续输入进行过滤`,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return DecorationSet.create(state.doc, decorations);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
}).configure({
|
||||
|
@ -56,9 +94,13 @@ export const Emoji = Node.create({
|
|||
render: () => {
|
||||
let component;
|
||||
let popup;
|
||||
let isEditable;
|
||||
|
||||
return {
|
||||
onStart: (props) => {
|
||||
isEditable = props.editor.isEditable;
|
||||
if (!isEditable) return;
|
||||
|
||||
component = new ReactRenderer(EmojiList, {
|
||||
props,
|
||||
editor: props.editor,
|
||||
|
@ -76,6 +118,8 @@ export const Emoji = Node.create({
|
|||
},
|
||||
|
||||
onUpdate(props) {
|
||||
if (!isEditable) return;
|
||||
|
||||
component.updateProps(props);
|
||||
popup[0].setProps({
|
||||
getReferenceClientRect: props.clientRect,
|
||||
|
@ -83,6 +127,8 @@ export const Emoji = Node.create({
|
|||
},
|
||||
|
||||
onKeyDown(props) {
|
||||
if (!isEditable) return;
|
||||
|
||||
if (props.event.key === 'Escape') {
|
||||
popup[0].hide();
|
||||
return true;
|
||||
|
@ -91,6 +137,8 @@ export const Emoji = Node.create({
|
|||
},
|
||||
|
||||
onExit() {
|
||||
if (!isEditable) return;
|
||||
|
||||
popup[0].destroy();
|
||||
component.destroy();
|
||||
},
|
||||
|
|
|
@ -0,0 +1,383 @@
|
|||
import { Node, findParentNode } from '@tiptap/core';
|
||||
import { ReactRenderer } from '@tiptap/react';
|
||||
import { Plugin, PluginKey } from 'prosemirror-state';
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view';
|
||||
import Suggestion from '@tiptap/suggestion';
|
||||
import tippy from 'tippy.js';
|
||||
import { Space } from '@douyinfe/semi-ui';
|
||||
import { IconList, IconOrderedList } from '@douyinfe/semi-icons';
|
||||
import {
|
||||
IconLink,
|
||||
IconQuote,
|
||||
IconHorizontalRule,
|
||||
IconTask,
|
||||
IconDocument,
|
||||
IconMind,
|
||||
IconTable,
|
||||
IconImage,
|
||||
IconCodeBlock,
|
||||
IconStatus,
|
||||
IconInfo,
|
||||
IconAttachment,
|
||||
IconMath,
|
||||
} from 'components/icons';
|
||||
import { Upload } from 'components/upload';
|
||||
import { MenuList } from '../components/menuList';
|
||||
import { getImageOriginSize } from '../services/image';
|
||||
|
||||
export const EvokeMenuPluginKey = new PluginKey('evokeMenu');
|
||||
|
||||
const COMMANDS = [
|
||||
{
|
||||
key: '标题1',
|
||||
label: '标题1',
|
||||
command: (editor) => editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
||||
},
|
||||
{
|
||||
key: '标题1',
|
||||
label: '标题2',
|
||||
command: (editor) => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
||||
},
|
||||
{
|
||||
key: '标题1',
|
||||
label: '标题3',
|
||||
command: (editor) => editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
||||
},
|
||||
{
|
||||
key: '标题1',
|
||||
label: '标题4',
|
||||
command: (editor) => editor.chain().focus().toggleHeading({ level: 4 }).run(),
|
||||
},
|
||||
{
|
||||
key: '标题1',
|
||||
label: '标题5',
|
||||
command: (editor) => editor.chain().focus().toggleHeading({ level: 5 }).run(),
|
||||
},
|
||||
{
|
||||
key: '标题1',
|
||||
label: '标题6',
|
||||
command: (editor) => editor.chain().focus().toggleHeading({ level: 6 }).run(),
|
||||
},
|
||||
{
|
||||
key: '无序列表',
|
||||
label: (
|
||||
<Space>
|
||||
<IconList />
|
||||
无序列表
|
||||
</Space>
|
||||
),
|
||||
command: (editor) => editor.chain().focus().toggleBulletList().run(),
|
||||
},
|
||||
{
|
||||
key: '有序列表',
|
||||
label: (
|
||||
<Space>
|
||||
<IconOrderedList />
|
||||
有序列表
|
||||
</Space>
|
||||
),
|
||||
command: (editor) => editor.chain().focus().toggleOrderedList().run(),
|
||||
},
|
||||
{
|
||||
key: '任务列表',
|
||||
label: (
|
||||
<Space>
|
||||
<IconTask />
|
||||
任务列表
|
||||
</Space>
|
||||
),
|
||||
command: (editor) => editor.chain().focus().toggleTaskList().run(),
|
||||
},
|
||||
{
|
||||
key: '链接',
|
||||
label: (
|
||||
<Space>
|
||||
<IconLink />
|
||||
链接
|
||||
</Space>
|
||||
),
|
||||
command: (editor) => editor.chain().focus().toggleLink().run(),
|
||||
},
|
||||
{
|
||||
key: '引用',
|
||||
label: (
|
||||
<Space>
|
||||
<IconQuote />
|
||||
引用
|
||||
</Space>
|
||||
),
|
||||
command: (editor) => editor.chain().focus().toggleBlockquote().run(),
|
||||
},
|
||||
{
|
||||
key: '分割线',
|
||||
label: (
|
||||
<Space>
|
||||
<IconHorizontalRule />
|
||||
分割线
|
||||
</Space>
|
||||
),
|
||||
command: (editor) => editor.chain().focus().setHorizontalRule().run(),
|
||||
},
|
||||
{
|
||||
key: '表格',
|
||||
label: (
|
||||
<Space>
|
||||
<IconTable />
|
||||
表格
|
||||
</Space>
|
||||
),
|
||||
command: (editor) =>
|
||||
editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(),
|
||||
},
|
||||
{
|
||||
key: '代码块',
|
||||
label: (
|
||||
<Space>
|
||||
<IconCodeBlock />
|
||||
代码块
|
||||
</Space>
|
||||
),
|
||||
command: (editor) => editor.chain().focus().toggleCodeBlock().run(),
|
||||
},
|
||||
{
|
||||
key: '图片',
|
||||
label: (editor) => (
|
||||
<Space>
|
||||
<IconImage />
|
||||
<Upload
|
||||
accept="image/*"
|
||||
onOK={async (url, fileName) => {
|
||||
const { width, height } = await getImageOriginSize(url);
|
||||
console.log('upload', width, height);
|
||||
editor.chain().focus().setImage({ src: url, alt: fileName, width, height }).run();
|
||||
}}
|
||||
>
|
||||
{() => '图片'}
|
||||
</Upload>
|
||||
</Space>
|
||||
),
|
||||
command: (editor) => {},
|
||||
},
|
||||
{
|
||||
key: '附件',
|
||||
label: (editor) => (
|
||||
<Space>
|
||||
<IconAttachment />
|
||||
<Upload
|
||||
onOK={(url, name) => {
|
||||
editor.chain().focus().setAttachment({ url, name }).run();
|
||||
}}
|
||||
>
|
||||
{() => '附件'}
|
||||
</Upload>
|
||||
</Space>
|
||||
),
|
||||
command: (editor) => {},
|
||||
},
|
||||
{
|
||||
key: '外链',
|
||||
label: (
|
||||
<Space>
|
||||
<IconLink />
|
||||
外链
|
||||
</Space>
|
||||
),
|
||||
command: (editor) => editor.chain().focus().insertIframe({ url: '' }).run(),
|
||||
},
|
||||
{
|
||||
key: '思维导图',
|
||||
label: (
|
||||
<Space>
|
||||
<IconMind />
|
||||
思维导图
|
||||
</Space>
|
||||
),
|
||||
command: (editor) => editor.chain().focus().insertMind().run(),
|
||||
},
|
||||
{
|
||||
key: '数学公式',
|
||||
label: (
|
||||
<Space>
|
||||
<IconMath />
|
||||
数学公式
|
||||
</Space>
|
||||
),
|
||||
command: (editor) => editor.chain().focus().setKatex().run(),
|
||||
},
|
||||
{
|
||||
key: '状态',
|
||||
label: (
|
||||
<Space>
|
||||
<IconStatus />
|
||||
状态
|
||||
</Space>
|
||||
),
|
||||
command: (editor) => editor.chain().focus().setStatus().run(),
|
||||
},
|
||||
{
|
||||
key: '信息框',
|
||||
label: (
|
||||
<Space>
|
||||
<IconInfo />
|
||||
信息框
|
||||
</Space>
|
||||
),
|
||||
command: (editor) => editor.chain().focus().setBanner({ type: 'info' }).run(),
|
||||
},
|
||||
{
|
||||
key: '文档',
|
||||
label: (
|
||||
<Space>
|
||||
<IconDocument />
|
||||
文档
|
||||
</Space>
|
||||
),
|
||||
command: (editor) => editor.chain().focus().setDocumentReference().run(),
|
||||
},
|
||||
{
|
||||
key: '子文档',
|
||||
label: (
|
||||
<Space>
|
||||
<IconDocument />
|
||||
子文档
|
||||
</Space>
|
||||
),
|
||||
command: (editor) => editor.chain().focus().setDocumentChildren().run(),
|
||||
},
|
||||
];
|
||||
|
||||
export const EvokeMenu = Node.create({
|
||||
name: 'evokeMenu',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
suggestion: {
|
||||
char: '/',
|
||||
pluginKey: EvokeMenuPluginKey,
|
||||
command: ({ editor, range, props }) => {
|
||||
const { state, dispatch } = editor.view;
|
||||
const $from = state.selection.$from;
|
||||
const tr = state.tr.deleteRange($from.start(), $from.pos);
|
||||
dispatch(tr);
|
||||
props?.command(editor);
|
||||
editor.view.focus();
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const { editor } = this;
|
||||
|
||||
return [
|
||||
Suggestion({
|
||||
editor: this.editor,
|
||||
...this.options.suggestion,
|
||||
}),
|
||||
|
||||
new Plugin({
|
||||
key: new PluginKey('evokeMenuPlaceholder'),
|
||||
props: {
|
||||
decorations: (state) => {
|
||||
if (!editor.isEditable) return;
|
||||
|
||||
const parent = findParentNode((node) => node.type.name === 'paragraph')(
|
||||
state.selection
|
||||
);
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const decorations: Decoration[] = [];
|
||||
const isEmpty = parent && parent.node.content.size === 0;
|
||||
const isSlash = parent && parent.node.textContent === '/';
|
||||
const isTopLevel = state.selection.$from.depth === 1;
|
||||
|
||||
if (isTopLevel) {
|
||||
if (isEmpty) {
|
||||
decorations.push(
|
||||
Decoration.node(parent.pos, parent.pos + parent.node.nodeSize, {
|
||||
'class': 'placeholder',
|
||||
'data-placeholder': '输入 / 唤起更多',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isSlash) {
|
||||
decorations.push(
|
||||
Decoration.node(parent.pos, parent.pos + parent.node.nodeSize, {
|
||||
'class': 'placeholder',
|
||||
'data-placeholder': ` 继续输入进行过滤`,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return DecorationSet.create(state.doc, decorations);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
}).configure({
|
||||
suggestion: {
|
||||
items: ({ query }) => {
|
||||
return COMMANDS.filter((command) => command.key.startsWith(query));
|
||||
},
|
||||
render: () => {
|
||||
let component;
|
||||
let popup;
|
||||
let isEditable;
|
||||
|
||||
return {
|
||||
onStart: (props) => {
|
||||
isEditable = props.editor.isEditable;
|
||||
if (!isEditable) return;
|
||||
|
||||
component = new ReactRenderer(MenuList, {
|
||||
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) {
|
||||
if (!isEditable) return;
|
||||
|
||||
component.updateProps(props);
|
||||
popup[0].setProps({
|
||||
getReferenceClientRect: props.clientRect,
|
||||
});
|
||||
},
|
||||
|
||||
onKeyDown(props) {
|
||||
if (!isEditable) return;
|
||||
|
||||
if (props.event.key === 'Escape') {
|
||||
popup[0].hide();
|
||||
return true;
|
||||
}
|
||||
return component.ref?.onKeyDown(props);
|
||||
},
|
||||
|
||||
onExit(props) {
|
||||
if (!isEditable) return;
|
||||
|
||||
popup[0].destroy();
|
||||
component.destroy();
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,11 +1,13 @@
|
|||
import BuiltInPlaceholder from '@tiptap/extension-placeholder';
|
||||
|
||||
export const Placeholder = BuiltInPlaceholder.configure({
|
||||
placeholder: ({ node }) => {
|
||||
placeholder: ({ node, editor }) => {
|
||||
if (!editor.isEditable) return;
|
||||
|
||||
if (node.type.name === 'title') {
|
||||
return '请输入标题';
|
||||
}
|
||||
return '请输入内容';
|
||||
return '输入 / 唤起更多';
|
||||
},
|
||||
showOnlyCurrent: false,
|
||||
showOnlyWhenEditable: true,
|
||||
|
|
|
@ -1 +1,120 @@
|
|||
export { TableCell } from '@tiptap/extension-table-cell';
|
||||
import { TableCell as BuiltInTable } from '@tiptap/extension-table-cell';
|
||||
import { Plugin, PluginKey } from 'prosemirror-state';
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view';
|
||||
import {
|
||||
getCellsInColumn,
|
||||
isRowSelected,
|
||||
isTableSelected,
|
||||
selectRow,
|
||||
selectTable,
|
||||
} from '../services/table';
|
||||
|
||||
export const TableCell = BuiltInTable.extend({
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey(`${this.name}FloatMenu`),
|
||||
// view: () =>
|
||||
// new FloatMenuView({
|
||||
// editor: this.editor,
|
||||
// // has one selected should show
|
||||
// shouldShow: ({ editor }) => {
|
||||
// if (!editor.isEditable) {
|
||||
// return false;
|
||||
// }
|
||||
// const cells = getCellsInColumn(0)(editor.state.selection);
|
||||
// return !!cells?.some((cell, index) =>
|
||||
// isRowSelected(index)(editor.state.selection)
|
||||
// );
|
||||
// },
|
||||
// init: (dom, editor) => {
|
||||
// const insertTop = buttonView({
|
||||
// id: "insert-top",
|
||||
// name: this.options.dictionary.insertTop,
|
||||
// icon: DoubleUp({}),
|
||||
// });
|
||||
// insertTop.button.addEventListener("click", () => {
|
||||
// editor.chain().addRowBefore().run();
|
||||
// });
|
||||
// const insertBottom = buttonView({
|
||||
// id: "insert-bottom",
|
||||
// name: this.options.dictionary.insertBottom,
|
||||
// icon: DoubleDown({}),
|
||||
// });
|
||||
// insertBottom.button.addEventListener("click", () => {
|
||||
// editor.chain().addRowAfter().run();
|
||||
// });
|
||||
// const remove = buttonView({
|
||||
// name: this.options.dictionary.delete,
|
||||
// icon: Delete({}),
|
||||
// });
|
||||
// remove.button.addEventListener("click", () => {
|
||||
// if (isTableSelected(editor.state.selection)) {
|
||||
// editor.chain().deleteTable().run();
|
||||
// } else {
|
||||
// editor.chain().deleteRow().run();
|
||||
// }
|
||||
// });
|
||||
|
||||
// dom.append(insertTop.button);
|
||||
// dom.append(insertBottom.button);
|
||||
// dom.append(remove.button);
|
||||
// },
|
||||
// }),
|
||||
props: {
|
||||
decorations: (state) => {
|
||||
const { doc, selection } = state;
|
||||
const decorations: Decoration[] = [];
|
||||
const cells = getCellsInColumn(0)(selection);
|
||||
|
||||
if (cells) {
|
||||
cells.forEach(({ pos }, index) => {
|
||||
if (index === 0) {
|
||||
decorations.push(
|
||||
Decoration.widget(pos + 1, () => {
|
||||
const grip = document.createElement('a');
|
||||
grip.classList.add('grip-table');
|
||||
if (isTableSelected(selection)) {
|
||||
grip.classList.add('selected');
|
||||
}
|
||||
grip.addEventListener('mousedown', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
this.editor.view.dispatch(selectTable(this.editor.state.tr));
|
||||
});
|
||||
return grip;
|
||||
})
|
||||
);
|
||||
}
|
||||
decorations.push(
|
||||
Decoration.widget(pos + 1, () => {
|
||||
const rowSelected = isRowSelected(index)(selection);
|
||||
const grip = document.createElement('a');
|
||||
grip.classList.add('grip-row');
|
||||
if (rowSelected) {
|
||||
grip.classList.add('selected');
|
||||
}
|
||||
if (index === 0) {
|
||||
grip.classList.add('first');
|
||||
}
|
||||
if (index === cells.length - 1) {
|
||||
grip.classList.add('last');
|
||||
}
|
||||
grip.addEventListener('mousedown', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
this.editor.view.dispatch(selectRow(index)(this.editor.state.tr));
|
||||
});
|
||||
return grip;
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return DecorationSet.create(doc, decorations);
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1 +1,94 @@
|
|||
export { TableHeader } from '@tiptap/extension-table-header';
|
||||
import { TableHeader as BuiltInTableHeader } from '@tiptap/extension-table-header';
|
||||
import { Plugin, PluginKey } from 'prosemirror-state';
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view';
|
||||
import { getCellsInRow, isColumnSelected, isTableSelected, selectColumn } from '../services/table';
|
||||
|
||||
export const TableHeader = BuiltInTableHeader.extend({
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey(`${this.name}FloatMenu`),
|
||||
// view: () =>
|
||||
// new FloatMenuView({
|
||||
// editor: this.editor,
|
||||
// // has one selected should show
|
||||
// shouldShow: ({ editor }) => {
|
||||
// if (!editor.isEditable) {
|
||||
// return false;
|
||||
// }
|
||||
// const selection = editor.state.selection;
|
||||
// if (isTableSelected(selection)) {
|
||||
// return false;
|
||||
// }
|
||||
// const cells = getCellsInRow(0)(selection);
|
||||
// return !!cells?.some((cell, index) =>
|
||||
// isColumnSelected(index)(selection)
|
||||
// );
|
||||
// },
|
||||
// init: (dom, editor) => {
|
||||
// const insertLeft = buttonView({
|
||||
// name: this.options.dictionary.insertLeft,
|
||||
// icon: DoubleLeft({}),
|
||||
// });
|
||||
// insertLeft.button.addEventListener("click", () => {
|
||||
// editor.chain().addColumnBefore().run();
|
||||
// });
|
||||
// const insertRight = buttonView({
|
||||
// name: this.options.dictionary.insertRight,
|
||||
// icon: DoubleRight({}),
|
||||
// });
|
||||
// insertRight.button.addEventListener("click", () => {
|
||||
// editor.chain().addColumnAfter().run();
|
||||
// });
|
||||
// const remove = buttonView({
|
||||
// name: this.options.dictionary.delete,
|
||||
// icon: Delete({}),
|
||||
// });
|
||||
// remove.button.addEventListener("click", () => {
|
||||
// editor.chain().deleteColumn().run();
|
||||
// });
|
||||
|
||||
// dom.append(insertLeft.button);
|
||||
// dom.append(insertRight.button);
|
||||
// dom.append(remove.button);
|
||||
// },
|
||||
// }),
|
||||
props: {
|
||||
decorations: (state) => {
|
||||
const { doc, selection } = state;
|
||||
const decorations: Decoration[] = [];
|
||||
const cells = getCellsInRow(0)(selection);
|
||||
|
||||
if (cells) {
|
||||
cells.forEach(({ pos }, index) => {
|
||||
decorations.push(
|
||||
Decoration.widget(pos + 1, () => {
|
||||
const colSelected = isColumnSelected(index)(selection);
|
||||
const grip = document.createElement('a');
|
||||
grip.classList.add('grip-column');
|
||||
if (colSelected) {
|
||||
grip.classList.add('selected');
|
||||
}
|
||||
if (index === 0) {
|
||||
grip.classList.add('first');
|
||||
} else if (index === cells.length - 1) {
|
||||
grip.classList.add('last');
|
||||
}
|
||||
grip.addEventListener('mousedown', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
this.editor.view.dispatch(selectColumn(index)(this.editor.state.tr));
|
||||
});
|
||||
return grip;
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return DecorationSet.create(doc, decorations);
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Button } from '@douyinfe/semi-ui';
|
||||
import { IconQuote, IconCheckboxIndeterminate, IconLink } from '@douyinfe/semi-icons';
|
||||
import { Tooltip } from 'components/tooltip';
|
||||
import { IconQuote, IconLink, IconHorizontalRule } from 'components/icons';
|
||||
import { isTitleActive } from '../services/isActive';
|
||||
import { Emoji } from './components/emoji';
|
||||
import { Search } from './search';
|
||||
|
@ -40,7 +40,7 @@ export const BaseInsertMenu: React.FC<{ editor: any }> = ({ editor }) => {
|
|||
<Button
|
||||
theme={'borderless'}
|
||||
type="tertiary"
|
||||
icon={<IconCheckboxIndeterminate />}
|
||||
icon={<IconHorizontalRule />}
|
||||
onClick={() => editor.chain().focus().setHorizontalRule().run()}
|
||||
disabled={isTitleActive(editor)}
|
||||
/>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Button } from '@douyinfe/semi-ui';
|
||||
import { IconList, IconOrderedList, IconIndentLeft, IconIndentRight } from '@douyinfe/semi-icons';
|
||||
import { IconIndentLeft, IconIndentRight } from '@douyinfe/semi-icons';
|
||||
import { IconOrderedList, IconList } from 'components/icons';
|
||||
import { Tooltip } from 'components/tooltip';
|
||||
import { IconTask } from 'components/icons';
|
||||
import { isTitleActive } from '../services/isActive';
|
||||
|
|
|
@ -48,7 +48,7 @@ export const TableBubbleMenu = ({ editor }) => {
|
|||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip content="删除当前列">
|
||||
<Tooltip content="删除当前列" hideOnClick>
|
||||
<Button
|
||||
onClick={() => editor.chain().focus().deleteColumn().run()}
|
||||
icon={<IconDeleteColumn />}
|
||||
|
@ -78,7 +78,7 @@ export const TableBubbleMenu = ({ editor }) => {
|
|||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="删除当前行">
|
||||
<Tooltip content="删除当前行" hideOnClick>
|
||||
<Button
|
||||
onClick={() => editor.chain().focus().deleteRow().run()}
|
||||
icon={<IconDeleteRow />}
|
||||
|
|
|
@ -0,0 +1,228 @@
|
|||
import { findParentNode } from '@tiptap/core';
|
||||
import { Selection, Transaction } from 'prosemirror-state';
|
||||
import { CellSelection, TableMap } from 'prosemirror-tables';
|
||||
import { Node, ResolvedPos } from 'prosemirror-model';
|
||||
|
||||
export const isRectSelected = (rect: any) => (selection: CellSelection) => {
|
||||
const map = TableMap.get(selection.$anchorCell.node(-1));
|
||||
const start = selection.$anchorCell.start(-1);
|
||||
const cells = map.cellsInRect(rect);
|
||||
const selectedCells = map.cellsInRect(
|
||||
map.rectBetween(selection.$anchorCell.pos - start, selection.$headCell.pos - start)
|
||||
);
|
||||
|
||||
for (let i = 0, count = cells.length; i < count; i++) {
|
||||
if (selectedCells.indexOf(cells[i]) === -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const findTable = (selection: Selection) =>
|
||||
findParentNode((node) => node.type.spec.tableRole && node.type.spec.tableRole === 'table')(
|
||||
selection
|
||||
);
|
||||
|
||||
export const isCellSelection = (selection: any) => {
|
||||
return selection instanceof CellSelection;
|
||||
};
|
||||
|
||||
export const isColumnSelected = (columnIndex: number) => (selection: any) => {
|
||||
if (isCellSelection(selection)) {
|
||||
const map = TableMap.get(selection.$anchorCell.node(-1));
|
||||
return isRectSelected({
|
||||
left: columnIndex,
|
||||
right: columnIndex + 1,
|
||||
top: 0,
|
||||
bottom: map.height,
|
||||
})(selection);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isRowSelected = (rowIndex: number) => (selection: any) => {
|
||||
if (isCellSelection(selection)) {
|
||||
const map = TableMap.get(selection.$anchorCell.node(-1));
|
||||
return isRectSelected({
|
||||
left: 0,
|
||||
right: map.width,
|
||||
top: rowIndex,
|
||||
bottom: rowIndex + 1,
|
||||
})(selection);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isTableSelected = (selection: any) => {
|
||||
if (isCellSelection(selection)) {
|
||||
const map = TableMap.get(selection.$anchorCell.node(-1));
|
||||
return isRectSelected({
|
||||
left: 0,
|
||||
right: map.width,
|
||||
top: 0,
|
||||
bottom: map.height,
|
||||
})(selection);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getCellsInColumn = (columnIndex: number | number[]) => (selection: Selection) => {
|
||||
const table = findTable(selection);
|
||||
if (table) {
|
||||
const map = TableMap.get(table.node);
|
||||
const indexes = Array.isArray(columnIndex) ? columnIndex : Array.from([columnIndex]);
|
||||
return indexes.reduce((acc, index) => {
|
||||
if (index >= 0 && index <= map.width - 1) {
|
||||
const cells = map.cellsInRect({
|
||||
left: index,
|
||||
right: index + 1,
|
||||
top: 0,
|
||||
bottom: map.height,
|
||||
});
|
||||
return acc.concat(
|
||||
cells.map((nodePos) => {
|
||||
const node = table.node.nodeAt(nodePos);
|
||||
const pos = nodePos + table.start;
|
||||
return { pos, start: pos + 1, node };
|
||||
})
|
||||
);
|
||||
}
|
||||
return acc;
|
||||
}, [] as { pos: number; start: number; node: Node | null | undefined }[]);
|
||||
}
|
||||
};
|
||||
|
||||
export const getCellsInRow = (rowIndex: number | number[]) => (selection: Selection) => {
|
||||
const table = findTable(selection);
|
||||
if (table) {
|
||||
const map = TableMap.get(table.node);
|
||||
const indexes = Array.isArray(rowIndex) ? rowIndex : Array.from([rowIndex]);
|
||||
return indexes.reduce((acc, index) => {
|
||||
if (index >= 0 && index <= map.height - 1) {
|
||||
const cells = map.cellsInRect({
|
||||
left: 0,
|
||||
right: map.width,
|
||||
top: index,
|
||||
bottom: index + 1,
|
||||
});
|
||||
return acc.concat(
|
||||
cells.map((nodePos) => {
|
||||
const node = table.node.nodeAt(nodePos);
|
||||
const pos = nodePos + table.start;
|
||||
return { pos, start: pos + 1, node };
|
||||
})
|
||||
);
|
||||
}
|
||||
return acc;
|
||||
}, [] as { pos: number; start: number; node: Node | null | undefined }[]);
|
||||
}
|
||||
};
|
||||
|
||||
export const getCellsInTable = (selection: Selection) => {
|
||||
const table = findTable(selection);
|
||||
if (table) {
|
||||
const map = TableMap.get(table.node);
|
||||
const cells = map.cellsInRect({
|
||||
left: 0,
|
||||
right: map.width,
|
||||
top: 0,
|
||||
bottom: map.height,
|
||||
});
|
||||
return cells.map((nodePos) => {
|
||||
const node = table.node.nodeAt(nodePos);
|
||||
const pos = nodePos + table.start;
|
||||
return { pos, start: pos + 1, node };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const findParentNodeClosestToPos = (
|
||||
$pos: ResolvedPos,
|
||||
predicate: (node: Node) => boolean
|
||||
) => {
|
||||
for (let i = $pos.depth; i > 0; i--) {
|
||||
const node = $pos.node(i);
|
||||
if (predicate(node)) {
|
||||
return {
|
||||
pos: i > 0 ? $pos.before(i) : 0,
|
||||
start: $pos.start(i),
|
||||
depth: i,
|
||||
node,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const findCellClosestToPos = ($pos: ResolvedPos) => {
|
||||
const predicate = (node: Node) =>
|
||||
node.type.spec.tableRole && /cell/i.test(node.type.spec.tableRole);
|
||||
return findParentNodeClosestToPos($pos, predicate);
|
||||
};
|
||||
|
||||
const select = (type: 'row' | 'column') => (index: number) => (tr: Transaction) => {
|
||||
const table = findTable(tr.selection);
|
||||
const isRowSelection = type === 'row';
|
||||
if (table) {
|
||||
const map = TableMap.get(table.node);
|
||||
|
||||
// Check if the index is valid
|
||||
if (index >= 0 && index < (isRowSelection ? map.height : map.width)) {
|
||||
const left = isRowSelection ? 0 : index;
|
||||
const top = isRowSelection ? index : 0;
|
||||
const right = isRowSelection ? map.width : index + 1;
|
||||
const bottom = isRowSelection ? index + 1 : map.height;
|
||||
|
||||
const cellsInFirstRow = map.cellsInRect({
|
||||
left,
|
||||
top,
|
||||
right: isRowSelection ? right : left + 1,
|
||||
bottom: isRowSelection ? top + 1 : bottom,
|
||||
});
|
||||
|
||||
const cellsInLastRow =
|
||||
bottom - top === 1
|
||||
? cellsInFirstRow
|
||||
: map.cellsInRect({
|
||||
left: isRowSelection ? left : right - 1,
|
||||
top: isRowSelection ? bottom - 1 : top,
|
||||
right,
|
||||
bottom,
|
||||
});
|
||||
|
||||
const head = table.start + cellsInFirstRow[0];
|
||||
const anchor = table.start + cellsInLastRow[cellsInLastRow.length - 1];
|
||||
const $head = tr.doc.resolve(head);
|
||||
const $anchor = tr.doc.resolve(anchor);
|
||||
|
||||
// @ts-ignore
|
||||
return tr.setSelection(new CellSelection($anchor, $head));
|
||||
}
|
||||
}
|
||||
return tr;
|
||||
};
|
||||
|
||||
export const selectColumn = select('column');
|
||||
|
||||
export const selectRow = select('row');
|
||||
|
||||
export const selectTable = (tr: Transaction) => {
|
||||
const table = findTable(tr.selection);
|
||||
if (table) {
|
||||
const { map } = TableMap.get(table.node);
|
||||
if (map && map.length) {
|
||||
const head = table.start + map[0];
|
||||
const anchor = table.start + map[map.length - 1];
|
||||
const $head = tr.doc.resolve(head);
|
||||
const $anchor = tr.doc.resolve(anchor);
|
||||
|
||||
// @ts-ignore
|
||||
return tr.setSelection(new CellSelection($anchor, $head));
|
||||
}
|
||||
}
|
||||
return tr;
|
||||
};
|
|
@ -50,6 +50,14 @@
|
|||
transform: translate(10px, 10px);
|
||||
}
|
||||
|
||||
.placeholder::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
color: #aaa;
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.hr-line {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
|
@ -102,6 +110,49 @@
|
|||
border-bottom: 1px solid var(--semi-color-border);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
cursor: text;
|
||||
|
||||
&::before {
|
||||
display: inline-block;
|
||||
font-family: var(--tiptap-font-family-mono);
|
||||
color: var(--tiptap-color-text-secondly);
|
||||
font-size: 13px;
|
||||
line-height: 0;
|
||||
margin-left: -24px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
h1:not(.title):before {
|
||||
content: 'H1';
|
||||
}
|
||||
|
||||
h2::before {
|
||||
content: 'H2';
|
||||
}
|
||||
|
||||
h3::before {
|
||||
content: 'H3';
|
||||
}
|
||||
|
||||
h4::before {
|
||||
content: 'H4';
|
||||
}
|
||||
|
||||
h5::before {
|
||||
content: 'H5';
|
||||
}
|
||||
|
||||
h6::before {
|
||||
content: 'H6';
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.87em;
|
||||
margin: 26px 0 12px 0;
|
||||
|
@ -234,18 +285,21 @@
|
|||
.tableWrapper {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
padding: 1em 0 0 1em;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
margin: 14px 0;
|
||||
overflow: hidden;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
margin: 14px 0;
|
||||
overflow: visible;
|
||||
|
||||
td,
|
||||
th {
|
||||
border: 1px solid var(--semi-color-border);
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--semi-color-fill-2);
|
||||
box-sizing: border-box;
|
||||
min-width: 1em;
|
||||
padding: 3px 5px;
|
||||
|
@ -263,7 +317,76 @@
|
|||
}
|
||||
|
||||
.selectedCell {
|
||||
background: var(--semi-color-fill-0);
|
||||
border-style: double;
|
||||
border-color: var(--semi-color-info);
|
||||
background: var(--semi-color-info-light-hover);
|
||||
}
|
||||
|
||||
.grip-column {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 0.7em;
|
||||
left: 0;
|
||||
top: -1em;
|
||||
margin-bottom: 3px;
|
||||
cursor: pointer;
|
||||
background: #ced4da;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
background: var(--semi-color-info);
|
||||
}
|
||||
|
||||
&.selected::after {
|
||||
background: var(--semi-color-info);
|
||||
}
|
||||
}
|
||||
.grip-row {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 0.7em;
|
||||
top: 0;
|
||||
left: -1em;
|
||||
margin-right: 3px;
|
||||
cursor: pointer;
|
||||
background: #ced4da;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
background: var(--semi-color-info);
|
||||
}
|
||||
|
||||
&.selected::after {
|
||||
background: var(--semi-color-info);
|
||||
}
|
||||
}
|
||||
.grip-table {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 0.8em;
|
||||
height: 0.8em;
|
||||
top: -1em;
|
||||
left: -1em;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
background: #ced4da;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
background: var(--semi-color-info);
|
||||
}
|
||||
|
||||
&.selected::after {
|
||||
background: var(--semi-color-info);
|
||||
}
|
||||
}
|
||||
|
||||
.column-resize-handle {
|
||||
|
|
|
@ -108,6 +108,7 @@ importers:
|
|||
marked: ^4.0.12
|
||||
next: 12.0.10
|
||||
prosemirror-markdown: ^1.7.0
|
||||
prosemirror-tables: ^1.1.1
|
||||
prosemirror-utils: ^0.9.6
|
||||
prosemirror-view: ^1.23.6
|
||||
react: 17.0.2
|
||||
|
@ -183,7 +184,8 @@ importers:
|
|||
marked: 4.0.12
|
||||
next: 12.0.10_react-dom@17.0.2+react@17.0.2
|
||||
prosemirror-markdown: 1.7.0
|
||||
prosemirror-utils: 0.9.6
|
||||
prosemirror-tables: 1.1.1
|
||||
prosemirror-utils: 0.9.6_prosemirror-tables@1.1.1
|
||||
prosemirror-view: 1.23.6
|
||||
react: 17.0.2
|
||||
react-dom: 17.0.2_react@17.0.2
|
||||
|
@ -6840,12 +6842,14 @@ packages:
|
|||
prosemirror-model: 1.16.1
|
||||
dev: false
|
||||
|
||||
/prosemirror-utils/0.9.6:
|
||||
/prosemirror-utils/0.9.6_prosemirror-tables@1.1.1:
|
||||
resolution: {integrity: sha512-UC+j9hQQ1POYfMc5p7UFxBTptRiGPR7Kkmbl3jVvU8VgQbkI89tR/GK+3QYC8n+VvBZrtAoCrJItNhWSxX3slA==}
|
||||
peerDependencies:
|
||||
prosemirror-model: ^1.0.0
|
||||
prosemirror-state: ^1.0.1
|
||||
prosemirror-tables: ^0.9.1
|
||||
dependencies:
|
||||
prosemirror-tables: 1.1.1
|
||||
dev: false
|
||||
|
||||
/prosemirror-view/1.23.6:
|
||||
|
|
Loading…
Reference in New Issue