From 880acbd703c837ecaa45cdba40e7202e300295cb Mon Sep 17 00:00:00 2001 From: fantasticit Date: Sat, 19 Nov 2022 12:22:39 +0800 Subject: [PATCH] improve columns --- .../src/components/icons/IconColumns.tsx | 43 +++ .../client/src/components/icons/index.tsx | 1 + .../src/tiptap/core/extensions/column.ts | 22 +- .../src/tiptap/core/extensions/columns.ts | 299 ++++-------------- .../src/tiptap/core/menus/columns/bubble.tsx | 37 ++- .../client/src/tiptap/core/menus/commands.tsx | 4 +- .../src/tiptap/core/styles/columns.scss | 22 +- .../core/wrappers/columns/index.module.scss | 8 - .../tiptap/core/wrappers/columns/index.tsx | 74 ----- .../client/src/tiptap/prose-utils/columns.ts | 141 +++++++++ .../client/src/tiptap/prose-utils/index.ts | 1 + 11 files changed, 299 insertions(+), 353 deletions(-) create mode 100644 packages/client/src/components/icons/IconColumns.tsx delete mode 100644 packages/client/src/tiptap/core/wrappers/columns/index.module.scss delete mode 100644 packages/client/src/tiptap/core/wrappers/columns/index.tsx create mode 100644 packages/client/src/tiptap/prose-utils/columns.ts diff --git a/packages/client/src/components/icons/IconColumns.tsx b/packages/client/src/components/icons/IconColumns.tsx new file mode 100644 index 00000000..15304199 --- /dev/null +++ b/packages/client/src/components/icons/IconColumns.tsx @@ -0,0 +1,43 @@ +import { Icon } from '@douyinfe/semi-ui'; + +export const IconAddColBefore: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => { + return ( + + + + + } + /> + ); +}; + +export const IconAddColAfter: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => { + return ( + + + + + } + /> + ); +}; + +export const IconDeleteCol: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => { + return ( + + + + + } + /> + ); +}; diff --git a/packages/client/src/components/icons/index.tsx b/packages/client/src/components/icons/index.tsx index e62abd00..220d4640 100644 --- a/packages/client/src/components/icons/index.tsx +++ b/packages/client/src/components/icons/index.tsx @@ -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'; diff --git a/packages/client/src/tiptap/core/extensions/column.ts b/packages/client/src/tiptap/core/extensions/column.ts index 8426d683..8ac78ba8 100644 --- a/packages/client/src/tiptap/core/extensions/column.ts +++ b/packages/client/src/tiptap/core/extensions/column.ts @@ -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 [ { diff --git a/packages/client/src/tiptap/core/extensions/columns.ts b/packages/client/src/tiptap/core/extensions/columns.ts index b2ceb7bf..d096dd71 100644 --- a/packages/client/src/tiptap/core/extensions/columns.ts +++ b/packages/client/src/tiptap/core/extensions/columns.ts @@ -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 { 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 = []; - 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', + }); + }, + }; }, }); diff --git a/packages/client/src/tiptap/core/menus/columns/bubble.tsx b/packages/client/src/tiptap/core/menus/columns/bubble.tsx index 6127c42f..c362980c 100644 --- a/packages/client/src/tiptap/core/menus/columns/bubble.tsx +++ b/packages/client/src/tiptap/core/menus/columns/bubble.tsx @@ -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(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 ( { > -