mirror of https://github.com/fantasticit/think.git
improve columns
parent
3ca99f255a
commit
880acbd703
|
@ -0,0 +1,43 @@
|
|||
import { Icon } from '@douyinfe/semi-ui';
|
||||
|
||||
export const IconAddColBefore: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill="none" d="M0 0H24V24H0z" />
|
||||
<path d="M20 3c.552 0 1 .448 1 1v16c0 .552-.448 1-1 1h-6c-.552 0-1-.448-1-1V4c0-.552.448-1 1-1h6zm-1 2h-4v14h4V5zM6 7c2.761 0 5 2.239 5 5s-2.239 5-5 5-5-2.239-5-5 2.239-5 5-5zm1 2H5v1.999L3 11v2l2-.001V15h2v-2.001L9 13v-2l-2-.001V9z" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const IconAddColAfter: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill="none" d="M0 0H24V24H0z" />
|
||||
<path d="M10 3c.552 0 1 .448 1 1v16c0 .552-.448 1-1 1H4c-.552 0-1-.448-1-1V4c0-.552.448-1 1-1h6zM9 5H5v14h4V5zm9 2c2.761 0 5 2.239 5 5s-2.239 5-5 5-5-2.239-5-5 2.239-5 5-5zm1 2h-2v1.999L15 11v2l2-.001V15h2v-2.001L21 13v-2l-2-.001V9z" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const IconDeleteCol: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fill="none" d="M0 0H24V24H0z" />
|
||||
<path d="M12 3c.552 0 1 .448 1 1v8c.835-.628 1.874-1 3-1 2.761 0 5 2.239 5 5s-2.239 5-5 5c-1.032 0-1.99-.313-2.787-.848L13 20c0 .552-.448 1-1 1H6c-.552 0-1-.448-1-1V4c0-.552.448-1 1-1h6zm-1 2H7v14h4V5zm8 10h-6v2h6v-2z" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -7,6 +7,7 @@ export * from './IconCallout';
|
|||
export * from './IconCenter';
|
||||
export * from './IconClear';
|
||||
export * from './IconCodeBlock';
|
||||
export * from './IconColumns';
|
||||
export * from './IconCountdown';
|
||||
export * from './IconDeleteColumn';
|
||||
export * from './IconDeleteRow';
|
||||
|
|
|
@ -1,18 +1,9 @@
|
|||
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;
|
||||
}
|
||||
import { mergeAttributes, Node } from '@tiptap/core';
|
||||
|
||||
export const Column = Node.create({
|
||||
name: 'column',
|
||||
group: 'block',
|
||||
content: '(paragraph|block)*',
|
||||
content: 'block+',
|
||||
isolating: true,
|
||||
selectable: false,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
|
@ -22,6 +13,15 @@ export const Column = Node.create({
|
|||
};
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
index: {
|
||||
default: 0,
|
||||
parseHTML: (element) => element.getAttribute('index'),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
|
|
|
@ -1,84 +1,28 @@
|
|||
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';
|
||||
import { mergeAttributes, Node } from '@tiptap/core';
|
||||
import { TextSelection } from 'prosemirror-state';
|
||||
import { addOrDeleteCol, createColumns, gotoCol } from 'tiptap/prose-utils';
|
||||
|
||||
export interface IColumnsAttrs {
|
||||
type?: 'left-right' | 'left-sidebar' | 'right-sidebar';
|
||||
columns?: number;
|
||||
}
|
||||
import { EXTENSION_PRIORITY_HIGHEST } from '../constants';
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
columns: {
|
||||
setColumns: (attrs?: IColumnsAttrs) => ReturnType;
|
||||
insertColumns: (attrs?: { cols: number }) => ReturnType;
|
||||
addColBefore: () => ReturnType;
|
||||
addColAfter: () => ReturnType;
|
||||
deleteCol: () => 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'),
|
||||
},
|
||||
};
|
||||
},
|
||||
allowGapCursor: false,
|
||||
content: 'column{2,}',
|
||||
priority: EXTENSION_PRIORITY_HIGHEST,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
|
@ -88,192 +32,79 @@ export const Columns = Node.create({
|
|||
};
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
cols: {
|
||||
default: 2,
|
||||
parseHTML: (element) => element.getAttribute('cols'),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'div[class=columns]',
|
||||
tag: 'div[class=grid]',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes, node }) {
|
||||
return [
|
||||
'div',
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
||||
class: `columns ${node.attrs.type}`,
|
||||
}),
|
||||
0,
|
||||
];
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setColumns:
|
||||
(options) =>
|
||||
({ state, tr, dispatch }) => {
|
||||
if (!dispatch) return;
|
||||
insertColumns:
|
||||
(attrs) =>
|
||||
({ tr, dispatch, editor }) => {
|
||||
const node = createColumns(editor.schema, (attrs && attrs.cols) || 3);
|
||||
|
||||
const currentNodeWithPos = findParentNodeClosestToPos(
|
||||
state.selection.$from,
|
||||
(node) => node.type.name === this.name
|
||||
);
|
||||
if (dispatch) {
|
||||
const offset = tr.selection.anchor + 1;
|
||||
|
||||
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;
|
||||
tr.replaceSelectionWith(node)
|
||||
.scrollIntoView()
|
||||
.setSelection(TextSelection.near(tr.doc.resolve(offset)));
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
addColBefore:
|
||||
() =>
|
||||
({ dispatch, state }) => {
|
||||
return addOrDeleteCol({ dispatch, state, type: 'addBefore' });
|
||||
},
|
||||
addColAfter:
|
||||
() =>
|
||||
({ dispatch, state }) => {
|
||||
return addOrDeleteCol({ dispatch, state, type: 'addAfter' });
|
||||
},
|
||||
deleteCol:
|
||||
() =>
|
||||
({ dispatch, state }) => {
|
||||
return addOrDeleteCol({ dispatch, state, type: 'delete' });
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
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 };
|
||||
},
|
||||
}),
|
||||
];
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
'Mod-Alt-G': () => this.editor.commands.insertColumns(),
|
||||
'Tab': () => {
|
||||
return gotoCol({
|
||||
state: this.editor.state,
|
||||
dispatch: this.editor.view.dispatch,
|
||||
type: 'after',
|
||||
});
|
||||
},
|
||||
'Shift-Tab': () => {
|
||||
return gotoCol({
|
||||
state: this.editor.state,
|
||||
dispatch: this.editor.view.dispatch,
|
||||
type: 'before',
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,27 +1,21 @@
|
|||
import { IconCopy, IconDelete } from '@douyinfe/semi-icons';
|
||||
import { Button, Space } from '@douyinfe/semi-ui';
|
||||
import { Divider } from 'components/divider';
|
||||
import { IconAddColAfter, IconAddColBefore, IconDeleteCol } from 'components/icons';
|
||||
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';
|
||||
import { Columns } from 'tiptap/core/extensions/columns';
|
||||
import { copyNode, deleteNode } 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')) {
|
||||
while (container && container.classList && !container.classList.contains('columns')) {
|
||||
container = container.parentElement;
|
||||
}
|
||||
|
||||
|
@ -31,6 +25,9 @@ export const ColumnsBubbleMenu = ({ editor }) => {
|
|||
const shouldShow = useCallback(() => editor.isActive(Columns.name), [editor]);
|
||||
const copyMe = useCallback(() => copyNode(Columns.name, editor), [editor]);
|
||||
const deleteMe = useCallback(() => deleteNode(Columns.name, editor), [editor]);
|
||||
const addColBefore = useCallback(() => editor.chain().focus().addColBefore().run(), [editor]);
|
||||
const addColAfter = useCallback(() => editor.chain().focus().addColAfter().run(), [editor]);
|
||||
const deleteCol = useCallback(() => editor.chain().focus().deleteCol().run(), [editor]);
|
||||
|
||||
return (
|
||||
<BubbleMenu
|
||||
|
@ -43,13 +40,27 @@ export const ColumnsBubbleMenu = ({ editor }) => {
|
|||
>
|
||||
<Space spacing={4}>
|
||||
<Tooltip content="复制">
|
||||
<Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
|
||||
<Button onClick={copyMe} icon={<IconCopy />} size="small" type="tertiary" theme="borderless" />
|
||||
</Tooltip>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Tooltip content="删除节点" hideOnClick>
|
||||
<Button onClick={deleteMe} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
||||
<Tooltip content="向前插入一列">
|
||||
<Button onClick={addColBefore} icon={<IconAddColBefore />} size="small" type="tertiary" theme="borderless" />
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="向后插入一列">
|
||||
<Button onClick={addColAfter} icon={<IconAddColAfter />} size="small" type="tertiary" theme="borderless" />
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="删除当前列">
|
||||
<Button onClick={deleteCol} icon={<IconDeleteCol />} size="small" type="tertiary" theme="borderless" />
|
||||
</Tooltip>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Tooltip content="删除">
|
||||
<Button size="small" type="tertiary" theme="borderless" icon={<IconDelete />} onClick={deleteMe} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</BubbleMenu>
|
||||
|
|
|
@ -110,7 +110,7 @@ export const COMMANDS: ICommand[] = [
|
|||
label: '布局',
|
||||
action: () => {
|
||||
editor.chain().focus().run();
|
||||
editor.chain().setColumns({ type: 'left-right', columns: cols }).focus().run();
|
||||
editor.chain().insertColumns({ cols }).focus().run();
|
||||
},
|
||||
})();
|
||||
}}
|
||||
|
@ -229,7 +229,7 @@ export const QUICK_INSERT_COMMANDS = [
|
|||
isBlock: true,
|
||||
icon: <IconLayout />,
|
||||
label: '布局',
|
||||
action: (editor) => editor.chain().focus().setColumns({ type: 'left-right', columns: 2 }).run(),
|
||||
action: (editor) => editor.chain().focus().insertColumns({ cols: 2 }).run(),
|
||||
},
|
||||
...COMMANDS.slice(4),
|
||||
];
|
||||
|
|
|
@ -2,19 +2,19 @@
|
|||
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;
|
||||
.column {
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--semi-color-border);
|
||||
border-radius: 2px;
|
||||
flex: 1 1 0%;
|
||||
box-sizing: border-box;
|
||||
|
||||
p {
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
p {
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
.wrap {
|
||||
> div {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
grid-gap: 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,141 @@
|
|||
import { Node as PMNode } from 'prosemirror-model';
|
||||
import { EditorState, TextSelection } from 'prosemirror-state';
|
||||
import { findParentNode } from 'prosemirror-utils';
|
||||
import { Column } from 'tiptap/core/extensions/column';
|
||||
import { Columns } from 'tiptap/core/extensions/columns';
|
||||
|
||||
export function createColumn(colType, index, colContent = null) {
|
||||
if (colContent) {
|
||||
return colType.createChecked({ index }, colContent);
|
||||
}
|
||||
|
||||
return colType.createAndFill({ index });
|
||||
}
|
||||
|
||||
export function getColumnsNodeTypes(schema) {
|
||||
if (schema.cached.columnsNodeTypes) {
|
||||
return schema.cached.columnsNodeTypes;
|
||||
}
|
||||
|
||||
const roles = {
|
||||
columns: schema.nodes['columns'],
|
||||
column: schema.nodes['column'],
|
||||
};
|
||||
|
||||
schema.cached.columnsNodeTypes = roles;
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
export function createColumns(schema, colsCount, colContent = null) {
|
||||
const types = getColumnsNodeTypes(schema);
|
||||
const cols = [];
|
||||
|
||||
for (let index = 0; index < colsCount; index += 1) {
|
||||
const col = createColumn(types.column, index, colContent);
|
||||
|
||||
if (col) {
|
||||
// @ts-ignore
|
||||
cols.push(col);
|
||||
}
|
||||
}
|
||||
|
||||
return types.columns.createChecked({ cols: colsCount }, cols);
|
||||
}
|
||||
|
||||
export function addOrDeleteCol({
|
||||
state,
|
||||
dispatch,
|
||||
type,
|
||||
}: {
|
||||
state: EditorState;
|
||||
dispatch: any;
|
||||
type: 'addBefore' | 'addAfter' | 'delete';
|
||||
}) {
|
||||
const maybeColumns = findParentNode((node: PMNode) => node.type.name === Columns.name)(state.selection);
|
||||
const maybeColumn = findParentNode((node: PMNode) => node.type.name === Column.name)(state.selection);
|
||||
|
||||
if (dispatch && maybeColumns && maybeColumn) {
|
||||
const cols = maybeColumns.node;
|
||||
const colIndex = maybeColumn.node.attrs.index;
|
||||
const colsJSON = cols.toJSON();
|
||||
|
||||
let nextIndex = colIndex;
|
||||
|
||||
if (type === 'delete') {
|
||||
nextIndex = colIndex - 1;
|
||||
colsJSON.content.splice(colIndex, 1);
|
||||
} else {
|
||||
nextIndex = type === 'addBefore' ? colIndex : colIndex + 1;
|
||||
colsJSON.content.splice(nextIndex, 0, {
|
||||
type: 'column',
|
||||
attrs: {
|
||||
index: colIndex,
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
colsJSON.attrs.cols = colsJSON.content.length;
|
||||
|
||||
colsJSON.content.forEach((colJSON, index) => {
|
||||
colJSON.attrs.index = index;
|
||||
});
|
||||
|
||||
const nextCols = PMNode.fromJSON(state.schema, colsJSON);
|
||||
|
||||
let nextSelectPos = maybeColumns.pos;
|
||||
nextCols.content.forEach((col, pos, index) => {
|
||||
if (index < nextIndex) {
|
||||
nextSelectPos += col.nodeSize;
|
||||
}
|
||||
});
|
||||
|
||||
const tr = state.tr.setTime(Date.now());
|
||||
|
||||
tr.replaceWith(maybeColumns.pos, maybeColumns.pos + maybeColumns.node.nodeSize, nextCols).setSelection(
|
||||
TextSelection.near(tr.doc.resolve(nextSelectPos))
|
||||
);
|
||||
|
||||
dispatch(tr);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function gotoCol({ state, dispatch, type }: { state: EditorState; dispatch: any; type: 'before' | 'after' }) {
|
||||
const maybeColumns = findParentNode((node: PMNode) => node.type.name === Columns.name)(state.selection);
|
||||
const maybeColumn = findParentNode((node: PMNode) => node.type.name === Column.name)(state.selection);
|
||||
|
||||
if (dispatch && maybeColumns && maybeColumn) {
|
||||
const cols = maybeColumns.node;
|
||||
const colIndex = maybeColumn.node.attrs.index;
|
||||
|
||||
let nextIndex = 0;
|
||||
|
||||
if (type === 'before') {
|
||||
nextIndex = (colIndex - 1 + cols.attrs.cols) % cols.attrs.cols;
|
||||
} else {
|
||||
nextIndex = (colIndex + 1) % cols.attrs.cols;
|
||||
}
|
||||
|
||||
let nextSelectPos = maybeColumns.pos;
|
||||
cols.content.forEach((col, pos, index) => {
|
||||
if (index < nextIndex) {
|
||||
nextSelectPos += col.nodeSize;
|
||||
}
|
||||
});
|
||||
|
||||
const tr = state.tr.setTime(Date.now());
|
||||
|
||||
tr.setSelection(TextSelection.near(tr.doc.resolve(nextSelectPos)));
|
||||
dispatch(tr);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
|
@ -2,6 +2,7 @@ export * from './active';
|
|||
export * from './clamp';
|
||||
export * from './code';
|
||||
export * from './color';
|
||||
export * from './columns';
|
||||
export * from './copy-node';
|
||||
export * from './create-node';
|
||||
export * from './debug';
|
||||
|
|
Loading…
Reference in New Issue