diff --git a/packages/client/src/components/icons/IconLineHeight.tsx b/packages/client/src/components/icons/IconLineHeight.tsx new file mode 100644 index 0000000..492905c --- /dev/null +++ b/packages/client/src/components/icons/IconLineHeight.tsx @@ -0,0 +1,17 @@ +import { Icon } from '@douyinfe/semi-ui'; + +export const IconLineHeight: 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 4c5cf48..c4715c4 100644 --- a/packages/client/src/components/icons/index.tsx +++ b/packages/client/src/components/icons/index.tsx @@ -30,6 +30,7 @@ export * from './IconInfo'; export * from './IconJSON'; export * from './IconLayout'; export * from './IconLeft'; +export * from './IconLineHeight'; export * from './IconLink'; export * from './IconList'; export * from './IconMarkdown'; diff --git a/packages/client/src/tiptap/core/all-kit.ts b/packages/client/src/tiptap/core/all-kit.ts index d7fe2a2..226505e 100644 --- a/packages/client/src/tiptap/core/all-kit.ts +++ b/packages/client/src/tiptap/core/all-kit.ts @@ -30,6 +30,7 @@ import { Image } from 'tiptap/core/extensions/image'; import { Indent } from 'tiptap/core/extensions/indent'; import { Italic } from 'tiptap/core/extensions/italic'; import { Katex } from 'tiptap/core/extensions/katex'; +import { LineHeight } from 'tiptap/core/extensions/line-height'; import { Link } from 'tiptap/core/extensions/link'; import { ListItem } from 'tiptap/core/extensions/listItem'; import { Loading } from 'tiptap/core/extensions/loading'; @@ -80,6 +81,7 @@ export const AllExtensions = [ Excalidraw, Focus, FontSize, + LineHeight, Gapcursor, HardBreak, Heading, diff --git a/packages/client/src/tiptap/core/extensions/heading.ts b/packages/client/src/tiptap/core/extensions/heading.ts index c163809..26c2fad 100644 --- a/packages/client/src/tiptap/core/extensions/heading.ts +++ b/packages/client/src/tiptap/core/extensions/heading.ts @@ -1 +1,111 @@ -export { Heading } from '@tiptap/extension-heading'; +import { mergeAttributes, Node, textblockTypeInputRule } from '@tiptap/core'; + +export type Level = 1 | 2 | 3 | 4 | 5 | 6; + +export interface HeadingOptions { + levels: Level[]; + HTMLAttributes: Record; +} + +declare module '@tiptap/core' { + interface Commands { + heading: { + /** + * Set a heading node + */ + setHeading: (attributes: { level: Level }) => ReturnType; + /** + * Toggle a heading node + */ + toggleHeading: (attributes: { level: Level }) => ReturnType; + }; + } +} + +export const Heading = Node.create({ + name: 'heading', + + addOptions() { + return { + levels: [1, 2, 3, 4, 5, 6], + HTMLAttributes: {}, + }; + }, + + content: 'inline*', + + group: 'block', + + defining: true, + + addAttributes() { + return { + level: { + default: 1, + rendered: false, + }, + lineHeight: { default: null }, + }; + }, + + parseHTML() { + return this.options.levels.map((level: Level) => ({ + tag: `h${level}`, + attrs: { level }, + })); + }, + + renderHTML({ node, HTMLAttributes }) { + const hasLevel = this.options.levels.includes(node.attrs.level); + const level = hasLevel ? node.attrs.level : this.options.levels[0]; + + return [`h${level}`, mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]; + }, + + addCommands() { + return { + setHeading: + (attributes) => + ({ commands }) => { + if (!this.options.levels.includes(attributes.level)) { + return false; + } + + return commands.setNode(this.name, attributes); + }, + toggleHeading: + (attributes) => + ({ commands }) => { + if (!this.options.levels.includes(attributes.level)) { + return false; + } + + return commands.toggleNode(this.name, 'paragraph', attributes); + }, + }; + }, + + addKeyboardShortcuts() { + return this.options.levels.reduce( + (items, level) => ({ + ...items, + ...{ + [`Mod-Alt-${level}`]: () => this.editor.commands.toggleHeading({ level }), + }, + }), + {} + ); + }, + + addInputRules() { + return this.options.levels.map((level) => { + return textblockTypeInputRule({ + find: new RegExp(`^(#{1,${level}})\\s$`), + type: this.type, + getAttributes: { + level, + }, + }); + }); + }, +}); diff --git a/packages/client/src/tiptap/core/extensions/indent.ts b/packages/client/src/tiptap/core/extensions/indent.ts index 519c1f4..44350cb 100644 --- a/packages/client/src/tiptap/core/extensions/indent.ts +++ b/packages/client/src/tiptap/core/extensions/indent.ts @@ -92,7 +92,7 @@ export const Indent = Extension.create({ indent: { default: this.options.defaultIndentLevel, renderHTML: (attributes) => ({ - style: `margin-left: ${attributes.indent}px!important;`, + style: `margin-left: ${attributes.indent}px;`, }), parseHTML: (element) => parseInt(element.style.marginLeft) || this.options.defaultIndentLevel, }, diff --git a/packages/client/src/tiptap/core/extensions/line-height.ts b/packages/client/src/tiptap/core/extensions/line-height.ts new file mode 100644 index 0000000..b63abf7 --- /dev/null +++ b/packages/client/src/tiptap/core/extensions/line-height.ts @@ -0,0 +1,58 @@ +import { Extension } from '@tiptap/core'; + +declare module '@tiptap/core' { + interface Commands { + lineHeight: { + setLineHeight: (val: number) => ReturnType; + unsetLineHeight: () => ReturnType; + }; + } +} + +export const LineHeight = Extension.create({ + name: 'lineHeight', + + addOptions() { + return { + types: ['heading', 'paragraph'], + }; + }, + + addGlobalAttributes() { + return [ + { + types: this.options.types, + attributes: { + fontSize: { + default: null, + parseHTML: (element) => element.style.lineHeight.replace(/['"]+/g, ''), + renderHTML: (attributes) => { + if (!attributes.lineHeight) { + return {}; + } + + return { + style: `line-height: ${attributes.lineHeight}`, + }; + }, + }, + }, + }, + ]; + }, + + addCommands() { + return { + setLineHeight: + (lineHeight) => + ({ commands }) => { + return this.options.types.every((type) => commands.updateAttributes(type, { lineHeight })); + }, + unsetLineHeight: + () => + ({ commands }) => { + return this.options.types.every((type) => commands.resetAttributes(type, 'lineHeight')); + }, + }; + }, +}); diff --git a/packages/client/src/tiptap/core/extensions/paragraph.ts b/packages/client/src/tiptap/core/extensions/paragraph.ts index bb06121..56a28f0 100644 --- a/packages/client/src/tiptap/core/extensions/paragraph.ts +++ b/packages/client/src/tiptap/core/extensions/paragraph.ts @@ -1,6 +1,62 @@ -import { mergeAttributes } from '@tiptap/core'; -import TitapParagraph from '@tiptap/extension-paragraph'; +import { mergeAttributes, Node } from '@tiptap/core'; -export const Paragraph = TitapParagraph.extend({ - selectable: true, +export interface ParagraphOptions { + HTMLAttributes: Record; +} + +declare module '@tiptap/core' { + interface Commands { + paragraph: { + /** + * Toggle a paragraph + */ + setParagraph: () => ReturnType; + }; + } +} + +export const Paragraph = Node.create({ + name: 'paragraph', + + priority: 1000, + + addOptions() { + return { + HTMLAttributes: {}, + }; + }, + + addAttributes() { + return { + lineHeight: { default: null }, + }; + }, + + group: 'block', + + content: 'inline*', + + parseHTML() { + return [{ tag: 'p' }]; + }, + + renderHTML({ HTMLAttributes, node }) { + return ['p', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]; + }, + + addCommands() { + return { + setParagraph: + () => + ({ commands }) => { + return commands.setNode(this.name); + }, + }; + }, + + addKeyboardShortcuts() { + return { + 'Mod-Alt-0': () => this.editor.commands.setParagraph(), + }; + }, }); diff --git a/packages/client/src/tiptap/core/extensions/status.ts b/packages/client/src/tiptap/core/extensions/status.ts index 2edca44..43f655a 100644 --- a/packages/client/src/tiptap/core/extensions/status.ts +++ b/packages/client/src/tiptap/core/extensions/status.ts @@ -24,6 +24,7 @@ export const Status = Node.create({ group: 'inline', inline: true, selectable: true, + draggable: true, atom: true, addAttributes() { diff --git a/packages/client/src/tiptap/core/menus/lineheight/index.tsx b/packages/client/src/tiptap/core/menus/lineheight/index.tsx new file mode 100644 index 0000000..68f1fb5 --- /dev/null +++ b/packages/client/src/tiptap/core/menus/lineheight/index.tsx @@ -0,0 +1,48 @@ +import { Button, Dropdown, Tooltip } from '@douyinfe/semi-ui'; +import { IconLineHeight } from 'components/icons'; +import React, { useCallback } from 'react'; +import { Editor } from 'tiptap/core'; +import { Title } from 'tiptap/core/extensions/title'; +import { useActive } from 'tiptap/core/hooks/use-active'; +import { useAttributes } from 'tiptap/core/hooks/use-attributes'; + +export const LINE_HEIGHT = [null, 1, 1.15, 1.5, 2, 2.5, 3]; + +export const LineHeight: React.FC<{ editor: Editor }> = ({ editor }) => { + const isTitleActive = useActive(editor, Title.name); + const currentValue = useAttributes(editor, 'textStyle', { lineHeight: null }, (attrs) => { + if (!attrs || !attrs.lineHeight) return null; + + const matches = attrs.lineHeight.match(/\d+/); + + if (!matches || !matches[0]) return 16; + return matches[0]; + }); + + const toggle = useCallback( + (val) => { + if (val) { + editor.chain().focus().setLineHeight(val).run(); + } else { + editor.chain().focus().unsetLineHeight().run(); + } + }, + [editor] + ); + + return ( + ( + toggle(val)}> + {val || '默认'} + + ))} + > + + +