import '../../note-slicer/index.js'; import { ShadowlessElement, WithDisposable } from '@blockeditor/lit'; import { css, html, nothing } from 'lit'; import { customElement, property, query, state } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { styleMap } from 'lit/directives/style-map.js'; import { EDGELESS_BLOCK_CHILD_PADDING } from '../../../../../_common/consts.js'; import { DEFAULT_NOTE_COLOR } from '../../../../../_common/edgeless/note/consts.js'; import { MoreIndicatorIcon } from '../../../../../_common/icons/edgeless.js'; import { NoteDisplayMode } from '../../../../../_common/types.js'; import { almostEqual } from '../../../../../_common/utils/math.js'; import { type NoteBlockModel } from '../../../../../note-block/note-model.js'; import { Bound, StrokeStyle } from '../../../../../surface-block/index.js'; import type { SurfaceBlockComponent } from '../../../../../surface-block/surface-block.js'; import type { EdgelessBlockModel } from '../../../type.js'; import { EdgelessPortalBase } from '../edgeless-portal-base.js'; const ACTIVE_NOTE_EXTRA_PADDING = 20; @customElement('edgeless-note-mask') export class EdgelessNoteMask extends WithDisposable(ShadowlessElement) { static override styles = css` .affine-note-mask:hover { background-color: var(--affine-hover-color); } `; @property({ attribute: false }) surface!: SurfaceBlockComponent; @property({ attribute: false }) model!: NoteBlockModel; get edgeless() { return this.surface.edgeless; } protected override firstUpdated() { this._disposables.add( this.edgeless.service.selection.slots.updated.on(() => { this.requestUpdate(); }) ); const maskDOM = this.renderRoot!.querySelector('.affine-note-mask'); const observer = new ResizeObserver(entries => { for (const entry of entries) { if (!this.model.edgeless.collapse) { const bound = Bound.deserialize(this.model.xywh); const scale = this.model.edgeless.scale ?? 1; const height = entry.contentRect.height * scale; if (almostEqual(bound.h, height)) { return; } bound.h = height; this.model.stash('xywh'); this.model.xywh = bound.serialize(); } } }); observer.observe(maskDOM!); this._disposables.add(() => { // check if model still exist if (this.model.page.getBlockById(this.model.id)) { this.model.pop('xywh'); } observer.disconnect(); }); } override render() { const selected = this.edgeless.service.selection.has(this.model.id) && this.edgeless.service.selection.selections.some( sel => sel.elements.includes(this.model.id) && sel.editing ); const style = { position: 'absolute', top: '0', left: '0', bottom: '0', right: '0', zIndex: '1', pointerEvents: selected ? 'none' : 'auto', borderRadius: `${ this.model.edgeless.style.borderRadius * this.edgeless.service.viewport.zoom }px`, }; return html`
`; } } @customElement('edgeless-block-portal-note') export class EdgelessBlockPortalNote extends EdgelessPortalBase { static override styles = css` .edgeless-note-collapse-button { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; z-index: 2; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); opacity: 0.2; transition: opacity 0.3s; } .edgeless-note-collapse-button:hover { opacity: 1; } .edgeless-note-collapse-button.flip { transform: translateX(-50%) rotate(180deg); } .edgeless-note-collapse-button.hide { display: none; } .edgeless-block-portal-note:has(.edgeless-note-collapse-button:hover) { .affine-note-mask { background-color: var(--affine-hover-color); } } `; @state() private _isSelected = false; @state() private _editing = false; @state() private _isResizing = false; @state() private _isHover = false; @state() private _noteFullHeight = 0; @query('affine-note') private _affineNote!: HTMLDivElement; private _handleEditingTransition() { const selection = this.edgeless.service.selection; this._disposables.add( selection.slots.updated.on(selections => { if (selection.has(this.model.id) && selections?.[0].editing) { this._editing = true; } else { this._editing = false; } }) ); } private get _isShowCollapsedContent() { return this.model.edgeless.collapse && (this._isResizing || this._isHover); } private _hovered() { if (!this._isHover && this.edgeless.service.selection.has(this.model.id)) { this._isHover = true; } } private _leaved() { if (this._isHover) { this._isHover = false; } } private _setCollapse(event: MouseEvent) { event.stopImmediatePropagation(); const { collapse, collapsedHeight } = this.model.edgeless; if (collapse) { this.model.page.updateBlock(this.model, () => { this.model.edgeless.collapse = false; }); } else if (collapsedHeight) { const { xywh, edgeless } = this.model; const bound = Bound.deserialize(xywh); bound.h = collapsedHeight * (edgeless.scale ?? 1); this.model.page.updateBlock(this.model, () => { this.model.edgeless.collapse = true; this.model.xywh = bound.serialize(); }); } this.edgeless.service.selection.clear(); } private _collapsedContent() { if (!this._isShowCollapsedContent || !this._affineNote) { return nothing; } const { xywh, edgeless } = this.model; const bound = Bound.deserialize(xywh); const scale = edgeless.scale ?? 1; const width = bound.w / scale; const height = bound.h / scale; const rect = this._affineNote.getBoundingClientRect(); const zoom = this.edgeless.service.viewport.zoom; this._noteFullHeight = rect.height / scale / zoom + 2 * EDGELESS_BLOCK_CHILD_PADDING; if (height >= this._noteFullHeight) { return nothing; } return html`
`; } override firstUpdated() { const { _disposables, edgeless } = this; const selection = this.edgeless.service.selection; this._handleEditingTransition(); _disposables.add( edgeless.slots.elementResizeStart.on(() => { if (selection.elements.includes(this.model as EdgelessBlockModel)) { this._isResizing = true; } }) ); _disposables.add( edgeless.slots.elementResizeEnd.on(() => { this._isResizing = false; }) ); _disposables.add( edgeless.service.selection.slots.updated.on(() => { this._isSelected = edgeless.service.selection.has(this.model.id); this._isHover = this._isSelected && edgeless.tools.getHoverState()?.content === this.model; }) ); const observer = new MutationObserver(() => { const affineNote = this._affineNote; if (!this._affineNote) return; const rect = affineNote.getBoundingClientRect(); const zoom = this.edgeless.service.viewport.zoom; const scale = this.model.edgeless.scale ?? 1; this._noteFullHeight = rect.height / scale / zoom + 2 * EDGELESS_BLOCK_CHILD_PADDING; }); observer.observe(this, { childList: true, subtree: true }); _disposables.add(() => observer.disconnect()); } override render() { const { model, surface, index } = this; const { displayMode } = model; if (!!displayMode && displayMode === NoteDisplayMode.DocOnly) return nothing; const { xywh, background, edgeless } = model; const { borderRadius, borderSize, borderStyle, shadowType } = edgeless.style; const { collapse, collapsedHeight, scale = 1 } = edgeless; const bound = Bound.deserialize(xywh); const width = bound.w / scale; const height = bound.h / scale; const style = { position: 'absolute', zIndex: `${index}`, width: `${width}px`, height: collapse ? `${height}px` : 'inherit', left: `${bound.x}px`, top: `${bound.y}px`, padding: `${EDGELESS_BLOCK_CHILD_PADDING}px`, boxSizing: 'border-box', borderRadius: borderRadius + 'px', pointerEvents: 'all', transformOrigin: '0 0', transform: `scale(${scale})`, }; const extra = this._editing ? ACTIVE_NOTE_EXTRA_PADDING : 0; const backgroundStyle = { position: 'absolute', left: `${-extra}px`, top: `${-extra}px`, width: `${width + extra * 2}px`, height: `calc(100% + ${extra * 2}px)`, borderRadius: borderRadius + 'px', transition: this._editing ? 'left 0.3s, top 0.3s, width 0.3s, height 0.3s' : 'none', background: `var(${background ?? DEFAULT_NOTE_COLOR})`, border: `${borderSize}px ${ borderStyle === StrokeStyle.Dashed ? 'dashed' : borderStyle } var(--affine-black-10)`, boxShadow: this._editing ? 'var(--affine-active-shadow)' : !shadowType ? 'none' : `var(${shadowType})`, }; const isCollapsable = collapse != null && collapsedHeight != null && collapsedHeight !== this._noteFullHeight; const isCollapseArrowUp = collapse ? this._noteFullHeight < height : !!collapsedHeight && collapsedHeight < height; return html`
${surface.renderModel(model)}
${isCollapsable ? html`
e.stopPropagation()} @mouseup=${(e: MouseEvent) => e.stopPropagation()} @click=${this._setCollapse} > ${MoreIndicatorIcon}
` : nothing} ${this._collapsedContent()}
`; } } declare global { interface HTMLElementTagNameMap { 'edgeless-block-portal-note': EdgelessBlockPortalNote; 'edgeless-note-mask': EdgelessNoteMask; } }