import { assertExists } from '@blockeditor/global/utils'; import type { EditorHost } from '@blockeditor/lit'; import { WithDisposable } from '@blockeditor/lit'; import { type BlockModel } from '@blockeditor/store'; import { html, LitElement } from 'lit'; import { customElement, property, query, state } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; import { cleanSpecifiedTail, createKeydownObserver, } from '../../../_common/components/utils.js'; import { getRichTextByModel } from '../../../_common/utils/query.js'; import type { LinkedDocOptions } from './config.js'; import { type LinkedDocGroup } from './config.js'; import { styles } from './styles.js'; @customElement('affine-linked-doc-popover') export class LinkedDocPopover extends WithDisposable(LitElement) { static override styles = styles; @property({ attribute: false }) options!: LinkedDocOptions; @property({ attribute: false }) triggerKey!: string; @state() private _position: { height: number; x: string; y: string; } | null = null; @state() private _query = ''; @state() private _activatedItemIndex = 0; private _actionGroup: LinkedDocGroup[] = []; private get _flattenActionList() { return this._actionGroup .map(group => group.items.map(item => ({ ...item, groupName: group.name })) ) .flat(); } private _updateActionList() { this._actionGroup = this.options.getMenus({ editorHost: this.editorHost, query: this._query, page: this._page, model: this.model, pageMetas: this._page.workspace.meta.pageMetas, }); } @query('.linked-doc-popover') linkedDocElement?: Element; private get _page() { return this.model.page; } constructor( private editorHost: EditorHost, private model: BlockModel, private abortController = new AbortController() ) { super(); } override connectedCallback() { super.connectedCallback(); const richText = getRichTextByModel(this.editorHost, this.model); assertExists(richText, 'RichText not found'); const inlineEditor = richText.inlineEditor; assertExists(inlineEditor, 'RichText InlineEditor not found'); // init this._updateActionList(); this._disposables.add( this.model.page.workspace.slots.pagesUpdated.on(() => { this._updateActionList(); }) ); this._disposables.addFromEvent(this, 'mousedown', e => { // Prevent input from losing focus e.preventDefault(); }); createKeydownObserver({ target: inlineEditor.eventSource, inlineEditor, onUpdateQuery: str => { this._query = str; this._activatedItemIndex = 0; this._updateActionList(); }, abortController: this.abortController, onMove: step => { const itemLen = this._flattenActionList.length; this._activatedItemIndex = (itemLen + this._activatedItemIndex + step) % itemLen; // Scroll to the active item const item = this._flattenActionList[this._activatedItemIndex]; const shadowRoot = this.shadowRoot; if (!shadowRoot) { console.warn('Failed to find the shadow root!', this); return; } const ele = shadowRoot.querySelector( `icon-button[data-id="${item.key}"]` ); if (!ele) { console.warn('Failed to find the active item!', item); return; } ele.scrollIntoView({ block: 'nearest', }); }, onConfirm: () => { this.abortController.abort(); cleanSpecifiedTail( this.editorHost, this.model, this.triggerKey + this._query ); this._flattenActionList[this._activatedItemIndex] .action() ?.catch(console.error); }, onEsc: () => { this.abortController.abort(); }, }); } updatePosition(position: { height: number; x: string; y: string }) { this._position = position; } override render() { const MAX_HEIGHT = 396; const style = this._position ? styleMap({ transform: `translate(${this._position.x}, ${this._position.y})`, maxHeight: `${Math.min(this._position.height, MAX_HEIGHT)}px`, }) : styleMap({ visibility: 'hidden', }); // XXX This is a side effect let accIdx = 0; return html`
${this._actionGroup .filter(group => group.items.length) .map((group, idx) => { return html`
${group.name}
${group.items.map(({ key, name, icon, action }) => { accIdx++; const curIdx = accIdx - 1; return html` { this.abortController.abort(); cleanSpecifiedTail( this.editorHost, this.model, this.triggerKey + this._query ); action()?.catch(console.error); }} @mousemove=${() => { // Use `mousemove` instead of `mouseover` to avoid navigate conflict with keyboard this._activatedItemIndex = curIdx; }} >${icon}`; })}
`; })}
`; } }