From d8567e669abc5c7fe1453464a64ab5c20d572867 Mon Sep 17 00:00:00 2001 From: fundon Date: Wed, 19 Mar 2025 02:24:26 +0000 Subject: [PATCH] refactor(editor): edgeless bookmark toolbar config extension (#10711) --- .../all/src/components/size-dropdown-menu.ts | 1 + blocksuite/affine/all/src/effects.ts | 2 + .../affine/blocks/block-bookmark/package.json | 1 + .../block-bookmark/src/bookmark-spec.ts | 14 +- .../block-bookmark/src/configs/toolbar.ts | 366 +++++++++++++++--- .../blocks/block-bookmark/tsconfig.json | 1 + .../edgeless/components/panel/scale-panel.ts | 3 +- .../src/edgeless/configs/toolbar/bookmark.ts | 11 - .../src/edgeless/configs/toolbar/index.ts | 6 - blocksuite/affine/components/package.json | 3 +- .../card-style-dropdown-menu/dropdown-menu.ts | 19 +- .../src/size-dropdown-menu/dropdown-menu.ts | 183 +++++++++ .../src/size-dropdown-menu/index.ts | 7 + .../src/view-dropdown-menu/dropdown-menu.ts | 10 +- .../extensions/editor-config/toolbar/index.ts | 9 +- tools/utils/src/workspace.gen.ts | 1 + yarn.lock | 1 + 17 files changed, 537 insertions(+), 101 deletions(-) create mode 100644 blocksuite/affine/all/src/components/size-dropdown-menu.ts delete mode 100644 blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/bookmark.ts create mode 100644 blocksuite/affine/components/src/size-dropdown-menu/dropdown-menu.ts create mode 100644 blocksuite/affine/components/src/size-dropdown-menu/index.ts diff --git a/blocksuite/affine/all/src/components/size-dropdown-menu.ts b/blocksuite/affine/all/src/components/size-dropdown-menu.ts new file mode 100644 index 0000000000..a1180509b4 --- /dev/null +++ b/blocksuite/affine/all/src/components/size-dropdown-menu.ts @@ -0,0 +1 @@ +export * from '@blocksuite/affine-components/size-dropdown-menu'; diff --git a/blocksuite/affine/all/src/effects.ts b/blocksuite/affine/all/src/effects.ts index 80dd5b8ed7..d2eec909b6 100644 --- a/blocksuite/affine/all/src/effects.ts +++ b/blocksuite/affine/all/src/effects.ts @@ -32,6 +32,7 @@ import { IconButton } from '@blocksuite/affine-components/icon-button'; import { effects as componentLinkPreviewEffects } from '@blocksuite/affine-components/link-preview'; import { effects as componentLinkedDocTitleEffects } from '@blocksuite/affine-components/linked-doc-title'; import { effects as componentPortalEffects } from '@blocksuite/affine-components/portal'; +import { effects as componentSizeDropdownMenuEffects } from '@blocksuite/affine-components/size-dropdown-menu'; import { SmoothCorner } from '@blocksuite/affine-components/smooth-corner'; import { effects as componentToggleButtonEffects } from '@blocksuite/affine-components/toggle-button'; import { ToggleSwitch } from '@blocksuite/affine-components/toggle-switch'; @@ -147,6 +148,7 @@ export function effects() { componentHighlightDropdownMenuEffects(); componentViewDropdownMenuEffects(); componentTooltipContentWithShortcutEffects(); + componentSizeDropdownMenuEffects(); widgetScrollAnchoringEffects(); widgetFrameTitleEffects(); diff --git a/blocksuite/affine/blocks/block-bookmark/package.json b/blocksuite/affine/blocks/block-bookmark/package.json index d25f63cb9c..a478e62fda 100644 --- a/blocksuite/affine/blocks/block-bookmark/package.json +++ b/blocksuite/affine/blocks/block-bookmark/package.json @@ -11,6 +11,7 @@ "license": "MIT", "dependencies": { "@blocksuite/affine-block-embed": "workspace:*", + "@blocksuite/affine-block-surface": "workspace:*", "@blocksuite/affine-components": "workspace:*", "@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-shared": "workspace:*", diff --git a/blocksuite/affine/blocks/block-bookmark/src/bookmark-spec.ts b/blocksuite/affine/blocks/block-bookmark/src/bookmark-spec.ts index d30f872ecd..c78ac7c3e7 100644 --- a/blocksuite/affine/blocks/block-bookmark/src/bookmark-spec.ts +++ b/blocksuite/affine/blocks/block-bookmark/src/bookmark-spec.ts @@ -1,16 +1,11 @@ import { BookmarkBlockSchema } from '@blocksuite/affine-model'; -import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services'; -import { - BlockFlavourIdentifier, - BlockViewExtension, - FlavourExtension, -} from '@blocksuite/block-std'; +import { BlockViewExtension, FlavourExtension } from '@blocksuite/block-std'; import type { ExtensionType } from '@blocksuite/store'; import { literal } from 'lit/static-html.js'; import { BookmarkBlockAdapterExtensions } from './adapters/extension'; import { BookmarkSlashMenuConfigExtension } from './configs/slash-menu'; -import { builtinToolbarConfig } from './configs/toolbar'; +import { createBuiltinToolbarConfigExtension } from './configs/toolbar'; const flavour = BookmarkBlockSchema.model.flavour; @@ -22,9 +17,6 @@ export const BookmarkBlockSpec: ExtensionType[] = [ : literal`affine-bookmark`; }), BookmarkBlockAdapterExtensions, - ToolbarModuleExtension({ - id: BlockFlavourIdentifier(flavour), - config: builtinToolbarConfig, - }), + createBuiltinToolbarConfigExtension(flavour), BookmarkSlashMenuConfigExtension, ].flat(); diff --git a/blocksuite/affine/blocks/block-bookmark/src/configs/toolbar.ts b/blocksuite/affine/blocks/block-bookmark/src/configs/toolbar.ts index d616b7a23c..1e09bf9b39 100644 --- a/blocksuite/affine/blocks/block-bookmark/src/configs/toolbar.ts +++ b/blocksuite/affine/blocks/block-bookmark/src/configs/toolbar.ts @@ -1,16 +1,29 @@ import { EmbedIframeService } from '@blocksuite/affine-block-embed'; +import { reassociateConnectorsCommand } from '@blocksuite/affine-block-surface'; import { toast } from '@blocksuite/affine-components/toast'; -import { BookmarkBlockModel } from '@blocksuite/affine-model'; +import { + BookmarkBlockModel, + BookmarkStyles, + type EmbedCardStyle, +} from '@blocksuite/affine-model'; +import { + EMBED_CARD_HEIGHT, + EMBED_CARD_WIDTH, +} from '@blocksuite/affine-shared/consts'; import { ActionPlacement, EmbedOptionProvider, FeatureFlagService, + type LinkEventType, type ToolbarAction, type ToolbarActionGroup, + type ToolbarContext, type ToolbarModuleConfig, + ToolbarModuleExtension, } from '@blocksuite/affine-shared/services'; import { getBlockProps } from '@blocksuite/affine-shared/utils'; -import { BlockSelection } from '@blocksuite/block-std'; +import { BlockFlavourIdentifier, BlockSelection } from '@blocksuite/block-std'; +import { Bound } from '@blocksuite/global/gfx'; import { CaptionIcon, CopyIcon, @@ -18,8 +31,8 @@ import { DuplicateIcon, ResetIcon, } from '@blocksuite/icons/lit'; -import { Slice, Text } from '@blocksuite/store'; -import { signal } from '@preact/signals-core'; +import { type ExtensionType, Slice, Text } from '@blocksuite/store'; +import { computed, signal } from '@preact/signals-core'; import { html } from 'lit'; import { keyed } from 'lit/directives/keyed.js'; import * as Y from 'yjs'; @@ -27,26 +40,62 @@ import * as Y from 'yjs'; import { BookmarkBlockComponent } from '../bookmark-block'; const trackBaseProps = { - segment: 'doc', - page: 'doc editor', - module: 'toolbar', category: 'bookmark', type: 'card view', }; -export const builtinToolbarConfig = { +const previewAction = { + id: 'a.preview', + content(ctx) { + const model = ctx.getCurrentModelByType(BookmarkBlockModel); + if (!model) return null; + + const { url } = model.props; + + return html``; + }, +} satisfies ToolbarAction; + +const captionAction = { + id: 'd.caption', + tooltip: 'Caption', + icon: CaptionIcon(), + run(ctx) { + const block = ctx.getCurrentBlockByType(BookmarkBlockComponent); + block?.captionEditor?.show(); + + ctx.track('OpenedCaptionEditor', { + ...trackBaseProps, + control: 'add caption', + }); + }, +} satisfies ToolbarAction; + +const createOnToggleFn = + ( + ctx: ToolbarContext, + name: Extract< + LinkEventType, + | 'OpenedViewSelector' + | 'OpenedCardStyleSelector' + | 'OpenedCardScaleSelector' + >, + control: string + ) => + (e: CustomEvent) => { + e.stopPropagation(); + const opened = e.detail; + if (!opened) return; + + ctx.track(name, { + ...trackBaseProps, + control, + }); + }; + +const builtinToolbarConfig = { actions: [ - { - id: 'a.preview', - content(ctx) { - const model = ctx.getCurrentModelByType(BookmarkBlockModel); - if (!model) return null; - - const { url } = model.props; - - return html``; - }, - }, + previewAction, { id: 'b.conversions', actions: [ @@ -181,15 +230,12 @@ export const builtinToolbarConfig = { if (!model) return null; const actions = this.actions.map(action => ({ ...action })); - const onToggle = (e: CustomEvent) => { - const opened = e.detail; - if (!opened) return; - - ctx.track('OpenedViewSelector', { - ...trackBaseProps, - control: 'switch view', - }); - }; + const viewType$ = signal(actions[1].label); + const onToggle = createOnToggleFn( + ctx, + 'OpenedViewSelector', + 'switch view' + ); return html`${keyed( model, @@ -197,7 +243,7 @@ export const builtinToolbarConfig = { @toggle=${onToggle} .actions=${actions} .context=${ctx} - .viewType$=${signal(actions[1].label)} + .viewType$=${viewType$} >` )}`; }, @@ -230,16 +276,11 @@ export const builtinToolbarConfig = { }); }, })) satisfies ToolbarAction[]; - - const onToggle = (e: CustomEvent) => { - const opened = e.detail; - if (!opened) return; - - ctx.track('OpenedCardStyleSelector', { - ...trackBaseProps, - control: 'switch card style', - }); - }; + const onToggle = createOnToggleFn( + ctx, + 'OpenedCardStyleSelector', + 'switch card style' + ); return html`${keyed( model, @@ -252,20 +293,7 @@ export const builtinToolbarConfig = { )}`; }, } satisfies ToolbarActionGroup, - { - id: 'd.caption', - tooltip: 'Caption', - icon: CaptionIcon(), - run(ctx) { - const block = ctx.getCurrentBlockByType(BookmarkBlockComponent); - block?.captionEditor?.show(); - - ctx.track('OpenedCaptionEditor', { - ...trackBaseProps, - control: 'add caption', - }); - }, - }, + captionAction, { placement: ActionPlacement.More, id: 'a.clipboard', @@ -331,3 +359,235 @@ export const builtinToolbarConfig = { }, ], } as const satisfies ToolbarModuleConfig; + +const builtinSurfaceToolbarConfig = { + actions: [ + previewAction, + { + id: 'b.conversions', + actions: [ + { + id: 'card', + label: 'Card view', + disabled: true, + }, + { + id: 'embed', + label: 'Embed view', + run(ctx) { + const model = ctx.getCurrentModelByType(BookmarkBlockModel); + if (!model) return; + + const { id: oldId, xywh, parent } = model; + const { url, caption } = model.props; + const options = ctx.std + .get(EmbedOptionProvider) + .getEmbedBlockOptions(url); + + if (options?.viewType !== 'embed') return; + + const { flavour, styles } = options; + let { style } = model.props; + + if (!styles.includes(style)) { + style = styles[0]; + } + + const bounds = Bound.deserialize(xywh); + bounds.w = EMBED_CARD_WIDTH[style]; + bounds.h = EMBED_CARD_HEIGHT[style]; + + const newId = ctx.store.addBlock( + flavour, + { url, caption, style, xywh: bounds.serialize() }, + parent + ); + + ctx.command.exec(reassociateConnectorsCommand, { oldId, newId }); + + ctx.store.deleteBlock(model); + + // Selects new block + ctx.gfx.selection.set({ editing: false, elements: [newId] }); + + ctx.track('SelectedView', { + ...trackBaseProps, + control: 'select view', + type: 'embed view', + }); + }, + }, + ], + when(ctx) { + const model = ctx.getCurrentModelByType(BookmarkBlockModel); + if (!model) return false; + + const { url } = model.props; + const options = ctx.std + .get(EmbedOptionProvider) + .getEmbedBlockOptions(url); + + return options?.viewType === 'embed'; + }, + content(ctx) { + const model = ctx.getCurrentModelByType(BookmarkBlockModel); + if (!model) return null; + + const actions = this.actions.map(action => ({ ...action })); + const viewType$ = signal('Card view'); + const onToggle = createOnToggleFn( + ctx, + 'OpenedViewSelector', + 'switch view' + ); + + return html`${keyed( + model, + html`` + )}`; + }, + } satisfies ToolbarActionGroup, + { + id: 'b.style', + actions: [ + { + id: 'horizontal', + label: 'Large horizontal style', + }, + { + id: 'list', + label: 'Small horizontal style', + }, + { + id: 'vertical', + label: 'Large vertical style', + }, + { + id: 'cube', + label: 'Small vertical style', + }, + ].filter(action => BookmarkStyles.includes(action.id as EmbedCardStyle)), + content(ctx) { + const model = ctx.getCurrentModelByType(BookmarkBlockModel); + if (!model) return null; + + const actions = this.actions.map(action => ({ + ...action, + run: ({ store }) => { + const style = action.id as EmbedCardStyle; + const bounds = Bound.deserialize(model.xywh); + bounds.w = EMBED_CARD_WIDTH[style]; + bounds.h = EMBED_CARD_HEIGHT[style]; + const xywh = bounds.serialize(); + + store.updateBlock(model, { style, xywh }); + + ctx.track('SelectedCardStyle', { + ...trackBaseProps, + control: 'select card style', + type: style, + }); + }, + })) satisfies ToolbarAction[]; + const style$ = model.props.style$; + const onToggle = createOnToggleFn( + ctx, + 'OpenedCardStyleSelector', + 'switch card style' + ); + + return html`${keyed( + model, + html`` + )}`; + }, + } satisfies ToolbarActionGroup, + { + ...captionAction, + id: 'c.caption', + }, + { + id: 'd.scale', + content(ctx) { + const model = ctx.getCurrentModelByType(BookmarkBlockModel); + if (!model) return null; + + const scale$ = computed(() => { + const { + xywh$: { value: xywh }, + } = model; + const { + style$: { value: style }, + } = model.props; + const bounds = Bound.deserialize(xywh); + const height = EMBED_CARD_HEIGHT[style]; + return Math.round(100 * (bounds.h / height)); + }); + const onSelect = (e: CustomEvent) => { + e.stopPropagation(); + + const scale = e.detail / 100; + + const bounds = Bound.deserialize(model.xywh); + const style = model.props.style; + bounds.w = EMBED_CARD_WIDTH[style] * scale; + bounds.h = EMBED_CARD_HEIGHT[style] * scale; + const xywh = bounds.serialize(); + + ctx.store.updateBlock(model, { xywh }); + + ctx.track('SelectedCardScale', { + ...trackBaseProps, + control: 'select card scale', + }); + }; + const onToggle = createOnToggleFn( + ctx, + 'OpenedCardScaleSelector', + 'switch card scale' + ); + const format = (value: number) => `${value}%`; + + return html`${keyed( + model, + html`` + )}`; + }, + }, + ], + + when: ctx => ctx.getSurfaceModelsByType(BookmarkBlockModel).length === 1, +} as const satisfies ToolbarModuleConfig; + +export const createBuiltinToolbarConfigExtension = ( + flavour: string +): ExtensionType[] => { + const name = flavour.split(':').pop(); + + return [ + ToolbarModuleExtension({ + id: BlockFlavourIdentifier(flavour), + config: builtinToolbarConfig, + }), + + ToolbarModuleExtension({ + id: BlockFlavourIdentifier(`affine:surface:${name}`), + config: builtinSurfaceToolbarConfig, + }), + ]; +}; diff --git a/blocksuite/affine/blocks/block-bookmark/tsconfig.json b/blocksuite/affine/blocks/block-bookmark/tsconfig.json index c11fe7abc3..e2b13ea526 100644 --- a/blocksuite/affine/blocks/block-bookmark/tsconfig.json +++ b/blocksuite/affine/blocks/block-bookmark/tsconfig.json @@ -8,6 +8,7 @@ "include": ["./src"], "references": [ { "path": "../block-embed" }, + { "path": "../block-surface" }, { "path": "../../components" }, { "path": "../../model" }, { "path": "../../shared" }, diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/components/panel/scale-panel.ts b/blocksuite/affine/blocks/block-root/src/edgeless/components/panel/scale-panel.ts index 186ae8c30b..71c60527d2 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/components/panel/scale-panel.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/components/panel/scale-panel.ts @@ -13,6 +13,7 @@ function format(scale: number) { return `${scale}%`; } +// TODO(@fundon): remove it after refacting is completed export class EdgelessScalePanel extends LitElement { static override styles = css` :host { @@ -28,7 +29,7 @@ export class EdgelessScalePanel extends LitElement { } .scale-input { - display: flx; + display: flex; align-self: stretch; border: 0.5px solid var(--affine-border-color); border-radius: 8px; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/bookmark.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/bookmark.ts deleted file mode 100644 index d38962d349..0000000000 --- a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/bookmark.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services'; - -export const builtinBookmarkToolbarConfig = { - actions: [ - { - id: 'a.test', - label: 'Bookmark', - run() {}, - }, - ], -} as const satisfies ToolbarModuleConfig; 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 07e72b4197..99242da8cb 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 @@ -2,7 +2,6 @@ import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services'; import { BlockFlavourIdentifier } from '@blocksuite/block-std'; import type { ExtensionType } from '@blocksuite/store'; -import { builtinBookmarkToolbarConfig } from './bookmark'; import { builtinBrushToolbarConfig } from './brush'; import { builtinConnectorToolbarConfig } from './connector'; import { builtinEmbedToolbarConfig } from './embed'; @@ -16,11 +15,6 @@ import { builtinShapeToolbarConfig } from './shape'; import { builtinTextToolbarConfig } from './text'; export const EdgelessElementToolbarExtension: ExtensionType[] = [ - ToolbarModuleExtension({ - id: BlockFlavourIdentifier('affine:surface:bookmark'), - config: builtinBookmarkToolbarConfig, - }), - ToolbarModuleExtension({ id: BlockFlavourIdentifier('affine:surface:image'), config: builtinImageToolbarConfig, diff --git a/blocksuite/affine/components/package.json b/blocksuite/affine/components/package.json index da19c78f56..9af98f77ae 100644 --- a/blocksuite/affine/components/package.json +++ b/blocksuite/affine/components/package.json @@ -65,7 +65,8 @@ "./view-dropdown-menu": "./src/view-dropdown-menu/index.ts", "./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" + "./tooltip-content-with-shortcut": "./src/tooltip-content-with-shortcut/index.ts", + "./size-dropdown-menu": "./src/size-dropdown-menu/index.ts" }, "files": [ "src", diff --git a/blocksuite/affine/components/src/card-style-dropdown-menu/dropdown-menu.ts b/blocksuite/affine/components/src/card-style-dropdown-menu/dropdown-menu.ts index 7be5297340..b470e178cd 100644 --- a/blocksuite/affine/components/src/card-style-dropdown-menu/dropdown-menu.ts +++ b/blocksuite/affine/components/src/card-style-dropdown-menu/dropdown-menu.ts @@ -3,11 +3,7 @@ import { type ToolbarAction, ToolbarContext, } from '@blocksuite/affine-shared/services'; -import { - PropTypes, - requiredProperties, - ShadowlessElement, -} from '@blocksuite/block-std'; +import { PropTypes, requiredProperties } from '@blocksuite/block-std'; import { SignalWatcher } from '@blocksuite/global/lit'; import { PaletteIcon } from '@blocksuite/icons/lit'; import { @@ -15,6 +11,7 @@ import { type ReadonlySignal, type Signal, } from '@preact/signals-core'; +import { LitElement } from 'lit'; import { property } from 'lit/decorators.js'; import { html, type TemplateResult } from 'lit-html'; import { ifDefined } from 'lit-html/directives/if-defined.js'; @@ -24,23 +21,29 @@ import { EmbedCardDarkCubeIcon, EmbedCardDarkHorizontalIcon, EmbedCardDarkListIcon, + EmbedCardDarkVerticalIcon, EmbedCardLightCubeIcon, EmbedCardLightHorizontalIcon, EmbedCardLightListIcon, + EmbedCardLightVerticalIcon, } from '../icons'; const cardStyleMap: Record> = { light: { + cube: EmbedCardLightCubeIcon, + cubeThick: EmbedCardLightCubeIcon, horizontal: EmbedCardLightHorizontalIcon, horizontalThin: EmbedCardLightListIcon, list: EmbedCardLightListIcon, - cubeThick: EmbedCardLightCubeIcon, + vertical: EmbedCardLightVerticalIcon, }, dark: { + cube: EmbedCardDarkCubeIcon, + cubeThick: EmbedCardDarkCubeIcon, horizontal: EmbedCardDarkHorizontalIcon, horizontalThin: EmbedCardDarkListIcon, list: EmbedCardDarkListIcon, - cubeThick: EmbedCardDarkCubeIcon, + vertical: EmbedCardDarkVerticalIcon, }, }; @@ -49,7 +52,7 @@ const cardStyleMap: Record> = { context: PropTypes.instanceOf(ToolbarContext), style$: PropTypes.object, }) -export class CardStyleDropdownMenu extends SignalWatcher(ShadowlessElement) { +export class CardStyleDropdownMenu extends SignalWatcher(LitElement) { @property({ attribute: false }) accessor actions!: ToolbarAction[]; diff --git a/blocksuite/affine/components/src/size-dropdown-menu/dropdown-menu.ts b/blocksuite/affine/components/src/size-dropdown-menu/dropdown-menu.ts new file mode 100644 index 0000000000..f1fb34c9e4 --- /dev/null +++ b/blocksuite/affine/components/src/size-dropdown-menu/dropdown-menu.ts @@ -0,0 +1,183 @@ +import { stopPropagation } from '@blocksuite/affine-shared/utils'; +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 { property, query } from 'lit/decorators.js'; +import { repeat } from 'lit-html/directives/repeat.js'; +import { when } from 'lit-html/directives/when.js'; +import clamp from 'lodash-es/clamp'; + +import type { EditorMenuButton } from '../toolbar'; + +type SizeItem = { key?: string | number; value: number }; + +const MIN_SIZE = 0; +const MAX_SIZE = 400; +const SIZE_LIST: SizeItem[] = [ + { value: 50 }, + { value: 100 }, + { value: 200 }, +] as const; + +@requiredProperties({ + size$: PropTypes.object, +}) +export class SizeDropdownMenu extends SignalWatcher(LitElement) { + static override styles = css` + div[data-orientation] { + width: 68px; + gap: 4px; + min-width: unset; + overflow: unset; + } + + editor-menu-action { + justify-content: space-between; + } + + :host([data-type='check']) editor-menu-action[data-selected] { + color: var(--affine-primary-color); + background-color: none; + } + + input { + display: flex; + align-self: stretch; + border: 0.5px solid var(--affine-border-color); + border-radius: 8px; + padding: 4px 8px; + box-sizing: border-box; + } + + input:focus { + outline-color: var(--affine-primary-color); + outline-width: 0.5px; + } + + input::placeholder { + color: var(--affine-placeholder-color); + } + `; + + @property({ attribute: false }) + accessor sizes: readonly SizeItem[] = SIZE_LIST; + + @property({ attribute: false }) + accessor size$!: Signal | ReadonlySignal; + + @property({ attribute: false }) + accessor maxSize: number = MAX_SIZE; + + @property({ attribute: false }) + accessor minSize: number = MIN_SIZE; + + @property({ attribute: false }) + accessor format: ((e: number) => string) | undefined; + + @property({ attribute: 'data-type' }) + accessor type: 'normal' | 'check' = 'normal'; + + clamp(value: number, min = this.minSize, max = this.maxSize) { + return clamp(value, min, max); + } + + select(value: number) { + const detail = this.clamp(value); + this.dispatchEvent(new CustomEvent('select', { detail })); + } + + private readonly _onKeydown = (e: KeyboardEvent) => { + e.stopPropagation(); + + if (e.isComposing) return; + if (e.key !== 'Enter') return; + + e.preventDefault(); + const input = e.target as HTMLInputElement; + const value = parseInt(input.value.trim()); + // Handle edge case where user enters a non-number + if (isNaN(value)) { + input.value = ''; + return; + } + + // Handle edge case when user enters a number that is out of range + this.select(value); + input.value = ''; + + this.menuButton.hide(); + }; + + @query('editor-menu-button') + accessor menuButton!: EditorMenuButton; + + override render() { + const { + sizes, + format, + type, + size$: { value: size }, + } = this; + const isCheckType = type === 'check'; + const placeholder = format?.(Math.trunc(size)) ?? Math.trunc(size); + + return html` + + ${format?.(size) ?? size} + ${ArrowDownSmallIcon()} + + `} + > +
+ ${repeat( + sizes, + ({ key, value }) => key ?? value, + ({ key, value }) => html` + this.select(value)} + > + ${key ?? format?.(value) ?? value} + ${when(isCheckType && size === value, () => DoneIcon())} + + ` + )} + + +
+
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-size-dropdown-menu': SizeDropdownMenu; + } +} diff --git a/blocksuite/affine/components/src/size-dropdown-menu/index.ts b/blocksuite/affine/components/src/size-dropdown-menu/index.ts new file mode 100644 index 0000000000..c98cf1f84d --- /dev/null +++ b/blocksuite/affine/components/src/size-dropdown-menu/index.ts @@ -0,0 +1,7 @@ +import { SizeDropdownMenu } from './dropdown-menu'; + +export * from './dropdown-menu'; + +export function effects() { + customElements.define('affine-size-dropdown-menu', SizeDropdownMenu); +} diff --git a/blocksuite/affine/components/src/view-dropdown-menu/dropdown-menu.ts b/blocksuite/affine/components/src/view-dropdown-menu/dropdown-menu.ts index 38115ee909..e4f8128663 100644 --- a/blocksuite/affine/components/src/view-dropdown-menu/dropdown-menu.ts +++ b/blocksuite/affine/components/src/view-dropdown-menu/dropdown-menu.ts @@ -2,14 +2,11 @@ import { type ToolbarAction, ToolbarContext, } from '@blocksuite/affine-shared/services'; -import { - PropTypes, - requiredProperties, - ShadowlessElement, -} from '@blocksuite/block-std'; +import { PropTypes, requiredProperties } from '@blocksuite/block-std'; import { SignalWatcher } from '@blocksuite/global/lit'; import { ArrowDownSmallIcon } from '@blocksuite/icons/lit'; import type { ReadonlySignal, Signal } from '@preact/signals-core'; +import { LitElement } from 'lit'; import { property } from 'lit/decorators.js'; import { html } from 'lit-html'; import { ifDefined } from 'lit-html/directives/if-defined.js'; @@ -20,7 +17,7 @@ import { repeat } from 'lit-html/directives/repeat.js'; context: PropTypes.instanceOf(ToolbarContext), viewType$: PropTypes.object, }) -export class ViewDropdownMenu extends SignalWatcher(ShadowlessElement) { +export class ViewDropdownMenu extends SignalWatcher(LitElement) { @property({ attribute: false }) accessor actions!: ToolbarAction[]; @@ -43,6 +40,7 @@ export class ViewDropdownMenu extends SignalWatcher(ShadowlessElement) { .button=${html`