diff --git a/blocksuite/affine/fragments/fragment-doc-title/src/doc-title.ts b/blocksuite/affine/fragments/fragment-doc-title/src/doc-title.ts index 3943122b47..d1f2d5255d 100644 --- a/blocksuite/affine/fragments/fragment-doc-title/src/doc-title.ts +++ b/blocksuite/affine/fragments/fragment-doc-title/src/doc-title.ts @@ -82,8 +82,10 @@ export class DocTitle extends WithDisposable(ShadowlessElement) { private readonly _onTitleKeyDown = (event: KeyboardEvent) => { if (event.isComposing || this.doc.readonly) return; + if (!this._std) return; if (event.key === 'Enter') { + this._std.event.active = true; event.preventDefault(); event.stopPropagation(); @@ -99,6 +101,7 @@ export class DocTitle extends WithDisposable(ShadowlessElement) { if (this._std) focusTextModel(this._std, newFirstParagraphId); } } else if (event.key === 'ArrowDown') { + this._std.event.active = true; event.preventDefault(); event.stopPropagation(); diff --git a/blocksuite/affine/fragments/fragment-outline/src/body/outline-panel-body.ts b/blocksuite/affine/fragments/fragment-outline/src/body/outline-panel-body.ts index fe215ed682..d188209fbb 100644 --- a/blocksuite/affine/fragments/fragment-outline/src/body/outline-panel-body.ts +++ b/blocksuite/affine/fragments/fragment-outline/src/body/outline-panel-body.ts @@ -1,8 +1,12 @@ import { changeNoteDisplayMode } from '@blocksuite/affine-block-note'; import { NoteBlockModel, NoteDisplayMode } from '@blocksuite/affine-model'; import { DocModeProvider } from '@blocksuite/affine-shared/services'; -import { matchModels } from '@blocksuite/affine-shared/utils'; -import { ShadowlessElement, SurfaceSelection } from '@blocksuite/block-std'; +import { focusTitle, matchModels } from '@blocksuite/affine-shared/utils'; +import { + BlockSelection, + ShadowlessElement, + SurfaceSelection, +} from '@blocksuite/block-std'; import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx'; import { Bound } from '@blocksuite/global/gfx'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; @@ -170,6 +174,19 @@ export class OutlinePanelBody extends SignalWatcher( } private async _scrollToBlock(blockId: string) { + // if focus title + if (blockId === this.doc.root?.id) { + this.editor.std.selection.setGroup('note', []); + this.editor.std.event.active = false; + focusTitle(this.editor); + } else { + this.editor.std.event.active = true; + this.editor.std.selection.setGroup('note', [ + this.editor.std.selection.create(BlockSelection, { + blockId, + }), + ]); + } this._lockActiveHeadingId = true; this._activeHeadingId$.value = blockId; this._clearHighlightMask = await scrollToBlockWithHighlight( diff --git a/blocksuite/affine/rich-text/src/dom.ts b/blocksuite/affine/rich-text/src/dom.ts index 831277b89d..b78e16818a 100644 --- a/blocksuite/affine/rich-text/src/dom.ts +++ b/blocksuite/affine/rich-text/src/dom.ts @@ -68,6 +68,7 @@ export function focusTextModel( id: string, offset: number = 0 ) { + std.event.active = true; selectTextModel(std, id, offset); } diff --git a/blocksuite/affine/shared/src/commands/selection/focus-block-end.ts b/blocksuite/affine/shared/src/commands/selection/focus-block-end.ts index a8e5906c71..88a736f2c1 100644 --- a/blocksuite/affine/shared/src/commands/selection/focus-block-end.ts +++ b/blocksuite/affine/shared/src/commands/selection/focus-block-end.ts @@ -19,6 +19,7 @@ export const focusBlockEnd: Command<{ const { selection } = std; + std.event.active = true; if (force) selection.clear(); selection.setGroup('note', [ diff --git a/blocksuite/framework/block-std/src/event/dispatcher.ts b/blocksuite/framework/block-std/src/event/dispatcher.ts index baf6ff2073..a52fd7c4d4 100644 --- a/blocksuite/framework/block-std/src/event/dispatcher.ts +++ b/blocksuite/framework/block-std/src/event/dispatcher.ts @@ -1,5 +1,6 @@ import { DisposableGroup } from '@blocksuite/global/disposable'; import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; +import { signal } from '@preact/signals-core'; import { LifeCycleWatcher } from '../extension/index.js'; import { KeymapIdentifier } from '../identifier.js'; @@ -81,7 +82,7 @@ export class UIEventDispatcher extends LifeCycleWatcher { static override readonly key = 'UIEventDispatcher'; - private _active = false; + private readonly _active = signal(false); private readonly _clipboardControl: ClipboardControl; @@ -105,6 +106,10 @@ export class UIEventDispatcher extends LifeCycleWatcher { } get active() { + return this._active.peek(); + } + + get active$() { return this._active; } @@ -302,19 +307,24 @@ export class UIEventDispatcher extends LifeCycleWatcher { if (active) { if (UIEventDispatcher._activeDispatcher !== this) { if (UIEventDispatcher._activeDispatcher) { - UIEventDispatcher._activeDispatcher._active = false; + UIEventDispatcher._activeDispatcher._active.value = false; } UIEventDispatcher._activeDispatcher = this; } - this._active = true; + this._active.value = true; } else { if (UIEventDispatcher._activeDispatcher === this) { UIEventDispatcher._activeDispatcher = null; } - this._active = false; + this._active.value = false; } } + set active(active: boolean) { + if (active === this._active.peek()) return; + this._setActive(active); + } + add(name: EventName, handler: UIEventHandler, options?: EventOptions) { const runner: EventHandlerRunner = { fn: handler, diff --git a/blocksuite/framework/block-std/src/inline/range/range-binding.ts b/blocksuite/framework/block-std/src/inline/range/range-binding.ts index 58b10ed19e..250c7bf148 100644 --- a/blocksuite/framework/block-std/src/inline/range/range-binding.ts +++ b/blocksuite/framework/block-std/src/inline/range/range-binding.ts @@ -169,7 +169,10 @@ export class RangeBinding { private readonly _onNativeSelectionChanged = async () => { if (this.isComposing) return; if (!this.host) return; // Unstable when switching views, card <-> embed - if (!isActiveInEditor(this.host)) return; + if (!isActiveInEditor(this.host)) { + this._prevTextSelection = null; + return; + } await this.host.updateComplete; @@ -251,6 +254,8 @@ export class RangeBinding { // TODO(@mirone): this is a trade-off, we need to use separate awareness store for every store to make sure the selection is isolated. const closestHost = document.activeElement?.closest('editor-host'); if (closestHost && closestHost !== this.host) return; + const active = this.host.event.active; + if (!active) return; const text = selections.find((selection): selection is TextSelection => @@ -266,7 +271,7 @@ export class RangeBinding { const id = text?.blockId; const path = id && this._computePath(id); - if (this.host.event.active) { + if (active) { const eq = text && this._prevTextSelection && path ? text.equals(this._prevTextSelection.selection) && diff --git a/tests/affine-local/e2e/blocksuite/editor.spec.ts b/tests/affine-local/e2e/blocksuite/editor.spec.ts index 753adba2ed..ab0eaf61b8 100644 --- a/tests/affine-local/e2e/blocksuite/editor.spec.ts +++ b/tests/affine-local/e2e/blocksuite/editor.spec.ts @@ -1,3 +1,4 @@ +import { waitNextFrame } from '@affine-test/kit/bs/misc'; import { test } from '@affine-test/kit/playwright'; import { locateEditorContainer } from '@affine-test/kit/utils/editor'; import { openHomePage } from '@affine-test/kit/utils/load-page'; @@ -55,6 +56,7 @@ test('link page is useable', async ({ page }) => { await page.keyboard.press('g'); await page.keyboard.press('e'); await page.keyboard.press('1'); + await waitNextFrame(page); await page.locator('icon-button:has-text("page1")').first().click(); const link = page.locator('.affine-reference'); await expect(link).toBeVisible(); diff --git a/tests/blocksuite/e2e/bookmark.spec.ts b/tests/blocksuite/e2e/bookmark.spec.ts index f55941da53..0ba1d4d968 100644 --- a/tests/blocksuite/e2e/bookmark.spec.ts +++ b/tests/blocksuite/e2e/bookmark.spec.ts @@ -127,6 +127,7 @@ test( await activeNoteInEdgeless(page, ids.noteId); await waitForInlineEditorStateUpdated(page); + await focusRichText(page); await selectAllByKeyboard(page); await copyByKeyboard(page); await pressArrowRight(page); diff --git a/tests/blocksuite/e2e/clipboard/clipboard.spec.ts b/tests/blocksuite/e2e/clipboard/clipboard.spec.ts index f844f46c16..d4508c48e2 100644 --- a/tests/blocksuite/e2e/clipboard/clipboard.spec.ts +++ b/tests/blocksuite/e2e/clipboard/clipboard.spec.ts @@ -5,7 +5,6 @@ import { expect } from '@playwright/test'; import { captureHistory, copyByKeyboard, - dragBetweenCoords, dragOverTitle, enterPlaygroundRoom, focusRichText, @@ -20,15 +19,18 @@ import { mockParseDocUrlService, pasteByKeyboard, pasteContent, + pressArrowRight, pressEnter, pressShiftTab, pressTab, resetHistory, + selectAllByKeyboard, setInlineRangeInSelectedRichText, setSelection, SHORT_KEY, type, undoByClick, + undoByKeyboard, waitNextFrame, } from '../utils/actions/index.js'; import { @@ -106,15 +108,15 @@ test(scoped`split block when paste`, async ({ page }) => { `, }; await type(page, 'abc'); - await captureHistory(page); await setInlineRangeInSelectedRichText(page, 1, 1); + await captureHistory(page); await pasteContent(page, clipData); await waitNextFrame(page); await assertRichTexts(page, ['atext', 'h1c']); - await undoByClick(page); + await undoByKeyboard(page); await assertRichTexts(page, ['abc']); await type(page, 'aa'); @@ -132,11 +134,12 @@ test(scoped`split block when paste`, async ({ page }) => { if (!bottomRight789) { throw new Error('bottomRight789 is not found'); } - await dragBetweenCoords(page, topLeft123, bottomRight789); + await selectAllByKeyboard(page); + await pressArrowRight(page); + await pressEnter(page); - // FIXME see https://github.com/toeverything/blocksuite/pull/878 - // await pasteContent(page, clipData); - // await assertRichTexts(page, ['aaa', 'bbc', 'text', 'h1']); + await pasteContent(page, clipData); + await assertRichTexts(page, ['aaa', 'bbc', 'text', 'h1']); }); test(scoped`copy clipItems format`, async ({ page }) => { diff --git a/tests/blocksuite/e2e/clipboard/list.spec.ts b/tests/blocksuite/e2e/clipboard/list.spec.ts index e59fb0fbcf..d6d6761268 100644 --- a/tests/blocksuite/e2e/clipboard/list.spec.ts +++ b/tests/blocksuite/e2e/clipboard/list.spec.ts @@ -481,6 +481,7 @@ test(scoped`copy when text note active in edgeless`, async ({ page }) => { await activeNoteInEdgeless(page, ids.noteId); await waitForInlineEditorStateUpdated(page); + await focusRichText(page); await setInlineRangeInSelectedRichText(page, 0, 4); await copyByKeyboard(page); await pressArrowRight(page); diff --git a/tests/blocksuite/e2e/link.spec.ts b/tests/blocksuite/e2e/link.spec.ts index cfabd48660..2e47dd7657 100644 --- a/tests/blocksuite/e2e/link.spec.ts +++ b/tests/blocksuite/e2e/link.spec.ts @@ -288,6 +288,7 @@ test('link bar should not be appear when the range is collapsed', async ({ await pressCreateLinkShortCut(page); await expect(linkPopoverLocator).toBeVisible(); + await focusRichText(page); // click to cancel the link popover await focusRichTextEnd(page); await pressEnter(page); // create auto line-break in span element diff --git a/tests/blocksuite/e2e/markdown.spec.ts b/tests/blocksuite/e2e/markdown.spec.ts index 7d5f42d915..86af8379c3 100644 --- a/tests/blocksuite/e2e/markdown.spec.ts +++ b/tests/blocksuite/e2e/markdown.spec.ts @@ -37,9 +37,9 @@ test('markdown shortcut', async ({ page }) => { await waitNextFrame(page); [id] = await getCursorBlockIdAndHeight(page); await assertBlockType(page, id, 'todo'); - await undoByClick(page); + await undoByKeyboard(page); await assertText(page, '[] '); - await undoByClick(page); + await undoByKeyboard(page); //FIXME: it just failed in playwright await focusRichText(page); await assertRichTexts(page, ['']); @@ -49,9 +49,9 @@ test('markdown shortcut', async ({ page }) => { await waitNextFrame(page); [id] = await getCursorBlockIdAndHeight(page); await assertBlockType(page, id, 'todo'); - await undoByClick(page); + await undoByKeyboard(page); await assertText(page, '[ ] '); - await undoByClick(page); + await undoByKeyboard(page); //FIXME: it just failed in playwright await focusRichText(page); await assertRichTexts(page, ['']); @@ -61,9 +61,9 @@ test('markdown shortcut', async ({ page }) => { await waitNextFrame(page); [id] = await getCursorBlockIdAndHeight(page); await assertBlockType(page, id, 'todo'); - await undoByClick(page); + await undoByKeyboard(page); await assertText(page, '[x] '); - await undoByClick(page); + await undoByKeyboard(page); await assertRichTexts(page, ['']); await waitNextFrame(page); @@ -71,9 +71,9 @@ test('markdown shortcut', async ({ page }) => { await waitNextFrame(page); [id] = await getCursorBlockIdAndHeight(page); await assertBlockType(page, id, 'bulleted'); - await undoByClick(page); + await undoByKeyboard(page); await assertText(page, '* '); - await undoByClick(page); + await undoByKeyboard(page); await assertRichTexts(page, ['']); await waitNextFrame(page); @@ -81,9 +81,9 @@ test('markdown shortcut', async ({ page }) => { await waitNextFrame(page); [id] = await getCursorBlockIdAndHeight(page); await assertBlockType(page, id, 'bulleted'); - await undoByClick(page); + await undoByKeyboard(page); await assertText(page, '- '); - await undoByClick(page); + await undoByKeyboard(page); await assertRichTexts(page, ['']); await waitNextFrame(page); @@ -91,9 +91,9 @@ test('markdown shortcut', async ({ page }) => { await waitNextFrame(page); [id] = await getCursorBlockIdAndHeight(page); await assertBlockType(page, id, 'numbered'); - await undoByClick(page); + await undoByKeyboard(page); await assertText(page, '1. '); - await undoByClick(page); + await undoByKeyboard(page); await assertRichTexts(page, ['']); await waitNextFrame(page); @@ -101,9 +101,9 @@ test('markdown shortcut', async ({ page }) => { await waitNextFrame(page); [id] = await getCursorBlockIdAndHeight(page); await assertBlockType(page, id, 'numbered'); - await undoByClick(page); + await undoByKeyboard(page); await assertText(page, '20. '); - await undoByClick(page); + await undoByKeyboard(page); await assertRichTexts(page, ['']); await waitNextFrame(page); @@ -111,9 +111,9 @@ test('markdown shortcut', async ({ page }) => { await waitNextFrame(page); [id] = await getCursorBlockIdAndHeight(page); await assertBlockType(page, id, 'h1'); - await undoByClick(page); + await undoByKeyboard(page); await assertText(page, '# '); - await undoByClick(page); + await undoByKeyboard(page); await assertRichTexts(page, ['']); await waitNextFrame(page); @@ -121,9 +121,9 @@ test('markdown shortcut', async ({ page }) => { await waitNextFrame(page); [id] = await getCursorBlockIdAndHeight(page); await assertBlockType(page, id, 'h2'); - await undoByClick(page); + await undoByKeyboard(page); await assertText(page, '## '); - await undoByClick(page); + await undoByKeyboard(page); await assertRichTexts(page, ['']); await waitNextFrame(page); @@ -131,9 +131,9 @@ test('markdown shortcut', async ({ page }) => { await waitNextFrame(page); [id] = await getCursorBlockIdAndHeight(page); await assertBlockType(page, id, 'h3'); - await undoByClick(page); + await undoByKeyboard(page); await assertText(page, '### '); - await undoByClick(page); + await undoByKeyboard(page); await assertRichTexts(page, ['']); await waitNextFrame(page); @@ -141,9 +141,9 @@ test('markdown shortcut', async ({ page }) => { await waitNextFrame(page); [id] = await getCursorBlockIdAndHeight(page); await assertBlockType(page, id, 'h4'); - await undoByClick(page); + await undoByKeyboard(page); await assertText(page, '#### '); - await undoByClick(page); + await undoByKeyboard(page); await assertRichTexts(page, ['']); await waitNextFrame(page); @@ -151,9 +151,9 @@ test('markdown shortcut', async ({ page }) => { await waitNextFrame(page); [id] = await getCursorBlockIdAndHeight(page); await assertBlockType(page, id, 'h5'); - await undoByClick(page); + await undoByKeyboard(page); await assertText(page, '##### '); - await undoByClick(page); + await undoByKeyboard(page); await assertRichTexts(page, ['']); await waitNextFrame(page); @@ -161,9 +161,9 @@ test('markdown shortcut', async ({ page }) => { await waitNextFrame(page); [id] = await getCursorBlockIdAndHeight(page); await assertBlockType(page, id, 'h6'); - await undoByClick(page); + await undoByKeyboard(page); await assertText(page, '###### '); - await undoByClick(page); + await undoByKeyboard(page); await assertRichTexts(page, ['']); await waitNextFrame(page); @@ -171,17 +171,17 @@ test('markdown shortcut', async ({ page }) => { await waitNextFrame(page); [id] = await getCursorBlockIdAndHeight(page); await assertBlockType(page, id, 'quote'); - await undoByClick(page); + await undoByKeyboard(page); await assertText(page, '> '); - await undoByClick(page); + await undoByKeyboard(page); await assertRichTexts(page, ['']); // testing various horizontal dividers await waitNextFrame(page); await type(page, '--- '); - await undoByClick(page); + await undoByKeyboard(page); await assertRichTexts(page, ['--- ']); - await undoByClick(page); + await undoByKeyboard(page); await assertRichTexts(page, ['']); await waitNextFrame(page);