Merge branch 'fix/callout-delete-merge' of github.com:toeverything/AFFiNE into fix/callout-delete-merge

This commit is contained in:
zzj3720
2025-09-17 19:43:06 +08:00
15 changed files with 170 additions and 185 deletions
+1
View File
@@ -2,6 +2,7 @@
**/node_modules
.yarn
.github/helm
.git
.vscode
.yarnrc.yml
.docker
@@ -24,7 +24,7 @@ import {
getPrevContentBlock,
matchModels,
} from '@blocksuite/affine-shared/utils';
import { IS_MOBILE } from '@blocksuite/global/env';
import { IS_ANDROID, IS_MOBILE } from '@blocksuite/global/env';
import { BlockSelection, type EditorHost } from '@blocksuite/std';
import type { BlockModel, Text } from '@blocksuite/store';
@@ -79,6 +79,28 @@ export function mergeWithPrev(editorHost: EditorHost, model: BlockModel) {
index: lengthBeforeJoin,
length: 0,
}).catch(console.error);
// due to some IME like Microsoft Swift IME on Android will reset range after join text,
// for example:
//
// $ZERO_WIDTH_FOR_EMPTY_LINE <--- p1
// |aaa <--- p2
//
// after pressing backspace, during beforeinput event, the native range is (p1, 1) -> (p2, 0)
// and after browser and IME handle the event, the native range is (p1, 1) -> (p1, 1)
//
// a|aa <--- p1
//
// so we need to set range again after join text.
if (IS_ANDROID) {
setTimeout(() => {
asyncSetInlineRange(editorHost.std, prevBlock, {
index: lengthBeforeJoin,
length: 0,
}).catch(console.error);
});
}
return true;
}
@@ -1,4 +1,5 @@
import { IS_MAC } from '@blocksuite/global/env';
import { DisposableGroup } from '@blocksuite/global/disposable';
import { IS_ANDROID, IS_MAC } from '@blocksuite/global/env';
import {
type UIEventHandler,
@@ -6,7 +7,7 @@ import {
UIEventStateContext,
} from '../base.js';
import type { EventOptions, UIEventDispatcher } from '../dispatcher.js';
import { bindKeymap } from '../keymap.js';
import { androidBindKeymapPatch, bindKeymap } from '../keymap.js';
import { KeyboardEventState } from '../state/index.js';
import { EventScopeSourceType, EventSourceState } from '../state/source.js';
@@ -87,15 +88,29 @@ export class KeyboardControl {
}
bindHotkey(keymap: Record<string, UIEventHandler>, options?: EventOptions) {
return this._dispatcher.add(
'keyDown',
ctx => {
if (this.composition) return false;
const binding = bindKeymap(keymap);
return binding(ctx);
},
options
const disposables = new DisposableGroup();
if (IS_ANDROID) {
disposables.add(
this._dispatcher.add('beforeInput', ctx => {
if (this.composition) return false;
const binding = androidBindKeymapPatch(keymap);
return binding(ctx);
})
);
}
disposables.add(
this._dispatcher.add(
'keyDown',
ctx => {
if (this.composition) return false;
const binding = bindKeymap(keymap);
return binding(ctx);
},
options
)
);
return () => disposables.dispose();
}
listen() {
@@ -103,3 +103,25 @@ export function bindKeymap(
return false;
};
}
// In some IME of Android like, the keypress event dose not contain
// the information about what key is pressed. See
// https://stackoverflow.com/a/68188679
// https://stackoverflow.com/a/66724830
export function androidBindKeymapPatch(
bindings: Record<string, UIEventHandler>
): UIEventHandler {
return ctx => {
const event = ctx.get('defaultState').event;
if (!(event instanceof InputEvent)) return;
if (
event.inputType === 'deleteContentBackward' &&
'Backspace' in bindings
) {
return bindings['Backspace'](ctx);
}
return false;
};
}
@@ -1,3 +1,4 @@
import { IS_ANDROID } from '@blocksuite/global/env';
import type { BaseTextAttributes } from '@blocksuite/store';
import type { InlineEditor } from '../inline-editor.js';
@@ -41,11 +42,10 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
}
};
private readonly _onBeforeInput = (event: InputEvent) => {
private readonly _onBeforeInput = async (event: InputEvent) => {
const range = this.editor.rangeService.getNativeRange();
if (
this.editor.isReadonly ||
this._isComposing ||
!range ||
!this._isRangeCompletelyInRoot(range)
)
@@ -54,33 +54,29 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
let inlineRange = this.editor.toInlineRange(range);
if (!inlineRange) return;
if (this._isComposing) {
if (IS_ANDROID && event.inputType === 'insertCompositionText') {
this._compositionInlineRange = inlineRange;
}
return;
}
let ifHandleTargetRange = true;
if (event.inputType.startsWith('delete')) {
if (
isInEmbedGap(range.commonAncestorContainer) &&
inlineRange.length === 0 &&
inlineRange.index > 0
) {
inlineRange = {
index: inlineRange.index - 1,
length: 1,
};
ifHandleTargetRange = false;
} else if (
isInEmptyLine(range.commonAncestorContainer) &&
inlineRange.length === 0 &&
inlineRange.index > 0
// eslint-disable-next-line sonarjs/no-duplicated-branches
) {
// do not use target range when deleting across lines
if (
event.inputType.startsWith('delete') &&
(isInEmbedGap(range.commonAncestorContainer) ||
// https://github.com/toeverything/blocksuite/issues/5381
inlineRange = {
index: inlineRange.index - 1,
length: 1,
};
ifHandleTargetRange = false;
}
isInEmptyLine(range.commonAncestorContainer)) &&
inlineRange.length === 0 &&
inlineRange.index > 0
) {
// do not use target range when deleting across lines
inlineRange = {
index: inlineRange.index - 1,
length: 1,
};
ifHandleTargetRange = false;
}
if (ifHandleTargetRange) {
@@ -97,11 +93,24 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
}
}
}
if (!inlineRange) return;
event.preventDefault();
if (IS_ANDROID) {
this.editor.rerenderWholeEditor();
await this.editor.waitForUpdate();
if (
event.inputType === 'deleteContentBackward' &&
!(inlineRange.index === 0 && inlineRange.length === 0)
) {
// when press backspace at offset 1, double characters will be removed.
// because we mock backspace key event `androidBindKeymapPatch` in blocksuite/framework/std/src/event/keymap.ts
// so we need to stop the event propagation to prevent the double characters removal.
event.stopPropagation();
}
}
const ctx: BeforeinputHookCtx<TextAttributes> = {
inlineEditor: this.editor,
raw: event,
@@ -346,11 +355,9 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
return;
}
this.editor.disposables.addFromEvent(
eventSource,
'beforeinput',
this._onBeforeInput
);
this.editor.disposables.addFromEvent(eventSource, 'beforeinput', e => {
this._onBeforeInput(e).catch(console.error);
});
this.editor.disposables.addFromEvent(
eventSource,
'compositionstart',
+1
View File
@@ -9,6 +9,7 @@
"**/node_modules",
".yarn",
".github/helm",
".git",
".vscode",
".yarnrc.yml",
".docker",
+1 -1
View File
@@ -82,7 +82,7 @@
"husky": "^9.1.7",
"lint-staged": "^16.0.0",
"msw": "^2.6.8",
"oxlint": "^1.11.1",
"oxlint": "^1.15.0",
"prettier": "^3.4.2",
"semver": "^7.6.3",
"serve": "^14.2.4",
@@ -84,7 +84,7 @@ export const useDraggable = <D extends DNDData = DNDData>(
: undefined,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...deps, context.toExternalData]);
}, [...deps, getOptions, context.toExternalData]);
useEffect(() => {
if (
@@ -207,7 +207,7 @@ export const useDropTarget = <D extends DNDData = DNDData>(
: undefined,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...deps, dropTargetContext.fromExternalData]);
}, [...deps, getOptions, dropTargetContext.fromExternalData]);
const getDropTargetOptions = useCallback(() => {
const wrappedCanDrop = dropTargetGet(options.canDrop, options);
@@ -95,7 +95,7 @@ export const useDndMonitor = <D extends DNDData = DNDData>(
: undefined,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...deps, getOptions]);
}, [...deps, getOptions, dropTargetContext.fromExternalData]);
const monitorOptions = useMemo(() => {
return {
@@ -14,12 +14,16 @@ export const useGuard = <
) => {
const guardService = useService(GuardService);
useEffect(() => {
// oxlint-disable-next-line exhaustive-deps
guardService.revalidateCan(action, ...args);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [action, guardService, ...args]);
const livedata$ = useMemo(
() => guardService.can$(action, ...args),
() => {
// oxlint-disable-next-line exhaustive-deps
return guardService.can$(action, ...args);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[action, guardService, ...args]
);
@@ -24,6 +24,6 @@ export function useAsyncCallback<T extends any[]>(
(...args: any) => {
callback(...args).catch(e => handleAsyncError(e));
},
[...deps] // eslint-disable-line react-hooks/exhaustive-deps
[callback, handleAsyncError, ...deps] // eslint-disable-line react-hooks/exhaustive-deps
);
}
+8 -27
View File
@@ -59,27 +59,9 @@ test.describe('comments', () => {
{ delay: 50 }
);
// Select some text using triple-click and then refine selection
// Triple-click to select the entire paragraph
await page.locator('affine-paragraph').first().click({ clickCount: 3 });
// Wait for selection
await page.waitForTimeout(100);
// Now we have the whole paragraph selected, let's use mouse to select just "some text"
const paragraph = page.locator('affine-paragraph').first();
const bbox = await paragraph.boundingBox();
if (!bbox) throw new Error('Paragraph not found');
// Click and drag to select "some text" portion
// Start roughly where "some" begins (estimated position)
await page.mouse.move(bbox.x + bbox.width * 0.45, bbox.y + bbox.height / 2);
await page.mouse.down();
await page.mouse.move(bbox.x + bbox.width * 0.58, bbox.y + bbox.height / 2);
await page.mouse.up();
// Wait a bit for selection to stabilize
await page.waitForTimeout(200);
for (let i = 0; i < 11; i++) {
await page.keyboard.press('Shift+ArrowLeft');
}
// Wait for the toolbar to appear after text selection
const toolbar = page.locator('editor-toolbar');
@@ -97,11 +79,14 @@ test.describe('comments', () => {
await page.waitForTimeout(300); // Wait for sidebar animation
// Find the comment editor
const commentEditor = page.locator('.comment-editor-viewport');
const commentEditor = page.locator(
'.comment-editor-viewport .page-editor-container'
);
await expect(commentEditor).toBeVisible();
// Enter comment text
await commentEditor.click();
await commentEditor.focus();
await page.keyboard.type('This is my first comment on this text', {
delay: 50,
});
@@ -125,11 +110,7 @@ test.describe('comments', () => {
// The preview should show the selected text that was commented on
// Target specifically the sidebar tab content to avoid conflicts with editor content
const sidebarTab = page.getByTestId('sidebar-tab-content-comment');
await expect(
sidebarTab.locator(
'text=This is a test paragraph with some text that we will comment on.'
)
).toBeVisible();
await expect(sidebarTab.locator('text=comment on.')).toBeVisible();
// This text should appear in the sidebar as the preview of what was commented on
+1
View File
@@ -281,6 +281,7 @@ test('link bar should not be appear when the range is collapsed', async ({
await expect(linkPopoverLocator).toBeVisible();
await focusRichText(page); // click to cancel the link popover
await waitNextFrame(page);
await focusRichTextEnd(page);
await pressShiftEnter(page);
await waitNextFrame(page);
+40 -109
View File
@@ -803,7 +803,7 @@ __metadata:
husky: "npm:^9.1.7"
lint-staged: "npm:^16.0.0"
msw: "npm:^2.6.8"
oxlint: "npm:^1.11.1"
oxlint: "npm:^1.15.0"
prettier: "npm:^3.4.2"
semver: "npm:^7.6.3"
serve: "npm:^14.2.4"
@@ -10698,100 +10698,58 @@ __metadata:
languageName: node
linkType: hard
"@oxlint-tsgolint/darwin-arm64@npm:0.0.1":
version: 0.0.1
resolution: "@oxlint-tsgolint/darwin-arm64@npm:0.0.1"
conditions: os=darwin
languageName: node
linkType: hard
"@oxlint-tsgolint/darwin-x64@npm:0.0.1":
version: 0.0.1
resolution: "@oxlint-tsgolint/darwin-x64@npm:0.0.1"
conditions: os=darwin
languageName: node
linkType: hard
"@oxlint-tsgolint/linux-arm64@npm:0.0.1":
version: 0.0.1
resolution: "@oxlint-tsgolint/linux-arm64@npm:0.0.1"
conditions: os=linux
languageName: node
linkType: hard
"@oxlint-tsgolint/linux-x64@npm:0.0.1":
version: 0.0.1
resolution: "@oxlint-tsgolint/linux-x64@npm:0.0.1"
conditions: os=linux
languageName: node
linkType: hard
"@oxlint-tsgolint/win32-arm64@npm:0.0.1":
version: 0.0.1
resolution: "@oxlint-tsgolint/win32-arm64@npm:0.0.1"
conditions: os=win32
languageName: node
linkType: hard
"@oxlint-tsgolint/win32-x64@npm:0.0.1":
version: 0.0.1
resolution: "@oxlint-tsgolint/win32-x64@npm:0.0.1"
conditions: os=win32
languageName: node
linkType: hard
"@oxlint/darwin-arm64@npm:1.11.1":
version: 1.11.1
resolution: "@oxlint/darwin-arm64@npm:1.11.1"
"@oxlint/darwin-arm64@npm:1.15.0":
version: 1.15.0
resolution: "@oxlint/darwin-arm64@npm:1.15.0"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@oxlint/darwin-x64@npm:1.11.1":
version: 1.11.1
resolution: "@oxlint/darwin-x64@npm:1.11.1"
"@oxlint/darwin-x64@npm:1.15.0":
version: 1.15.0
resolution: "@oxlint/darwin-x64@npm:1.15.0"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@oxlint/linux-arm64-gnu@npm:1.11.1":
version: 1.11.1
resolution: "@oxlint/linux-arm64-gnu@npm:1.11.1"
"@oxlint/linux-arm64-gnu@npm:1.15.0":
version: 1.15.0
resolution: "@oxlint/linux-arm64-gnu@npm:1.15.0"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@oxlint/linux-arm64-musl@npm:1.11.1":
version: 1.11.1
resolution: "@oxlint/linux-arm64-musl@npm:1.11.1"
"@oxlint/linux-arm64-musl@npm:1.15.0":
version: 1.15.0
resolution: "@oxlint/linux-arm64-musl@npm:1.15.0"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@oxlint/linux-x64-gnu@npm:1.11.1":
version: 1.11.1
resolution: "@oxlint/linux-x64-gnu@npm:1.11.1"
"@oxlint/linux-x64-gnu@npm:1.15.0":
version: 1.15.0
resolution: "@oxlint/linux-x64-gnu@npm:1.15.0"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@oxlint/linux-x64-musl@npm:1.11.1":
version: 1.11.1
resolution: "@oxlint/linux-x64-musl@npm:1.11.1"
"@oxlint/linux-x64-musl@npm:1.15.0":
version: 1.15.0
resolution: "@oxlint/linux-x64-musl@npm:1.15.0"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@oxlint/win32-arm64@npm:1.11.1":
version: 1.11.1
resolution: "@oxlint/win32-arm64@npm:1.11.1"
"@oxlint/win32-arm64@npm:1.15.0":
version: 1.15.0
resolution: "@oxlint/win32-arm64@npm:1.15.0"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@oxlint/win32-x64@npm:1.11.1":
version: 1.11.1
resolution: "@oxlint/win32-x64@npm:1.11.1"
"@oxlint/win32-x64@npm:1.15.0":
version: 1.15.0
resolution: "@oxlint/win32-x64@npm:1.15.0"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
@@ -28685,48 +28643,20 @@ __metadata:
languageName: node
linkType: hard
"oxlint-tsgolint@npm:>=0.0.1":
version: 0.0.1
resolution: "oxlint-tsgolint@npm:0.0.1"
"oxlint@npm:^1.15.0":
version: 1.15.0
resolution: "oxlint@npm:1.15.0"
dependencies:
"@oxlint-tsgolint/darwin-arm64": "npm:0.0.1"
"@oxlint-tsgolint/darwin-x64": "npm:0.0.1"
"@oxlint-tsgolint/linux-arm64": "npm:0.0.1"
"@oxlint-tsgolint/linux-x64": "npm:0.0.1"
"@oxlint-tsgolint/win32-arm64": "npm:0.0.1"
"@oxlint-tsgolint/win32-x64": "npm:0.0.1"
dependenciesMeta:
"@oxlint-tsgolint/darwin-arm64":
optional: true
"@oxlint-tsgolint/darwin-x64":
optional: true
"@oxlint-tsgolint/linux-arm64":
optional: true
"@oxlint-tsgolint/linux-x64":
optional: true
"@oxlint-tsgolint/win32-arm64":
optional: true
"@oxlint-tsgolint/win32-x64":
optional: true
bin:
tsgolint: bin/tsgolint.js
checksum: 10/2cadb04d5597f425564ed080ca608edb1014aebc85a7a9336a49285c2cb4289379f3eb614694666c8802618a28f619ab2f37dd1ac86cba33a309bc69d8ff47f1
languageName: node
linkType: hard
"oxlint@npm:^1.11.1":
version: 1.11.1
resolution: "oxlint@npm:1.11.1"
dependencies:
"@oxlint/darwin-arm64": "npm:1.11.1"
"@oxlint/darwin-x64": "npm:1.11.1"
"@oxlint/linux-arm64-gnu": "npm:1.11.1"
"@oxlint/linux-arm64-musl": "npm:1.11.1"
"@oxlint/linux-x64-gnu": "npm:1.11.1"
"@oxlint/linux-x64-musl": "npm:1.11.1"
"@oxlint/win32-arm64": "npm:1.11.1"
"@oxlint/win32-x64": "npm:1.11.1"
oxlint-tsgolint: "npm:>=0.0.1"
"@oxlint/darwin-arm64": "npm:1.15.0"
"@oxlint/darwin-x64": "npm:1.15.0"
"@oxlint/linux-arm64-gnu": "npm:1.15.0"
"@oxlint/linux-arm64-musl": "npm:1.15.0"
"@oxlint/linux-x64-gnu": "npm:1.15.0"
"@oxlint/linux-x64-musl": "npm:1.15.0"
"@oxlint/win32-arm64": "npm:1.15.0"
"@oxlint/win32-x64": "npm:1.15.0"
peerDependencies:
oxlint-tsgolint: ">=0.2.0"
dependenciesMeta:
"@oxlint/darwin-arm64":
optional: true
@@ -28744,12 +28674,13 @@ __metadata:
optional: true
"@oxlint/win32-x64":
optional: true
peerDependenciesMeta:
oxlint-tsgolint:
optional: true
bin:
oxc_language_server: bin/oxc_language_server
oxlint: bin/oxlint
checksum: 10/bdf6cb7f6d74b1d6c63ddfdc9597f5394857b1bbee2fb5ab6b86bae9bb58e3ca707ce345f488ae6087ffd909122b615c6020843f7669f6dade14e0396c107a9c
checksum: 10/1ee632ad359b3e63a3a5fccadfcab23ac4b0881b06f2e6c29431db56377858571592005459f247b2eef822d1da4c9d68afdf23965afe6ec6a5fe092f60239fa8
languageName: node
linkType: hard