From 5c0d9f54e493ca95241d1628d8043f11b0bace43 Mon Sep 17 00:00:00 2001 From: fantasticit Date: Mon, 2 May 2022 13:57:56 +0800 Subject: [PATCH] refactor: improve render performence --- .../src/components/document/editor/editor.tsx | 37 ++--- packages/client/src/helpers/lru-cache.ts | 8 +- packages/client/src/tiptap/divider.tsx | 8 +- .../client/src/tiptap/extensions/indent.ts | 18 +-- .../client/src/tiptap/extensions/search.ts | 10 +- .../client/src/tiptap/hooks/use-active.tsx | 22 +++ .../src/tiptap/hooks/use-attributes.tsx | 48 +++++++ packages/client/src/tiptap/menubar.tsx | 2 + .../client/src/tiptap/menus/align/index.tsx | 31 ++-- .../src/tiptap/menus/attachment/bubble.tsx | 24 ++-- .../src/tiptap/menus/attachment/index.tsx | 4 - .../tiptap/menus/background-color/index.tsx | 48 ++++--- .../src/tiptap/menus/blockquote/index.tsx | 20 +-- .../client/src/tiptap/menus/bold/index.tsx | 11 +- .../src/tiptap/menus/bullet-list/index.tsx | 19 +-- .../src/tiptap/menus/callout/bubble.tsx | 25 ++-- .../client/src/tiptap/menus/callout/index.tsx | 4 - .../menus/clear-node-and-marks/index.tsx | 17 +-- .../src/tiptap/menus/code-block/bubble.tsx | 24 ++-- .../src/tiptap/menus/code-block/index.tsx | 4 - .../client/src/tiptap/menus/code/index.tsx | 11 +- .../src/tiptap/menus/countdown/bubble.tsx | 24 ++-- .../src/tiptap/menus/countdown/index.tsx | 4 - .../tiptap/menus/document-children/bubble.tsx | 22 +-- .../tiptap/menus/document-children/index.tsx | 4 - .../menus/document-reference/bubble.tsx | 21 +-- .../tiptap/menus/document-reference/index.tsx | 4 - .../src/tiptap/menus/fontsize/index.tsx | 17 ++- .../client/src/tiptap/menus/heading/index.tsx | 39 +++--- .../tiptap/menus/horizontal-rule/index.tsx | 15 +- .../client/src/tiptap/menus/ident/index.tsx | 31 ++-- .../client/src/tiptap/menus/iframe/bubble.tsx | 27 ++-- .../client/src/tiptap/menus/iframe/index.tsx | 4 - .../client/src/tiptap/menus/image/bubble.tsx | 132 +++++++----------- .../client/src/tiptap/menus/image/index.tsx | 4 - .../client/src/tiptap/menus/insert/index.tsx | 8 +- .../client/src/tiptap/menus/italic/index.tsx | 11 +- .../client/src/tiptap/menus/link/bubble.tsx | 6 +- .../client/src/tiptap/menus/link/index.tsx | 17 ++- .../client/src/tiptap/menus/link/modal.tsx | 7 +- .../client/src/tiptap/menus/mind/bubble.tsx | 27 ++-- .../client/src/tiptap/menus/mind/index.tsx | 4 - .../src/tiptap/menus/ordered-list/index.tsx | 19 +-- .../client/src/tiptap/menus/search/index.tsx | 53 ++++--- .../client/src/tiptap/menus/strike/index.tsx | 11 +- .../src/tiptap/menus/subscript/index.tsx | 11 +- .../src/tiptap/menus/superscript/index.tsx | 11 +- .../client/src/tiptap/menus/table/bubble.tsx | 91 ++++-------- .../client/src/tiptap/menus/table/index.tsx | 4 - .../src/tiptap/menus/task-list/index.tsx | 19 +-- .../src/tiptap/menus/text-color/index.tsx | 50 ++++--- .../src/tiptap/menus/underline/index.tsx | 7 +- 52 files changed, 569 insertions(+), 530 deletions(-) create mode 100644 packages/client/src/tiptap/hooks/use-active.tsx create mode 100644 packages/client/src/tiptap/hooks/use-attributes.tsx diff --git a/packages/client/src/components/document/editor/editor.tsx b/packages/client/src/components/document/editor/editor.tsx index 5a899fc5..9cfa0e4d 100644 --- a/packages/client/src/components/document/editor/editor.tsx +++ b/packages/client/src/components/document/editor/editor.tsx @@ -64,23 +64,26 @@ export const Editor: React.FC = ({ user: currentUser, documentId, author }, }); }, [documentId, currentUser, toggleLoading]); - const editor = useEditor({ - editable: authority && authority.editable, - extensions: [ - ...BaseKit, - DocumentWithTitle, - getCollaborationExtension(provider), - getCollaborationCursorExtension(provider, currentUser), - ], - onTransaction: debounce(({ transaction }) => { - try { - const title = transaction.doc.content.firstChild.content.firstChild.textContent; - triggerChangeDocumentTitle(title); - } catch (e) { - // - } - }, 50), - }); + const editor = useEditor( + { + editable: authority && authority.editable, + extensions: [ + ...BaseKit, + DocumentWithTitle, + getCollaborationExtension(provider), + getCollaborationCursorExtension(provider, currentUser), + ], + onTransaction: debounce(({ transaction }) => { + try { + const title = transaction.doc.content.firstChild.content.firstChild.textContent; + triggerChangeDocumentTitle(title); + } catch (e) { + // + } + }, 50), + }, + [authority, provider] + ); const [mentionUsersSettingVisible, toggleMentionUsersSettingVisible] = useToggle(false); const [mentionUsers, setMentionUsers] = useState([]); diff --git a/packages/client/src/helpers/lru-cache.ts b/packages/client/src/helpers/lru-cache.ts index bb0a0dce..29394e69 100644 --- a/packages/client/src/helpers/lru-cache.ts +++ b/packages/client/src/helpers/lru-cache.ts @@ -120,7 +120,13 @@ export const createKeysLocalStorageLRUCache = (storageKey, capacity) => { const lruCache = new LRUCache(capacity); if (USED_STORAGE_KEYS.includes(storageKey)) { - throw new Error(`Storage Key ${storageKey} has been used!`); + // @ts-ignore + if (module.hot) { + console.error(`Storage Key ${storageKey} has been used!`); + return; + } else { + throw new Error(`Storage Key ${storageKey} has been used!`); + } } USED_STORAGE_KEYS.push(storageKey); diff --git a/packages/client/src/tiptap/divider.tsx b/packages/client/src/tiptap/divider.tsx index 8f44ee33..fed9059b 100644 --- a/packages/client/src/tiptap/divider.tsx +++ b/packages/client/src/tiptap/divider.tsx @@ -1,4 +1,6 @@ -export const Divider = ({ vertical = false }) => { +import React from 'react'; + +export const _Divider = ({ vertical = false }) => { return (
{ >
); }; + +export const Divider = React.memo(_Divider, (prevProps, nextProps) => { + return prevProps.vertical === nextProps.vertical; +}); diff --git a/packages/client/src/tiptap/extensions/indent.ts b/packages/client/src/tiptap/extensions/indent.ts index fc7ad7bd..74d3393e 100644 --- a/packages/client/src/tiptap/extensions/indent.ts +++ b/packages/client/src/tiptap/extensions/indent.ts @@ -3,21 +3,21 @@ import { sinkListItem, liftListItem } from 'prosemirror-schema-list'; import { TextSelection, AllSelection, Transaction } from 'prosemirror-state'; import { isListActive, isListNode, clamp, getNodeType } from 'tiptap/prose-utils'; +declare module '@tiptap/core' { + interface Commands { + indent: { + indent: () => ReturnType; + outdent: () => ReturnType; + }; + } +} + 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, diff --git a/packages/client/src/tiptap/extensions/search.ts b/packages/client/src/tiptap/extensions/search.ts index 3dce9c99..1419db37 100644 --- a/packages/client/src/tiptap/extensions/search.ts +++ b/packages/client/src/tiptap/extensions/search.ts @@ -43,6 +43,7 @@ interface SearchOptions { searchResultCurrentClass: string; caseSensitive: boolean; disableRegex: boolean; + onChange?: () => void; } interface TextNodesWithPosition { @@ -216,6 +217,7 @@ export const SearchNReplace = Extension.create({ searchResultCurrentClass: 'search-result-current', caseSensitive: false, disableRegex: false, + onChange: () => {}, }; }, @@ -279,7 +281,7 @@ export const SearchNReplace = Extension.create({ const { currentIndex, results, searchResultCurrentClass } = this.options; const nextIndex = (currentIndex + results.length - 1) % results.length; this.options.currentIndex = nextIndex; - + this.options.onChange && this.options.onChange(); return gotoSearchResult({ view, tr, @@ -294,7 +296,7 @@ export const SearchNReplace = Extension.create({ const { currentIndex, results, searchResultCurrentClass } = this.options; const nextIndex = (currentIndex + 1) % results.length; this.options.currentIndex = nextIndex; - + this.options.onChange && this.options.onChange(); return gotoSearchResult({ view, tr, @@ -331,6 +333,10 @@ export const SearchNReplace = Extension.create({ ); extensionThis.options.results = results; + if (results.length && searchTerm) { + extensionThis.options.onChange && extensionThis.options.onChange(); + } + if (ctx.getMeta('directDecoration')) { const { fromPos, toPos, attrs } = ctx.getMeta('directDecoration'); decorationsToReturn.push(Decoration.inline(fromPos, toPos, attrs)); diff --git a/packages/client/src/tiptap/hooks/use-active.tsx b/packages/client/src/tiptap/hooks/use-active.tsx new file mode 100644 index 00000000..0d26cdcb --- /dev/null +++ b/packages/client/src/tiptap/hooks/use-active.tsx @@ -0,0 +1,22 @@ +import React, { useEffect } from 'react'; +import { Editor } from '@tiptap/core'; +import { useToggle } from 'hooks/use-toggle'; + +export const useActive = (editor: Editor, ...args) => { + const [active, toggleActive] = useToggle(false); + + useEffect(() => { + const listener = () => { + // eslint-disable-next-line prefer-spread + toggleActive(editor.isActive.apply(editor, args)); + }; + + editor.on('selectionUpdate', listener); + + return () => { + editor.off('selectionUpdate', listener); + }; + }, [editor, args, toggleActive]); + + return active; +}; diff --git a/packages/client/src/tiptap/hooks/use-attributes.tsx b/packages/client/src/tiptap/hooks/use-attributes.tsx new file mode 100644 index 00000000..2238bfdc --- /dev/null +++ b/packages/client/src/tiptap/hooks/use-attributes.tsx @@ -0,0 +1,48 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Editor } from '@tiptap/core'; +import deepEqual from 'deep-equal'; + +type MapFn = (arg: T) => R; + +function mapSelf(d: T): T { + return d; +} + +export function useAttributes, R>( + editor: Editor, + attrbute: string, + defaultValue?: T, + map?: (arg: T) => R +) { + const mapFn = (map || mapSelf) as MapFn; + const [value, setValue] = useState(mapFn(defaultValue)); + const prevValueCache = useRef(value); + + useEffect(() => { + const listener = () => { + const attrs = { ...defaultValue, ...editor.getAttributes(attrbute) }; + Object.keys(attrs).forEach((key) => { + if (attrs[key] === null || attrs[key] === undefined) { + // @ts-ignore + attrs[key] = defaultValue[key]; + } + }); + const nextAttrs = mapFn(attrs); + if (deepEqual(prevValueCache.current, nextAttrs)) { + return; + } + setValue(nextAttrs); + prevValueCache.current = nextAttrs; + }; + + editor.on('selectionUpdate', listener); + editor.on('transaction', listener); + + return () => { + editor.off('selectionUpdate', listener); + editor.off('transaction', listener); + }; + }, [editor, defaultValue, attrbute, mapFn]); + + return value; +} diff --git a/packages/client/src/tiptap/menubar.tsx b/packages/client/src/tiptap/menubar.tsx index a77c1867..86c6c989 100644 --- a/packages/client/src/tiptap/menubar.tsx +++ b/packages/client/src/tiptap/menubar.tsx @@ -44,6 +44,8 @@ import { Table } from './menus/table'; import { Mind } from './menus/mind'; const _MenuBar: React.FC<{ editor: any }> = ({ editor }) => { + if (!editor) return null; + return (
diff --git a/packages/client/src/tiptap/menus/align/index.tsx b/packages/client/src/tiptap/menus/align/index.tsx index f3ee85a5..394ba5c6 100644 --- a/packages/client/src/tiptap/menus/align/index.tsx +++ b/packages/client/src/tiptap/menus/align/index.tsx @@ -1,26 +1,35 @@ -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { Editor } from '@tiptap/core'; import { Button, Dropdown, Tooltip } from '@douyinfe/semi-ui'; import { IconAlignLeft, IconAlignCenter, IconAlignRight, IconAlignJustify } from '@douyinfe/semi-icons'; -import { isTitleActive } from 'tiptap/prose-utils'; +import { useActive } from 'tiptap/hooks/use-active'; +import { Title } from 'tiptap/extensions/title'; export const Align: React.FC<{ editor: Editor }> = ({ editor }) => { - const current = (() => { - if (editor.isActive({ textAlign: 'center' })) { + const isTitleActive = useActive(editor, Title.name); + const isAlignCenter = useActive(editor, { textAlign: 'center' }); + const isAlignRight = useActive(editor, { textAlign: 'right' }); + const isAlignJustify = useActive(editor, { textAlign: 'justify' }); + + const current = useMemo(() => { + if (isAlignCenter) { return ; } - if (editor.isActive({ textAlign: 'right' })) { + if (isAlignRight) { return ; } - if (editor.isActive({ textAlign: 'justify' })) { + if (isAlignJustify) { return ; } return ; - })(); + }, [isAlignCenter, isAlignRight, isAlignJustify]); - const toggle = (align) => { - return () => editor.chain().focus().setTextAlign(align).run(); - }; + const toggle = useCallback( + (align) => { + return () => editor.chain().focus().setTextAlign(align).run(); + }, + [editor] + ); return ( = ({ editor }) => { > - + diff --git a/packages/client/src/tiptap/menus/attachment/bubble.tsx b/packages/client/src/tiptap/menus/attachment/bubble.tsx index 28286e8a..4774a124 100644 --- a/packages/client/src/tiptap/menus/attachment/bubble.tsx +++ b/packages/client/src/tiptap/menus/attachment/bubble.tsx @@ -5,37 +5,29 @@ import { BubbleMenu } from 'tiptap/views/bubble-menu'; import { Attachment } from 'tiptap/extensions/attachment'; import { copyNode, deleteNode } from 'tiptap/prose-utils'; import { Divider } from 'tiptap/divider'; +import { useCallback } from 'react'; export const AttachmentBubbleMenu = ({ editor }) => { + const copyMe = useCallback(() => copyNode(Attachment.name, editor), [editor]); + const deleteMe = useCallback(() => deleteNode(Attachment.name, editor), [editor]); + return ( editor.isActive(Attachment.name)} tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }} > - + -
diff --git a/packages/client/src/tiptap/menus/italic/index.tsx b/packages/client/src/tiptap/menus/italic/index.tsx index addb8049..4efe630d 100644 --- a/packages/client/src/tiptap/menus/italic/index.tsx +++ b/packages/client/src/tiptap/menus/italic/index.tsx @@ -3,17 +3,22 @@ import { Editor } from '@tiptap/core'; import { Button } from '@douyinfe/semi-ui'; import { IconItalic } from '@douyinfe/semi-icons'; import { Tooltip } from 'components/tooltip'; -import { isTitleActive } from 'tiptap/prose-utils'; +import { useActive } from 'tiptap/hooks/use-active'; +import { Title } from 'tiptap/extensions/title'; +import { Italic as ItalicExtension } from 'tiptap/extensions/italic'; export const Italic: React.FC<{ editor: Editor }> = ({ editor }) => { + const isTitleActive = useActive(editor, Title.name); + const isItalicActive = useActive(editor, ItalicExtension.name); + return ( - - - diff --git a/packages/client/src/tiptap/menus/strike/index.tsx b/packages/client/src/tiptap/menus/strike/index.tsx index 16333464..d2b1a898 100644 --- a/packages/client/src/tiptap/menus/strike/index.tsx +++ b/packages/client/src/tiptap/menus/strike/index.tsx @@ -3,17 +3,22 @@ import { Editor } from '@tiptap/core'; import { Button } from '@douyinfe/semi-ui'; import { IconStrikeThrough } from '@douyinfe/semi-icons'; import { Tooltip } from 'components/tooltip'; -import { isTitleActive } from 'tiptap/prose-utils'; +import { useActive } from 'tiptap/hooks/use-active'; +import { Title } from 'tiptap/extensions/title'; +import { Strike as StrikeExtension } from 'tiptap/extensions/strike'; export const Strike: React.FC<{ editor: Editor }> = ({ editor }) => { + const isTitleActive = useActive(editor, Title.name); + const isStrikeActive = useActive(editor, StrikeExtension.name); + return (