diff --git a/packages/affine/components/src/portal/portal.ts b/packages/affine/components/src/portal/portal.ts index 62bd0dee266d..231febcb8415 100644 --- a/packages/affine/components/src/portal/portal.ts +++ b/packages/affine/components/src/portal/portal.ts @@ -1,4 +1,4 @@ -import { html, LitElement } from 'lit'; +import { html, LitElement, type TemplateResult } from 'lit'; import { property } from 'lit/decorators.js'; /** @@ -50,7 +50,7 @@ export class Portal extends LitElement { accessor shadowDom: boolean | ShadowRootInit = true; @property({ attribute: false }) - accessor template = html``; + accessor template: TemplateResult | undefined = html``; } declare global { diff --git a/packages/blocks/src/effects.ts b/packages/blocks/src/effects.ts index 37ac67b5dbc9..7c16cbd2d9d4 100644 --- a/packages/blocks/src/effects.ts +++ b/packages/blocks/src/effects.ts @@ -613,7 +613,6 @@ export function effects() { 'edgeless-change-attachment-button', EdgelessChangeAttachmentButton ); - customElements.define('import-doc', ImportDoc); customElements.define('edgeless-more-button', EdgelessMoreButton); customElements.define('edgeless-shape-style-panel', EdgelessShapeStylePanel); customElements.define( diff --git a/packages/blocks/src/root-block/widgets/keyboard-toolbar/config.ts b/packages/blocks/src/root-block/widgets/keyboard-toolbar/config.ts index a3b56e710174..bf65e88805f4 100644 --- a/packages/blocks/src/root-block/widgets/keyboard-toolbar/config.ts +++ b/packages/blocks/src/root-block/widgets/keyboard-toolbar/config.ts @@ -315,8 +315,14 @@ const pageToolGroup: KeyboardToolPanelGroup = { action: ({ rootComponent, closeToolbar }) => { const { std } = rootComponent; - const triggerKey = - std.getConfig('affine:page')?.linkedWidget?.triggerKeys?.[0] ?? '@'; + const linkedDocWidget = std.view.getWidget( + 'affine-linked-doc-widget', + rootComponent.model.id + ); + if (!linkedDocWidget) return; + assertType(linkedDocWidget); + + const triggerKey = linkedDocWidget.config.triggerKeys[0]; std.command .chain() @@ -328,17 +334,10 @@ const pageToolGroup: KeyboardToolPanelGroup = { const currentModel = selectedModels[0]; insertContent(std.host, currentModel, triggerKey); - const linkedDocWidget = std.view.getWidget( - 'affine-linked-doc-widget', - rootComponent.model.id - ); - if (!linkedDocWidget) return; - assertType(linkedDocWidget); - const inlineEditor = getInlineEditorByModel(std.host, currentModel); // Wait for range to be updated inlineEditor?.slots.inlineRangeSync.once(() => { - linkedDocWidget.showLinkedDocPopover(inlineEditor, triggerKey); + linkedDocWidget.showLinkedDocPopover(); closeToolbar(); }); }) diff --git a/packages/blocks/src/root-block/widgets/linked-doc/config.ts b/packages/blocks/src/root-block/widgets/linked-doc/config.ts index 9ffbf94484d3..7a431c5c4164 100644 --- a/packages/blocks/src/root-block/widgets/linked-doc/config.ts +++ b/packages/blocks/src/root-block/widgets/linked-doc/config.ts @@ -1,4 +1,4 @@ -import type { EditorHost } from '@blocksuite/block-std'; +import type { BlockStdScope, EditorHost } from '@blocksuite/block-std'; import type { TemplateResult } from 'lit'; import { @@ -42,6 +42,14 @@ export type LinkedMenuGroup = { overflowText?: string; }; +export type LinkedDocContext = { + std: BlockStdScope; + inlineEditor: AffineInlineEditor; + triggerKey: string; + getMenus: typeof getMenus; + close: () => void; +}; + const DEFAULT_DOC_NAME = 'Untitled'; const DISPLAY_NAME_LENGTH = 8; diff --git a/packages/blocks/src/root-block/widgets/linked-doc/effects.ts b/packages/blocks/src/root-block/widgets/linked-doc/effects.ts index eacd1b8935ff..9e888939d726 100644 --- a/packages/blocks/src/root-block/widgets/linked-doc/effects.ts +++ b/packages/blocks/src/root-block/widgets/linked-doc/effects.ts @@ -1,7 +1,9 @@ +import { ImportDoc } from './import-doc/import-doc.js'; import { AFFINE_LINKED_DOC_WIDGET, AffineLinkedDocWidget } from './index.js'; import { LinkedDocPopover } from './linked-doc-popover.js'; export function effects() { customElements.define('affine-linked-doc-popover', LinkedDocPopover); customElements.define(AFFINE_LINKED_DOC_WIDGET, AffineLinkedDocWidget); + customElements.define('import-doc', ImportDoc); } diff --git a/packages/blocks/src/root-block/widgets/linked-doc/index.ts b/packages/blocks/src/root-block/widgets/linked-doc/index.ts index 23d9041279e9..9e993081d376 100644 --- a/packages/blocks/src/root-block/widgets/linked-doc/index.ts +++ b/packages/blocks/src/root-block/widgets/linked-doc/index.ts @@ -2,18 +2,19 @@ import type { AffineInlineEditor } from '@blocksuite/affine-components/rich-text import type { EditorHost, UIEventStateContext } from '@blocksuite/block-std'; import { getInlineEditorByModel } from '@blocksuite/affine-components/rich-text'; -import { - getCurrentNativeRange, - getViewportElement, - matchFlavours, -} from '@blocksuite/affine-shared/utils'; +import { matchFlavours } from '@blocksuite/affine-shared/utils'; import { WidgetComponent } from '@blocksuite/block-std'; -import { DisposableGroup, throttle } from '@blocksuite/global/utils'; import { InlineEditor } 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 { getPopperPosition } from '../../../root-block/utils/position.js'; -import { getMenus, type LinkedMenuGroup } from './config.js'; -import { LinkedDocPopover } from './linked-doc-popover.js'; +import { + getMenus, + type LinkedDocContext, + type LinkedMenuGroup, +} from './config.js'; export const AFFINE_LINKED_DOC_WIDGET = 'affine-linked-doc-widget'; @@ -38,10 +39,10 @@ export interface LinkedWidgetConfig { } export class AffineLinkedDocWidget extends WidgetComponent { - private _abortController: AbortController | null = null; - - private _getInlineEditor = (evt: KeyboardEvent | CompositionEvent) => { - if (evt.target instanceof HTMLElement) { + private readonly _getInlineEditor = ( + evt?: KeyboardEvent | CompositionEvent + ) => { + if (evt && evt.target instanceof HTMLElement) { const editor = ( evt.target.closest('.can-link-doc > .inline-editor') as { inlineEditor?: AffineInlineEditor; @@ -55,19 +56,19 @@ export class AffineLinkedDocWidget extends WidgetComponent { const text = this.host.selection.value.find(selection => selection.is('text') ); - if (!text) return; + if (!text) return null; const model = this.host.doc.getBlockById(text.blockId); - if (!model) return; + if (!model) return null; if (matchFlavours(model, this.config.ignoreBlockTypes)) { - return; + return null; } return getInlineEditorByModel(this.host, model); }; - private _onCompositionEnd = (ctx: UIEventStateContext) => { + private readonly _onCompositionEnd = (ctx: UIEventStateContext) => { const event = ctx.get('defaultState').event as CompositionEvent; const key = event.data; @@ -78,13 +79,13 @@ export class AffineLinkedDocWidget extends WidgetComponent { ) return; - const inlineEditor = this._getInlineEditor(event); - if (!inlineEditor) return; + this._inlineEditor = this._getInlineEditor(event); + if (!this._inlineEditor) return; - this._handleInput(inlineEditor, true); + this._handleInput(true); }; - private _onKeyDown = (ctx: UIEventStateContext) => { + private readonly _onKeyDown = (ctx: UIEventStateContext) => { const eventState = ctx.get('keyboardState'); const event = eventState.raw; @@ -96,9 +97,10 @@ export class AffineLinkedDocWidget extends WidgetComponent { ) return; - const inlineEditor = this._getInlineEditor(event); - if (!inlineEditor) return; - const inlineRange = inlineEditor.getInlineRange(); + this._inlineEditor = this._getInlineEditor(event); + if (!this._inlineEditor) return; + + const inlineRange = this._inlineEditor.getInlineRange(); if (!inlineRange) return; if (inlineRange.length > 0) { @@ -108,62 +110,44 @@ export class AffineLinkedDocWidget extends WidgetComponent { return; } - this._handleInput(inlineEditor, false); + this._handleInput(false); }; - showLinkedDocPopover = ( - inlineEditor: AffineInlineEditor, - triggerKey: string - ) => { - const curRange = getCurrentNativeRange(); - if (!curRange) return; - - this._abortController?.abort(); - this._abortController = new AbortController(); - const disposables = new DisposableGroup(); - this._abortController.signal.addEventListener('abort', () => - disposables.dispose() - ); - - const linkedDoc = new LinkedDocPopover( - triggerKey, - this.config.getMenus, - this.host, - inlineEditor, - this._abortController - ); - - // Mount - document.body.append(linkedDoc); - disposables.add(() => linkedDoc.remove()); - - // Handle position - const updatePosition = throttle(() => { - const linkedDocElement = linkedDoc.linkedDocElement; - if (!linkedDocElement) return; - const position = getPopperPosition(linkedDocElement, curRange); - linkedDoc.updatePosition(position); - }, 10); - disposables.addFromEvent(window, 'resize', updatePosition); - const scrollContainer = getViewportElement(this.host); - if (scrollContainer) { - // Note: in edgeless mode, the scroll container is not exist! - disposables.addFromEvent(scrollContainer, 'scroll', updatePosition, { - passive: true, - }); - } + private readonly _renderDesktopLinkedDocPopover = () => { + return html``; + }; - // Wait for node to be mounted - setTimeout(updatePosition); + private readonly _show = signal<'desktop' | 'none'>('none'); - disposables.addFromEvent(window, 'mousedown', (e: Event) => { - if (e.target === linkedDoc) return; - this._abortController?.abort(); - }); + closeLinkedDocPopover = () => { + this._inlineEditor = null; + this._triggerKey = ''; + this._show.value = 'none'; + }; - return linkedDoc; + showLinkedDocPopover = () => { + if (this._inlineEditor === null) { + this._inlineEditor = this._getInlineEditor(); + } + if (this._triggerKey === '') { + this._triggerKey = this.config.triggerKeys[0]; + } + this._show.value = 'desktop'; + return; }; + private get _context(): LinkedDocContext { + return { + std: this.std, + inlineEditor: this._inlineEditor!, + triggerKey: this._triggerKey, + getMenus: this.config.getMenus, + close: this.closeLinkedDocPopover, + }; + } + get config(): LinkedWidgetConfig { return { triggerKeys: ['@', '[[', '【【'], @@ -174,9 +158,12 @@ export class AffineLinkedDocWidget extends WidgetComponent { }; } - private _handleInput(inlineEditor: InlineEditor, isCompositionEnd: boolean) { + private _handleInput(isCompositionEnd: boolean) { const primaryTriggerKey = this.config.triggerKeys[0]; + const inlineEditor = this._inlineEditor; + if (!inlineEditor) return; + const inlineRangeApplyCallback = (callback: () => void) => { // the inline ranged updated in compositionEnd event before this event callback if (isCompositionEnd) callback(); @@ -205,6 +192,7 @@ export class AffineLinkedDocWidget extends WidgetComponent { // Convert to the primary trigger key // e.g. [[ -> @ + this._triggerKey = primaryTriggerKey; const startIdxBeforeMatchKey = inlineRange.index - matchedKey.length; inlineEditor.deleteText({ index: startIdxBeforeMatchKey, @@ -219,11 +207,13 @@ export class AffineLinkedDocWidget extends WidgetComponent { length: 0, }); inlineEditor.slots.inlineRangeSync.once(() => { - this.showLinkedDocPopover(inlineEditor, primaryTriggerKey); + this.showLinkedDocPopover(); }); return; + } else { + this._triggerKey = matchedKey; + this.showLinkedDocPopover(); } - this.showLinkedDocPopover(inlineEditor, matchedKey); }); } @@ -232,6 +222,25 @@ export class AffineLinkedDocWidget extends WidgetComponent { this.handleEvent('keyDown', this._onKeyDown); this.handleEvent('compositionEnd', this._onCompositionEnd); } + + override render() { + if (this._show.value === 'none') return nothing; + + return html` html`${nothing}` + )} + >`; + } + + @state() + private accessor _inlineEditor: AffineInlineEditor | null = null; + + @state() + private accessor _triggerKey = ''; } declare global { diff --git a/packages/blocks/src/root-block/widgets/linked-doc/linked-doc-popover.ts b/packages/blocks/src/root-block/widgets/linked-doc/linked-doc-popover.ts index 4cca5174fdb7..8f1965d2826e 100644 --- a/packages/blocks/src/root-block/widgets/linked-doc/linked-doc-popover.ts +++ b/packages/blocks/src/root-block/widgets/linked-doc/linked-doc-popover.ts @@ -1,52 +1,60 @@ -import type { AffineInlineEditor } from '@blocksuite/affine-components/rich-text'; -import type { EditorHost } from '@blocksuite/block-std'; +import type { InlineRange } from '@blocksuite/inline'; import { MoreHorizontalIcon } from '@blocksuite/affine-components/icons'; -import { WithDisposable } from '@blocksuite/global/utils'; +import { + getCurrentNativeRange, + getViewportElement, +} from '@blocksuite/affine-shared/utils'; +import { PropTypes, requiredProperties } from '@blocksuite/block-std'; +import { throttle, WithDisposable } from '@blocksuite/global/utils'; import { html, LitElement, nothing } from 'lit'; -import { query, queryAll, state } from 'lit/decorators.js'; +import { property, query, queryAll, state } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; import type { IconButton } from '../../../_common/components/button.js'; -import type { LinkedMenuGroup } from './config.js'; +import type { LinkedDocContext, LinkedMenuGroup } from './config.js'; import { cleanSpecifiedTail, createKeydownObserver, getQuery, } from '../../../_common/components/utils.js'; +import { getPopperPosition } from '../../utils/position.js'; import { styles } from './styles.js'; +@requiredProperties({ + context: PropTypes.object, +}) export class LinkedDocPopover extends WithDisposable(LitElement) { static override styles = styles; private _abort = () => { // remove popover dom - this.abortController.abort(); + this.context.close(); // clear input query cleanSpecifiedTail( - this.editorHost, - this.inlineEditor, - this.triggerKey + (this._query || '') + this.context.std.host, + this.context.inlineEditor, + this.context.triggerKey + (this._query || '') ); }; private _expanded = new Map(); - private _startRange = this.inlineEditor.getInlineRange(); + private _startRange: InlineRange | null = null; private _updateLinkedDocGroup = async () => { const query = this._query; if (query === null) { - this.abortController.abort(); + this.context.close(); return; } - this._linkedDocGroup = await this.getMenus( + this._linkedDocGroup = await this.context.getMenus( query, this._abort, - this.editorHost, - this.inlineEditor + this.context.std.host, + this.context.inlineEditor ); }; @@ -68,22 +76,7 @@ export class LinkedDocPopover extends WithDisposable(LitElement) { } private get _query() { - return getQuery(this.inlineEditor, this._startRange); - } - - constructor( - private triggerKey: string, - private getMenus: ( - query: string, - abort: () => void, - editorHost: EditorHost, - inlineEditor: AffineInlineEditor - ) => Promise, - private editorHost: EditorHost, - private inlineEditor: AffineInlineEditor, - private abortController: AbortController - ) { - super(); + return getQuery(this.context.inlineEditor, this._startRange); } private _getActionItems(group: LinkedMenuGroup) { @@ -115,23 +108,33 @@ export class LinkedDocPopover extends WithDisposable(LitElement) { super.connectedCallback(); // init + this._startRange = this.context.inlineEditor.getInlineRange(); + void this._updateLinkedDocGroup(); this._disposables.addFromEvent(this, 'mousedown', e => { // Prevent input from losing focus e.preventDefault(); }); + this._disposables.addFromEvent(window, 'mousedown', e => { + if (e.target === this) return; + this._abort(); + }); + + const keydownObserverAbortController = new AbortController(); + this._disposables.add(() => keydownObserverAbortController.abort()); - const { eventSource } = this.inlineEditor; + const { eventSource } = this.context.inlineEditor; if (!eventSource) return; + createKeydownObserver({ target: eventSource, - signal: this.abortController.signal, + signal: keydownObserverAbortController.signal, onInput: isComposition => { this._activatedItemIndex = 0; if (isComposition) { this._updateLinkedDocGroup().catch(console.error); } else { - this.inlineEditor.slots.renderComplete.once( + this.context.inlineEditor.slots.renderComplete.once( this._updateLinkedDocGroup ); } @@ -143,15 +146,17 @@ export class LinkedDocPopover extends WithDisposable(LitElement) { }, 50); }, onDelete: () => { - const curRange = this.inlineEditor.getInlineRange(); + const curRange = this.context.inlineEditor.getInlineRange(); if (!this._startRange || !curRange) { return; } if (curRange.index < this._startRange.index) { - this.abortController.abort(); + this.context.close(); } this._activatedItemIndex = 0; - this.inlineEditor.slots.renderComplete.once(this._updateLinkedDocGroup); + this.context.inlineEditor.slots.renderComplete.once( + this._updateLinkedDocGroup + ); }, onMove: step => { const itemLen = this._flattenActionList.length; @@ -182,7 +187,7 @@ export class LinkedDocPopover extends WithDisposable(LitElement) { ?.catch(console.error); }, onAbort: () => { - this.abortController.abort(); + this.context.close(); }, }); } @@ -253,6 +258,33 @@ export class LinkedDocPopover extends WithDisposable(LitElement) { 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); + }, 10); + + this.disposables.addFromEvent(window, 'resize', updatePosition); + const scrollContainer = getViewportElement(this.context.std.host); + if (scrollContainer) { + // Note: in edgeless mode, the scroll container is not exist! + this.disposables.addFromEvent( + scrollContainer, + 'scroll', + updatePosition, + { + passive: true, + } + ); + } + updatePosition(); + } + } + @state() private accessor _activatedItemIndex = 0; @@ -269,6 +301,9 @@ export class LinkedDocPopover extends WithDisposable(LitElement) { @state() private accessor _showTooltip = false; + @property({ attribute: false }) + accessor context!: LinkedDocContext; + @queryAll('icon-button') accessor iconButtons!: NodeListOf; diff --git a/packages/blocks/src/root-block/widgets/slash-menu/config.ts b/packages/blocks/src/root-block/widgets/slash-menu/config.ts index 7a53fefd8775..c1e89f3e0efc 100644 --- a/packages/blocks/src/root-block/widgets/slash-menu/config.ts +++ b/packages/blocks/src/root-block/widgets/slash-menu/config.ts @@ -232,8 +232,6 @@ export const defaultSlashMenuConfig: SlashMenuConfig = { return true; }, action: ({ model, rootComponent }) => { - const triggerKey = '@'; - insertContent(rootComponent.host, model, triggerKey); const { std } = rootComponent; const linkedDocWidget = std.view.getWidget( @@ -243,10 +241,14 @@ export const defaultSlashMenuConfig: SlashMenuConfig = { if (!linkedDocWidget) return; assertType(linkedDocWidget); + const triggerKey = linkedDocWidget.config.triggerKeys[0]; + + insertContent(rootComponent.host, model, triggerKey); + const inlineEditor = getInlineEditorByModel(rootComponent.host, model); // Wait for range to be updated inlineEditor?.slots.inlineRangeSync.once(() => { - linkedDocWidget.showLinkedDocPopover(inlineEditor, triggerKey); + linkedDocWidget.showLinkedDocPopover(); }); }, },