From 575aa3c1c15827fa3bdaa44f709dbf0f98388e3d Mon Sep 17 00:00:00 2001 From: pengx17 Date: Tue, 15 Apr 2025 04:44:26 +0000 Subject: [PATCH] fix(editor): rework disable middle click settings for linux (#11556) fix BS-3028 Unfortunately, I don't find out a way to disable this behavior on ff linux --- .../blocksuite-editor-container.tsx | 192 ----------- .../block-suite-editor/blocksuite-editor.tsx | 305 +++++++++++++----- .../blocksuite/block-suite-editor/index.ts | 1 - .../hooks/affine/use-export-page.ts | 2 +- .../general-setting/editor/general.tsx | 7 +- .../core/src/modules/editor-setting/schema.ts | 2 +- packages/frontend/i18n/src/i18n.gen.ts | 2 +- packages/frontend/i18n/src/resources/en.json | 2 +- 8 files changed, 234 insertions(+), 279 deletions(-) delete mode 100644 packages/frontend/core/src/blocksuite/block-suite-editor/blocksuite-editor-container.tsx diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/blocksuite-editor-container.tsx b/packages/frontend/core/src/blocksuite/block-suite-editor/blocksuite-editor-container.tsx deleted file mode 100644 index a009ea779f..0000000000 --- a/packages/frontend/core/src/blocksuite/block-suite-editor/blocksuite-editor-container.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import type { - EdgelessEditor, - PageEditor, -} from '@affine/core/blocksuite/editors'; -import { FeatureFlagService } from '@affine/core/modules/feature-flag'; -import { appendParagraphCommand } from '@blocksuite/affine/blocks/paragraph'; -import type { DocTitle } from '@blocksuite/affine/fragments/doc-title'; -import type { DocMode, RootBlockModel } from '@blocksuite/affine/model'; -import { focusBlockEnd } from '@blocksuite/affine/shared/commands'; -import { getLastNoteBlock } from '@blocksuite/affine/shared/utils'; -import type { BlockStdScope, EditorHost } from '@blocksuite/affine/std'; -import { type Store } from '@blocksuite/affine/store'; -import { useLiveData, useService } from '@toeverything/infra'; -import clsx from 'clsx'; -import type React from 'react'; -import { - forwardRef, - useCallback, - useImperativeHandle, - useMemo, - useRef, -} from 'react'; - -import type { DefaultOpenProperty } from '../../components/doc-properties'; -import { BlocksuiteDocEditor, BlocksuiteEdgelessEditor } from './lit-adaper'; -import * as styles from './styles.css'; - -interface BlocksuiteEditorContainerProps - extends React.HTMLAttributes { - page: Store; - mode: DocMode; - shared?: boolean; - readonly?: boolean; - defaultOpenProperty?: DefaultOpenProperty; -} - -export interface AffineEditorContainer extends HTMLElement { - page: Store; - doc: Store; - docTitle: DocTitle; - host?: EditorHost; - model: RootBlockModel | null; - updateComplete: Promise; - mode: DocMode; - origin: HTMLDivElement; - std: BlockStdScope; -} - -export const BlocksuiteEditorContainer = forwardRef< - AffineEditorContainer, - BlocksuiteEditorContainerProps ->(function AffineEditorContainer( - { page, mode, shared, readonly, defaultOpenProperty, ...props }, - ref -) { - const rootRef = useRef(null); - const docRef = useRef(null); - const docTitleRef = useRef(null); - const edgelessRef = useRef(null); - const featureFlags = useService(FeatureFlagService).flags; - const enableEditorRTL = useLiveData(featureFlags.enable_editor_rtl.$); - - /** - * mimic an AffineEditorContainer using proxy - */ - const affineEditorContainerProxy = useMemo(() => { - const api = { - get page() { - return page; - }, - get doc() { - return page; - }, - get docTitle() { - return docTitleRef.current; - }, - get host() { - return ( - (mode === 'page' - ? docRef.current?.host - : edgelessRef.current?.host) ?? null - ); - }, - get model() { - return page.root as any; - }, - get updateComplete() { - return mode === 'page' - ? docRef.current?.updateComplete - : edgelessRef.current?.updateComplete; - }, - get mode() { - return mode; - }, - get origin() { - return rootRef.current; - }, - get std() { - return mode === 'page' ? docRef.current?.std : edgelessRef.current?.std; - }, - }; - - const proxy = new Proxy(api, { - has(_, prop) { - return ( - Reflect.has(api, prop) || - (rootRef.current ? Reflect.has(rootRef.current, prop) : false) - ); - }, - get(_, prop) { - if (Reflect.has(api, prop)) { - return api[prop as keyof typeof api]; - } - if (rootRef.current && Reflect.has(rootRef.current, prop)) { - const maybeFn = Reflect.get(rootRef.current, prop); - if (typeof maybeFn === 'function') { - return maybeFn.bind(rootRef.current); - } else { - return maybeFn; - } - } - return undefined; - }, - }) as AffineEditorContainer; - - return proxy; - }, [mode, page]); - - useImperativeHandle(ref, () => affineEditorContainerProxy, [ - affineEditorContainerProxy, - ]); - - const handleClickPageModeBlank = useCallback(() => { - if (shared || readonly || page.readonly) return; - const std = affineEditorContainerProxy.host?.std; - if (!std) { - return; - } - const note = getLastNoteBlock(page); - if (note) { - const lastBlock = note.lastChild(); - if ( - lastBlock && - lastBlock.flavour === 'affine:paragraph' && - lastBlock.text?.length === 0 - ) { - const focusBlock = std.view.getBlock(lastBlock.id) ?? undefined; - std.command.exec(focusBlockEnd, { - focusBlock, - force: true, - }); - return; - } - } - - std.command.exec(appendParagraphCommand); - }, [affineEditorContainerProxy.host?.std, page, readonly, shared]); - - return ( -
- {mode === 'page' ? ( - - ) : ( - - )} -
- ); -}); diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/blocksuite-editor.tsx b/packages/frontend/core/src/blocksuite/block-suite-editor/blocksuite-editor.tsx index 6b53c52133..9f57bc686f 100644 --- a/packages/frontend/core/src/blocksuite/block-suite-editor/blocksuite-editor.tsx +++ b/packages/frontend/core/src/blocksuite/block-suite-editor/blocksuite-editor.tsx @@ -1,30 +1,50 @@ -import { useRefEffect } from '@affine/component'; import { EditorLoading } from '@affine/component/page-detail-skeleton'; +import type { + EdgelessEditor, + PageEditor, +} from '@affine/core/blocksuite/editors'; import { ServerService } from '@affine/core/modules/cloud'; import { EditorSettingService, fontStyleOptions, } from '@affine/core/modules/editor-setting'; +import { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { customImageProxyMiddleware, ImageProxyService, } from '@blocksuite/affine/blocks/image'; +import { appendParagraphCommand } from '@blocksuite/affine/blocks/paragraph'; +import type { DocTitle } from '@blocksuite/affine/fragments/doc-title'; import { DisposableGroup } from '@blocksuite/affine/global/disposable'; -import type { DocMode } from '@blocksuite/affine/model'; +import type { DocMode, RootBlockModel } from '@blocksuite/affine/model'; +import { focusBlockEnd } from '@blocksuite/affine/shared/commands'; import { LinkPreviewerService } from '@blocksuite/affine/shared/services'; +import { getLastNoteBlock } from '@blocksuite/affine/shared/utils'; +import type { BlockStdScope, EditorHost } from '@blocksuite/affine/std'; import type { Store } from '@blocksuite/affine/store'; import { Slot } from '@radix-ui/react-slot'; import { useLiveData, useService } from '@toeverything/infra'; import { cssVar } from '@toeverything/theme'; +import clsx from 'clsx'; import type { CSSProperties, HTMLAttributes } from 'react'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { DefaultOpenProperty } from '../../components/doc-properties'; -import { - type AffineEditorContainer, - BlocksuiteEditorContainer, -} from './blocksuite-editor-container'; +import { BlocksuiteDocEditor, BlocksuiteEdgelessEditor } from './lit-adaper'; import { NoPageRootError } from './no-page-error'; +import * as styles from './styles.css'; + +export interface AffineEditorContainer extends HTMLElement { + page: Store; + doc: Store; + docTitle: DocTitle; + host?: EditorHost; + model: RootBlockModel | null; + updateComplete: Promise; + mode: DocMode; + origin: HTMLDivElement; + std: BlockStdScope; +} export interface EditorProps extends HTMLAttributes { page: Store; @@ -47,6 +67,113 @@ const BlockSuiteEditorImpl = ({ defaultOpenProperty, ...props }: EditorProps) => { + const rootRef = useRef(null); + const docRef = useRef(null); + const docTitleRef = useRef(null); + const edgelessRef = useRef(null); + const featureFlags = useService(FeatureFlagService).flags; + const enableEditorRTL = useLiveData(featureFlags.enable_editor_rtl.$); + const editorSetting = useService(EditorSettingService).editorSetting; + const server = useService(ServerService).server; + + const { enableMiddleClickPaste } = useLiveData( + editorSetting.settings$.selector(s => ({ + enableMiddleClickPaste: s.enableMiddleClickPaste, + })) + ); + + /** + * mimic an AffineEditorContainer using proxy + */ + const affineEditorContainerProxy = useMemo(() => { + const api = { + get page() { + return page; + }, + get doc() { + return page; + }, + get docTitle() { + return docTitleRef.current; + }, + get host() { + return ( + (mode === 'page' + ? docRef.current?.host + : edgelessRef.current?.host) ?? null + ); + }, + get model() { + return page.root as any; + }, + get updateComplete() { + return mode === 'page' + ? docRef.current?.updateComplete + : edgelessRef.current?.updateComplete; + }, + get mode() { + return mode; + }, + get origin() { + return rootRef.current; + }, + get std() { + return mode === 'page' ? docRef.current?.std : edgelessRef.current?.std; + }, + }; + + const proxy = new Proxy(api, { + has(_, prop) { + return ( + Reflect.has(api, prop) || + (rootRef.current ? Reflect.has(rootRef.current, prop) : false) + ); + }, + get(_, prop) { + if (Reflect.has(api, prop)) { + return api[prop as keyof typeof api]; + } + if (rootRef.current && Reflect.has(rootRef.current, prop)) { + const maybeFn = Reflect.get(rootRef.current, prop); + if (typeof maybeFn === 'function') { + return maybeFn.bind(rootRef.current); + } else { + return maybeFn; + } + } + return undefined; + }, + }) as AffineEditorContainer; + + return proxy; + }, [mode, page]); + + const handleClickPageModeBlank = useCallback(() => { + if (shared || readonly || page.readonly) return; + const std = affineEditorContainerProxy.host?.std; + if (!std) { + return; + } + const note = getLastNoteBlock(page); + if (note) { + const lastBlock = note.lastChild(); + if ( + lastBlock && + lastBlock.flavour === 'affine:paragraph' && + lastBlock.text?.length === 0 + ) { + const focusBlock = std.view.getBlock(lastBlock.id) ?? undefined; + std.command.exec(focusBlockEnd, { + focusBlock, + force: true, + }); + return; + } + } + + std.command.exec(appendParagraphCommand); + }, [affineEditorContainerProxy.host?.std, page, readonly, shared]); + useEffect(() => { const disposable = page.slots.blockUpdated.subscribe(() => { disposable.unsubscribe(); @@ -59,67 +186,104 @@ const BlockSuiteEditorImpl = ({ }; }, [page]); - const server = useService(ServerService).server; - - const editorRef = useRefEffect( - (editor: AffineEditorContainer) => { - globalThis.currentEditor = editor; - let canceled = false; - const disposableGroup = new DisposableGroup(); - - // Invoke onLoad once the editor has been mounted to the DOM. - if (canceled) { - return; - } - - // provide image proxy endpoint to blocksuite - const imageProxyUrl = new URL( - BUILD_CONFIG.imageProxyUrl, - server.baseUrl - ).toString(); - - const linkPreviewUrl = new URL( - BUILD_CONFIG.linkPreviewUrl, - server.baseUrl - ).toString(); - - editor.std.clipboard.use(customImageProxyMiddleware(imageProxyUrl)); - page.get(LinkPreviewerService).setEndpoint(linkPreviewUrl); - page.get(ImageProxyService).setImageProxyURL(imageProxyUrl); - - editor.updateComplete - .then(() => { - if (onEditorReady) { - const dispose = onEditorReady(editor); - if (dispose) { - disposableGroup.add(dispose); - } - } - }) - .catch(error => { - console.error('Error updating editor', error); - }); - - return () => { - canceled = true; - disposableGroup.dispose(); + useEffect(() => { + const editorContainer = rootRef.current; + if (editorContainer) { + const handleMiddleClick = (e: MouseEvent) => { + if (!enableMiddleClickPaste && e.button === 1) { + e.preventDefault(); + } }; - }, - [onEditorReady, page, server] - ); + editorContainer.addEventListener('pointerup', handleMiddleClick, { + capture: true, + }); + editorContainer.addEventListener('auxclick', handleMiddleClick, { + capture: true, + }); + return () => { + editorContainer?.removeEventListener('pointerup', handleMiddleClick, { + capture: true, + }); + editorContainer?.removeEventListener('auxclick', handleMiddleClick, { + capture: true, + }); + }; + } + return; + }, [enableMiddleClickPaste]); + + useEffect(() => { + const editor = affineEditorContainerProxy; + globalThis.currentEditor = editor; + const disposableGroup = new DisposableGroup(); + let canceled = false; + + // provide image proxy endpoint to blocksuite + const imageProxyUrl = new URL( + BUILD_CONFIG.imageProxyUrl, + server.baseUrl + ).toString(); + + const linkPreviewUrl = new URL( + BUILD_CONFIG.linkPreviewUrl, + server.baseUrl + ).toString(); + + editor.std.clipboard.use(customImageProxyMiddleware(imageProxyUrl)); + page.get(LinkPreviewerService).setEndpoint(linkPreviewUrl); + page.get(ImageProxyService).setImageProxyURL(imageProxyUrl); + + editor.updateComplete + .then(() => { + if (onEditorReady && !canceled) { + const dispose = onEditorReady(editor); + if (dispose) { + disposableGroup.add(dispose); + } + } + }) + .catch(error => { + console.error('Error updating editor', error); + }); + + return () => { + canceled = true; + disposableGroup.dispose(); + }; + }, [affineEditorContainerProxy, onEditorReady, page, server]); return ( - + data-affine-editor-container + ref={rootRef} + > + {mode === 'page' ? ( + + ) : ( + + )} + ); }; @@ -133,7 +297,6 @@ export const BlockSuiteEditor = (props: EditorProps) => { fontFamily: s.fontFamily, customFontFamily: s.customFontFamily, fullWidthLayout: s.fullWidthLayout, - disableMiddleClickPaste: s.disableMiddleClickPaste, })) ); const fontFamily = useMemo(() => { @@ -170,24 +333,12 @@ export const BlockSuiteEditor = (props: EditorProps) => { }; }, [props.page]); - const handleMouseDown = useCallback( - (e: React.MouseEvent) => { - if (settings.disableMiddleClickPaste && e.button === 1) { - e.preventDefault(); - } - }, - [settings.disableMiddleClickPaste] - ); - if (error) { throw error; } return ( - + {isLoading ? ( ) : ( diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/index.ts b/packages/frontend/core/src/blocksuite/block-suite-editor/index.ts index 068e2ff98b..2f7395b2f3 100644 --- a/packages/frontend/core/src/blocksuite/block-suite-editor/index.ts +++ b/packages/frontend/core/src/blocksuite/block-suite-editor/index.ts @@ -10,4 +10,3 @@ registerAIEffects(); registerTemplates(); export * from './blocksuite-editor'; -export * from './blocksuite-editor-container'; diff --git a/packages/frontend/core/src/components/hooks/affine/use-export-page.ts b/packages/frontend/core/src/components/hooks/affine/use-export-page.ts index 920c0428e8..da4b1479ab 100644 --- a/packages/frontend/core/src/components/hooks/affine/use-export-page.ts +++ b/packages/frontend/core/src/components/hooks/affine/use-export-page.ts @@ -3,6 +3,7 @@ import { pushGlobalLoadingEventAtom, resolveGlobalLoadingEventAtom, } from '@affine/component/global-loading'; +import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-editor/blocksuite-editor'; import { EditorService } from '@affine/core/modules/editor'; import { getAFFiNEWorkspaceSchema } from '@affine/core/modules/workspace/global-schema'; import { useI18n } from '@affine/i18n'; @@ -29,7 +30,6 @@ import { useLiveData, useService } from '@toeverything/infra'; import { useSetAtom } from 'jotai'; import { nanoid } from 'nanoid'; -import type { AffineEditorContainer } from '../../../blocksuite/block-suite-editor/blocksuite-editor-container'; import { useAsyncCallback } from '../affine-async-hooks'; type ExportType = 'pdf' | 'html' | 'png' | 'markdown' | 'snapshot'; diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/general.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/general.tsx index 85e6537bb3..931a5dc107 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/general.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/general.tsx @@ -493,10 +493,7 @@ const MiddleClickPasteSettings = () => { const settings = useLiveData(editorSettingService.editorSetting.settings$); const onToggleMiddleClickPaste = useCallback( (checked: boolean) => { - editorSettingService.editorSetting.set( - 'disableMiddleClickPaste', - checked - ); + editorSettingService.editorSetting.set('enableMiddleClickPaste', checked); }, [editorSettingService.editorSetting] ); @@ -510,7 +507,7 @@ const MiddleClickPasteSettings = () => { ]()} > diff --git a/packages/frontend/core/src/modules/editor-setting/schema.ts b/packages/frontend/core/src/modules/editor-setting/schema.ts index 4bbb4f5e18..9a36d735ab 100644 --- a/packages/frontend/core/src/modules/editor-setting/schema.ts +++ b/packages/frontend/core/src/modules/editor-setting/schema.ts @@ -35,7 +35,7 @@ const AffineEditorSettingSchema = z.object({ ]) .default('open-in-active-view'), // linux only: - disableMiddleClickPaste: z.boolean().default(false), + enableMiddleClickPaste: z.boolean().default(false), }); export const EditorSettingSchema = BSEditorSettingSchema.merge( diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index 353ab114a3..c5c84ac664 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -5176,7 +5176,7 @@ export function useAFFiNEI18N(): { */ ["com.affine.settings.editorSettings.general.middle-click-paste.title"](): string; /** - * `Disable default middle click paste behavior on Linux.` + * `Enable default middle click paste behavior on Linux.` */ ["com.affine.settings.editorSettings.general.middle-click-paste.description"](): string; /** diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 168c9382ec..b68843182b 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1289,7 +1289,7 @@ "com.affine.settings.editorSettings.general.spell-check.restart-hint": "Settings changed; please restart the app. <1>Restart", "com.affine.settings.editorSettings.page": "Page", "com.affine.settings.editorSettings.general.middle-click-paste.title": "Middle click paste", - "com.affine.settings.editorSettings.general.middle-click-paste.description": "Disable default middle click paste behavior on Linux.", + "com.affine.settings.editorSettings.general.middle-click-paste.description": "Enable default middle click paste behavior on Linux.", "com.affine.settings.editorSettings.page.display-bi-link.description": "Display bi-directional links on the doc.", "com.affine.settings.editorSettings.page.display-bi-link.title": "Display bi-directional links", "com.affine.settings.editorSettings.page.display-doc-info.description": "Display document information on the doc.",