From e320552594f9efe23e37663e48577ad154d1cc96 Mon Sep 17 00:00:00 2001 From: fundon Date: Wed, 19 Mar 2025 12:34:18 +0000 Subject: [PATCH] refactor(editor): edgeless note toolbar config extension (#10719) --- .../components/edgeless-line-styles-panel.ts | 1 + .../components/edgeless-line-width-panel.ts | 1 + blocksuite/affine/all/src/effects.ts | 4 + .../affine/blocks/block-note/package.json | 2 + .../edgeless-note-border-dropdown-menu.ts | 41 ++ ...dgeless-note-display-mode-dropdown-menu.ts | 58 ++ .../edgeless-note-shadow-dropdown-menu.ts | 177 ++++++ .../blocks/block-note/src/components/icons.ts | 84 +++ .../src/components/view-in-page-notify.css.ts | 24 + .../blocks/block-note/src/configs/toolbar.ts | 599 ++++++++++++++++++ .../affine/blocks/block-note/src/effects.ts | 15 + .../affine/blocks/block-note/src/note-spec.ts | 8 +- .../blocks/block-root/src/configs/toolbar.ts | 2 +- .../src/edgeless/configs/toolbar/index.ts | 6 - .../src/edgeless/configs/toolbar/note.ts | 11 - blocksuite/affine/components/package.json | 4 +- .../src/color-picker/color-panel.ts | 7 +- .../src/edgeless-line-styles-panel/index.ts | 10 + .../line-styles-panel.ts | 107 ++++ .../src/edgeless-line-width-panel/index.ts | 10 + .../line-width-panel.ts | 244 +++++++ .../src/size-dropdown-menu/dropdown-menu.ts | 21 +- .../src/services/toolbar-service/action.ts | 2 +- .../src/services/toolbar-service/context.ts | 4 + .../src/services/toolbar-service/registry.ts | 4 +- .../affine/shared/src/utils/computing.ts | 23 + blocksuite/affine/shared/src/utils/event.ts | 16 +- blocksuite/affine/shared/src/utils/index.ts | 1 + .../widgets/widget-toolbar/src/toolbar.ts | 57 +- yarn.lock | 2 + 30 files changed, 1486 insertions(+), 59 deletions(-) create mode 100644 blocksuite/affine/all/src/components/edgeless-line-styles-panel.ts create mode 100644 blocksuite/affine/all/src/components/edgeless-line-width-panel.ts create mode 100644 blocksuite/affine/blocks/block-note/src/components/edgeless-note-border-dropdown-menu.ts create mode 100644 blocksuite/affine/blocks/block-note/src/components/edgeless-note-display-mode-dropdown-menu.ts create mode 100644 blocksuite/affine/blocks/block-note/src/components/edgeless-note-shadow-dropdown-menu.ts create mode 100644 blocksuite/affine/blocks/block-note/src/components/icons.ts create mode 100644 blocksuite/affine/blocks/block-note/src/components/view-in-page-notify.css.ts create mode 100644 blocksuite/affine/blocks/block-note/src/configs/toolbar.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/note.ts create mode 100644 blocksuite/affine/components/src/edgeless-line-styles-panel/index.ts create mode 100644 blocksuite/affine/components/src/edgeless-line-styles-panel/line-styles-panel.ts create mode 100644 blocksuite/affine/components/src/edgeless-line-width-panel/index.ts create mode 100644 blocksuite/affine/components/src/edgeless-line-width-panel/line-width-panel.ts create mode 100644 blocksuite/affine/shared/src/utils/computing.ts diff --git a/blocksuite/affine/all/src/components/edgeless-line-styles-panel.ts b/blocksuite/affine/all/src/components/edgeless-line-styles-panel.ts new file mode 100644 index 000000000..a1c6fd0bd --- /dev/null +++ b/blocksuite/affine/all/src/components/edgeless-line-styles-panel.ts @@ -0,0 +1 @@ +export * from '@blocksuite/affine-components/edgeless-line-styles-panel'; diff --git a/blocksuite/affine/all/src/components/edgeless-line-width-panel.ts b/blocksuite/affine/all/src/components/edgeless-line-width-panel.ts new file mode 100644 index 000000000..40fbc3fbd --- /dev/null +++ b/blocksuite/affine/all/src/components/edgeless-line-width-panel.ts @@ -0,0 +1 @@ +export * from '@blocksuite/affine-components/edgeless-line-width-panel'; diff --git a/blocksuite/affine/all/src/effects.ts b/blocksuite/affine/all/src/effects.ts index d2eec909b..937b3fcd3 100644 --- a/blocksuite/affine/all/src/effects.ts +++ b/blocksuite/affine/all/src/effects.ts @@ -25,6 +25,8 @@ import { effects as componentColorPickerEffects } from '@blocksuite/affine-compo import { effects as componentContextMenuEffects } from '@blocksuite/affine-components/context-menu'; import { effects as componentDatePickerEffects } from '@blocksuite/affine-components/date-picker'; import { effects as componentDropIndicatorEffects } from '@blocksuite/affine-components/drop-indicator'; +import { effects as componentEdgelessLineStylesEffects } from '@blocksuite/affine-components/edgeless-line-styles-panel'; +import { effects as componentEdgelessLineWidthEffects } from '@blocksuite/affine-components/edgeless-line-width-panel'; import { effects as componentEmbedCardModalEffects } from '@blocksuite/affine-components/embed-card-modal'; import { FilterableListComponent } from '@blocksuite/affine-components/filterable-list'; import { effects as componentHighlightDropdownMenuEffects } from '@blocksuite/affine-components/highlight-dropdown-menu'; @@ -149,6 +151,8 @@ export function effects() { componentViewDropdownMenuEffects(); componentTooltipContentWithShortcutEffects(); componentSizeDropdownMenuEffects(); + componentEdgelessLineWidthEffects(); + componentEdgelessLineStylesEffects(); widgetScrollAnchoringEffects(); widgetFrameTitleEffects(); diff --git a/blocksuite/affine/blocks/block-note/package.json b/blocksuite/affine/blocks/block-note/package.json index 84f05638d..4df4f0032 100644 --- a/blocksuite/affine/blocks/block-note/package.json +++ b/blocksuite/affine/blocks/block-note/package.json @@ -26,9 +26,11 @@ "@lit/context": "^1.1.2", "@preact/signals-core": "^1.8.0", "@toeverything/theme": "^1.1.12", + "@types/lodash-es": "^4.17.12", "@types/mdast": "^4.0.4", "@vanilla-extract/css": "^1.17.0", "lit": "^3.2.0", + "lodash-es": "^4.17.21", "minimatch": "^10.0.1", "rxjs": "^7.8.1", "zod": "^3.23.8" diff --git a/blocksuite/affine/blocks/block-note/src/components/edgeless-note-border-dropdown-menu.ts b/blocksuite/affine/blocks/block-note/src/components/edgeless-note-border-dropdown-menu.ts new file mode 100644 index 000000000..735b32b14 --- /dev/null +++ b/blocksuite/affine/blocks/block-note/src/components/edgeless-note-border-dropdown-menu.ts @@ -0,0 +1,41 @@ +import { LineWidth, type StrokeStyle } from '@blocksuite/affine-model'; +import { ShadowlessElement } from '@blocksuite/block-std'; +import { ArrowDownSmallIcon, LineStyleIcon } from '@blocksuite/icons/lit'; +import { html } from 'lit'; +import { property } from 'lit/decorators.js'; + +export class EdgelessNoteBorderDropdownMenu extends ShadowlessElement { + override render() { + const { lineSize, lineStyle } = this; + + return html` + + ${LineStyleIcon()} ${ArrowDownSmallIcon()} + + `} + > + + + `; + } + + @property({ attribute: false }) + accessor lineStyle!: StrokeStyle; + + @property({ attribute: false }) + accessor lineSize: LineWidth = LineWidth.Two; +} + +declare global { + interface HTMLElementTagNameMap { + 'edgeless-note-border-dropdown-menu': EdgelessNoteBorderDropdownMenu; + } +} diff --git a/blocksuite/affine/blocks/block-note/src/components/edgeless-note-display-mode-dropdown-menu.ts b/blocksuite/affine/blocks/block-note/src/components/edgeless-note-display-mode-dropdown-menu.ts new file mode 100644 index 000000000..b9d02eb4e --- /dev/null +++ b/blocksuite/affine/blocks/block-note/src/components/edgeless-note-display-mode-dropdown-menu.ts @@ -0,0 +1,58 @@ +import { NoteDisplayMode } from '@blocksuite/affine-model'; +import { ShadowlessElement } from '@blocksuite/block-std'; +import { ArrowDownSmallIcon } from '@blocksuite/icons/lit'; +import { html } from 'lit'; +import { property } from 'lit/decorators.js'; + +const DisplayModeMap = { + [NoteDisplayMode.DocAndEdgeless]: 'Both', + [NoteDisplayMode.EdgelessOnly]: 'Edgeless', + [NoteDisplayMode.DocOnly]: 'Page', +} as const satisfies Record; + +export class EdgelessNoteDisplayModeDropdownMenu extends ShadowlessElement { + get mode() { + return DisplayModeMap[this.displayMode]; + } + + select(detail: NoteDisplayMode) { + this.dispatchEvent(new CustomEvent('select', { detail })); + } + + override render() { + const { displayMode, mode } = this; + + return html` + Show in + + ${mode} + ${ArrowDownSmallIcon()} + + `} + > + this.select(newMode)} + > + + + `; + } + + @property({ attribute: false }) + accessor displayMode!: NoteDisplayMode; +} + +declare global { + interface HTMLElementTagNameMap { + 'edgeless-note-display-mode-dropdown-menu': EdgelessNoteDisplayModeDropdownMenu; + } +} diff --git a/blocksuite/affine/blocks/block-note/src/components/edgeless-note-shadow-dropdown-menu.ts b/blocksuite/affine/blocks/block-note/src/components/edgeless-note-shadow-dropdown-menu.ts new file mode 100644 index 000000000..55f1979b8 --- /dev/null +++ b/blocksuite/affine/blocks/block-note/src/components/edgeless-note-shadow-dropdown-menu.ts @@ -0,0 +1,177 @@ +import { ColorScheme, NoteShadow } from '@blocksuite/affine-model'; +import { + ArrowDownSmallIcon, + NoteShadowDuotoneIcon, +} from '@blocksuite/icons/lit'; +import { css, html, LitElement } from 'lit'; +import { property } from 'lit/decorators.js'; +import { repeat } from 'lit/directives/repeat.js'; +import { styleMap } from 'lit/directives/style-map.js'; + +import { NoteNoShadowIcon, NoteShadowSampleIcon } from './icons'; + +const SHADOWS = [ + { + type: NoteShadow.None, + styles: { + light: '', + dark: '', + }, + tooltip: 'No shadow', + }, + { + type: NoteShadow.Box, + styles: { + light: + '0px 0.2px 4.8px 0px rgba(66, 65, 73, 0.2), 0px 0px 1.6px 0px rgba(66, 65, 73, 0.2)', + dark: '0px 0.2px 6px 0px rgba(0, 0, 0, 0.44), 0px 0px 2px 0px rgba(0, 0, 0, 0.66)', + }, + tooltip: 'Box shadow', + }, + { + type: NoteShadow.Sticker, + styles: { + light: + '0px 9.6px 10.4px -4px rgba(66, 65, 73, 0.07), 0px 10.4px 7.2px -8px rgba(66, 65, 73, 0.22)', + dark: '0px 9.6px 10.4px -4px rgba(0, 0, 0, 0.66), 0px 10.4px 7.2px -8px rgba(0, 0, 0, 0.44)', + }, + tooltip: 'Sticker shadow', + }, + { + type: NoteShadow.Paper, + styles: { + light: + '0px 0px 0px 4px rgba(255, 255, 255, 1), 0px 1.2px 2.4px 4.8px rgba(66, 65, 73, 0.16)', + dark: '0px 1.2px 2.4px 4.8px rgba(0, 0, 0, 0.36), 0px 0px 0px 3.4px rgba(75, 75, 75, 1)', + }, + tooltip: 'Paper shadow', + }, + { + type: NoteShadow.Float, + styles: { + light: + '0px 5.2px 12px 0px rgba(66, 65, 73, 0.13), 0px 0px 0.4px 1px rgba(0, 0, 0, 0.06)', + dark: '0px 5.2px 12px 0px rgba(0, 0, 0, 0.66), 0px 0px 0.4px 1px rgba(0, 0, 0, 0.44)', + }, + tooltip: 'Floation shadow', + }, + { + type: NoteShadow.Film, + styles: { + light: + '0px 0px 0px 1.4px rgba(0, 0, 0, 1), 2.4px 2.4px 0px 1px rgba(0, 0, 0, 1)', + dark: '0px 0px 0px 1.4px rgba(178, 178, 178, 1), 2.4px 2.4px 0px 1px rgba(178, 178, 178, 1)', + }, + tooltip: 'Film shadow', + }, +]; + +export class EdgelessNoteShadowDropdownMenu extends LitElement { + static override styles = css` + :host { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + } + + .item { + padding: 8px; + border-radius: 4px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + } + + .item-icon { + display: flex; + justify-content: center; + align-items: center; + } + + .item-icon svg rect:first-of-type { + fill: var(--background); + } + + .item:hover { + background-color: var(--affine-hover-color); + } + + .item[data-selected] { + border: 1px solid var(--affine-brand-color); + } + `; + + select(value: NoteShadow) { + this.dispatchEvent(new CustomEvent('select', { detail: value })); + } + + override render() { + const { value, background, theme } = this; + const isDark = theme === ColorScheme.Dark; + + return html` + + ${NoteShadowDuotoneIcon()} ${ArrowDownSmallIcon()} + + `} + > +
+ ${repeat( + SHADOWS, + shadow => shadow.type, + ({ type, tooltip, styles: { dark, light } }, index) => + html`
this.select(type)} + > + + ${index === 0 ? NoteNoShadowIcon : NoteShadowSampleIcon} + +
` + )} +
+
+ `; + } + + @property({ attribute: false }) + accessor background!: string; + + @property({ attribute: false }) + accessor theme!: ColorScheme; + + @property({ attribute: false }) + accessor value!: NoteShadow; +} + +declare global { + interface HTMLElementTagNameMap { + 'edgeless-note-shadow-dropdown-menu': EdgelessNoteShadowDropdownMenu; + } +} diff --git a/blocksuite/affine/blocks/block-note/src/components/icons.ts b/blocksuite/affine/blocks/block-note/src/components/icons.ts new file mode 100644 index 000000000..42a224a69 --- /dev/null +++ b/blocksuite/affine/blocks/block-note/src/components/icons.ts @@ -0,0 +1,84 @@ +import { html } from 'lit'; + +export const NoteNoShadowIcon = html` + + + + + +`; + +export const NoteShadowSampleIcon = html` + + + + + + + + +`; diff --git a/blocksuite/affine/blocks/block-note/src/components/view-in-page-notify.css.ts b/blocksuite/affine/blocks/block-note/src/components/view-in-page-notify.css.ts new file mode 100644 index 000000000..409eefeac --- /dev/null +++ b/blocksuite/affine/blocks/block-note/src/components/view-in-page-notify.css.ts @@ -0,0 +1,24 @@ +import { cssVar } from '@toeverything/theme'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const viewInPageNotifyFooter = style({ + display: 'flex', + justifyContent: 'flex-end', + gap: '12px', +}); + +export const viewInPageNotifyFooterButton = style({ + padding: '0px 6px', + borderRadius: '4px', + color: cssVarV2('text/primary'), + + fontSize: cssVar('fontSm'), + lineHeight: '22px', + fontWeight: '500', + textAlign: 'center', + + ':hover': { + background: cssVarV2('layer/background/hoverOverlay'), + }, +}); diff --git a/blocksuite/affine/blocks/block-note/src/configs/toolbar.ts b/blocksuite/affine/blocks/block-note/src/configs/toolbar.ts new file mode 100644 index 000000000..9f4566cd2 --- /dev/null +++ b/blocksuite/affine/blocks/block-note/src/configs/toolbar.ts @@ -0,0 +1,599 @@ +import { + EdgelessCRUDExtension, + EdgelessLegacySlotIdentifier, +} from '@blocksuite/affine-block-surface'; +import { + packColor, + type PickColorEvent, +} from '@blocksuite/affine-components/color-picker'; +import type { LineDetailType } from '@blocksuite/affine-components/edgeless-line-styles-panel'; +import { + DefaultTheme, + NoteBlockModel, + NoteDisplayMode, + type NoteShadow, + resolveColor, +} from '@blocksuite/affine-model'; +import { + FeatureFlagService, + NotificationProvider, + SidebarExtensionIdentifier, + type ToolbarAction, + type ToolbarContext, + type ToolbarModuleConfig, + ToolbarModuleExtension, +} from '@blocksuite/affine-shared/services'; +import { getMostCommonResolvedValue } from '@blocksuite/affine-shared/utils'; +import { + BlockFlavourIdentifier, + EditorLifeCycleExtension, +} from '@blocksuite/block-std'; +import { Bound } from '@blocksuite/global/gfx'; +import { + AutoHeightIcon, + CornerIcon, + CustomizedHeightIcon, + LinkedPageIcon, + ScissorsIcon, +} from '@blocksuite/icons/lit'; +import type { ExtensionType } from '@blocksuite/store'; +import { computed } from '@preact/signals-core'; +import { html } from 'lit'; +import { keyed } from 'lit/directives/keyed.js'; + +import { changeNoteDisplayMode } from '../commands'; +import * as styles from '../components/view-in-page-notify.css'; +import { NoteConfigExtension } from '../config'; + +const trackBaseProps = { + category: 'note', +}; + +const CORNER_LIST = [ + { key: 'None', value: 0 }, + { key: 'Small', value: 8 }, + { key: 'Medium', value: 16 }, + { key: 'Large', value: 24 }, + { key: 'Huge', value: 32 }, +] as const; + +const builtinSurfaceToolbarConfig = { + actions: [ + { + id: 'a.show-in', + when(ctx) { + return ( + ctx.getSurfaceModelsByType(NoteBlockModel).length === 1 && + ctx.std + .get(FeatureFlagService) + .getFlag('enable_advanced_block_visibility') + ); + }, + content(ctx) { + const models = ctx.getSurfaceModelsByType(NoteBlockModel); + if (!models.length) return null; + + const firstModel = models[0]; + const { displayMode } = firstModel.props; + const onSelect = (e: CustomEvent) => { + e.stopPropagation(); + + const newMode = e.detail; + setDisplayMode(ctx, firstModel, newMode); + }; + + return html` + + + `; + }, + }, + { + id: 'b.display-in-page', + when(ctx) { + const elements = ctx.getSurfaceModelsByType(NoteBlockModel); + return ( + elements.length === 1 && + !elements[0].isPageBlock() && + !ctx.std + .get(FeatureFlagService) + .getFlag('enable_advanced_block_visibility') + ); + }, + generate(ctx) { + const models = ctx.getSurfaceModelsByType(NoteBlockModel); + if (!models.length) return null; + + const firstModel = models[0]; + const shouldShowTooltip$ = computed( + () => + firstModel.props.displayMode$.value === + NoteDisplayMode.DocAndEdgeless + ); + const label$ = computed(() => + firstModel.props.displayMode$.value === NoteDisplayMode.EdgelessOnly + ? 'Display In Page' + : 'Displayed In Page' + ); + const onSelect = () => { + const newMode = + firstModel.props.displayMode === NoteDisplayMode.EdgelessOnly + ? NoteDisplayMode.DocAndEdgeless + : NoteDisplayMode.EdgelessOnly; + setDisplayMode(ctx, firstModel, newMode); + }; + + return { + content: html` onSelect()} + > + ${LinkedPageIcon()} + ${label$.value} + `, + }; + }, + }, + { + id: 'c.color-picker', + when(ctx) { + const elements = ctx.getSurfaceModelsByType(NoteBlockModel); + return ( + elements.length > 0 && + elements[0].props.displayMode !== NoteDisplayMode.DocOnly + ); + }, + content(ctx) { + const models = ctx.getSurfaceModelsByType(NoteBlockModel); + if (!models.length) return null; + + const enableCustomColor = ctx.std + .get(FeatureFlagService) + .getFlag('enable_color_picker'); + const theme = ctx.themeProvider.edgelessTheme; + + const firstModel = models[0]; + const background = + getMostCommonResolvedValue( + models.map(model => model.props), + 'background', + background => resolveColor(background, theme) + ) ?? resolveColor(DefaultTheme.noteBackgrounColor, theme); + const onPick = (e: PickColorEvent) => { + const field = 'background'; + + if (e.type === 'pick') { + const color = e.detail.value; + for (const model of models) { + const props = packColor(field, color); + ctx.std.get(EdgelessCRUDExtension).updateElement(model.id, props); + } + return; + } + + for (const model of models) { + model[e.type === 'start' ? 'stash' : 'pop'](field); + } + }; + + return html` + + + `; + }, + }, + { + id: 'd.style', + when(ctx) { + const elements = ctx.getSurfaceModelsByType(NoteBlockModel); + return ( + elements.length > 0 && + elements[0].props.displayMode !== NoteDisplayMode.DocOnly + ); + }, + actions: [ + { + id: 'a.shadow-style', + content(ctx) { + const models = ctx.getSurfaceModelsByType(NoteBlockModel); + if (!models.length) return null; + + const theme = ctx.themeProvider.edgelessTheme; + + const firstModel = models[0]; + const { shadowType } = firstModel.props.edgeless.style; + const background = + getMostCommonResolvedValue( + models.map(model => model.props), + 'background', + background => resolveColor(background, theme) + ) ?? resolveColor(DefaultTheme.noteBackgrounColor, theme); + const onSelect = (e: CustomEvent) => { + e.stopPropagation(); + + const shadowType = e.detail; + for (const model of models) { + const edgeless = model.props.edgeless; + ctx.std.get(EdgelessCRUDExtension).updateElement(model.id, { + edgeless: { + ...edgeless, + style: { + ...edgeless.style, + shadowType, + }, + }, + }); + } + }; + + return html`${keyed( + firstModel, + html`` + )}`; + }, + } satisfies ToolbarAction, + { + id: 'b.border-style', + content(ctx) { + const models = ctx.getSurfaceModelsByType(NoteBlockModel); + if (!models.length) return null; + + const firstModel = models[0]; + const { borderSize, borderStyle } = firstModel.props.edgeless.style; + const onSelect = (e: CustomEvent) => { + e.stopPropagation(); + + const { type, value } = e.detail; + + if (type === 'size') { + const borderSize = value; + for (const model of models) { + const edgeless = model.props.edgeless; + ctx.std.get(EdgelessCRUDExtension).updateElement(model.id, { + edgeless: { + ...edgeless, + style: { + ...edgeless.style, + borderSize, + }, + }, + }); + } + return; + } + + const borderStyle = value; + for (const model of models) { + const edgeless = model.props.edgeless; + ctx.std.get(EdgelessCRUDExtension).updateElement(model.id, { + edgeless: { + ...edgeless, + style: { + ...edgeless.style, + borderStyle, + }, + }, + }); + } + }; + + return html`${keyed( + firstModel, + html` + + ` + )}`; + }, + } satisfies ToolbarAction, + { + id: 'c.corners', + label: 'Corners', + content(ctx) { + const models = ctx.getSurfaceModelsByType(NoteBlockModel); + if (!models.length) return null; + + const label = this.label; + const firstModel = models[0]; + const borderRadius$ = computed( + () => firstModel.props.edgeless$.value.style.borderRadius + ); + const onSelect = (e: CustomEvent) => { + e.stopPropagation(); + + const borderRadius = e.detail; + for (const model of models) { + const edgeless = model.props.edgeless; + ctx.std.get(EdgelessCRUDExtension).updateElement(model.id, { + edgeless: { + ...edgeless, + style: { + ...edgeless.style, + borderRadius, + }, + }, + }); + } + }; + + return html`${keyed( + firstModel, + html`` + )}`; + }, + } satisfies ToolbarAction, + ], + }, + { + id: 'e.slicer', + icon: ScissorsIcon(), + tooltip: html``, + active: false, + when(ctx) { + return ( + ctx.getSurfaceModelsByType(NoteBlockModel).length === 1 && + ctx.std + .get(FeatureFlagService) + .getFlag('enable_advanced_block_visibility') + ); + }, + run(ctx) { + ctx.std.get(EdgelessLegacySlotIdentifier).toggleNoteSlicer.next(); + }, + }, + { + id: 'f.auto-height', + when(ctx) { + const elements = ctx.getSurfaceModelsByType(NoteBlockModel); + return ( + elements.length > 0 && + (!elements[0].isPageBlock() || + !ctx.std.getOptional(NoteConfigExtension.identifier) + ?.edgelessNoteHeader) + ); + }, + generate(ctx) { + const models = ctx.getSurfaceModelsByType(NoteBlockModel); + if (!models.length) return null; + + const firstModel = models[0]; + const { collapse } = firstModel.props.edgeless$.value; + const options: Pick = collapse + ? { + tooltip: 'Auto height', + icon: AutoHeightIcon(), + } + : { + tooltip: 'Customized height', + icon: CustomizedHeightIcon(), + }; + + return { + ...options, + run(ctx) { + for (const model of models) { + const edgeless = model.props.edgeless; + + if (edgeless.collapse) { + ctx.store.updateBlock(model, () => { + model.props.edgeless.collapse = false; + }); + continue; + } + + if (edgeless.collapsedHeight) { + const bounds = Bound.deserialize(model.xywh); + bounds.h = edgeless.collapsedHeight * (edgeless.scale ?? 1); + const xywh = bounds.serialize(); + + ctx.store.updateBlock(model, () => { + model.xywh = xywh; + model.props.edgeless.collapse = true; + }); + } + } + }, + }; + }, + }, + { + id: 'g.scale', + content(ctx) { + const models = ctx.getSurfaceModelsByType(NoteBlockModel); + if (!models.length) return null; + + const firstModel = models[0]; + const scale$ = computed(() => { + const scale = firstModel.props.edgeless$.value.scale ?? 1; + return Math.round(100 * scale); + }); + const onSelect = (e: CustomEvent) => { + e.stopPropagation(); + + const scale = e.detail / 100; + + models.forEach(model => { + const bounds = Bound.deserialize(model.xywh); + const oldScale = model.props.edgeless.scale ?? 1; + const ratio = scale / oldScale; + bounds.w *= ratio; + bounds.h *= ratio; + const xywh = bounds.serialize(); + + ctx.store.updateBlock(model, () => { + model.xywh = xywh; + model.props.edgeless.scale = scale; + }); + }); + + ctx.track('SelectedCardScale', { + ...trackBaseProps, + control: 'select card scale', + }); + }; + const onToggle = (e: CustomEvent) => { + e.stopPropagation(); + + const opened = e.detail; + if (!opened) return; + + ctx.track('OpenedCardScaleSelector', { + ...trackBaseProps, + control: 'switch card scale', + }); + }; + const format = (value: number) => `${value}%`; + + return html`${keyed( + firstModel, + html`` + )}`; + }, + }, + ], + + when: ctx => ctx.getSurfaceModelsByType(NoteBlockModel).length > 0, +} as const satisfies ToolbarModuleConfig; + +function setDisplayMode( + ctx: ToolbarContext, + model: NoteBlockModel, + newMode: NoteDisplayMode +) { + const displayMode = model.props.displayMode; + + ctx.command.exec(changeNoteDisplayMode, { + noteId: model.id, + mode: newMode, + stopCapture: true, + }); + + // if change note to page only, should clear the selection + if (newMode === NoteDisplayMode.DocOnly) { + ctx.selection.clear(); + } + + const abortController = new AbortController(); + const clear = () => { + ctx.history.off('stack-item-added', addHandler); + ctx.history.off('stack-item-popped', popHandler); + disposable.unsubscribe(); + }; + const closeNotify = () => { + abortController.abort(); + clear(); + }; + + const addHandler = ctx.history.on('stack-item-added', closeNotify); + const popHandler = ctx.history.on('stack-item-popped', closeNotify); + const disposable = ctx.std + .get(EditorLifeCycleExtension) + .slots.unmounted.subscribe(closeNotify); + + const undo = () => { + ctx.store.undo(); + closeNotify(); + }; + + const viewInToc = () => { + const sidebar = ctx.std.getOptional(SidebarExtensionIdentifier); + sidebar?.open('outline'); + closeNotify(); + }; + + const data = + newMode === NoteDisplayMode.EdgelessOnly + ? { + title: 'Note removed from Page Mode', + message: 'Content removed from your page.', + } + : { + title: 'Note displayed in Page Mode', + message: 'Content added to your page.', + }; + + const notification = ctx.std.getOptional(NotificationProvider); + notification?.notify({ + title: data.title, + message: `${data.message}. Find it in the TOC for quick navigation.`, + accent: 'success', + duration: 5 * 1000, + footer: html`
+ + +
`, + abort: abortController.signal, + onClose: () => { + clear(); + }, + }); + + ctx.track('NoteDisplayModeChanged', { + ...trackBaseProps, + control: 'display mode', + other: `from ${displayMode} to ${newMode}`, + }); +} + +export const createBuiltinToolbarConfigExtension = ( + flavour: string +): ExtensionType[] => { + const name = flavour.split(':').pop(); + + return [ + ToolbarModuleExtension({ + id: BlockFlavourIdentifier(`affine:surface:${name}`), + config: builtinSurfaceToolbarConfig, + }), + ]; +}; diff --git a/blocksuite/affine/blocks/block-note/src/effects.ts b/blocksuite/affine/blocks/block-note/src/effects.ts index 8100164d1..2bff9c8c0 100644 --- a/blocksuite/affine/blocks/block-note/src/effects.ts +++ b/blocksuite/affine/blocks/block-note/src/effects.ts @@ -1,5 +1,8 @@ import { EdgelessNoteBackground } from './components/edgeless-note-background'; +import { EdgelessNoteBorderDropdownMenu } from './components/edgeless-note-border-dropdown-menu'; +import { EdgelessNoteDisplayModeDropdownMenu } from './components/edgeless-note-display-mode-dropdown-menu'; import { EdgelessNoteMask } from './components/edgeless-note-mask'; +import { EdgelessNoteShadowDropdownMenu } from './components/edgeless-note-shadow-dropdown-menu'; import { EdgelessPageBlockTitle } from './components/edgeless-page-block-title'; import { NoteBlockComponent } from './note-block'; import { @@ -13,4 +16,16 @@ export function effects() { customElements.define('edgeless-note-mask', EdgelessNoteMask); customElements.define('edgeless-note-background', EdgelessNoteBackground); customElements.define('edgeless-page-block-title', EdgelessPageBlockTitle); + customElements.define( + 'edgeless-note-shadow-dropdown-menu', + EdgelessNoteShadowDropdownMenu + ); + customElements.define( + 'edgeless-note-border-dropdown-menu', + EdgelessNoteBorderDropdownMenu + ); + customElements.define( + 'edgeless-note-display-mode-dropdown-menu', + EdgelessNoteDisplayModeDropdownMenu + ); } diff --git a/blocksuite/affine/blocks/block-note/src/note-spec.ts b/blocksuite/affine/blocks/block-note/src/note-spec.ts index 04c316b82..26d2a1782 100644 --- a/blocksuite/affine/blocks/block-note/src/note-spec.ts +++ b/blocksuite/affine/blocks/block-note/src/note-spec.ts @@ -6,9 +6,10 @@ import { literal } from 'lit/static-html.js'; import { DocNoteBlockAdapterExtensions, EdgelessNoteBlockAdapterExtensions, -} from './adapters/index.js'; -import { NoteSlashMenuConfigExtension } from './configs/slash-menu.js'; -import { NoteBlockService } from './note-service.js'; +} from './adapters/index'; +import { NoteSlashMenuConfigExtension } from './configs/slash-menu'; +import { createBuiltinToolbarConfigExtension } from './configs/toolbar'; +import { NoteBlockService } from './note-service'; const flavour = NoteBlockSchema.model.flavour; @@ -26,4 +27,5 @@ export const EdgelessNoteBlockSpec: ExtensionType[] = [ BlockViewExtension(flavour, literal`affine-edgeless-note`), EdgelessNoteBlockAdapterExtensions, NoteSlashMenuConfigExtension, + createBuiltinToolbarConfigExtension(flavour), ].flat(); diff --git a/blocksuite/affine/blocks/block-root/src/configs/toolbar.ts b/blocksuite/affine/blocks/block-root/src/configs/toolbar.ts index a697aac45..d6140df9a 100644 --- a/blocksuite/affine/blocks/block-root/src/configs/toolbar.ts +++ b/blocksuite/affine/blocks/block-root/src/configs/toolbar.ts @@ -92,7 +92,7 @@ const conversionsActionGroup = { .button=${html` ${conversion.icon} ${ArrowDownSmallIcon()} diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/index.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/index.ts index e8d589e98..2b7534a22 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/index.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/index.ts @@ -8,7 +8,6 @@ import { builtinFrameToolbarConfig } from './frame'; import { builtinGroupToolbarConfig } from './group'; import { builtinMindmapToolbarConfig } from './mindmap'; import { builtinMiscToolbarConfig } from './misc'; -import { builtinNoteToolbarConfig } from './note'; import { builtinShapeToolbarConfig } from './shape'; import { builtinTextToolbarConfig } from './text'; @@ -38,11 +37,6 @@ export const EdgelessElementToolbarExtension: ExtensionType[] = [ config: builtinMindmapToolbarConfig, }), - ToolbarModuleExtension({ - id: BlockFlavourIdentifier('affine:surface:note'), - config: builtinNoteToolbarConfig, - }), - ToolbarModuleExtension({ id: BlockFlavourIdentifier('affine:surface:shape'), config: builtinShapeToolbarConfig, diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/note.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/note.ts deleted file mode 100644 index 66866d08d..000000000 --- a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/note.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services'; - -export const builtinNoteToolbarConfig = { - actions: [ - { - id: 'a.test', - label: 'Note', - run() {}, - }, - ], -} as const satisfies ToolbarModuleConfig; diff --git a/blocksuite/affine/components/package.json b/blocksuite/affine/components/package.json index 9af98f77a..e83847f0f 100644 --- a/blocksuite/affine/components/package.json +++ b/blocksuite/affine/components/package.json @@ -66,7 +66,9 @@ "./card-style-dropdown-menu": "./src/card-style-dropdown-menu/index.ts", "./highlight-dropdown-menu": "./src/highlight-dropdown-menu/index.ts", "./tooltip-content-with-shortcut": "./src/tooltip-content-with-shortcut/index.ts", - "./size-dropdown-menu": "./src/size-dropdown-menu/index.ts" + "./size-dropdown-menu": "./src/size-dropdown-menu/index.ts", + "./edgeless-line-width-panel": "./src/edgeless-line-width-panel/index.ts", + "./edgeless-line-styles-panel": "./src/edgeless-line-styles-panel/index.ts" }, "files": [ "src", diff --git a/blocksuite/affine/components/src/color-picker/color-panel.ts b/blocksuite/affine/components/src/color-picker/color-panel.ts index bd9eeeaae..18a002c59 100644 --- a/blocksuite/affine/components/src/color-picker/color-panel.ts +++ b/blocksuite/affine/components/src/color-picker/color-panel.ts @@ -205,12 +205,13 @@ export class EdgelessColorPanel extends LitElement { } `; - onSelect(palette: Palette) { + select(palette: Palette) { this.dispatchEvent( new ColorEvent('select', { detail: palette, - composed: true, bubbles: true, + composed: true, + cancelable: true, }) ); } @@ -236,7 +237,7 @@ export class EdgelessColorPanel extends LitElement { .hollowCircle=${this.hollowCircle} ?active=${activated} @click=${() => { - this.onSelect(palette); + this.select(palette); this.value = resolvedColor; }} > diff --git a/blocksuite/affine/components/src/edgeless-line-styles-panel/index.ts b/blocksuite/affine/components/src/edgeless-line-styles-panel/index.ts new file mode 100644 index 000000000..7ddf7fa56 --- /dev/null +++ b/blocksuite/affine/components/src/edgeless-line-styles-panel/index.ts @@ -0,0 +1,10 @@ +import { EdgelessLineStylesPanel } from './line-styles-panel'; + +export * from './line-styles-panel'; + +export function effects() { + customElements.define( + 'affine-edgeless-line-styles-panel', + EdgelessLineStylesPanel + ); +} diff --git a/blocksuite/affine/components/src/edgeless-line-styles-panel/line-styles-panel.ts b/blocksuite/affine/components/src/edgeless-line-styles-panel/line-styles-panel.ts new file mode 100644 index 000000000..074735a49 --- /dev/null +++ b/blocksuite/affine/components/src/edgeless-line-styles-panel/line-styles-panel.ts @@ -0,0 +1,107 @@ +import { LineWidth, StrokeStyle } from '@blocksuite/affine-model'; +import { BanIcon, DashLineIcon, StraightLineIcon } from '@blocksuite/icons/lit'; +import { html, LitElement } from 'lit'; +import { property } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; +import { repeat } from 'lit/directives/repeat.js'; + +export type LineDetailType = + | { + type: 'size'; + value: LineWidth; + } + | { + type: 'style'; + value: StrokeStyle; + }; + +const LINE_STYLE_LIST = [ + { + name: 'Solid', + value: StrokeStyle.Solid, + icon: StraightLineIcon(), + }, + { + name: 'Dash', + value: StrokeStyle.Dash, + icon: DashLineIcon(), + }, + { + name: 'None', + value: StrokeStyle.None, + icon: BanIcon(), + }, +]; + +export class EdgelessLineStylesPanel extends LitElement { + select(detail: LineDetailType) { + this.dispatchEvent( + new CustomEvent('select', { + detail, + bubbles: true, + composed: true, + cancelable: true, + }) + ); + } + + override render() { + const { lineSize, lineStyle, lineStyles } = this; + + return html` + ) => { + e.stopPropagation(); + this.select({ type: 'size', value: e.detail }); + }} + > + + + + ${repeat( + LINE_STYLE_LIST.filter(item => lineStyles.includes(item.value)), + item => item.value, + ({ name, icon, value }) => { + const active = lineStyle === value; + const classInfo = { + 'line-style-button': true, + [`mode-${value}`]: true, + }; + if (active) classInfo['active'] = true; + + return html` + this.select({ type: 'style', value })} + > + ${icon} + + `; + } + )} + `; + } + + @property({ attribute: false }) + accessor lineStyle!: StrokeStyle; + + @property({ attribute: false }) + accessor lineSize: LineWidth = LineWidth.Two; + + @property({ attribute: false }) + accessor lineStyles: StrokeStyle[] = [ + StrokeStyle.Solid, + StrokeStyle.Dash, + StrokeStyle.None, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-edgeless-line-styles-panel': EdgelessLineStylesPanel; + } +} diff --git a/blocksuite/affine/components/src/edgeless-line-width-panel/index.ts b/blocksuite/affine/components/src/edgeless-line-width-panel/index.ts new file mode 100644 index 000000000..fec9bc358 --- /dev/null +++ b/blocksuite/affine/components/src/edgeless-line-width-panel/index.ts @@ -0,0 +1,10 @@ +import { EdgelessLineWidthPanel } from './line-width-panel'; + +export * from './line-width-panel'; + +export function effects() { + customElements.define( + 'affine-edgeless-line-width-panel', + EdgelessLineWidthPanel + ); +} diff --git a/blocksuite/affine/components/src/edgeless-line-width-panel/line-width-panel.ts b/blocksuite/affine/components/src/edgeless-line-width-panel/line-width-panel.ts new file mode 100644 index 000000000..c42d10f60 --- /dev/null +++ b/blocksuite/affine/components/src/edgeless-line-width-panel/line-width-panel.ts @@ -0,0 +1,244 @@ +import { LINE_WIDTHS, LineWidth } from '@blocksuite/affine-model'; +import { on, once } from '@blocksuite/affine-shared/utils'; +import { WithDisposable } from '@blocksuite/global/lit'; +import { css, html, LitElement, nothing, type PropertyValues } from 'lit'; +import { property, query } from 'lit/decorators.js'; +import { repeat } from 'lit/directives/repeat.js'; +import clamp from 'lodash-es/clamp'; + +interface Config { + width: number; + itemSize: number; + itemIconSize: number; + dragHandleSize: number; + count: number; +} + +export class EdgelessLineWidthPanel extends WithDisposable(LitElement) { + static override styles = css` + :host { + display: flex; + align-items: center; + justify-content: center; + align-self: stretch; + + --width: 140px; + --item-size: 16px; + --item-icon-size: 8px; + --drag-handle-size: 14px; + --cursor: 0; + --count: 6; + /* (16 - 14) / 2 + (cursor / (count - 1)) * (140 - 16) */ + --drag-handle-center-x: calc( + (var(--item-size) - var(--drag-handle-size)) / 2 + + (var(--cursor) / (var(--count) - 1)) * + (var(--width) - var(--item-size)) + ); + } + + :host([disabled]) { + opacity: 0.5; + pointer-events: none; + } + + .line-width-panel { + width: var(--width); + height: 24px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + position: relative; + cursor: default; + } + + .line-width-button { + display: flex; + align-items: center; + justify-content: center; + width: var(--item-size); + height: var(--item-size); + z-index: 2; + } + + .line-width-icon { + width: var(--item-icon-size); + height: var(--item-icon-size); + background-color: var(--affine-border-color); + border-radius: 50%; + } + + .line-width-button[data-selected] .line-width-icon { + background-color: var(--affine-icon-color); + } + + .drag-handle { + position: absolute; + width: var(--drag-handle-size); + height: var(--drag-handle-size); + border-radius: 50%; + background-color: var(--affine-icon-color); + z-index: 3; + transform: translateX(var(--drag-handle-center-x)); + } + + .bottom-line, + .line-width-overlay { + position: absolute; + height: 1px; + left: calc(var(--item-size) / 2); + } + + .bottom-line { + width: calc(100% - var(--item-size)); + background-color: var(--affine-border-color); + } + + .line-width-overlay { + background-color: var(--affine-icon-color); + z-index: 1; + width: var(--drag-handle-center-x); + } + `; + + private readonly _getDragHandlePosition = (e: PointerEvent) => { + return clamp(e.offsetX, 0, this.config.width); + }; + + private readonly _onPointerDown = (e: PointerEvent) => { + e.preventDefault(); + this._onPointerMove(e); + + const dispose = on(this, 'pointermove', this._onPointerMove); + this._disposables.add(once(this, 'pointerup', dispose)); + this._disposables.add(once(this, 'pointerout', dispose)); + }; + + private readonly _onPointerMove = (e: PointerEvent) => { + e.preventDefault(); + + const x = this._getDragHandlePosition(e); + + this._updateLineWidthPanelByDragHandlePosition(x); + }; + + private _onSelect(lineWidth: LineWidth) { + // If the selected size is the same as the previous one, do nothing. + if (lineWidth === this.selectedSize) return; + this.dispatchEvent( + new CustomEvent('select', { + detail: lineWidth, + bubbles: true, + composed: true, + cancelable: true, + }) + ); + this.selectedSize = lineWidth; + } + + private _updateLineWidthPanel(selectedSize: LineWidth) { + if (!this._lineWidthOverlay) return; + const index = this.lineWidths.findIndex(w => w === selectedSize); + if (index === -1) return; + + this.style.setProperty('--cursor', `${index}`); + } + + private _updateLineWidthPanelByDragHandlePosition(x: number) { + // Calculate the selected size based on the drag handle position. + // Need to select the nearest size. + + const { + config: { width, itemSize, count }, + lineWidths, + } = this; + const targetWidth = width - itemSize; + const halfItemSize = itemSize / 2; + const offsetX = halfItemSize + (width - itemSize * count) / (count - 1) / 2; + const selectedSize = lineWidths.findLast((_, n) => { + const cx = halfItemSize + (n / (count - 1)) * targetWidth; + return x >= cx - offsetX && x < cx + offsetX; + }); + if (!selectedSize) return; + + this._updateLineWidthPanel(selectedSize); + this._onSelect(selectedSize); + } + + override connectedCallback() { + super.connectedCallback(); + const { + style, + config: { width, itemSize, itemIconSize, dragHandleSize, count }, + } = this; + style.setProperty('--width', `${width}px`); + style.setProperty('--item-size', `${itemSize}px`); + style.setProperty('--item-icon-size', `${itemIconSize}px`); + style.setProperty('--drag-handle-size', `${dragHandleSize}px`); + style.setProperty('--count', `${count}`); + } + + override firstUpdated() { + this._updateLineWidthPanel(this.selectedSize); + this._disposables.addFromEvent(this, 'pointerdown', this._onPointerDown); + } + + override render() { + return html`
+ ${repeat( + this.lineWidths, + w => w, + (w, n) => + html`
+
+
` + )} +
+
+
+ ${this.hasTooltip + ? html`Thickness` + : nothing} +
`; + } + + override willUpdate(changedProperties: PropertyValues) { + if (changedProperties.has('selectedSize')) { + this._updateLineWidthPanel(this.selectedSize); + } + } + + @query('.line-width-overlay') + private accessor _lineWidthOverlay!: HTMLElement; + + accessor config: Config = { + width: 140, + itemSize: 16, + itemIconSize: 8, + dragHandleSize: 14, + count: LINE_WIDTHS.length, + }; + + @property({ attribute: false, type: Boolean }) + accessor disabled = false; + + @property({ attribute: false }) + accessor hasTooltip = true; + + @property({ attribute: false }) + accessor lineWidths: LineWidth[] = LINE_WIDTHS; + + @property({ attribute: false }) + accessor selectedSize: LineWidth = LineWidth.Two; +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-edgeless-line-width-panel': EdgelessLineWidthPanel; + } +} diff --git a/blocksuite/affine/components/src/size-dropdown-menu/dropdown-menu.ts b/blocksuite/affine/components/src/size-dropdown-menu/dropdown-menu.ts index f1fb34c9e..bda94f029 100644 --- a/blocksuite/affine/components/src/size-dropdown-menu/dropdown-menu.ts +++ b/blocksuite/affine/components/src/size-dropdown-menu/dropdown-menu.ts @@ -3,7 +3,7 @@ import { PropTypes, requiredProperties } from '@blocksuite/block-std'; import { SignalWatcher } from '@blocksuite/global/lit'; import { ArrowDownSmallIcon, DoneIcon } from '@blocksuite/icons/lit'; import type { ReadonlySignal, Signal } from '@preact/signals-core'; -import { css, html, LitElement } from 'lit'; +import { css, html, LitElement, type TemplateResult } from 'lit'; import { property, query } from 'lit/decorators.js'; import { repeat } from 'lit-html/directives/repeat.js'; import { when } from 'lit-html/directives/when.js'; @@ -39,7 +39,7 @@ export class SizeDropdownMenu extends SignalWatcher(LitElement) { :host([data-type='check']) editor-menu-action[data-selected] { color: var(--affine-primary-color); - background-color: none; + background-color: unset; } input { @@ -76,6 +76,12 @@ export class SizeDropdownMenu extends SignalWatcher(LitElement) { @property({ attribute: false }) accessor format: ((e: number) => string) | undefined; + @property({ attribute: false }) + accessor label: string = 'Scale'; + + @property({ attribute: false }) + accessor icon: TemplateResult | undefined; + @property({ attribute: 'data-type' }) accessor type: 'normal' | 'check' = 'normal'; @@ -118,6 +124,8 @@ export class SizeDropdownMenu extends SignalWatcher(LitElement) { sizes, format, type, + icon, + label, size$: { value: size }, } = this; const isCheckType = type === 'check'; @@ -128,13 +136,14 @@ export class SizeDropdownMenu extends SignalWatcher(LitElement) { .contentPadding="${'8px'}" .button=${html` - ${format?.(size) ?? size} + ${icon ?? + html`${format?.(size) ?? size}`} ${ArrowDownSmallIcon()} `} diff --git a/blocksuite/affine/shared/src/services/toolbar-service/action.ts b/blocksuite/affine/shared/src/services/toolbar-service/action.ts index 58cfc7a33..ba7be5107 100644 --- a/blocksuite/affine/shared/src/services/toolbar-service/action.ts +++ b/blocksuite/affine/shared/src/services/toolbar-service/action.ts @@ -20,7 +20,7 @@ type ActionBase = { export type ToolbarAction = ActionBase & { label?: string; icon?: TemplateResult; - tooltip?: string; + tooltip?: string | TemplateResult; variant?: 'destructive'; disabled?: ((cx: ToolbarContext) => boolean) | boolean; content?: diff --git a/blocksuite/affine/shared/src/services/toolbar-service/context.ts b/blocksuite/affine/shared/src/services/toolbar-service/context.ts index 6d2426ef0..0f0095677 100644 --- a/blocksuite/affine/shared/src/services/toolbar-service/context.ts +++ b/blocksuite/affine/shared/src/services/toolbar-service/context.ts @@ -56,6 +56,10 @@ abstract class ToolbarContextBase { return this.std.store; } + get history() { + return this.store.history; + } + get view() { return this.std.view; } diff --git a/blocksuite/affine/shared/src/services/toolbar-service/registry.ts b/blocksuite/affine/shared/src/services/toolbar-service/registry.ts index e56ebbb29..ed053a29c 100644 --- a/blocksuite/affine/shared/src/services/toolbar-service/registry.ts +++ b/blocksuite/affine/shared/src/services/toolbar-service/registry.ts @@ -25,14 +25,14 @@ export function ToolbarModuleExtension(module: ToolbarModule): ExtensionType { export class ToolbarRegistryExtension extends Extension { flavour$ = signal('affine:note'); + elementsMap$ = signal>(new Map()); + message$ = signal<{ flavour: string; element: Element; setFloating: (element?: Element) => void; } | null>(null); - elementsMap$ = signal>(new Map()); - flags = new Flags(); constructor(readonly std: BlockStdScope) { diff --git a/blocksuite/affine/shared/src/utils/computing.ts b/blocksuite/affine/shared/src/utils/computing.ts new file mode 100644 index 000000000..843d0b51a --- /dev/null +++ b/blocksuite/affine/shared/src/utils/computing.ts @@ -0,0 +1,23 @@ +import groupBy from 'lodash-es/groupBy'; +import maxBy from 'lodash-es/maxBy'; + +export function getMostCommonValue( + records: T[], + field: F +) { + const grouped = groupBy(records, record => record[field]); + const values = Object.values(grouped); + const record = maxBy(values, records => records.length)?.[0]; + return record?.[field]; +} + +export function getMostCommonResolvedValue< + T, + F extends Exclude, + U, +>(records: T[], field: F, resolve: (value: T[F]) => U) { + return getMostCommonValue( + records.map(record => ({ [field]: resolve(record[field]) })), + field + ); +} diff --git a/blocksuite/affine/shared/src/utils/event.ts b/blocksuite/affine/shared/src/utils/event.ts index 0439cdd53..b2f8e9b66 100644 --- a/blocksuite/affine/shared/src/utils/event.ts +++ b/blocksuite/affine/shared/src/utils/event.ts @@ -359,18 +359,4 @@ export const createKeydownObserver = ({ target.addEventListener('compositionend', () => onInput?.(true), { signal }); }; -export class ColorEvent extends Event { - detail: Palette; - - constructor( - type: string, - { - detail, - composed, - bubbles, - }: { detail: Palette; composed: boolean; bubbles: boolean } - ) { - super(type, { bubbles, composed }); - this.detail = detail; - } -} +export class ColorEvent extends CustomEvent {} diff --git a/blocksuite/affine/shared/src/utils/index.ts b/blocksuite/affine/shared/src/utils/index.ts index 82b4f350f..f09d95f8e 100644 --- a/blocksuite/affine/shared/src/utils/index.ts +++ b/blocksuite/affine/shared/src/utils/index.ts @@ -1,6 +1,7 @@ export * from './auto-scroll'; export * from './button-popper'; export * from './collapsed'; +export * from './computing'; export * from './dnd'; export * from './dom'; export * from './drag-helper'; diff --git a/blocksuite/affine/widgets/widget-toolbar/src/toolbar.ts b/blocksuite/affine/widgets/widget-toolbar/src/toolbar.ts index d48452b0a..545986180 100644 --- a/blocksuite/affine/widgets/widget-toolbar/src/toolbar.ts +++ b/blocksuite/affine/widgets/widget-toolbar/src/toolbar.ts @@ -147,7 +147,7 @@ export class AffineToolbarWidget extends WidgetComponent { host, std, } = this; - const { flags, elementsMap$, flavour$, message$ } = toolbarRegistry; + const { flags, flavour$, message$, elementsMap$ } = toolbarRegistry; const context = new ToolbarContext(std); // TODO(@fundon): fix toolbar position shaking when the wheel scrolls @@ -392,14 +392,14 @@ export class AffineToolbarWidget extends WidgetComponent { }) ); + // Handles blocks when adding // TODO(@fundon): improve these cases // Waits until the view is created when switching the view mode. // `card view` or `embed view` disposables.add( std.view.viewUpdated.subscribe(record => { - const hasAddedBlock = - record.type === 'block' && record.method === 'add'; - if (!hasAddedBlock) return; + const hasAdded = record.type === 'block' && record.method === 'add'; + if (!hasAdded) return; if (flags.isBlock()) { const blockIds = std.selection @@ -426,16 +426,53 @@ export class AffineToolbarWidget extends WidgetComponent { }) ); + // Handles blocks when updating disposables.add( + // TODO(@fundon): use rxjs' filter std.store.slots.blockUpdated.subscribe(record => { - if ( - flags.isBlock() && - record.type === 'update' && - record.props.key === 'text' - ) { - flags.refresh(Flag.Block); + const hasUpdated = record.type === 'update'; + if (!hasUpdated) return; + + if (flags.isBlock()) { + const blockIds = std.selection + .filter$(BlockSelection) + .peek() + .map(s => s.blockId); + if (blockIds.includes(record.id)) { + batch(() => { + this.setReferenceElementWithBlocks( + blockIds + .map(id => std.view.getBlock(id)) + .filter(block => block !== null) + ); + flags.refresh(Flag.Block); + }); + } return; } + + if (flags.isSurface()) { + flags.refresh(Flag.Surface); + return; + } + }) + ); + + // Handles elemets when updating + disposables.add( + effect(() => { + const surface = context.gfx.surface$.value; + if (!surface) return; + + const subscription = surface.elementUpdated.subscribe(() => { + if (!flags.isSurface()) return; + + flags.refresh(Flag.Surface); + }); + + return () => { + subscription.unsubscribe(); + }; }) ); diff --git a/yarn.lock b/yarn.lock index 52adb771a..6c455411d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2678,9 +2678,11 @@ __metadata: "@lit/context": "npm:^1.1.2" "@preact/signals-core": "npm:^1.8.0" "@toeverything/theme": "npm:^1.1.12" + "@types/lodash-es": "npm:^4.17.12" "@types/mdast": "npm:^4.0.4" "@vanilla-extract/css": "npm:^1.17.0" lit: "npm:^3.2.0" + lodash-es: "npm:^4.17.21" minimatch: "npm:^10.0.1" rxjs: "npm:^7.8.1" zod: "npm:^3.23.8"