From 9a17422d3631efd0b123208b4ca57e465a13789f Mon Sep 17 00:00:00 2001 From: L-Sun Date: Thu, 13 Feb 2025 01:56:00 +0000 Subject: [PATCH] fix(editor): wrong position of remote selection and at menu in edgeless (#10137) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close [BS-2552](https://linear.app/affine-design/issue/BS-2552/menu-loading-时滚动,定位错误), [BS-2490](https://linear.app/affine-design/issue/BS-2490/note-block-的menu的输入阴影错位), [BS-2300](https://linear.app/affine-design/issue/BS-2300/at-menu的输入阴影在暗黑模式看不见) ### What Changes - fix the position of remote selection mask and @ menu input mask in edgeless - fix the position of @ menu is no updated during edgeless viewport change - update @ menu mask color in dark mode ### Before https://github.com/user-attachments/assets/f44f618e-a791-497a-9f53-74824fe48dea ### After https://github.com/user-attachments/assets/5d87b999-deae-4435-9b8b-4cdf55393395 --- .../affine/shared/src/utils/dom/viewport.ts | 27 +--- .../src/doc/doc-remote-selection.ts | 52 +++++--- .../blocks/src/root-block/utils/position.ts | 9 ++ .../root-block/widgets/linked-doc/index.ts | 115 ++++++++++++------ .../widgets/linked-doc/linked-doc-popover.ts | 14 ++- .../root-block/widgets/slash-menu/index.ts | 22 ---- .../widgets/slash-menu/slash-menu-popover.ts | 35 ++++-- 7 files changed, 160 insertions(+), 114 deletions(-) diff --git a/blocksuite/affine/shared/src/utils/dom/viewport.ts b/blocksuite/affine/shared/src/utils/dom/viewport.ts index f8dc956a75..e05d958ae9 100644 --- a/blocksuite/affine/shared/src/utils/dom/viewport.ts +++ b/blocksuite/affine/shared/src/utils/dom/viewport.ts @@ -1,6 +1,4 @@ -import type { BlockComponent, EditorHost } from '@blocksuite/block-std'; - -import { isInsidePageEditor } from './checker.js'; +import type { EditorHost } from '@blocksuite/block-std'; /** * Get editor viewport element. @@ -13,22 +11,9 @@ import { isInsidePageEditor } from './checker.js'; * }); * ``` */ -export function getViewportElement(editorHost: EditorHost): HTMLElement | null { - if (!isInsidePageEditor(editorHost)) return null; - const doc = editorHost.doc; - if (!doc.root) { - console.error('Failed to get root doc'); - return null; - } - const rootComponent = editorHost.view.getBlock(doc.root.id); - - if ( - !rootComponent || - rootComponent.closest('affine-page-root') !== rootComponent - ) { - console.error('Failed to get viewport element!'); - return null; - } - return (rootComponent as BlockComponent & { viewportElement: HTMLElement }) - .viewportElement; +export function getViewportElement(editorHost: EditorHost) { + return ( + editorHost.closest('.affine-page-viewport') ?? + editorHost.closest('.affine-edgeless-viewport') + ); } diff --git a/blocksuite/affine/widget-remote-selection/src/doc/doc-remote-selection.ts b/blocksuite/affine/widget-remote-selection/src/doc/doc-remote-selection.ts index 9017242d37..378a79e8eb 100644 --- a/blocksuite/affine/widget-remote-selection/src/doc/doc-remote-selection.ts +++ b/blocksuite/affine/widget-remote-selection/src/doc/doc-remote-selection.ts @@ -14,6 +14,7 @@ import { TextSelection, WidgetComponent, } from '@blocksuite/block-std'; +import { GfxController } from '@blocksuite/block-std/gfx'; import { throttle } from '@blocksuite/global/utils'; import type { BaseSelection, UserInfo } from '@blocksuite/store'; import { computed, effect } from '@preact/signals-core'; @@ -268,24 +269,28 @@ export class AffineDocRemoteSelectionWidget extends WidgetComponent { this._abortController.abort(); } - private readonly _updateSelections = throttle( - (selections: typeof this._remoteSelections.value) => { - const remoteUsers = new Set(); - this._selections = selections.flatMap(({ selections, id, user }) => { - if (remoteUsers.has(id)) { - return []; - } else { - remoteUsers.add(id); - } + private readonly _updateSelections = ( + selections: typeof this._remoteSelections.value + ) => { + const remoteUsers = new Set(); + this._selections = selections.flatMap(({ selections, id, user }) => { + if (remoteUsers.has(id)) { + return []; + } else { + remoteUsers.add(id); + } - return { - id, - selections, - rects: this._getSelectionRect(selections), - user, - }; - }); - }, + return { + id, + selections, + rects: this._getSelectionRect(selections), + user, + }; + }); + }; + + private readonly _updateSelectionsThrottled = throttle( + this._updateSelections, 60 ); @@ -293,13 +298,22 @@ export class AffineDocRemoteSelectionWidget extends WidgetComponent { this.disposables.add( effect(() => { const selections = this._remoteSelections.value; - this._updateSelections(selections); + this._updateSelectionsThrottled(selections); }) ); this.disposables.add( this.std.store.slots.blockUpdated.on(() => { - this._updateSelections(this._remoteSelections.peek()); + this._updateSelectionsThrottled(this._remoteSelections.peek()); + }) + ); + + const gfx = this.std.getOptional(GfxController); + if (!gfx) return; + this.disposables.add( + gfx.viewport.viewportUpdated.on(() => { + const selections = this._remoteSelections.peek(); + this._updateSelections(selections); }) ); } diff --git a/blocksuite/blocks/src/root-block/utils/position.ts b/blocksuite/blocks/src/root-block/utils/position.ts index 965e8a277e..4585b6ea52 100644 --- a/blocksuite/blocks/src/root-block/utils/position.ts +++ b/blocksuite/blocks/src/root-block/utils/position.ts @@ -70,6 +70,7 @@ export function compareTopAndBottomSpace( /** * Get the position of the popper element with flip. + * return null if the reference rect is all zero. */ export function getPopperPosition( popper: { @@ -93,6 +94,14 @@ export function getPopperPosition( ); const referenceRect = reference.getBoundingClientRect(); + if ( + referenceRect.x === 0 && + referenceRect.y === 0 && + referenceRect.width === 0 && + referenceRect.height === 0 + ) + return null; + const positioningPoint = { x: referenceRect.x, y: referenceRect.y + (placement === 'bottom' ? referenceRect.height : 0), diff --git a/blocksuite/blocks/src/root-block/widgets/linked-doc/index.ts b/blocksuite/blocks/src/root-block/widgets/linked-doc/index.ts index 92fcf3febb..2036d0fb5c 100644 --- a/blocksuite/blocks/src/root-block/widgets/linked-doc/index.ts +++ b/blocksuite/blocks/src/root-block/widgets/linked-doc/index.ts @@ -7,6 +7,7 @@ import { FeatureFlagService } from '@blocksuite/affine-shared/services'; import { getViewportElement } from '@blocksuite/affine-shared/utils'; import type { BlockComponent } from '@blocksuite/block-std'; import { BLOCK_ID_ATTR, WidgetComponent } from '@blocksuite/block-std'; +import { GfxController } from '@blocksuite/block-std/gfx'; import { IS_MOBILE } from '@blocksuite/global/env'; import { INLINE_ROOT_ATTR, @@ -15,7 +16,6 @@ import { } from '@blocksuite/inline'; import { signal } from '@preact/signals-core'; import { html, nothing } from 'lit'; -import { state } from 'lit/decorators.js'; import { choose } from 'lit/directives/choose.js'; import { repeat } from 'lit/directives/repeat.js'; import { styleMap } from 'lit/directives/style-map.js'; @@ -37,6 +37,40 @@ export class AffineLinkedDocWidget extends WidgetComponent< > { static override styles = linkedDocWidgetStyles; + private _context: LinkedDocContext | null = null; + + private readonly _inputRects$ = signal([]); + + private readonly _mode$ = signal<'desktop' | 'mobile' | 'none'>('none'); + + private _updateInputRects() { + if (!this._context) return; + const { inlineEditor, startRange, triggerKey } = this._context; + + const currentInlineRange = inlineEditor.getInlineRange(); + if (!currentInlineRange) return; + + const startIndex = startRange.index - triggerKey.length; + const range = inlineEditor.toDomRange({ + index: startIndex, + length: currentInlineRange.index - startIndex, + }); + if (!range) return; + + this._inputRects$.value = getRangeRects( + range, + getViewportElement(this.host) + ); + } + + private get _isCursorAtEnd() { + if (!this._context) return false; + const { inlineEditor } = this._context; + const currentInlineRange = inlineEditor.getInlineRange(); + if (!currentInlineRange) return false; + return currentInlineRange.index === inlineEditor.yTextLength; + } + private readonly _renderLinkedDocMenu = () => { if (!this.block.rootComponent) return nothing; @@ -52,13 +86,13 @@ export class AffineLinkedDocWidget extends WidgetComponent< >`; }; - @state() - private accessor _inputRects: SelectionRect[] = []; private _renderInputMask() { return html`${repeat( - this._inputRects, + this._inputRects$.value, ({ top, left, width, height }, index) => { - const last = index === this._inputRects.length - 1; + const last = + index === this._inputRects$.value.length - 1 && this._isCursorAtEnd; + const padding = 2; return html`
('none'); - - get config(): LinkedWidgetConfig { - return { - triggerKeys: ['@', '[[', '【【'], - ignoreBlockTypes: ['affine:code'], - ignoreSelector: - 'edgeless-text-editor, edgeless-shape-text-editor, edgeless-group-title-editor, edgeless-frame-title-editor, edgeless-connector-label-editor', - convertTriggerKey: true, - getMenus, - mobile: { - useScreenHeight: false, - scrollContainer: getViewportElement(this.std.host) ?? window, - scrollTopOffset: 46, - }, - ...this.std.getConfig('affine:page')?.linkedWidget, - }; - } - - private _context: LinkedDocContext | null = null; - override connectedCallback() { - super.connectedCallback(); - + private _watchInput() { this.handleEvent('beforeInput', ctx => { if (this._mode$.peek() !== 'none') return; @@ -184,6 +196,40 @@ export class AffineLinkedDocWidget extends WidgetComponent< }); } + private _watchViewportChange() { + const gfx = this.std.getOptional(GfxController); + if (!gfx) return; + this.disposables.add( + gfx.viewport.viewportUpdated.on(() => { + this._updateInputRects(); + }) + ); + } + + get config(): LinkedWidgetConfig { + return { + triggerKeys: ['@', '[[', '【【'], + ignoreBlockTypes: ['affine:code'], + ignoreSelector: + 'edgeless-text-editor, edgeless-shape-text-editor, edgeless-group-title-editor, edgeless-frame-title-editor, edgeless-connector-label-editor', + convertTriggerKey: true, + getMenus, + mobile: { + useScreenHeight: false, + scrollContainer: getViewportElement(this.std.host) ?? window, + scrollTopOffset: 46, + }, + ...this.std.getConfig('affine:page')?.linkedWidget, + }; + } + + override connectedCallback() { + super.connectedCallback(); + + this._watchInput(); + this._watchViewportChange(); + } + show(props?: { inlineEditor?: InlineEditor; primaryTriggerKey?: string; @@ -229,14 +275,7 @@ export class AffineLinkedDocWidget extends WidgetComponent< } const disposable = inlineEditor.slots.renderComplete.on(() => { - const currentInlineRange = inlineEditor.getInlineRange(); - if (!currentInlineRange) return; - const range = inlineEditor.toDomRange({ - index: inlineRange.index, - length: currentInlineRange.index - inlineRange.index, - }); - if (!range) return; - this._inputRects = getRangeRects(range, getViewportElement(host)); + this._updateInputRects(); }); this._context = { std: this.std, @@ -246,12 +285,14 @@ export class AffineLinkedDocWidget extends WidgetComponent< config: this.config, close: () => { disposable.dispose(); - this._inputRects = []; + this._inputRects$.value = []; this._mode$.value = 'none'; this._context = null; }, }; + this._updateInputRects(); + const enableMobile = this.doc .get(FeatureFlagService) .getFlag('enable_mobile_linked_doc_menu'); diff --git a/blocksuite/blocks/src/root-block/widgets/linked-doc/linked-doc-popover.ts b/blocksuite/blocks/src/root-block/widgets/linked-doc/linked-doc-popover.ts index fd55f90dfa..8f02960aea 100644 --- a/blocksuite/blocks/src/root-block/widgets/linked-doc/linked-doc-popover.ts +++ b/blocksuite/blocks/src/root-block/widgets/linked-doc/linked-doc-popover.ts @@ -12,6 +12,7 @@ import { getViewportElement, } from '@blocksuite/affine-shared/utils'; import { PropTypes, requiredProperties } from '@blocksuite/block-std'; +import { GfxController } from '@blocksuite/block-std/gfx'; import { SignalWatcher, throttle, @@ -317,18 +318,13 @@ export class LinkedDocPopover extends SignalWatcher(
`; } - updatePosition(position: { height: number; x: string; y: string }) { - this._position = position; - } - override willUpdate() { if (!this.hasUpdated) { const curRange = getCurrentNativeRange(); if (!curRange) return; const updatePosition = throttle(() => { - const position = getPopperPosition(this, curRange); - this.updatePosition(position); + this._position = getPopperPosition(this, curRange); }, 10); this.disposables.addFromEvent(window, 'resize', updatePosition); @@ -344,6 +340,12 @@ export class LinkedDocPopover extends SignalWatcher( } ); } + + const gfx = this.context.std.getOptional(GfxController); + if (gfx) { + this.disposables.add(gfx.viewport.viewportUpdated.on(updatePosition)); + } + updatePosition(); } } diff --git a/blocksuite/blocks/src/root-block/widgets/slash-menu/index.ts b/blocksuite/blocks/src/root-block/widgets/slash-menu/index.ts index 4d9b0c7a6e..1df1dcae51 100644 --- a/blocksuite/blocks/src/root-block/widgets/slash-menu/index.ts +++ b/blocksuite/blocks/src/root-block/widgets/slash-menu/index.ts @@ -2,20 +2,16 @@ import { type AffineInlineEditor, getInlineEditorByModel, } from '@blocksuite/affine-components/rich-text'; -import { getCurrentNativeRange } from '@blocksuite/affine-shared/utils'; import type { UIEventStateContext } from '@blocksuite/block-std'; import { TextSelection, WidgetComponent } from '@blocksuite/block-std'; import { - assertExists, assertType, debounce, DisposableGroup, - throttle, } from '@blocksuite/global/utils'; import { InlineEditor } from '@blocksuite/inline'; import type { RootBlockComponent } from '../../types.js'; -import { getPopperPosition } from '../../utils/position.js'; import { defaultSlashMenuConfig, type SlashMenuActionItem, @@ -56,9 +52,6 @@ const showSlashMenu = debounce( config: SlashMenuStaticConfig; triggerKey: string; }) => { - const curRange = getCurrentNativeRange(); - if (!curRange) return; - globalAbortController = abortController; const disposables = new DisposableGroup(); abortController.signal.addEventListener('abort', () => @@ -76,25 +69,10 @@ const showSlashMenu = debounce( slashMenu.config = config; slashMenu.triggerKey = triggerKey; - // Handle position - const updatePosition = throttle(() => { - const slashMenuElement = slashMenu.slashMenuElement; - assertExists( - slashMenuElement, - 'You should render the slash menu node even if no position' - ); - const position = getPopperPosition(slashMenuElement, curRange); - slashMenu.updatePosition(position); - }, 10); - - disposables.addFromEvent(window, 'resize', updatePosition); - // FIXME(Flrande): It is not a best practice, // but merely a temporary measure for reusing previous components. // Mount container.append(slashMenu); - // Wait for the Node to be mounted - setTimeout(updatePosition); return slashMenu; }, 100 diff --git a/blocksuite/blocks/src/root-block/widgets/slash-menu/slash-menu-popover.ts b/blocksuite/blocks/src/root-block/widgets/slash-menu/slash-menu-popover.ts index f57bc3b993..2702ff1208 100644 --- a/blocksuite/blocks/src/root-block/widgets/slash-menu/slash-menu-popover.ts +++ b/blocksuite/blocks/src/root-block/widgets/slash-menu/slash-menu-popover.ts @@ -8,17 +8,23 @@ import { } from '@blocksuite/affine-components/rich-text'; import { createKeydownObserver, + getCurrentNativeRange, isControlledKeyboardEvent, isFuzzyMatch, substringMatchScore, } from '@blocksuite/affine-shared/utils'; -import { assertExists, WithDisposable } from '@blocksuite/global/utils'; +import { + assertExists, + throttle, + WithDisposable, +} from '@blocksuite/global/utils'; import { autoPlacement, offset } from '@floating-ui/dom'; import { html, LitElement, nothing, type PropertyValues } from 'lit'; -import { property, query, state } from 'lit/decorators.js'; +import { property, state } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { styleMap } from 'lit/directives/style-map.js'; +import { getPopperPosition } from '../../utils/position.js'; import type { SlashMenuActionItem, SlashMenuContext, @@ -139,10 +145,6 @@ export class SlashMenu extends WithDisposable(LitElement) { this._queryState = this._filteredItems.length === 0 ? 'no_result' : 'on'; }; - updatePosition = (position: { x: string; y: string; height: number }) => { - this._position = position; - }; - private get _query() { return getTextContentFromInlineRange(this.inlineEditor, this._startRange); } @@ -247,6 +249,24 @@ export class SlashMenu extends WithDisposable(LitElement) { }); } + protected override willUpdate() { + if (!this.hasUpdated) { + const currRage = getCurrentNativeRange(); + if (!currRage) { + this.abortController.abort(); + return; + } + + // Handle position + const updatePosition = throttle(() => { + this._position = getPopperPosition(this, currRage); + }, 10); + + this.disposables.addFromEvent(window, 'resize', updatePosition); + updatePosition(); + } + } + override render() { const slashMenuStyles = this._position ? { @@ -291,9 +311,6 @@ export class SlashMenu extends WithDisposable(LitElement) { @property({ attribute: false }) accessor context!: SlashMenuContext; - @query('inner-slash-menu') - accessor slashMenuElement!: HTMLElement; - @property({ attribute: false }) accessor triggerKey!: string; }