import '../buttons/tool-icon-button.js'; import '../toolbar/shape/shape-menu.js'; import '../panel/color-panel.js'; import './component-toolbar-menu-divider.js'; import '../panel/note-shadow-panel.js'; import '../panel/note-display-mode-panel.js'; import { assertExists } from '@blockeditor/global/utils'; import { WithDisposable } from '@blockeditor/lit'; import { css, html, LitElement, nothing, type PropertyValues } from 'lit'; import { customElement, property, query, state } from 'lit/decorators.js'; import { ExpandIcon, LineStyleIcon, NoteCornerIcon, NoteShadowIcon, ScissorsIcon, ShrinkIcon, SmallArrowDownIcon, } from '../../../../_common/icons/index.js'; import type { CssVariableName } from '../../../../_common/theme/css-variables.js'; import { NoteDisplayMode } from '../../../../_common/types.js'; import { matchFlavours } from '../../../../_common/utils/model.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 ColorEvent, ColorUnit } from '../panel/color-panel.js'; import { LineStylesPanel, type LineStylesPanelClickedButton, lineStylesPanelStyles, } from '../panel/line-styles-panel.js'; import { createButtonPopper, getTooltipWithShortcut } from '../utils.js'; const NOTE_BACKGROUND: CssVariableName[] = [ '--affine-tag-red', '--affine-tag-orange', '--affine-tag-yellow', '--affine-tag-green', '--affine-tag-teal', '--affine-tag-blue', '--affine-tag-purple', '--affine-tag-pink', '--affine-tag-gray', '--affine-palette-transparent', ]; @customElement('edgeless-change-note-button') export class EdgelessChangeNoteButton extends WithDisposable(LitElement) { static override styles = [ lineStylesPanelStyles, css` :host { display: flex; color: var(--affine-text-primary-color); fill: currentColor; align-items: center; justify-content: center; gap: 12px; } .fill-color-container { display: flex; justify-content: center; align-items: center; width: 20px; height: 20px; } .item { width: 40px; height: 24px; border-radius: 4px; display: flex; justify-content: center; align-items: center; } .item:hover { background-color: var(--affine-hover-color); } .display-mode-button-group { display: flex; align-items: center; gap: 4px; padding: 2px; font-size: 12px; font-weight: 500; line-height: 20px; } .display-mode-button { display: flex; border-radius: 4px; background-color: var(--affine-hover-color); align-items: center; gap: 2px; padding: 2px; } .current-mode-label { display: flex; padding: 2px 0px 2px 4px; align-items: center; } edgeless-size-panel { border-radius: 8px; } edgeless-color-panel { width: 168px; display: none; justify-content: center; align-items: center; background: var(--affine-background-overlay-panel-color); box-shadow: var(--affine-shadow-2); border-radius: 8px; } note-display-mode-panel, edgeless-note-shadow-panel, edgeless-size-panel { display: none; } note-display-mode-panel[data-show], edgeless-note-shadow-panel[data-show], edgeless-color-panel[data-show], edgeless-size-panel[data-show] { display: flex; } component-toolbar-menu-divider { width: 4px; height: 24px; } .line-style-panel { display: none; padding: 6px; } .line-style-panel component-toolbar-menu-divider { margin: 0 8px; } .line-style-panel[data-show] { display: flex; } `, ]; @property({ attribute: false }) notes: NoteBlockModel[] = []; @property({ attribute: false }) surface!: SurfaceBlockComponent; @state() private _queryCache = false; @state() private _showPopper = false; @state() private _enableNoteSlicer = false; @query('.fill-color-button') private _fillColorButton!: HTMLDivElement; @query('edgeless-color-panel') private _fillColorMenu!: HTMLDivElement; private _fillColorPopper: ReturnType | null = null; @query('.border-style-button') private _borderStyleButton!: HTMLDivElement; @query('.line-style-panel') private _borderStylesPanel!: HTMLDivElement; private _borderStylePopper: ReturnType | null = null; @query('.border-radius-button') private _borderRadiusButton!: HTMLDivElement; @query('edgeless-size-panel') private _boderRadiusPanel!: HTMLDivElement; private _borderRadiusPopper: ReturnType | null = null; @query('.shadow-style-button') private _shadowTypeButton!: HTMLDivElement; @query('edgeless-note-shadow-panel') private _shadowTypesPanel!: HTMLDivElement; private _shadowTypePopper: ReturnType | null = null; @query('.display-mode-button-group') private _displayModeButtonGroup!: HTMLDivElement; @query('note-display-mode-panel') private _displayModePanel!: HTMLDivElement; private _displayModePopper: ReturnType | null = null; private get page() { return this.surface.page; } private _setBackground(color: CssVariableName) { this.notes.forEach(note => { this.page.updateBlock(note, { background: color }); }); } private _setShadowType(shadowType: string) { this.notes.forEach(note => { this.page.updateBlock(note, () => { note.edgeless.style.shadowType = shadowType; }); }); } private _setDisplayMode(note: NoteBlockModel, newMode: NoteDisplayMode) { const { displayMode: currentMode } = note; if (newMode === currentMode) { return; } this.page.updateBlock(note, { displayMode: newMode }); const noteParent = this.page.getParent(note); assertExists(noteParent); const noteParentChildNotes = noteParent.children.filter(block => matchFlavours(block, ['affine:note']) ) as NoteBlockModel[]; const noteParentLastNote = noteParentChildNotes[noteParentChildNotes.length - 1]; if ( currentMode === NoteDisplayMode.EdgelessOnly && newMode !== NoteDisplayMode.EdgelessOnly && note !== noteParentLastNote ) { // move to the end this.page.moveBlocks([note], noteParent, noteParentLastNote, false); } // if change note to page only, should clear the selection if (newMode === NoteDisplayMode.DocOnly) { this.surface.edgeless.service.selection.clear(); } this._queryCache = !this._queryCache; } private _setStrokeWidth(borderSize: number) { this.notes.forEach(note => { this.page.updateBlock(note, () => { note.edgeless.style.borderSize = borderSize; }); }); } private _setStrokeStyle(borderStyle: StrokeStyle) { this.notes.forEach(note => { this.page.updateBlock(note, () => { note.edgeless.style.borderStyle = borderStyle; }); }); } private _setStyles({ type, value }: LineStylesPanelClickedButton) { if (type === 'size') { this._setStrokeWidth(value); } else if (type === 'lineStyle') { switch (value) { case 'solid': { this._setStrokeStyle(StrokeStyle.Solid); break; } case 'dash': { this._setStrokeStyle(StrokeStyle.Dashed); break; } case 'none': { this._setStrokeStyle(StrokeStyle.None); break; } } } } private _setBorderRadius = (size: number) => { this.notes.forEach(note => { this.page.updateBlock(note, () => { note.edgeless.style.borderRadius = size; }); }); }; private _setCollapse() { this.notes.forEach(note => { const { collapse, collapsedHeight } = note.edgeless; if (collapse) { this.page.updateBlock(note, () => { note.edgeless.collapse = false; }); } else if (collapsedHeight) { const { xywh, edgeless } = note; const bound = Bound.deserialize(xywh); bound.h = collapsedHeight * (edgeless.scale ?? 1); this.page.updateBlock(note, () => { note.edgeless.collapse = true; note.xywh = bound.serialize(); }); } }); this.requestUpdate(); } private _handleNoteSlicerButtonClick() { const surfaceService = this.surface.service; if (!surfaceService) return; this.surface.edgeless.slots.toggleNoteSlicer.emit(); } private _getCurrentModeLabel(mode: NoteDisplayMode) { switch (mode) { case NoteDisplayMode.DocAndEdgeless: return 'Both'; case NoteDisplayMode.EdgelessOnly: return 'Edgeless'; case NoteDisplayMode.DocOnly: return 'Page'; default: return 'Both'; } } override updated(_changedProperties: PropertyValues) { const { _disposables } = this; if (_changedProperties.has('_queryCache')) { this._displayModePopper = createButtonPopper( this._displayModeButtonGroup, this._displayModePanel, ({ display }) => { this._showPopper = display === 'show'; }, -154, 90 ); _disposables.add(this._displayModePopper); this._fillColorPopper = createButtonPopper( this._fillColorButton, this._fillColorMenu, ({ display }) => (this._showPopper = display === 'show') ); this._disposables.add(this._fillColorPopper); this._shadowTypePopper = createButtonPopper( this._shadowTypeButton, this._shadowTypesPanel, ({ display }) => { this._showPopper = display === 'show'; } ); _disposables.add(this._shadowTypePopper); this._borderStylePopper = createButtonPopper( this._borderStyleButton, this._borderStylesPanel, ({ display }) => { this._showPopper = display === 'show'; } ); _disposables.add(this._borderStylePopper); this._borderRadiusPopper = createButtonPopper( this._borderRadiusButton, this._boderRadiusPanel, ({ display }) => { this._showPopper = display === 'show'; } ); _disposables.add(this._borderRadiusPopper); } } override firstUpdated() { const { edgeless } = this.surface; this.disposables.add( edgeless.slots.toggleNoteSlicer.on(() => { this._enableNoteSlicer = !this._enableNoteSlicer; }) ); this.disposables.add( edgeless.service.selection.slots.updated.on(() => { this._enableNoteSlicer = false; }) ); } override render() { const length = this.notes.length; const note = this.notes[0]; const { background, edgeless, displayMode } = note; const { shadowType, borderRadius, borderSize, borderStyle } = edgeless.style; const { collapse } = edgeless; const currentMode = this._getCurrentModeLabel(displayMode); return html` ${length === 1 ? html`
Show in this._displayModePopper?.toggle()} >
${currentMode} ${SmallArrowDownIcon}
{ this._setDisplayMode(note, newMode); this._displayModePopper?.hide(); }} > ` : nothing} ${displayMode === NoteDisplayMode.DocOnly ? nothing : html` this._fillColorPopper?.toggle()} >
${ColorUnit(background)}
this._setBackground(e.detail)} >
this._shadowTypePopper?.toggle()} > ${NoteShadowIcon}${SmallArrowDownIcon}
this._setShadowType(value)} >
this._borderStylePopper?.toggle()} > ${LineStyleIcon}${SmallArrowDownIcon}
${LineStylesPanel({ selectedLineSize: borderSize, selectedLineStyle: borderStyle, onClick: event => { this._setStyles(event); }, })}
this._borderRadiusPopper?.toggle()} > ${NoteCornerIcon}${SmallArrowDownIcon}
{ this._setBorderRadius(size); }} .onPopperCose=${() => this._borderRadiusPopper?.hide()} > `} { this._setCollapse(); }} > ${collapse ? ExpandIcon : ShrinkIcon} ${length === 1 ? html` ${ScissorsIcon} ` : nothing} `; } } declare global { interface HTMLElementTagNameMap { 'edgeless-change-note-button': EdgelessChangeNoteButton; } }