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
This commit is contained in:
pengx17
2025-04-15 04:44:26 +00:00
parent 4011214451
commit 575aa3c1c1
8 changed files with 234 additions and 279 deletions

View File

@@ -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<HTMLDivElement> {
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<boolean>;
mode: DocMode;
origin: HTMLDivElement;
std: BlockStdScope;
}
export const BlocksuiteEditorContainer = forwardRef<
AffineEditorContainer,
BlocksuiteEditorContainerProps
>(function AffineEditorContainer(
{ page, mode, shared, readonly, defaultOpenProperty, ...props },
ref
) {
const rootRef = useRef<HTMLDivElement>(null);
const docRef = useRef<PageEditor>(null);
const docTitleRef = useRef<DocTitle>(null);
const edgelessRef = useRef<EdgelessEditor>(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 (
<div
{...props}
data-testid={`editor-${page.id}`}
dir={enableEditorRTL ? 'rtl' : 'ltr'}
className={clsx(
`editor-wrapper ${mode}-mode`,
styles.docEditorRoot,
props.className
)}
data-affine-editor-container
style={props.style}
ref={rootRef}
>
{mode === 'page' ? (
<BlocksuiteDocEditor
shared={shared}
page={page}
ref={docRef}
readonly={readonly}
titleRef={docTitleRef}
onClickBlank={handleClickPageModeBlank}
defaultOpenProperty={defaultOpenProperty}
/>
) : (
<BlocksuiteEdgelessEditor
shared={shared}
page={page}
ref={edgelessRef}
/>
)}
</div>
);
});

View File

@@ -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<boolean>;
mode: DocMode;
origin: HTMLDivElement;
std: BlockStdScope;
}
export interface EditorProps extends HTMLAttributes<HTMLDivElement> {
page: Store;
@@ -47,6 +67,113 @@ const BlockSuiteEditorImpl = ({
defaultOpenProperty,
...props
}: EditorProps) => {
const rootRef = useRef<HTMLDivElement>(null);
const docRef = useRef<PageEditor>(null);
const docTitleRef = useRef<DocTitle>(null);
const edgelessRef = useRef<EdgelessEditor>(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 (
<BlocksuiteEditorContainer
<div
{...props}
mode={mode}
page={page}
shared={shared}
readonly={readonly}
defaultOpenProperty={defaultOpenProperty}
ref={editorRef}
className={className}
data-testid={`editor-${page.id}`}
dir={enableEditorRTL ? 'rtl' : 'ltr'}
className={clsx(
`editor-wrapper ${mode}-mode`,
styles.docEditorRoot,
className
)}
style={style}
/>
data-affine-editor-container
ref={rootRef}
>
{mode === 'page' ? (
<BlocksuiteDocEditor
shared={shared}
page={page}
ref={docRef}
readonly={readonly}
titleRef={docTitleRef}
onClickBlank={handleClickPageModeBlank}
defaultOpenProperty={defaultOpenProperty}
/>
) : (
<BlocksuiteEdgelessEditor
shared={shared}
page={page}
ref={edgelessRef}
/>
)}
</div>
);
};
@@ -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<HTMLDivElement>) => {
if (settings.disableMiddleClickPaste && e.button === 1) {
e.preventDefault();
}
},
[settings.disableMiddleClickPaste]
);
if (error) {
throw error;
}
return (
<Slot
style={{ '--affine-font-family': fontFamily } as CSSProperties}
onMouseDown={handleMouseDown}
>
<Slot style={{ '--affine-font-family': fontFamily } as CSSProperties}>
{isLoading ? (
<EditorLoading />
) : (

View File

@@ -10,4 +10,3 @@ registerAIEffects();
registerTemplates();
export * from './blocksuite-editor';
export * from './blocksuite-editor-container';

View File

@@ -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';

View File

@@ -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 = () => {
]()}
>
<Switch
checked={settings.disableMiddleClickPaste}
checked={settings.enableMiddleClickPaste}
onChange={onToggleMiddleClickPaste}
/>
</SettingRow>

View File

@@ -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(

View File

@@ -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;
/**

View File

@@ -1289,7 +1289,7 @@
"com.affine.settings.editorSettings.general.spell-check.restart-hint": "Settings changed; please restart the app. <1>Restart</1>",
"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.",