import { DefaultTheme, NoteBlockModel, StrokeStyle, } from '@blocksuite/affine-model'; import { ThemeProvider } from '@blocksuite/affine-shared/services'; import { getClosestBlockComponentByPoint, handleNativeRangeAtPoint, matchFlavours, stopPropagation, } from '@blocksuite/affine-shared/utils'; import { type BlockComponent, type BlockStdScope, PropTypes, requiredProperties, ShadowlessElement, stdContext, TextSelection, } from '@blocksuite/block-std'; import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx'; import { clamp, Point, SignalWatcher, WithDisposable, } from '@blocksuite/global/utils'; import type { BlockModel } from '@blocksuite/store'; import { consume } from '@lit/context'; import { computed } from '@preact/signals-core'; import { html, nothing } from 'lit'; import { property } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; import { isPageBlock } from '../utils'; import * as styles from './edgeless-note-background.css'; @requiredProperties({ note: PropTypes.instanceOf(NoteBlockModel), }) export class EdgelessNoteBackground extends SignalWatcher( WithDisposable(ShadowlessElement) ) { readonly backgroundStyle$ = computed(() => { const themeProvider = this.std.get(ThemeProvider); const theme = themeProvider.theme$.value; const backgroundColor = themeProvider.generateColorProperty( this.note.background$.value, DefaultTheme.noteBackgrounColor, theme ); const { borderRadius, borderSize, borderStyle, shadowType } = this.note.edgeless$.value.style; return { borderRadius: borderRadius + 'px', backgroundColor: backgroundColor, borderWidth: `${borderSize}px`, borderStyle: borderStyle === StrokeStyle.Dash ? 'dashed' : borderStyle, boxShadow: !shadowType ? 'none' : `var(${shadowType})`, }; }); get gfx() { return this.std.get(GfxControllerIdentifier); } get doc() { return this.std.host.doc; } private _tryAddParagraph(x: number, y: number) { const nearest = getClosestBlockComponentByPoint( new Point(x, y) ) as BlockComponent | null; if (!nearest) return; const nearestBBox = nearest.getBoundingClientRect(); const yRel = y - nearestBBox.top; const insertPos: 'before' | 'after' = yRel < nearestBBox.height / 2 ? 'before' : 'after'; const nearestModel = nearest.model as BlockModel; const nearestModelIdx = this.note.children.indexOf(nearestModel); const children = this.note.children; const siblingModel = children[ clamp( nearestModelIdx + (insertPos === 'before' ? -1 : 1), 0, children.length ) ]; if ( (!nearestModel.text || !matchFlavours(nearestModel, ['affine:paragraph', 'affine:list'])) && (!siblingModel || !siblingModel.text || !matchFlavours(siblingModel, ['affine:paragraph', 'affine:list'])) ) { const [pId] = this.doc.addSiblingBlocks( nearestModel, [{ flavour: 'affine:paragraph' }], insertPos ); this.updateComplete .then(() => { this.std.selection.setGroup('note', [ this.std.selection.create(TextSelection, { from: { blockId: pId, index: 0, length: 0, }, to: null, }), ]); }) .catch(console.error); } } private _handleClickAtBackground(e: MouseEvent) { e.stopPropagation(); if (!this.editing) return; const { zoom } = this.gfx.viewport; const rect = this.getBoundingClientRect(); const offsetY = 16 * zoom; const offsetX = 2 * zoom; const x = clamp(e.x, rect.left + offsetX, rect.right - offsetX); const y = clamp(e.y, rect.top + offsetY, rect.bottom - offsetY); handleNativeRangeAtPoint(x, y); if (this.std.host.doc.readonly) return; this._tryAddParagraph(x, y); } private _renderHeader() { const header = this.std .getConfig('affine:note') ?.edgelessNoteHeader({ note: this.note, std: this.std }); return header; } override render() { return html`
${isPageBlock(this.std, this.note) ? this._renderHeader() : nothing}
`; } @consume({ context: stdContext }) accessor std!: BlockStdScope; @property({ attribute: false }) accessor editing: boolean = false; @property({ attribute: false }) accessor note!: NoteBlockModel; } declare global { interface HTMLElementTagNameMap { 'edgeless-note-background': EdgelessNoteBackground; } }