mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 18:26:05 +08:00
fix(editor): wrong position of remote selection and at menu in edgeless (#10137)
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
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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<SelectionRect[]>([]);
|
||||
|
||||
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<
|
||||
></affine-linked-doc-popover>`;
|
||||
};
|
||||
|
||||
@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`<div
|
||||
class="input-mask"
|
||||
@@ -73,29 +107,7 @@ export class AffineLinkedDocWidget extends WidgetComponent<
|
||||
)}`;
|
||||
}
|
||||
|
||||
private readonly _mode$ = signal<'desktop' | 'mobile' | 'none'>('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');
|
||||
|
||||
@@ -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(
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user