refactor: refactor the tiptap code

This commit is contained in:
fantasticit 2022-03-19 16:12:12 +08:00
parent 1f90942823
commit 511953ce79
18 changed files with 177 additions and 11 deletions

View File

@ -22,6 +22,7 @@ import { HorizontalRule } from './extensions/horizontalRule';
import { HTMLMarks } from './extensions/htmlMarks'; import { HTMLMarks } from './extensions/htmlMarks';
import { Iframe } from './extensions/iframe'; import { Iframe } from './extensions/iframe';
import { Image } from './extensions/image'; import { Image } from './extensions/image';
import { Indent } from './extensions/indent';
import { Italic } from './extensions/italic'; import { Italic } from './extensions/italic';
import { Katex } from './extensions/katex'; import { Katex } from './extensions/katex';
import { Link } from './extensions/link'; import { Link } from './extensions/link';
@ -72,6 +73,7 @@ export const BaseKit = [
...HTMLMarks, ...HTMLMarks,
Iframe, Iframe,
Image, Image,
Indent,
Italic, Italic,
Katex, Katex,
Link, Link,

View File

@ -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<IndentOptions>({
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();
},
};
},
});

View File

@ -6,7 +6,7 @@ import {
IconAlignRight, IconAlignRight,
IconAlignJustify, IconAlignJustify,
} from '@douyinfe/semi-icons'; } from '@douyinfe/semi-icons';
import { isTitleActive } from './utils/active'; import { isTitleActive } from '../services/active';
export const AlignMenu = ({ editor }) => { export const AlignMenu = ({ editor }) => {
const current = (() => { const current = (() => {

View File

@ -9,7 +9,7 @@ import {
import { BubbleMenu } from './components/bubble-menu'; import { BubbleMenu } from './components/bubble-menu';
import { Divider } from '../components/divider'; import { Divider } from '../components/divider';
import { Banner } from '../extensions/banner'; import { Banner } from '../extensions/banner';
import { deleteNode } from './utils/delete'; import { deleteNode } from '../services//delete';
export const BannerBubbleMenu = ({ editor }) => { export const BannerBubbleMenu = ({ editor }) => {
return ( return (

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Button, Tooltip } from '@douyinfe/semi-ui'; import { Button, Tooltip } from '@douyinfe/semi-ui';
import { IconQuote, IconCheckboxIndeterminate, IconLink } from '@douyinfe/semi-icons'; import { IconQuote, IconCheckboxIndeterminate, IconLink } from '@douyinfe/semi-icons';
import { isTitleActive } from './utils/active'; import { isTitleActive } from '../services/active';
import { Emoji } from './components/emoji'; import { Emoji } from './components/emoji';
export const BaseInsertMenu: React.FC<{ editor: any }> = ({ editor }) => { export const BaseInsertMenu: React.FC<{ editor: any }> = ({ editor }) => {

View File

@ -7,7 +7,7 @@ import {
IconUnderline, IconUnderline,
IconCode, IconCode,
} from '@douyinfe/semi-icons'; } from '@douyinfe/semi-icons';
import { isTitleActive } from './utils/active'; import { isTitleActive } from '../services/active';
import { ColorMenu } from './color'; import { ColorMenu } from './color';
export const BaseMenu: React.FC<{ editor: any }> = ({ editor }) => { export const BaseMenu: React.FC<{ editor: any }> = ({ editor }) => {

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Button, Tooltip } from '@douyinfe/semi-ui'; import { Button, Tooltip } from '@douyinfe/semi-ui';
import { IconFont, IconMark } from '@douyinfe/semi-icons'; import { IconFont, IconMark } from '@douyinfe/semi-icons';
import { isTitleActive } from './utils/active'; import { isTitleActive } from '../services/active';
import { Color } from './components/color'; import { Color } from './components/color';
export const ColorMenu: React.FC<{ editor: any }> = ({ editor }) => { export const ColorMenu: React.FC<{ editor: any }> = ({ editor }) => {

View File

@ -1,6 +1,6 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { Select } from '@douyinfe/semi-ui'; 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]; export const FONT_SIZES = [12, 13, 14, 15, 16, 19, 22, 24, 29, 32, 40, 48];

View File

@ -1,6 +1,6 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { Select } from '@douyinfe/semi-ui'; import { Select } from '@douyinfe/semi-ui';
import { isTitleActive } from '../utils/active'; import { isTitleActive } from '../../services/active';
const getCurrentCaretTitle = (editor) => { const getCurrentCaretTitle = (editor) => {
if (editor.isActive('heading', { level: 1 })) return 1; if (editor.isActive('heading', { level: 1 })) return 1;

View File

@ -11,7 +11,7 @@ import { Upload } from 'components/upload';
import { BubbleMenu } from './components/bubble-menu'; import { BubbleMenu } from './components/bubble-menu';
import { Divider } from '../components/divider'; import { Divider } from '../components/divider';
import { Image } from '../extensions/image'; import { Image } from '../extensions/image';
import { getImageOriginSize } from './utils/image'; import { getImageOriginSize } from '../services/image';
const { Text } = Typography; const { Text } = Typography;

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Button, Tooltip } from '@douyinfe/semi-ui'; import { Button, Tooltip } from '@douyinfe/semi-ui';
import { IconList, IconOrderedList, IconIndentLeft, IconIndentRight } from '@douyinfe/semi-icons'; import { IconList, IconOrderedList, IconIndentLeft, IconIndentRight } from '@douyinfe/semi-icons';
import { IconTask } from 'components/icons'; import { IconTask } from 'components/icons';
import { isTitleActive } from './utils/active'; import { isTitleActive } from '../services/active';
export const ListMenu: React.FC<{ editor: any }> = ({ editor }) => { export const ListMenu: React.FC<{ editor: any }> = ({ editor }) => {
if (!editor) { if (!editor) {

View File

@ -15,8 +15,8 @@ import {
IconMath, IconMath,
} from 'components/icons'; } from 'components/icons';
import { GridSelect } from 'components/grid-select'; import { GridSelect } from 'components/grid-select';
import { isTitleActive } from './utils/active'; import { isTitleActive } from '../services/active';
import { getImageOriginSize } from './utils/image'; import { getImageOriginSize } from '../services/image';
export const MediaInsertMenu: React.FC<{ editor: any }> = ({ editor }) => { export const MediaInsertMenu: React.FC<{ editor: any }> = ({ editor }) => {
if (!editor) { if (!editor) {