import type { UIEventStateContext } from '@blockeditor/block-std'; import { assertExists, DisposableGroup } from '@blockeditor/global/utils'; import type { InlineRange } from '@blockeditor/inline'; import { WidgetElement } from '@blockeditor/lit'; // import type { BlockModel } from '@blockeditor/store'; import { customElement } from 'lit/decorators.js'; import { getInlineEditorByModel, isControlledKeyboardEvent, matchFlavours, } from '../../../_common/utils/index.js'; import type { AffineInlineEditor } from '../../../_common/inline/presets/affine-inline-specs.js'; import { toggleAIPopup } from '../../../_common/inline/presets/nodes/ai-node/ai-popup/toggle-ai-popup.js'; // import type { PageBlockComponent } from '../../../page-block/types.js'; // import { isPageComponent } from '../../../page-block/utils/guard.js'; // import { getPopperPosition } from '../../../page-block/utils/position.js'; import type { AIMenuOptions } from './utils.js'; let globalAbortController = new AbortController(); function showAIMenu({ inlineEditors, targetInlineRanges, abortController = new AbortController(), }: { inlineEditors: AffineInlineEditor[]; targetInlineRanges: InlineRange[]; abortController?: AbortController; }) { // Abort previous format quick bar globalAbortController.abort(); globalAbortController = abortController; const disposables = new DisposableGroup(); abortController.signal.addEventListener('abort', () => disposables.dispose()); const aiMenu = toggleAIPopup(inlineEditors, 'space', targetInlineRanges); // const aiMenu = new AIMenu(); disposables.add(() => aiMenu.remove()); // aiMenu.model = model; // aiMenu.abortController = abortController; // aiMenu.options = options; // aiMenu.pageElement = pageElement; // aiMenu.triggerKey = triggerKey; // Handle position // const updatePosition = throttle(() => { // const aiMenuElement = aiMenu.aiMenuElement; // assertExists( // aiMenuElement, // 'You should render the ai menu node even if no position' // ); // const position = getPopperPosition(aiMenuElement, range); // aiMenu.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.appendChild(aiMenu); // Wait for the Node to be mounted // setTimeout(updatePosition); return aiMenu; } export const AFFINE_AI_MENU_WIDGET = 'affine-ai-menu-widget'; @customElement(AFFINE_AI_MENU_WIDGET) export class AffineAIMenuWidget extends WidgetElement { static DEFAULT_OPTIONS: AIMenuOptions = { isTriggerKey: (event: KeyboardEvent): false | string => { const triggerKeys = [' ']; if (isControlledKeyboardEvent(event) || event.key.length !== 1) return false; const triggerKey = triggerKeys.find(key => key === event.key); if (!triggerKey) return false; return triggerKey; }, }; options = AffineAIMenuWidget.DEFAULT_OPTIONS; override connectedCallback() { super.connectedCallback(); this.handleEvent('keyDown', this._onKeyDown); } private _onKeyDown = (ctx: UIEventStateContext) => { const eventState = ctx.get('keyboardState'); const event = eventState.raw; const triggerKey = this.options.isTriggerKey(event); if (triggerKey === false) return; const text = this.host.selection.value.find(selection => selection.is('text') ); if (!text) { return; } const model = this.host.page.getBlockById(text.blockId); if (!model || !model.text || model.text.length > 1) { return; } if (matchFlavours(model, ['affine:code'])) return; const inlineEditors: AffineInlineEditor[] = []; const targetInlineRanges: InlineRange[] = []; const page = this.host.page; page.captureSync(); const parent = page.getParent(model); assertExists(parent); const _index = parent.children.indexOf(model); // console.log(9, typeof parent.children, index, this.host.page._blockTree.blocks) parent.children.forEach((block, index) => { if (index <= _index) { const inlineEditor = getInlineEditorByModel(this.host, block); if (!inlineEditor) return; inlineEditors.push(inlineEditor); targetInlineRanges.push({ index: 0, length: inlineEditor.yTextLength, }); } else if (index > _index) { } }); // console.log(8, inlineEditors) showAIMenu({ inlineEditors, targetInlineRanges, }); // const inlineEditor = getInlineEditorByModel(this.host, model); // if (!inlineEditor) return; // inlineEditor.slots.inlineRangeApply.once(() => { // const pageElement = this.blockElement; // if (!isPageComponent(pageElement)) { // throw new Error('AIMenuWidget should be used in PageBlock'); // } // // Wait for dom update, see this case https://github.com/toeverything/blockeditor/issues/2611 // requestAnimationFrame(() => { // const curRange = getCurrentNativeRange(); // showAIMenu({ // pageElement, // model, // range: curRange, // triggerKey, // options: this.options, // inlineEditor, // }); // }); // }); }; } declare global { interface HTMLElementTagNameMap { [AFFINE_AI_MENU_WIDGET]: AffineAIMenuWidget; } }