mirror of https://github.com/fantasticit/think.git
tiptap: add support for columns
parent
e08e72e1ae
commit
57fb18e40a
|
@ -0,0 +1,20 @@
|
|||
import { Icon } from '@douyinfe/semi-ui';
|
||||
|
||||
export const IconLayout: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" role="presentation">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path
|
||||
d="M5 5h5a1 1 0 011 1v12a1 1 0 01-1 1H5a1 1 0 01-1-1V6a1 1 0 011-1zm9 0h5a1 1 0 011 1v12a1 1 0 01-1 1h-5a1 1 0 01-1-1V6a1 1 0 011-1z"
|
||||
fill="currentColor"
|
||||
fillRule="nonzero"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -27,6 +27,7 @@ export * from './IconHorizontalRule';
|
|||
export * from './IconImage';
|
||||
export * from './IconInfo';
|
||||
export * from './IconJSON';
|
||||
export * from './IconLayout';
|
||||
export * from './IconLeft';
|
||||
export * from './IconLink';
|
||||
export * from './IconList';
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { mergeAttributes, Node, nodeInputRule } from '@tiptap/core';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { ColumnsWrapper } from 'tiptap/core/wrappers/columns';
|
||||
import { getDatasetAttribute, nodeAttrsToDataset } from 'tiptap/prose-utils';
|
||||
|
||||
export interface IColumnsAttrs {
|
||||
columns?: number;
|
||||
}
|
||||
|
||||
export const Column = Node.create({
|
||||
name: 'column',
|
||||
group: 'block',
|
||||
content: '(paragraph|block)*',
|
||||
isolating: true,
|
||||
selectable: false,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {
|
||||
class: 'column',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'div[class=column]',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||
},
|
||||
});
|
|
@ -0,0 +1,279 @@
|
|||
import { mergeAttributes, Node, nodeInputRule } from '@tiptap/core';
|
||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { Node as ProseMirrorNode, Transaction } from 'prosemirror-model';
|
||||
import { NodeSelection, Plugin, PluginKey, State, TextSelection } from 'prosemirror-state';
|
||||
import { findParentNodeOfType, findSelectedNodeOfType } from 'prosemirror-utils';
|
||||
import { ColumnsWrapper } from 'tiptap/core/wrappers/columns';
|
||||
import { findParentNodeClosestToPos, getDatasetAttribute, getStepRange } from 'tiptap/prose-utils';
|
||||
|
||||
export interface IColumnsAttrs {
|
||||
type?: 'left-right' | 'left-sidebar' | 'right-sidebar';
|
||||
columns?: number;
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
columns: {
|
||||
setColumns: (attrs?: IColumnsAttrs) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const ColumnsPluginKey = new PluginKey('columns');
|
||||
|
||||
const fixColumnSizes = (changedTr: Transaction, state: State) => {
|
||||
const columns = state.schema.nodes.columns;
|
||||
|
||||
const range = getStepRange(changedTr);
|
||||
|
||||
if (!range) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let change;
|
||||
|
||||
changedTr.doc.nodesBetween(range.from, range.to, (node, pos) => {
|
||||
if (node.type !== columns) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node.childCount !== node.attrs.columns) {
|
||||
const json = node.toJSON();
|
||||
|
||||
if (json && json.content && json.content.length) {
|
||||
change = {
|
||||
from: pos + 1,
|
||||
to: pos + node.nodeSize - 1,
|
||||
node: ProseMirrorNode.fromJSON(state.schema, {
|
||||
...json,
|
||||
content: json.content.slice(0, node.attrs.columns),
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return change;
|
||||
};
|
||||
|
||||
export const Columns = Node.create({
|
||||
name: 'columns',
|
||||
group: 'block',
|
||||
content: 'column{2,}*',
|
||||
defining: true,
|
||||
selectable: true,
|
||||
draggable: true,
|
||||
isolating: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
type: {
|
||||
default: 'left-right',
|
||||
parseHTML: getDatasetAttribute('type'),
|
||||
},
|
||||
columns: {
|
||||
default: 2,
|
||||
parseHTML: getDatasetAttribute('columns'),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {
|
||||
class: 'columns',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'div[class=columns]',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes, node }) {
|
||||
return [
|
||||
'div',
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
||||
class: `columns ${node.attrs.type}`,
|
||||
}),
|
||||
0,
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setColumns:
|
||||
(options) =>
|
||||
({ state, tr, dispatch }) => {
|
||||
if (!dispatch) return;
|
||||
|
||||
const currentNodeWithPos = findParentNodeClosestToPos(
|
||||
state.selection.$from,
|
||||
(node) => node.type.name === this.name
|
||||
);
|
||||
|
||||
if (currentNodeWithPos) {
|
||||
let nodes: Array<ProseMirrorNode> = [];
|
||||
currentNodeWithPos.node.descendants((node, _, parent) => {
|
||||
if (parent?.type.name === 'column') {
|
||||
nodes.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
nodes = nodes.reverse().filter((node) => node.content.size > 0);
|
||||
|
||||
const resolvedPos = tr.doc.resolve(currentNodeWithPos.pos);
|
||||
const sel = new NodeSelection(resolvedPos);
|
||||
|
||||
tr = tr.setSelection(sel);
|
||||
nodes.forEach((node) => (tr = tr.insert(currentNodeWithPos.pos, node)));
|
||||
tr = tr.deleteSelection();
|
||||
dispatch(tr);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const { schema } = state;
|
||||
const { columns: n = 2 } = options;
|
||||
const selectionContent = tr.selection.content().toJSON();
|
||||
const firstColumn = {
|
||||
type: 'column',
|
||||
content: selectionContent ? selectionContent.content : [{ type: 'paragraph', content: [] }],
|
||||
};
|
||||
const otherColumns = Array.from({ length: n - 1 }, () => ({
|
||||
type: 'column',
|
||||
content: [{ type: 'paragraph', content: [] }],
|
||||
}));
|
||||
const columns = { type: this.name, content: [firstColumn, ...otherColumns] };
|
||||
const newNode = ProseMirrorNode.fromJSON(schema, columns);
|
||||
newNode.attrs = options;
|
||||
const offset = tr.selection.anchor + 1;
|
||||
|
||||
dispatch(
|
||||
tr
|
||||
.replaceSelectionWith(newNode)
|
||||
.scrollIntoView()
|
||||
.setSelection(TextSelection.near(tr.doc.resolve(offset)))
|
||||
);
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: ColumnsPluginKey,
|
||||
state: {
|
||||
init: (_, state) => {
|
||||
const maybeColumns = findParentNodeOfType(state.schema.nodes.columns)(state.selection);
|
||||
|
||||
return {
|
||||
pos: maybeColumns ? maybeColumns.pos : null,
|
||||
selectedColumns: maybeColumns ? maybeColumns.node : null,
|
||||
};
|
||||
},
|
||||
apply: (tr, pluginState, _oldState, newState) => {
|
||||
if (tr.docChanged || tr.selectionSet) {
|
||||
const columns = newState.schema.nodes.columns;
|
||||
|
||||
const maybeColumns =
|
||||
findParentNodeOfType(columns)(newState.selection) ||
|
||||
findSelectedNodeOfType([columns])(newState.selection);
|
||||
|
||||
const newPluginState = {
|
||||
...pluginState,
|
||||
pos: maybeColumns ? maybeColumns.pos : null,
|
||||
selectedColumns: maybeColumns ? maybeColumns.node : null,
|
||||
};
|
||||
|
||||
return newPluginState;
|
||||
}
|
||||
return pluginState;
|
||||
},
|
||||
},
|
||||
|
||||
appendTransaction: (transactions, _oldState, newState) => {
|
||||
const changes = [];
|
||||
|
||||
transactions.forEach((prevTr) => {
|
||||
changes.forEach((change) => {
|
||||
return {
|
||||
from: prevTr.mapping.map(change.from),
|
||||
to: prevTr.mapping.map(change.to),
|
||||
node: change.node,
|
||||
};
|
||||
});
|
||||
|
||||
if (!prevTr.docChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
const change = fixColumnSizes(prevTr, newState);
|
||||
|
||||
if (change) {
|
||||
changes.push(change);
|
||||
}
|
||||
});
|
||||
|
||||
if (changes.length) {
|
||||
const tr = newState.tr;
|
||||
const selection = newState.selection.toJSON();
|
||||
|
||||
changes.forEach((change) => {
|
||||
tr.replaceRangeWith(change.from, change.to, change.node);
|
||||
});
|
||||
|
||||
if (tr.docChanged) {
|
||||
const { pos, selectedColumns } = ColumnsPluginKey.getState(newState);
|
||||
|
||||
if (pos !== null && selectedColumns != null) {
|
||||
let endOfColumns = pos - 1;
|
||||
|
||||
for (let i = 0; i < selectedColumns?.attrs?.columns; i++) {
|
||||
endOfColumns += selectedColumns?.content?.content?.[i]?.nodeSize;
|
||||
}
|
||||
|
||||
const selectionPos$ = tr.doc.resolve(endOfColumns);
|
||||
|
||||
tr.setSelection(
|
||||
selection instanceof NodeSelection
|
||||
? new NodeSelection(selectionPos$)
|
||||
: new TextSelection(selectionPos$)
|
||||
);
|
||||
}
|
||||
|
||||
tr.setMeta('addToHistory', false);
|
||||
return tr;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(ColumnsWrapper);
|
||||
},
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
nodeInputRule({
|
||||
find: /^\$columns $/,
|
||||
type: this.type,
|
||||
getAttributes: () => {
|
||||
return { type: 'left-right', columns: 2 };
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
import { IconCopy, IconDelete } from '@douyinfe/semi-icons';
|
||||
import { Button, Space } from '@douyinfe/semi-ui';
|
||||
import { Divider } from 'components/divider';
|
||||
import { Tooltip } from 'components/tooltip';
|
||||
import { useCallback } from 'react';
|
||||
import { BubbleMenu } from 'tiptap/core/bubble-menu';
|
||||
import { Columns, IColumnsAttrs } from 'tiptap/core/extensions/columns';
|
||||
import { useAttributes } from 'tiptap/core/hooks/use-attributes';
|
||||
import { copyNode, deleteNode, getEditorContainerDOMSize } from 'tiptap/prose-utils';
|
||||
|
||||
export const ColumnsBubbleMenu = ({ editor }) => {
|
||||
const attrs = useAttributes<IColumnsAttrs>(editor, Columns.name, {
|
||||
type: 'left-right',
|
||||
columns: 2,
|
||||
});
|
||||
const { type, columns } = attrs;
|
||||
|
||||
const getRenderContainer = useCallback((node) => {
|
||||
let container = node;
|
||||
if (!container.tag) {
|
||||
container = node.parentElement;
|
||||
}
|
||||
|
||||
while (container && container.classList && !container.classList.contains('node-columns')) {
|
||||
container = container.parentElement;
|
||||
}
|
||||
|
||||
return container;
|
||||
}, []);
|
||||
|
||||
const shouldShow = useCallback(() => editor.isActive(Columns.name), [editor]);
|
||||
const copyMe = useCallback(() => copyNode(Columns.name, editor), [editor]);
|
||||
const deleteMe = useCallback(() => deleteNode(Columns.name, editor), [editor]);
|
||||
|
||||
return (
|
||||
<BubbleMenu
|
||||
className={'bubble-menu'}
|
||||
editor={editor}
|
||||
pluginKey="columns-bubble-menu"
|
||||
shouldShow={shouldShow}
|
||||
getRenderContainer={getRenderContainer}
|
||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||
>
|
||||
<Space spacing={4}>
|
||||
<Tooltip content="复制">
|
||||
<Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
|
||||
</Tooltip>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Tooltip content="删除节点" hideOnClick>
|
||||
<Button onClick={deleteMe} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</BubbleMenu>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
import { Editor } from 'tiptap/core';
|
||||
|
||||
import { ColumnsBubbleMenu } from './bubble';
|
||||
|
||||
export const Columns: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||
return (
|
||||
<>
|
||||
<ColumnsBubbleMenu editor={editor} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -9,6 +9,7 @@ import {
|
|||
IconDocument,
|
||||
IconFlow,
|
||||
IconImage,
|
||||
IconLayout,
|
||||
IconLink,
|
||||
IconMath,
|
||||
IconMind,
|
||||
|
@ -50,6 +51,7 @@ export const COMMANDS: ICommand[] = [
|
|||
{
|
||||
title: '通用',
|
||||
},
|
||||
|
||||
{
|
||||
icon: <IconTableOfContents />,
|
||||
label: '目录',
|
||||
|
@ -85,6 +87,38 @@ export const COMMANDS: ICommand[] = [
|
|||
</Popover>
|
||||
),
|
||||
},
|
||||
{
|
||||
isBlock: true,
|
||||
icon: <IconLayout />,
|
||||
label: '布局',
|
||||
custom: (editor, runCommand) => (
|
||||
<Popover
|
||||
key="table"
|
||||
showArrow
|
||||
position="rightTop"
|
||||
zIndex={10000}
|
||||
content={
|
||||
<div style={{ padding: 0 }}>
|
||||
<GridSelect
|
||||
rows={1}
|
||||
cols={5}
|
||||
onSelect={({ cols }) => {
|
||||
return runCommand({
|
||||
label: '布局',
|
||||
action: () => editor.chain().focus().setColumns({ type: 'left-right', columns: cols }).run(),
|
||||
})();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Dropdown.Item>
|
||||
<IconLayout />
|
||||
布局
|
||||
</Dropdown.Item>
|
||||
</Popover>
|
||||
),
|
||||
},
|
||||
{
|
||||
isBlock: true,
|
||||
icon: <IconCodeBlock />,
|
||||
|
@ -185,7 +219,13 @@ export const QUICK_INSERT_COMMANDS = [
|
|||
label: '表格',
|
||||
action: (editor: Editor) => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(),
|
||||
},
|
||||
...COMMANDS.slice(3),
|
||||
{
|
||||
isBlock: true,
|
||||
icon: <IconLayout />,
|
||||
label: '布局',
|
||||
action: (editor) => editor.chain().focus().setColumns({ type: 'left-right', columns: 2 }).run(),
|
||||
},
|
||||
...COMMANDS.slice(4),
|
||||
];
|
||||
|
||||
export const transformToCommands = (commands, data: string[]) => {
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
.columns {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.column {
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--semi-color-border);
|
||||
border-radius: var(--border-radius);
|
||||
flex: 1 1 0%;
|
||||
box-sizing: border-box;
|
||||
|
||||
p {
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,3 +18,4 @@
|
|||
@import './title.scss';
|
||||
@import './kityminder.scss';
|
||||
@import './drag.scss';
|
||||
@import './columns.scss';
|
||||
|
|
|
@ -39,7 +39,8 @@
|
|||
.node-codeBlock,
|
||||
.node-documentChildren,
|
||||
.node-documentReference,
|
||||
.node-excalidraw {
|
||||
.node-excalidraw,
|
||||
.node-columns {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
|
|
|
@ -111,4 +111,27 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-columns {
|
||||
&.selected-node {
|
||||
.column {
|
||||
position: relative;
|
||||
border: 1px solid var(--node-selected-border-color) !important;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
border-color: var(--node-selected-border-color);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
background-color: rgb(179 212 255 / 30%);
|
||||
content: '';
|
||||
inset: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
.wrap {
|
||||
> div {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
grid-gap: 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import { Space, Spin, Typography } from '@douyinfe/semi-ui';
|
||||
import { NodeViewContent, NodeViewWrapper } from '@tiptap/react';
|
||||
import cls from 'classnames';
|
||||
import { IconMind } from 'components/icons';
|
||||
import { Resizeable } from 'components/resizeable';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import VisibilitySensor from 'react-visibility-sensor';
|
||||
import { Columns } from 'tiptap/core/extensions/columns';
|
||||
import { getEditorContainerDOMSize } from 'tiptap/prose-utils';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const INHERIT_SIZE_STYLE = { width: '100%', height: '100%', maxWidth: '100%' };
|
||||
|
||||
export const ColumnsWrapper = ({ editor, node, updateAttributes }) => {
|
||||
const exportToSvgRef = useRef(null);
|
||||
const isEditable = editor.isEditable;
|
||||
const isActive = editor.isActive(Columns.name);
|
||||
const { width: maxWidth } = getEditorContainerDOMSize(editor);
|
||||
const { data, width, height } = node.attrs;
|
||||
const [Svg, setSvg] = useState<SVGElement | null>(null);
|
||||
const [loading, toggleLoading] = useToggle(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [visible, toggleVisible] = useToggle(false);
|
||||
|
||||
const onResize = useCallback(
|
||||
(size) => {
|
||||
updateAttributes({ width: size.width, height: size.height });
|
||||
},
|
||||
[updateAttributes]
|
||||
);
|
||||
|
||||
const onViewportChange = useCallback(
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
toggleVisible(true);
|
||||
}
|
||||
},
|
||||
[toggleVisible]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
import('@excalidraw/excalidraw')
|
||||
.then((res) => {
|
||||
exportToSvgRef.current = res.exportToSvg;
|
||||
})
|
||||
.catch(setError)
|
||||
.finally(() => toggleLoading(false));
|
||||
}, [toggleLoading, data]);
|
||||
|
||||
useEffect(() => {
|
||||
const setContent = async () => {
|
||||
if (loading || error || !visible || !data) return;
|
||||
|
||||
const svg: SVGElement = await exportToSvgRef.current(data);
|
||||
|
||||
svg.setAttribute('width', '100%');
|
||||
svg.setAttribute('height', '100%');
|
||||
svg.setAttribute('display', 'block');
|
||||
|
||||
setSvg(svg);
|
||||
};
|
||||
setContent();
|
||||
}, [data, loading, error, visible]);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper>
|
||||
<NodeViewContent className={cls(styles.wrap, 'render-wrap')} />
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
|
@ -12,6 +12,7 @@ import { Callout } from 'tiptap/core/menus/callout';
|
|||
import { CleadrNodeAndMarks } from 'tiptap/core/menus/clear-node-and-marks';
|
||||
import { Code } from 'tiptap/core/menus/code';
|
||||
import { CodeBlock } from 'tiptap/core/menus/code-block';
|
||||
import { Columns } from 'tiptap/core/menus/columns';
|
||||
import { Countdonw } from 'tiptap/core/menus/countdown';
|
||||
import { DocumentChildren } from 'tiptap/core/menus/document-children';
|
||||
import { DocumentReference } from 'tiptap/core/menus/document-reference';
|
||||
|
@ -109,6 +110,7 @@ const _MenuBar: React.FC<{ editor: Editor }> = ({ editor }) => {
|
|||
<Katex editor={editor} />
|
||||
<Mind editor={editor} />
|
||||
<Excalidraw editor={editor} />
|
||||
<Columns editor={editor} />
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -11,6 +11,8 @@ import { Code, CodeMarkPlugin } from 'tiptap/core/extensions/code';
|
|||
import { CodeBlock } from 'tiptap/core/extensions/code-block';
|
||||
import { Color } from 'tiptap/core/extensions/color';
|
||||
import { ColorHighlighter } from 'tiptap/core/extensions/color-highlighter';
|
||||
import { Column } from 'tiptap/core/extensions/column';
|
||||
import { Columns } from 'tiptap/core/extensions/columns';
|
||||
import { Countdown } from 'tiptap/core/extensions/countdown';
|
||||
// 基础扩展
|
||||
import { Document } from 'tiptap/core/extensions/document';
|
||||
|
@ -102,6 +104,8 @@ export const CollaborationKit = [
|
|||
CodeBlock,
|
||||
Color,
|
||||
ColorHighlighter,
|
||||
Column,
|
||||
Columns,
|
||||
Dropcursor,
|
||||
Excalidraw,
|
||||
EventEmitter,
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import { ReadonlyTransaction, Transaction } from 'prosemirror-state';
|
||||
|
||||
export const getStepRange = (transaction: Transaction | ReadonlyTransaction): { from: number; to: number } | null => {
|
||||
let from = -1;
|
||||
let to = -1;
|
||||
|
||||
transaction.steps.forEach((step) => {
|
||||
step.getMap().forEach((_oldStart, _oldEnd, newStart, newEnd) => {
|
||||
from = newStart < from || from === -1 ? newStart : from;
|
||||
to = newEnd < to || to === -1 ? newEnd : to;
|
||||
});
|
||||
});
|
||||
|
||||
if (from !== -1) {
|
||||
return { from, to };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
|
@ -6,6 +6,7 @@ export * from './copy-node';
|
|||
export * from './create-node';
|
||||
export * from './debug';
|
||||
export * from './delete-node';
|
||||
export * from './document';
|
||||
export * from './dom';
|
||||
export * from './dom-dataset';
|
||||
export * from './download';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Editor } from '@tiptap/core';
|
||||
import { Node } from 'prosemirror-model';
|
||||
import { Node, ResolvedPos } from 'prosemirror-model';
|
||||
import { EditorState } from 'prosemirror-state';
|
||||
|
||||
export function isTitleNode(node: Node): boolean {
|
||||
|
|
Loading…
Reference in New Issue