import type { IVec } from '@blocksuite/global/utils'; import { WithDisposable } from '@blocksuite/global/utils'; import { html, LitElement } from 'lit'; import { property, state } from 'lit/decorators.js'; import type { PieNodeModel } from './base.js'; import type { PieMenu } from './menu.js'; import { pieNodeStyles } from './styles.js'; import { isAngleBetween, isColorNode, isCommandNode, isNodeWithAction, isNodeWithChildren, isRootNode, } from './utils.js'; export class PieNode extends WithDisposable(LitElement) { static override styles = pieNodeStyles; private readonly _handleChildNodeClick = () => { this.select(); if (isCommandNode(this.model)) this.menu.close(); }; private readonly _handleGoBack = () => { // If the node is not active and if it is hovered then we can go back to that node if (this.menu.activeNode !== this) { this.menu.popSelectionChainTo(this); } }; private readonly _onPointerAngleUpdated = (angle: number | null) => { this._rotatorAngle = angle; this.menu.activeNode.requestUpdate(); if (isRootNode(this.model) || !this.menu.isChildOfActiveNode(this)) return; if (angle === null) { this._isHovering = false; this.menu.setHovered(null); return; } if (isAngleBetween(angle, this.startAngle, this.endAngle)) { if (this.menu.hoveredNode !== this) { this._isHovering = true; this.menu.setHovered(this); } } else { this._isHovering = false; } }; private _rotatorAngle: number | null = null; get icon() { const icon = this.model.icon; if (typeof icon === 'function') { const { menu } = this; const { rootComponent, widgetComponent } = menu; return icon({ rootComponent, menu, widgetComponent, node: this, }); } return icon; } private _renderCenterNode() { const isActiveNode = this.isActive(); return html` `; } private _renderChildNode() { const visible = this.menu.isChildOfActiveNode(this); return html` `; } // for selecting with keyboard private _setupEvents() { this._disposables.add( this.menu.slots.pointerAngleUpdated.on(this._onPointerAngleUpdated) ); this._disposables.add( this.menu.slots.requestNodeUpdate.on(() => this.requestUpdate()) ); } override connectedCallback(): void { super.connectedCallback(); this._setupEvents(); } isActive() { return this.menu.isActiveNode(this); } isCenterNode() { return ( isNodeWithChildren(this.model) && this.menu.selectionChain.includes(this) ); } protected override render() { return this.isCenterNode() ? this._renderCenterNode() : this._renderChildNode(); } select() { const schema = this.model; if (isRootNode(schema)) return; const ctx = { rootComponent: this.menu.rootComponent, menu: this.menu, widgetComponent: this.menu.widgetComponent, node: this, }; if (isNodeWithAction(schema)) { schema.action(ctx); } else if (isColorNode(schema)) { schema.onChange(schema.color, ctx); } this.requestUpdate(); } @state() private accessor _isHovering = false; @property({ attribute: false }) accessor angle!: number; @property({ attribute: false }) accessor containerNode: PieNode | null = null; @property({ attribute: false }) accessor endAngle!: number; @property({ attribute: false }) accessor index!: number; @property({ attribute: false }) accessor menu!: PieMenu; @property({ attribute: false }) accessor model!: PieNodeModel; @property({ attribute: false }) accessor position!: IVec; @property({ attribute: false }) accessor startAngle!: number; } declare global { interface HTMLElementTagNameMap { 'pie-node': PieNode; } }