/// import '../_common/components/rich-text/rich-text.js'; import { assertExists } from '@blockeditor/global/utils'; import type { InlineRangeProvider } from '@blockeditor/inline'; import { BlockElement, getInlineRangeProvider } from '@blockeditor/lit'; import { html, nothing, type TemplateResult } from 'lit'; import { customElement, query, state } from 'lit/decorators.js'; import { when } from 'lit/directives/when.js'; import { bindContainerHotkey } from '../_common/components/rich-text/keymap/index.js'; import type { RichText } from '../_common/components/rich-text/rich-text.js'; import { BLOCK_CHILDREN_CONTAINER_PADDING_LEFT } from '../_common/consts.js'; import type { NoteBlockComponent } from '../note-block/note-block.js'; import { EdgelessPageBlockComponent } from '../page-block/edgeless/edgeless-page-block.js'; import type { ListBlockModel } from './list-model.js'; import type { ListService } from './list-service.js'; import { styles } from './styles.js'; import { ListIcon } from './utils/get-list-icon.js'; import { getListInfo } from './utils/get-list-info.js'; import { playCheckAnimation, toggleDown, toggleRight } from './utils/icons.js'; @customElement('affine-list') export class ListBlockComponent extends BlockElement< ListBlockModel, ListService > { static override styles = styles; get inlineManager() { const inlineManager = this.service?.inlineManager; assertExists(inlineManager); return inlineManager; } get attributesSchema() { return this.inlineManager.getSchema(); } get attributeRenderer() { return this.inlineManager.getRenderer(); } get markdownShortcutHandler() { return this.inlineManager.markdownShortcutHandler; } get embedChecker() { return this.inlineManager.embedChecker; } @state() private _isCollapsedWhenReadOnly = !!this.model?.collapsed; private _select() { const selection = this.host.selection; selection.update(selList => { return selList .filter(sel => !sel.is('text') && !sel.is('block')) .concat(selection.create('block', { path: this.path })); }); } private _onClickIcon = (e: MouseEvent) => { e.stopPropagation(); if (this.model.type === 'toggle') { this._toggleChildren(); return; } else if (this.model.type === 'todo') { this.model.page.captureSync(); const checkedPropObj = { checked: !this.model.checked }; this.model.page.updateBlock(this.model, checkedPropObj); if (this.model.checked) { const checkEl = this.querySelector('.affine-list-block__todo-prefix'); assertExists(checkEl); playCheckAnimation(checkEl).catch(console.error); } return; } this._select(); }; @query('rich-text') private _richTextElement?: RichText; private _inlineRangeProvider: InlineRangeProvider | null = null; override get topContenteditableElement() { if (this.rootBlockElement instanceof EdgelessPageBlockComponent) { const note = this.closest('affine-note'); return note; } return this.rootBlockElement; } override async getUpdateComplete() { const result = await super.getUpdateComplete(); await this._richTextElement?.updateComplete; return result; } override firstUpdated() { this._updateFollowingListSiblings(); this.model.childrenUpdated.on(() => { this._updateFollowingListSiblings(); }); } private _updateFollowingListSiblings() { this.updateComplete .then(() => { let current: BlockElement | undefined = this as BlockElement; while (current && current.tagName == 'AFFINE-LIST') { current.requestUpdate(); current = this.std.view.findNext(current.path, () => { return true; })?.view as BlockElement; } }) .catch(console.error); } override connectedCallback() { super.connectedCallback(); bindContainerHotkey(this); this._inlineRangeProvider = getInlineRangeProvider(this); } private _toggleChildren() { if (this.page.readonly) { this._isCollapsedWhenReadOnly = !this._isCollapsedWhenReadOnly; return; } const newCollapsedState = !this.model.collapsed; this._isCollapsedWhenReadOnly = newCollapsedState; this.page.captureSync(); this.page.updateBlock(this.model, { collapsed: newCollapsedState, } as Partial); } private _toggleTemplate(isCollapsed: boolean) { const noChildren = this.model.children.length === 0; if (noChildren) return nothing; const toggleDownTemplate = html`
${toggleDown}
`; const toggleRightTemplate = html`
${toggleRight}
`; return isCollapsed ? toggleRightTemplate : toggleDownTemplate; } override renderBlock(): TemplateResult<1> { const { deep, index } = getListInfo(this.model); const { model, _onClickIcon } = this; const collapsed = this.page.readonly ? this._isCollapsedWhenReadOnly : !!model.collapsed; const listIcon = ListIcon(model, !collapsed, _onClickIcon); // For the first list item, we need to add a margin-top to make it align with the text const shouldAddMarginTop = index === 0 && deep === 0; const top = shouldAddMarginTop ? 'affine-list-block-container--first' : ''; const checked = this.model.type === 'todo' && this.model.checked ? 'affine-list--checked' : ''; const children = html`
${this.renderModelChildren(this.model)}
`; return html`
${this._toggleTemplate(collapsed)} ${listIcon}
${collapsed ? nothing : children} ${when( this.selected?.is('block'), () => html`` )}
`; } } declare global { interface HTMLElementTagNameMap { 'affine-list': ListBlockComponent; } }