mirror of https://github.com/fantasticit/think.git
improve columns
This commit is contained in:
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 './IconCenter';
|
||||||
export * from './IconClear';
|
export * from './IconClear';
|
||||||
export * from './IconCodeBlock';
|
export * from './IconCodeBlock';
|
||||||
|
export * from './IconColumns';
|
||||||
export * from './IconCountdown';
|
export * from './IconCountdown';
|
||||||
export * from './IconDeleteColumn';
|
export * from './IconDeleteColumn';
|
||||||
export * from './IconDeleteRow';
|
export * from './IconDeleteRow';
|
||||||
|
|
|
@ -1,18 +1,9 @@
|
||||||
import { mergeAttributes, Node, nodeInputRule } from '@tiptap/core';
|
import { mergeAttributes, Node } 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({
|
export const Column = Node.create({
|
||||||
name: 'column',
|
name: 'column',
|
||||||
group: 'block',
|
content: 'block+',
|
||||||
content: '(paragraph|block)*',
|
|
||||||
isolating: true,
|
isolating: true,
|
||||||
selectable: false,
|
|
||||||
|
|
||||||
addOptions() {
|
addOptions() {
|
||||||
return {
|
return {
|
||||||
|
@ -22,6 +13,15 @@ export const Column = Node.create({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
index: {
|
||||||
|
default: 0,
|
||||||
|
parseHTML: (element) => element.getAttribute('index'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,84 +1,28 @@
|
||||||
import { mergeAttributes, Node, nodeInputRule } from '@tiptap/core';
|
import { mergeAttributes, Node } from '@tiptap/core';
|
||||||
import { ReactNodeViewRenderer } from '@tiptap/react';
|
import { TextSelection } from 'prosemirror-state';
|
||||||
import { Node as ProseMirrorNode, Transaction } from 'prosemirror-model';
|
import { addOrDeleteCol, createColumns, gotoCol } from 'tiptap/prose-utils';
|
||||||
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 {
|
import { EXTENSION_PRIORITY_HIGHEST } from '../constants';
|
||||||
type?: 'left-right' | 'left-sidebar' | 'right-sidebar';
|
|
||||||
columns?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
declare module '@tiptap/core' {
|
||||||
interface Commands<ReturnType> {
|
interface Commands<ReturnType> {
|
||||||
columns: {
|
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({
|
export const Columns = Node.create({
|
||||||
name: 'columns',
|
name: 'columns',
|
||||||
group: 'block',
|
group: 'block',
|
||||||
content: 'column{2,}*',
|
|
||||||
defining: true,
|
defining: true,
|
||||||
selectable: true,
|
|
||||||
draggable: true,
|
|
||||||
isolating: true,
|
isolating: true,
|
||||||
|
allowGapCursor: false,
|
||||||
addAttributes() {
|
content: 'column{2,}',
|
||||||
return {
|
priority: EXTENSION_PRIORITY_HIGHEST,
|
||||||
type: {
|
|
||||||
default: 'left-right',
|
|
||||||
parseHTML: getDatasetAttribute('type'),
|
|
||||||
},
|
|
||||||
columns: {
|
|
||||||
default: 2,
|
|
||||||
parseHTML: getDatasetAttribute('columns'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
addOptions() {
|
addOptions() {
|
||||||
return {
|
return {
|
||||||
|
@ -88,192 +32,79 @@ export const Columns = Node.create({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
cols: {
|
||||||
|
default: 2,
|
||||||
|
parseHTML: (element) => element.getAttribute('cols'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
tag: 'div[class=columns]',
|
tag: 'div[class=grid]',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes, node }) {
|
renderHTML({ HTMLAttributes }) {
|
||||||
return [
|
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||||
'div',
|
|
||||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
|
||||||
class: `columns ${node.attrs.type}`,
|
|
||||||
}),
|
|
||||||
0,
|
|
||||||
];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
addCommands() {
|
addCommands() {
|
||||||
return {
|
return {
|
||||||
setColumns:
|
insertColumns:
|
||||||
(options) =>
|
(attrs) =>
|
||||||
({ state, tr, dispatch }) => {
|
({ tr, dispatch, editor }) => {
|
||||||
if (!dispatch) return;
|
const node = createColumns(editor.schema, (attrs && attrs.cols) || 3);
|
||||||
|
|
||||||
const currentNodeWithPos = findParentNodeClosestToPos(
|
if (dispatch) {
|
||||||
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;
|
const offset = tr.selection.anchor + 1;
|
||||||
|
|
||||||
dispatch(
|
tr.replaceSelectionWith(node)
|
||||||
tr
|
|
||||||
.replaceSelectionWith(newNode)
|
|
||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
.setSelection(TextSelection.near(tr.doc.resolve(offset)))
|
.setSelection(TextSelection.near(tr.doc.resolve(offset)));
|
||||||
);
|
}
|
||||||
|
|
||||||
return true;
|
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() {
|
addKeyboardShortcuts() {
|
||||||
return [
|
|
||||||
new Plugin({
|
|
||||||
key: ColumnsPluginKey,
|
|
||||||
state: {
|
|
||||||
init: (_, state) => {
|
|
||||||
const maybeColumns = findParentNodeOfType(state.schema.nodes.columns)(state.selection);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pos: maybeColumns ? maybeColumns.pos : null,
|
'Mod-Alt-G': () => this.editor.commands.insertColumns(),
|
||||||
selectedColumns: maybeColumns ? maybeColumns.node : null,
|
'Tab': () => {
|
||||||
};
|
return gotoCol({
|
||||||
},
|
state: this.editor.state,
|
||||||
apply: (tr, pluginState, _oldState, newState) => {
|
dispatch: this.editor.view.dispatch,
|
||||||
if (tr.docChanged || tr.selectionSet) {
|
type: 'after',
|
||||||
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) {
|
'Shift-Tab': () => {
|
||||||
return;
|
return gotoCol({
|
||||||
}
|
state: this.editor.state,
|
||||||
|
dispatch: this.editor.view.dispatch,
|
||||||
const change = fixColumnSizes(prevTr, newState);
|
type: 'before',
|
||||||
|
|
||||||
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 };
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,27 +1,21 @@
|
||||||
import { IconCopy, IconDelete } from '@douyinfe/semi-icons';
|
import { IconCopy, IconDelete } from '@douyinfe/semi-icons';
|
||||||
import { Button, Space } from '@douyinfe/semi-ui';
|
import { Button, Space } from '@douyinfe/semi-ui';
|
||||||
import { Divider } from 'components/divider';
|
import { Divider } from 'components/divider';
|
||||||
|
import { IconAddColAfter, IconAddColBefore, IconDeleteCol } from 'components/icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { BubbleMenu } from 'tiptap/core/bubble-menu';
|
import { BubbleMenu } from 'tiptap/core/bubble-menu';
|
||||||
import { Columns, IColumnsAttrs } from 'tiptap/core/extensions/columns';
|
import { Columns } from 'tiptap/core/extensions/columns';
|
||||||
import { useAttributes } from 'tiptap/core/hooks/use-attributes';
|
import { copyNode, deleteNode } from 'tiptap/prose-utils';
|
||||||
import { copyNode, deleteNode, getEditorContainerDOMSize } from 'tiptap/prose-utils';
|
|
||||||
|
|
||||||
export const ColumnsBubbleMenu = ({ editor }) => {
|
export const ColumnsBubbleMenu = ({ editor }) => {
|
||||||
const attrs = useAttributes<IColumnsAttrs>(editor, Columns.name, {
|
|
||||||
type: 'left-right',
|
|
||||||
columns: 2,
|
|
||||||
});
|
|
||||||
const { type, columns } = attrs;
|
|
||||||
|
|
||||||
const getRenderContainer = useCallback((node) => {
|
const getRenderContainer = useCallback((node) => {
|
||||||
let container = node;
|
let container = node;
|
||||||
if (!container.tag) {
|
if (!container.tag) {
|
||||||
container = node.parentElement;
|
container = node.parentElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (container && container.classList && !container.classList.contains('node-columns')) {
|
while (container && container.classList && !container.classList.contains('columns')) {
|
||||||
container = container.parentElement;
|
container = container.parentElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +25,9 @@ export const ColumnsBubbleMenu = ({ editor }) => {
|
||||||
const shouldShow = useCallback(() => editor.isActive(Columns.name), [editor]);
|
const shouldShow = useCallback(() => editor.isActive(Columns.name), [editor]);
|
||||||
const copyMe = useCallback(() => copyNode(Columns.name, editor), [editor]);
|
const copyMe = useCallback(() => copyNode(Columns.name, editor), [editor]);
|
||||||
const deleteMe = useCallback(() => deleteNode(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 (
|
return (
|
||||||
<BubbleMenu
|
<BubbleMenu
|
||||||
|
@ -43,13 +40,27 @@ export const ColumnsBubbleMenu = ({ editor }) => {
|
||||||
>
|
>
|
||||||
<Space spacing={4}>
|
<Space spacing={4}>
|
||||||
<Tooltip content="复制">
|
<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>
|
</Tooltip>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Tooltip content="删除节点" hideOnClick>
|
<Tooltip content="向前插入一列">
|
||||||
<Button onClick={deleteMe} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
<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>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</BubbleMenu>
|
</BubbleMenu>
|
||||||
|
|
|
@ -110,7 +110,7 @@ export const COMMANDS: ICommand[] = [
|
||||||
label: '布局',
|
label: '布局',
|
||||||
action: () => {
|
action: () => {
|
||||||
editor.chain().focus().run();
|
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,
|
isBlock: true,
|
||||||
icon: <IconLayout />,
|
icon: <IconLayout />,
|
||||||
label: '布局',
|
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),
|
...COMMANDS.slice(4),
|
||||||
];
|
];
|
||||||
|
|
|
@ -2,13 +2,12 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
|
||||||
|
|
||||||
.column {
|
.column {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border: 1px solid var(--semi-color-border);
|
border: 1px solid var(--semi-color-border);
|
||||||
border-radius: var(--border-radius);
|
border-radius: 2px;
|
||||||
flex: 1 1 0%;
|
flex: 1 1 0%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
@ -17,4 +16,5 @@
|
||||||
margin-top: 0;
|
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 './clamp';
|
||||||
export * from './code';
|
export * from './code';
|
||||||
export * from './color';
|
export * from './color';
|
||||||
|
export * from './columns';
|
||||||
export * from './copy-node';
|
export * from './copy-node';
|
||||||
export * from './create-node';
|
export * from './create-node';
|
||||||
export * from './debug';
|
export * from './debug';
|
||||||
|
|
Loading…
Reference in New Issue