diff --git a/packages/client/src/components/tiptap/basekit.tsx b/packages/client/src/components/tiptap/basekit.tsx index 8c94d4ef..f6558c42 100644 --- a/packages/client/src/components/tiptap/basekit.tsx +++ b/packages/client/src/components/tiptap/basekit.tsx @@ -22,6 +22,7 @@ import { HorizontalRule } from './extensions/horizontalRule'; import { HTMLMarks } from './extensions/htmlMarks'; import { Iframe } from './extensions/iframe'; import { Image } from './extensions/image'; +import { Indent } from './extensions/indent'; import { Italic } from './extensions/italic'; import { Katex } from './extensions/katex'; import { Link } from './extensions/link'; @@ -72,6 +73,7 @@ export const BaseKit = [ ...HTMLMarks, Iframe, Image, + Indent, Italic, Katex, Link, diff --git a/packages/client/src/components/tiptap/extensions/indent.ts b/packages/client/src/components/tiptap/extensions/indent.ts new file mode 100644 index 00000000..00dc5d39 --- /dev/null +++ b/packages/client/src/components/tiptap/extensions/indent.ts @@ -0,0 +1,164 @@ +import { Command, Extension } from '@tiptap/core'; +import { sinkListItem, liftListItem } from 'prosemirror-schema-list'; +import { TextSelection, AllSelection, Transaction } from 'prosemirror-state'; +import { isListActive } from '../services/active'; +import { clamp } from '../services/clamp'; +import { getNodeType } from '../services/type'; +import { isListNode } from '../services/node'; + +type IndentOptions = { + types: string[]; + indentLevels: number[]; + defaultIndentLevel: number; +}; + +declare module '@tiptap/core' { + interface Commands { + indent: { + indent: () => Command; + outdent: () => Command; + }; + } +} + +export enum IndentProps { + min = 0, + max = 210, + more = 30, + less = -30, +} + +function setNodeIndentMarkup(tr: Transaction, pos: number, delta: number): Transaction { + if (!tr.doc) return tr; + + const node = tr.doc.nodeAt(pos); + if (!node) return tr; + + const minIndent = IndentProps.min; + const maxIndent = IndentProps.max; + + const indent = clamp((node.attrs.indent || 0) + delta, minIndent, maxIndent); + + if (indent === node.attrs.indent) return tr; + + const nodeAttrs = { + ...node.attrs, + indent, + }; + + return tr.setNodeMarkup(pos, node.type, nodeAttrs, node.marks); +} + +function updateIndentLevel(tr: Transaction, delta: number): Transaction { + const { doc, selection } = tr; + + if (!doc || !selection) return tr; + + if (!(selection instanceof TextSelection || selection instanceof AllSelection)) { + return tr; + } + + const { from, to } = selection; + + doc.nodesBetween(from, to, (node, pos) => { + const nodeType = node.type; + + if (nodeType.name === 'paragraph' || nodeType.name === 'heading') { + tr = setNodeIndentMarkup(tr, pos, delta); + return false; + } + if (isListNode(node)) { + return false; + } + return true; + }); + + return tr; +} + +export const Indent = Extension.create({ + name: 'indent', + + addOptions() { + return { + types: ['heading', 'paragraph'], + indentLevels: [0, 30, 60, 90, 120, 150, 180, 210], + defaultIndentLevel: 0, + }; + }, + + addGlobalAttributes() { + return [ + { + types: this.options.types, + attributes: { + indent: { + default: this.options.defaultIndentLevel, + renderHTML: (attributes) => ({ + style: `margin-left: ${attributes.indent}px!important;`, + }), + parseHTML: (element) => + parseInt(element.style.marginLeft) || this.options.defaultIndentLevel, + }, + }, + }, + ]; + }, + + addCommands() { + return { + indent: + () => + ({ tr, state, dispatch }) => { + if (isListActive(this.editor)) { + const name = this.editor.can().liftListItem('taskItem') ? 'taskItem' : 'listItem'; + const type = getNodeType(name, state.schema); + return sinkListItem(type)(state, dispatch); + } + + const { selection } = state; + tr = tr.setSelection(selection); + tr = updateIndentLevel(tr, IndentProps.more); + + if (tr.docChanged) { + dispatch && dispatch(tr); + return true; + } + + return false; + }, + outdent: + () => + ({ tr, state, dispatch }) => { + if (isListActive(this.editor)) { + const name = this.editor.can().liftListItem('taskItem') ? 'taskItem' : 'listItem'; + const type = getNodeType(name, state.schema); + return liftListItem(type)(state, dispatch); + } + + const { selection } = state; + tr = tr.setSelection(selection); + tr = updateIndentLevel(tr, IndentProps.less); + + if (tr.docChanged) { + dispatch && dispatch(tr); + return true; + } + + return false; + }, + }; + }, + + // @ts-ignore + addKeyboardShortcuts() { + return { + 'Tab': () => { + return this.editor.commands.indent(); + }, + 'Shift-Tab': () => { + return this.editor.commands.outdent(); + }, + }; + }, +}); diff --git a/packages/client/src/components/tiptap/menus/align.tsx b/packages/client/src/components/tiptap/menus/align.tsx index 0ee3d336..bbced9de 100644 --- a/packages/client/src/components/tiptap/menus/align.tsx +++ b/packages/client/src/components/tiptap/menus/align.tsx @@ -6,7 +6,7 @@ import { IconAlignRight, IconAlignJustify, } from '@douyinfe/semi-icons'; -import { isTitleActive } from './utils/active'; +import { isTitleActive } from '../services/active'; export const AlignMenu = ({ editor }) => { const current = (() => { diff --git a/packages/client/src/components/tiptap/menus/banner.tsx b/packages/client/src/components/tiptap/menus/banner.tsx index 6328476c..4177198c 100644 --- a/packages/client/src/components/tiptap/menus/banner.tsx +++ b/packages/client/src/components/tiptap/menus/banner.tsx @@ -9,7 +9,7 @@ import { import { BubbleMenu } from './components/bubble-menu'; import { Divider } from '../components/divider'; import { Banner } from '../extensions/banner'; -import { deleteNode } from './utils/delete'; +import { deleteNode } from '../services//delete'; export const BannerBubbleMenu = ({ editor }) => { return ( diff --git a/packages/client/src/components/tiptap/menus/base-insert.tsx b/packages/client/src/components/tiptap/menus/base-insert.tsx index 42c55d0e..bcd9bcc4 100644 --- a/packages/client/src/components/tiptap/menus/base-insert.tsx +++ b/packages/client/src/components/tiptap/menus/base-insert.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Button, Tooltip } from '@douyinfe/semi-ui'; import { IconQuote, IconCheckboxIndeterminate, IconLink } from '@douyinfe/semi-icons'; -import { isTitleActive } from './utils/active'; +import { isTitleActive } from '../services/active'; import { Emoji } from './components/emoji'; export const BaseInsertMenu: React.FC<{ editor: any }> = ({ editor }) => { diff --git a/packages/client/src/components/tiptap/menus/base-menu.tsx b/packages/client/src/components/tiptap/menus/base-menu.tsx index ea94bc87..d4d24dac 100644 --- a/packages/client/src/components/tiptap/menus/base-menu.tsx +++ b/packages/client/src/components/tiptap/menus/base-menu.tsx @@ -7,7 +7,7 @@ import { IconUnderline, IconCode, } from '@douyinfe/semi-icons'; -import { isTitleActive } from './utils/active'; +import { isTitleActive } from '../services/active'; import { ColorMenu } from './color'; export const BaseMenu: React.FC<{ editor: any }> = ({ editor }) => { diff --git a/packages/client/src/components/tiptap/menus/color.tsx b/packages/client/src/components/tiptap/menus/color.tsx index 44523e07..66561618 100644 --- a/packages/client/src/components/tiptap/menus/color.tsx +++ b/packages/client/src/components/tiptap/menus/color.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Button, Tooltip } from '@douyinfe/semi-ui'; import { IconFont, IconMark } from '@douyinfe/semi-icons'; -import { isTitleActive } from './utils/active'; +import { isTitleActive } from '../services/active'; import { Color } from './components/color'; export const ColorMenu: React.FC<{ editor: any }> = ({ editor }) => { diff --git a/packages/client/src/components/tiptap/menus/components/font-size.tsx b/packages/client/src/components/tiptap/menus/components/font-size.tsx index 47185357..bc4bfae7 100644 --- a/packages/client/src/components/tiptap/menus/components/font-size.tsx +++ b/packages/client/src/components/tiptap/menus/components/font-size.tsx @@ -1,6 +1,6 @@ import React, { useCallback } from 'react'; import { Select } from '@douyinfe/semi-ui'; -import { isTitleActive } from '../utils/active'; +import { isTitleActive } from '../../services/active'; export const FONT_SIZES = [12, 13, 14, 15, 16, 19, 22, 24, 29, 32, 40, 48]; diff --git a/packages/client/src/components/tiptap/menus/components/paragraph.tsx b/packages/client/src/components/tiptap/menus/components/paragraph.tsx index fc967047..89b6509d 100644 --- a/packages/client/src/components/tiptap/menus/components/paragraph.tsx +++ b/packages/client/src/components/tiptap/menus/components/paragraph.tsx @@ -1,6 +1,6 @@ import React, { useCallback } from 'react'; import { Select } from '@douyinfe/semi-ui'; -import { isTitleActive } from '../utils/active'; +import { isTitleActive } from '../../services/active'; const getCurrentCaretTitle = (editor) => { if (editor.isActive('heading', { level: 1 })) return 1; diff --git a/packages/client/src/components/tiptap/menus/image.tsx b/packages/client/src/components/tiptap/menus/image.tsx index ecd1d4c5..f8ec7f6f 100644 --- a/packages/client/src/components/tiptap/menus/image.tsx +++ b/packages/client/src/components/tiptap/menus/image.tsx @@ -11,7 +11,7 @@ import { Upload } from 'components/upload'; import { BubbleMenu } from './components/bubble-menu'; import { Divider } from '../components/divider'; import { Image } from '../extensions/image'; -import { getImageOriginSize } from './utils/image'; +import { getImageOriginSize } from '../services/image'; const { Text } = Typography; diff --git a/packages/client/src/components/tiptap/menus/list.tsx b/packages/client/src/components/tiptap/menus/list.tsx index a07f814a..89a15cc4 100644 --- a/packages/client/src/components/tiptap/menus/list.tsx +++ b/packages/client/src/components/tiptap/menus/list.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Button, Tooltip } from '@douyinfe/semi-ui'; import { IconList, IconOrderedList, IconIndentLeft, IconIndentRight } from '@douyinfe/semi-icons'; import { IconTask } from 'components/icons'; -import { isTitleActive } from './utils/active'; +import { isTitleActive } from '../services/active'; export const ListMenu: React.FC<{ editor: any }> = ({ editor }) => { if (!editor) { diff --git a/packages/client/src/components/tiptap/menus/media-insert.tsx b/packages/client/src/components/tiptap/menus/media-insert.tsx index 4399289e..5e2d4a04 100644 --- a/packages/client/src/components/tiptap/menus/media-insert.tsx +++ b/packages/client/src/components/tiptap/menus/media-insert.tsx @@ -15,8 +15,8 @@ import { IconMath, } from 'components/icons'; import { GridSelect } from 'components/grid-select'; -import { isTitleActive } from './utils/active'; -import { getImageOriginSize } from './utils/image'; +import { isTitleActive } from '../services/active'; +import { getImageOriginSize } from '../services/image'; export const MediaInsertMenu: React.FC<{ editor: any }> = ({ editor }) => { if (!editor) { diff --git a/packages/client/src/components/tiptap/menus/utils/active.ts b/packages/client/src/components/tiptap/services/active.ts similarity index 100% rename from packages/client/src/components/tiptap/menus/utils/active.ts rename to packages/client/src/components/tiptap/services/active.ts diff --git a/packages/client/src/components/tiptap/menus/utils/shared.ts b/packages/client/src/components/tiptap/services/clamp.ts similarity index 100% rename from packages/client/src/components/tiptap/menus/utils/shared.ts rename to packages/client/src/components/tiptap/services/clamp.ts diff --git a/packages/client/src/components/tiptap/menus/utils/delete.tsx b/packages/client/src/components/tiptap/services/delete.tsx similarity index 100% rename from packages/client/src/components/tiptap/menus/utils/delete.tsx rename to packages/client/src/components/tiptap/services/delete.tsx diff --git a/packages/client/src/components/tiptap/menus/utils/image.ts b/packages/client/src/components/tiptap/services/image.ts similarity index 100% rename from packages/client/src/components/tiptap/menus/utils/image.ts rename to packages/client/src/components/tiptap/services/image.ts diff --git a/packages/client/src/components/tiptap/menus/utils/node.ts b/packages/client/src/components/tiptap/services/node.ts similarity index 100% rename from packages/client/src/components/tiptap/menus/utils/node.ts rename to packages/client/src/components/tiptap/services/node.ts diff --git a/packages/client/src/components/tiptap/menus/utils/type.ts b/packages/client/src/components/tiptap/services/type.ts similarity index 100% rename from packages/client/src/components/tiptap/menus/utils/type.ts rename to packages/client/src/components/tiptap/services/type.ts