import { assertExists } from '@blockeditor/global/utils'; import type { InlineRange } from '@blockeditor/inline'; import type { BlockElement } from '@blockeditor/lit'; import { WithDisposable } from '@blockeditor/lit'; import { computePosition, flip, inline, offset, shift } from '@floating-ui/dom'; import { html, LitElement } from 'lit'; import { customElement, property, query } from 'lit/decorators.js'; import type { PageBlockComponent } from '../../../../../page-block/types.js'; import { createLitPortal } from '../../../../components/portal.js'; import { BLOCK_ID_ATTR } from '../../../../consts.js'; import { BookmarkIcon, MoreVerticalIcon } from '../../../../icons/index.js'; import { LinkIcon, OpenIcon } from '../../../../icons/text.js'; import type { AffineInlineEditor } from '../../affine-inline-specs.js'; import { ReferencePopupMoreMenu } from './reference-popup-more-menu-popup.js'; import { styles } from './styles.js'; @customElement('reference-popup') export class ReferencePopup extends WithDisposable(LitElement) { static override styles = styles; @property({ attribute: false }) inlineEditor!: AffineInlineEditor; @property({ attribute: false }) targetInlineRange!: InlineRange; @property({ attribute: false }) pageTitle!: string; @property({ attribute: false }) abortController!: AbortController; @query('.affine-reference-popover-container') popupContainer!: HTMLDivElement; get referencePageId() { const pageId = this.inlineEditor.getFormat(this.targetInlineRange).reference ?.pageId; assertExists(pageId); return pageId; } private _moreMenuAbortController: AbortController | null = null; override connectedCallback() { super.connectedCallback(); if (this.targetInlineRange.length === 0) { throw new Error('Cannot toggle reference popup on empty range'); } const parent = this.blockElement.host.page.getParent( this.blockElement.model ); assertExists(parent); this.disposables.add( parent.childrenUpdated.on(() => { const children = parent.children; if (children.includes(this.blockElement.model)) return; this.abortController.abort(); }) ); } override updated() { assertExists(this.popupContainer); const range = this.inlineEditor.toDomRange(this.targetInlineRange); assertExists(range); const visualElement = { getBoundingClientRect: () => range.getBoundingClientRect(), getClientRects: () => range.getClientRects(), }; computePosition(visualElement, this.popupContainer, { middleware: [ offset(10), inline(), shift({ padding: 6, }), ], }) .then(({ x, y }) => { const popupContainer = this.popupContainer; if (!popupContainer) return; popupContainer.style.left = `${x}px`; popupContainer.style.top = `${y}px`; }) .catch(console.error); } get blockElement() { const blockElement = this.inlineEditor.rootElement.closest( `[${BLOCK_ID_ATTR}]` ); assertExists(blockElement); return blockElement; } get std() { const std = this.blockElement.std; assertExists(std); return std; } private _openPage() { const refPageId = this.referencePageId; const blockElement = this.blockElement; if (refPageId === blockElement.model.page.id) return; const pageElement = this.std.view.viewFromPath('block', [ blockElement.model.page.root?.id ?? '', ]) as PageBlockComponent | null; assertExists(pageElement); pageElement.slots.pageLinkClicked.emit({ pageId: refPageId }); } private _convertToCardView() { const blockElement = this.blockElement; const page = blockElement.host.page; const parent = page.getParent(blockElement.model); assertExists(parent); const index = parent.children.indexOf(blockElement.model); const pageId = this.referencePageId; page.addBlock('affine:embed-linked-doc', { pageId }, parent, index + 1); const totalTextLength = this.inlineEditor.yTextLength; const inlineTextLength = this.targetInlineRange.length; if (totalTextLength === inlineTextLength) { page.deleteBlock(blockElement.model); } else { this.inlineEditor.insertText(this.targetInlineRange, this.pageTitle); } this.abortController.abort(); } private _toggleMoreMenu() { if (this._moreMenuAbortController) { this._moreMenuAbortController.abort(); this._moreMenuAbortController = null; return; } this._moreMenuAbortController = new AbortController(); const referencePopupMoreMenu = new ReferencePopupMoreMenu(); referencePopupMoreMenu.abortController = this.abortController; referencePopupMoreMenu.inlineEditor = this.inlineEditor; referencePopupMoreMenu.targetInlineRange = this.targetInlineRange; createLitPortal({ template: referencePopupMoreMenu, container: this.popupContainer, computePosition: { referenceElement: this.popupContainer, placement: 'top-end', middleware: [flip(), offset(4)], autoUpdate: true, }, abortController: this._moreMenuAbortController, }); } override render() { return html`
${OpenIcon} ${'Click to open page'}
${LinkIcon} ${'Inline view'} this._convertToCardView()} > ${BookmarkIcon} ${'Card view'}
this._toggleMoreMenu()}> ${MoreVerticalIcon} More
`; } } declare global { interface HTMLElementTagNameMap { 'reference-popup': ReferencePopup; } } export function toggleReferencePopup( inlineEditor: AffineInlineEditor, targetInlineRange: InlineRange, pageTitle: string, abortController: AbortController ): ReferencePopup { const popup = new ReferencePopup(); popup.inlineEditor = inlineEditor; popup.targetInlineRange = targetInlineRange; popup.pageTitle = pageTitle; popup.abortController = abortController; document.body.appendChild(popup); return popup; }