mirror of https://github.com/fantasticit/think.git
refactor: refactor the tiptap code
parent
1f90942823
commit
511953ce79
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
IconAlignJustify,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { isTitleActive } from './utils/active';
|
||||
import { isTitleActive } from '../services/active';
|
||||
|
||||
export const AlignMenu = ({ editor }) => {
|
||||
const current = (() => {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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];
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue