import { assertExists } from '@blockeditor/global/utils';
import type { BlockElement } from '@blockeditor/lit';
import type { Page } from '@blockeditor/store';
import { computePosition, flip, offset, shift } from '@floating-ui/dom';
import { html } from 'lit';
import { ref, type RefOrCallback } from 'lit/directives/ref.js';
import { whenHover } from '../../../../_common/components/hover/index.js';
import { textConversionConfigs } from '../../../../_common/configs/text-conversion.js';
import { ArrowDownIcon } from '../../../../_common/icons/index.js';
import type { Flavour, ParagraphBlockModel } from '../../../../models.js';
import { isPageComponent } from '../../../../page-block/utils/guard.js';
import { updateBlockElementType } from '../../../../page-block/utils/operations/element/block-level.js';
import type { AffineFormatBarWidget } from '../format-bar.js';
interface ParagraphPanelProps {
page: Page;
selectedBlockElements: BlockElement[];
ref?: RefOrCallback;
}
const updateParagraphType = (
selectedBlockElements: BlockElement[],
flavour: Flavour,
type?: string
) => {
if (selectedBlockElements.length === 0) {
throw new Error('No models to update!');
}
const { flavour: defaultFlavour, type: defaultType } =
textConversionConfigs[0];
const targetFlavour = selectedBlockElements.every(
el =>
el.flavour === flavour && (el.model as ParagraphBlockModel).type === type
)
? defaultFlavour
: flavour;
const targetType = selectedBlockElements.every(
el =>
el.flavour === flavour && (el.model as ParagraphBlockModel).type === type
)
? defaultType
: type;
updateBlockElementType(selectedBlockElements, targetFlavour, targetType);
};
const ParagraphPanel = ({
page,
selectedBlockElements,
ref: containerRef,
}: ParagraphPanelProps) => {
return html`
${textConversionConfigs
.filter(({ flavour }) => flavour !== 'affine:divider')
.filter(({ flavour }) => page.schema.flavourSchemaMap.has(flavour))
.map(
({ flavour, type, name, icon }) =>
html`
updateParagraphType(selectedBlockElements, flavour, type)}"
>
${icon}
`
)}
`;
};
export const ParagraphButton = (formatBar: AffineFormatBarWidget) => {
if (formatBar.displayType !== 'text' && formatBar.displayType !== 'block') {
return null;
}
const selectedBlockElements = formatBar.selectedBlockElements;
// only support model with text
if (selectedBlockElements.some(el => !el.model.text)) {
return null;
}
const paragraphIcon =
selectedBlockElements.length < 1
? textConversionConfigs[0].icon
: textConversionConfigs.find(
({ flavour, type }) =>
selectedBlockElements[0].flavour === flavour &&
(selectedBlockElements[0].model as ParagraphBlockModel).type ===
type
)?.icon ?? textConversionConfigs[0].icon;
const pageElement = formatBar.blockElement;
if (!isPageComponent(pageElement)) {
throw new Error('paragraph button host is not a page component');
}
const { setFloating, setReference } = whenHover(isHover => {
if (!isHover) {
const panel =
formatBar.shadowRoot?.querySelector('.paragraph-panel');
if (!panel) return;
panel.style.display = 'none';
return;
}
const formatQuickBarElement = formatBar.formatBarElement;
const button =
formatBar.shadowRoot?.querySelector('.paragraph-button');
const panel =
formatBar.shadowRoot?.querySelector('.paragraph-panel');
assertExists(button);
assertExists(panel);
assertExists(formatQuickBarElement, 'format quick bar should exist');
panel.style.display = 'block';
computePosition(formatQuickBarElement, panel, {
placement: 'top-start',
middleware: [
flip(),
offset(4),
shift({
padding: 6,
}),
],
})
.then(({ x, y }) => {
panel.style.left = `${x}px`;
panel.style.top = `${y}px`;
})
.catch(console.error);
});
const paragraphPanel = ParagraphPanel({
selectedBlockElements,
page: formatBar.host.page,
ref: setFloating,
});
return html`
${paragraphIcon} ${ArrowDownIcon}
${paragraphPanel}
`;
};