import { ExpandIcon, LineStyleIcon, NoteCornerIcon, NoteShadowIcon, ScissorsIcon, ShrinkIcon, SmallArrowDownIcon, } from '@blocksuite/affine-components/icons'; import { type EditorMenuButton, renderToolbarSeparator, } from '@blocksuite/affine-components/toolbar'; import { type ColorScheme, DEFAULT_NOTE_BACKGROUND_COLOR, NOTE_BACKGROUND_COLORS, type NoteBlockModel, NoteDisplayMode, type StrokeStyle, } from '@blocksuite/affine-model'; import { ThemeProvider } from '@blocksuite/affine-shared/services'; import { matchFlavours } from '@blocksuite/affine-shared/utils'; import { assertExists, Bound, countBy, maxBy, WithDisposable, } from '@blocksuite/global/utils'; import { html, LitElement, nothing, type TemplateResult } from 'lit'; import { property, query } from 'lit/decorators.js'; import { join } from 'lit/directives/join.js'; import { createRef, type Ref, ref } from 'lit/directives/ref.js'; import { when } from 'lit/directives/when.js'; import type { EdgelessColorPickerButton, PickColorEvent, } from '../../edgeless/components/color-picker/index.js'; import { packColor, packColorsWithColorScheme, } from '../../edgeless/components/color-picker/utils.js'; import type { ColorEvent } from '../../edgeless/components/panel/color-panel.js'; import { type LineStyleEvent, LineStylesPanel, } from '../../edgeless/components/panel/line-styles-panel.js'; import { getTooltipWithShortcut } from '../../edgeless/components/utils.js'; import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; const SIZE_LIST = [ { name: 'None', value: 0 }, { name: 'Small', value: 8 }, { name: 'Medium', value: 16 }, { name: 'Large', value: 24 }, { name: 'Huge', value: 32 }, ] as const; const DisplayModeMap = { [NoteDisplayMode.DocAndEdgeless]: 'Both', [NoteDisplayMode.EdgelessOnly]: 'Edgeless', [NoteDisplayMode.DocOnly]: 'Page', } as const satisfies Record; function getMostCommonBackground( elements: NoteBlockModel[], colorScheme: ColorScheme ): string | null { const colors = countBy(elements, (ele: NoteBlockModel) => { return typeof ele.background === 'object' ? (ele.background[colorScheme] ?? ele.background.normal ?? null) : ele.background; }); const max = maxBy(Object.entries(colors), ([_k, count]) => count); return max ? (max[0] as string) : null; } export class EdgelessChangeNoteButton extends WithDisposable(LitElement) { private readonly _setBorderRadius = (borderRadius: number) => { this.notes.forEach(note => { const props = { edgeless: { style: { ...note.edgeless.style, borderRadius, }, }, }; this.edgeless.service.updateElement(note.id, props); }); }; private readonly _setNoteScale = (scale: number) => { this.notes.forEach(note => { this.doc.updateBlock(note, () => { const bound = Bound.deserialize(note.xywh); const oldScale = note.edgeless.scale ?? 1; const ratio = scale / oldScale; bound.w *= ratio; bound.h *= ratio; const xywh = bound.serialize(); note.xywh = xywh; note.edgeless.scale = scale; }); }); }; pickColor = (event: PickColorEvent) => { if (event.type === 'pick') { this.notes.forEach(element => { const props = packColor('background', { ...event.detail }); this.edgeless.service.updateElement(element.id, props); }); return; } this.notes.forEach(ele => ele[event.type === 'start' ? 'stash' : 'pop']('background') ); }; private get _advancedVisibilityEnabled() { return this.doc.awarenessStore.getFlag('enable_advanced_block_visibility'); } private get doc() { return this.edgeless.doc; } private _getScaleLabel(scale: number) { return Math.round(scale * 100) + '%'; } private _handleNoteSlicerButtonClick() { const surfaceService = this.edgeless.service; if (!surfaceService) return; this.edgeless.slots.toggleNoteSlicer.emit(); } private _setBackground(background: string) { this.notes.forEach(element => { this.edgeless.service.updateElement(element.id, { background }); }); } private _setCollapse() { this.notes.forEach(note => { const { collapse, collapsedHeight } = note.edgeless; if (collapse) { this.doc.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.doc.updateBlock(note, () => { note.edgeless.collapse = true; note.xywh = bound.serialize(); }); } }); this.requestUpdate(); } private _setDisplayMode(note: NoteBlockModel, newMode: NoteDisplayMode) { const { displayMode: currentMode } = note; if (newMode === currentMode) { return; } this.edgeless.service.updateElement(note.id, { displayMode: newMode }); const noteParent = this.doc.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.doc.moveBlocks([note], noteParent, noteParentLastNote, false); } // if change note to page only, should clear the selection if (newMode === NoteDisplayMode.DocOnly) { this.edgeless.service.selection.clear(); } } private _setShadowType(shadowType: string) { this.notes.forEach(note => { const props = { edgeless: { style: { ...note.edgeless.style, shadowType, }, }, }; this.edgeless.service.updateElement(note.id, props); }); } private _setStrokeStyle(borderStyle: StrokeStyle) { this.notes.forEach(note => { const props = { edgeless: { style: { ...note.edgeless.style, borderStyle, }, }, }; this.edgeless.service.updateElement(note.id, props); }); } private _setStrokeWidth(borderSize: number) { this.notes.forEach(note => { const props = { edgeless: { style: { ...note.edgeless.style, borderSize, }, }, }; this.edgeless.service.updateElement(note.id, props); }); } private _setStyles({ type, value }: LineStyleEvent) { if (type === 'size') { this._setStrokeWidth(value); return; } if (type === 'lineStyle') { this._setStrokeStyle(value); } } override render() { const len = this.notes.length; const note = this.notes[0]; const { edgeless, displayMode } = note; const { shadowType, borderRadius, borderSize, borderStyle } = edgeless.style; const colorScheme = this.edgeless.surface.renderer.getColorScheme(); const background = getMostCommonBackground(this.notes, colorScheme) ?? DEFAULT_NOTE_BACKGROUND_COLOR; const { collapse } = edgeless; const scale = edgeless.scale ?? 1; const currentMode = DisplayModeMap[displayMode]; const onlyOne = len === 1; const isDocOnly = displayMode === NoteDisplayMode.DocOnly; const theme = this.edgeless.std.get(ThemeProvider).theme; const buttons = [ onlyOne && this._advancedVisibilityEnabled ? html` Show in ${currentMode} ${SmallArrowDownIcon} `} > this._setDisplayMode(note, newMode)} > ` : nothing, isDocOnly ? nothing : when( this.edgeless.doc.awarenessStore.getFlag('enable_color_picker'), () => { const { type, colors } = packColorsWithColorScheme( colorScheme, background, note.background ); return html` `; }, () => html` `} > this._setBackground(e.detail)} > ` ), isDocOnly ? nothing : html` ${NoteShadowIcon}${SmallArrowDownIcon} `} > this._setShadowType(value)} > ${LineStyleIcon}${SmallArrowDownIcon} `} >
${LineStylesPanel({ selectedLineSize: borderSize, selectedLineStyle: borderStyle, onClick: event => this._setStyles(event), })}
${NoteCornerIcon}${SmallArrowDownIcon} `} > this._setBorderRadius(size)} .onPopperCose=${() => this._cornersPanelRef.value?.hide()} > `, onlyOne && this._advancedVisibilityEnabled ? html` this._handleNoteSlicerButtonClick()} > ${ScissorsIcon} ` : nothing, onlyOne ? this.quickConnectButton : nothing, html` this._setCollapse()} > ${collapse ? ExpandIcon : ShrinkIcon} ${this._getScaleLabel(scale)}${SmallArrowDownIcon} `} > this._setNoteScale(scale)} .onPopperCose=${() => this._scalePanelRef.value?.hide()} > `, ]; return join( buttons.filter(button => button !== nothing), renderToolbarSeparator ); } private accessor _cornersPanelRef: Ref = createRef(); private accessor _scalePanelRef: Ref = createRef(); @query('edgeless-color-picker-button.background') accessor backgroundButton!: EdgelessColorPickerButton; @property({ attribute: false }) accessor edgeless!: EdgelessRootBlockComponent; @property({ attribute: false }) accessor enableNoteSlicer!: boolean; @property({ attribute: false }) accessor notes: NoteBlockModel[] = []; @property({ attribute: false }) accessor quickConnectButton!: TemplateResult<1> | typeof nothing; } export function renderNoteButton( edgeless: EdgelessRootBlockComponent, notes?: NoteBlockModel[], quickConnectButton?: TemplateResult<1>[] ) { if (!notes?.length) return nothing; return html` `; }