import type { AffineInlineEditor } from '@blocksuite/affine-components/rich-text';
import { getInlineEditorByModel } from '@blocksuite/affine-components/rich-text';
import type { RootBlockModel } from '@blocksuite/affine-model';
import type { SelectionRect } from '@blocksuite/affine-shared/commands';
import {
getViewportElement,
matchFlavours,
} from '@blocksuite/affine-shared/utils';
import type { UIEventStateContext } from '@blocksuite/block-std';
import { WidgetComponent } from '@blocksuite/block-std';
import { IS_MOBILE } from '@blocksuite/global/env';
import type { Disposable } from '@blocksuite/global/utils';
import { InlineEditor, type InlineRange } 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';
import type { PageRootBlockComponent } from '../../index.js';
import {
getMenus,
type LinkedDocContext,
type LinkedWidgetConfig,
} from './config.js';
import { linkedDocWidgetStyles } from './styles.js';
export { type LinkedWidgetConfig } from './config.js';
export const AFFINE_LINKED_DOC_WIDGET = 'affine-linked-doc-widget';
export class AffineLinkedDocWidget extends WidgetComponent<
RootBlockModel,
PageRootBlockComponent
> {
static override styles = linkedDocWidgetStyles;
private _disposeObserveInputRects: Disposable | null = null;
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;
}
)?.inlineEditor;
if (editor instanceof InlineEditor) {
return editor;
}
}
const text = this.host.selection.value.find(selection =>
selection.is('text')
);
if (!text) return null;
const model = this.host.doc.getBlockById(text.blockId);
if (!model) return null;
if (matchFlavours(model, this.config.ignoreBlockTypes)) {
return null;
}
return getInlineEditorByModel(this.host, model);
};
private _inlineEditor: AffineInlineEditor | null = null;
private readonly _observeInputRects = () => {
if (!this._inlineEditor) return;
const updateInputRects = () => {
const blockId =
this.std.command.exec('getSelectedModels').selectedModels?.[0]?.id;
if (!blockId) return;
if (!this._startRange) return;
const index = this._startRange.index - this._triggerKey.length;
if (index < 0) return;
const currentRange = this._inlineEditor?.getInlineRange();
if (!currentRange) return;
const length = currentRange.index + currentRange.length - index;
const textSelection = this.std.selection.create('text', {
from: { blockId, index, length },
to: null,
});
const { selectionRects } = this.std.command.exec('getSelectionRects', {
textSelection,
});
if (!selectionRects) return;
this._inputRects = selectionRects;
};
updateInputRects();
this._disposeObserveInputRects =
this._inlineEditor.slots.renderComplete.on(updateInputRects);
};
private readonly _onCompositionEnd = (ctx: UIEventStateContext) => {
const event = ctx.get('defaultState').event as CompositionEvent;
const key = event.data;
if (
!key ||
!this.config.triggerKeys.some(triggerKey => triggerKey.includes(key))
)
return;
this._inlineEditor = this._getInlineEditor(event);
if (!this._inlineEditor) return;
this._handleInput(true);
};
private readonly _onKeyDown = (ctx: UIEventStateContext) => {
const eventState = ctx.get('keyboardState');
const event = eventState.raw;
const key = event.key;
if (
key === undefined || // in mac os, the key may be undefined
key === 'Process' ||
event.isComposing
)
return;
if (!this.config.triggerKeys.some(triggerKey => triggerKey.includes(key)))
return;
this._inlineEditor = this._getInlineEditor(event);
if (!this._inlineEditor) return;
const inlineRange = this._inlineEditor.getInlineRange();
if (!inlineRange) return;
if (inlineRange.length > 0) {
// When select text and press `[[` should not trigger transform,
// since it will break the bracket complete.
// Expected `[[selected text]]` instead of `@selected text]]`
return;
}
this._handleInput(false);
};
private readonly _renderLinkedDocMenu = () => {
if (!this.block.rootComponent) return nothing;
return html`