mirror of https://github.com/fantasticit/think.git
refactor: refactor the tiptap code
This commit is contained in:
parent
1f90942823
commit
511953ce79
|
@ -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,
|
||||||
|
|
|
@ -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();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
|
@ -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 = (() => {
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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];
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue