mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-26 10:45:57 +08:00
refactor(editor): extract common toolbar config (#11069)
This commit is contained in:
@@ -6,6 +6,11 @@ import {
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import { EditorChevronDown } from '@blocksuite/affine-components/toolbar';
|
||||
import type { ToolbarContext } from '@blocksuite/affine-shared/services';
|
||||
import type {
|
||||
Menu,
|
||||
MenuItem,
|
||||
} from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
import { renderMenuItems } from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
import type { GfxModel } from '@blocksuite/block-std/gfx';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import {
|
||||
@@ -23,9 +28,6 @@ import {
|
||||
import { html } from 'lit';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import type { Menu, MenuItem } from './types';
|
||||
import { renderMenuItems } from './utils';
|
||||
|
||||
enum Alignment {
|
||||
None,
|
||||
AutoArrange,
|
||||
|
||||
@@ -30,6 +30,13 @@ import {
|
||||
getMostCommonResolvedValue,
|
||||
getMostCommonValue,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { MenuItem } from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
import {
|
||||
createTextActions,
|
||||
getRootBlock,
|
||||
LINE_STYLE_LIST,
|
||||
renderMenu,
|
||||
} from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import {
|
||||
AddTextIcon,
|
||||
@@ -51,10 +58,6 @@ import { html } from 'lit';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { mountConnectorLabelEditor } from '../../utils/text';
|
||||
import { LINE_STYLE_LIST } from './consts';
|
||||
import { createTextActions } from './text-common';
|
||||
import type { MenuItem } from './types';
|
||||
import { getRootBlock, renderMenu } from './utils';
|
||||
|
||||
const FRONT_ENDPOINT_STYLE_LIST = [
|
||||
{
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { StyleGeneralIcon, StyleScribbleIcon } from '@blocksuite/icons/lit';
|
||||
|
||||
import type { MenuItem } from './types';
|
||||
|
||||
export const LINE_STYLE_LIST = [
|
||||
{
|
||||
key: 'General',
|
||||
value: false,
|
||||
icon: StyleGeneralIcon(),
|
||||
},
|
||||
{
|
||||
key: 'Scribbled',
|
||||
value: true,
|
||||
icon: StyleScribbleIcon(),
|
||||
},
|
||||
] as const satisfies MenuItem<boolean>[];
|
||||
@@ -1,7 +1,6 @@
|
||||
import { EdgelessTextBlockModel } from '@blocksuite/affine-model';
|
||||
import { type ToolbarModuleConfig } from '@blocksuite/affine-shared/services';
|
||||
|
||||
import { createTextActions } from './text-common';
|
||||
import { createTextActions } from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
|
||||
export const builtinEdgelessTextToolbarConfig = {
|
||||
// No need to adjust element bounds, which updates itself using ResizeObserver
|
||||
|
||||
@@ -9,11 +9,12 @@ import {
|
||||
} from '@blocksuite/affine-model';
|
||||
import { type ToolbarModuleConfig } from '@blocksuite/affine-shared/services';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import { getRootBlock } from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import { EditIcon, PageIcon, UngroupIcon } from '@blocksuite/icons/lit';
|
||||
|
||||
import { EdgelessRootService } from '../../edgeless-root-service';
|
||||
import { mountGroupTitleEditor } from '../../utils/text';
|
||||
import { getEdgelessWith, getRootBlock } from './utils';
|
||||
|
||||
export const builtinGroupToolbarConfig = {
|
||||
actions: [
|
||||
@@ -84,11 +85,10 @@ export const builtinGroupToolbarConfig = {
|
||||
const models = ctx.getSurfaceModelsByType(GroupElementModel);
|
||||
if (!models.length) return;
|
||||
|
||||
const edgeless = getEdgelessWith(ctx);
|
||||
if (!edgeless) return;
|
||||
const edgelessService = ctx.std.get(EdgelessRootService);
|
||||
|
||||
for (const model of models) {
|
||||
edgeless.service.ungroup(model);
|
||||
edgelessService.ungroup(model);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -14,11 +14,12 @@ import type {
|
||||
ToolbarModuleConfig,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { getMostCommonValue } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
type MenuItem,
|
||||
renderMenu,
|
||||
} from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
import { RadiantIcon, RightLayoutIcon, StyleIcon } from '@blocksuite/icons/lit';
|
||||
|
||||
import type { MenuItem } from './types';
|
||||
import { renderMenu } from './utils';
|
||||
|
||||
const MINDMAP_STYLE_LIST = [
|
||||
{
|
||||
value: MindmapStyle.ONE,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import { EdgelessFrameManagerIdentifier } from '@blocksuite/affine-block-frame';
|
||||
import {
|
||||
EdgelessCRUDIdentifier,
|
||||
getSurfaceComponent,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
ConnectorElementModel,
|
||||
DEFAULT_CONNECTOR_MODE,
|
||||
@@ -25,9 +29,9 @@ import {
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { html } from 'lit';
|
||||
|
||||
import { EdgelessRootService } from '../../edgeless-root-service';
|
||||
import { renderAlignmentMenu } from './alignment';
|
||||
import { moreActions } from './more';
|
||||
import { getEdgelessWith } from './utils';
|
||||
|
||||
export const builtinMiscToolbarConfig = {
|
||||
actions: [
|
||||
@@ -88,14 +92,16 @@ export const builtinMiscToolbarConfig = {
|
||||
const models = ctx.getSurfaceModels();
|
||||
if (models.length < 2) return;
|
||||
|
||||
const edgeless = getEdgelessWith(ctx);
|
||||
if (!edgeless) return;
|
||||
const surface = getSurfaceComponent(ctx.std);
|
||||
if (!surface) return;
|
||||
|
||||
const frame = edgeless.service.frame.createFrameOnSelected();
|
||||
const frameManager = ctx.std.get(EdgelessFrameManagerIdentifier);
|
||||
|
||||
const frame = frameManager.createFrameOnSelected();
|
||||
if (!frame) return;
|
||||
|
||||
// TODO(@fundon): should be a command
|
||||
edgeless.surface.fitToViewport(Bound.deserialize(frame.xywh));
|
||||
surface.fitToViewport(Bound.deserialize(frame.xywh));
|
||||
|
||||
ctx.track('CanvasElementAdded', {
|
||||
control: 'context-menu',
|
||||
@@ -131,11 +137,10 @@ export const builtinMiscToolbarConfig = {
|
||||
const models = ctx.getSurfaceModels();
|
||||
if (models.length < 2) return;
|
||||
|
||||
const edgeless = getEdgelessWith(ctx);
|
||||
if (!edgeless) return;
|
||||
const service = ctx.std.get(EdgelessRootService);
|
||||
|
||||
// TODO(@fundon): should be a command
|
||||
edgeless.service.createGroupFromSelected();
|
||||
service.createGroupFromSelected();
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -216,9 +221,6 @@ export const builtinMiscToolbarConfig = {
|
||||
const models = ctx.getSurfaceModels();
|
||||
if (!models.length) return;
|
||||
|
||||
const edgeless = getEdgelessWith(ctx);
|
||||
if (!edgeless) return;
|
||||
|
||||
// get most top selected elements(*) from tree, like in a tree below
|
||||
// G0
|
||||
// / \
|
||||
@@ -266,10 +268,8 @@ export const builtinMiscToolbarConfig = {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupId = edgeless.service.createGroup([
|
||||
topElement,
|
||||
...otherElements,
|
||||
]);
|
||||
const service = ctx.std.get(EdgelessRootService);
|
||||
const groupId = service.createGroup([topElement, ...otherElements]);
|
||||
|
||||
if (groupId) {
|
||||
const element = ctx.std
|
||||
@@ -325,9 +325,6 @@ export const builtinLockedToolbarConfig = {
|
||||
const models = ctx.getSurfaceModels();
|
||||
if (!models.length) return;
|
||||
|
||||
const edgeless = getEdgelessWith(ctx);
|
||||
if (!edgeless) return;
|
||||
|
||||
const elements = new Set(
|
||||
models.map(model =>
|
||||
ctx.matchModel(model.group, MindmapElementModel)
|
||||
@@ -338,9 +335,11 @@ export const builtinLockedToolbarConfig = {
|
||||
|
||||
ctx.store.captureSync();
|
||||
|
||||
const service = ctx.std.get(EdgelessRootService);
|
||||
|
||||
for (const element of elements) {
|
||||
if (element instanceof GroupElementModel) {
|
||||
edgeless.service.ungroup(element);
|
||||
service.ungroup(element);
|
||||
} else {
|
||||
element.lockedBySelf = false;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ import {
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import { EdgelessFrameManagerIdentifier } from '@blocksuite/affine-block-frame';
|
||||
import { ImageBlockComponent } from '@blocksuite/affine-block-image';
|
||||
import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
EdgelessCRUDIdentifier,
|
||||
getSurfaceComponent,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
AttachmentBlockModel,
|
||||
BookmarkBlockModel,
|
||||
@@ -42,6 +45,7 @@ import {
|
||||
ResetIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
|
||||
import { EdgelessRootService } from '../../edgeless-root-service';
|
||||
import { duplicate } from '../../utils/clipboard-utils';
|
||||
import { getSortedCloneElements } from '../../utils/clone-utils';
|
||||
import { moveConnectors } from '../../utils/connector';
|
||||
@@ -67,10 +71,10 @@ export const moreActions = [
|
||||
.createFrameOnSelected();
|
||||
if (!frame) return;
|
||||
|
||||
const edgeless = getEdgelessWith(ctx);
|
||||
if (!edgeless) return;
|
||||
const surface = getSurfaceComponent(ctx.std);
|
||||
if (!surface) return;
|
||||
|
||||
edgeless.surface.fitToViewport(Bound.deserialize(frame.xywh));
|
||||
surface.fitToViewport(Bound.deserialize(frame.xywh));
|
||||
|
||||
ctx.track('CanvasElementAdded', {
|
||||
control: 'context-menu',
|
||||
@@ -88,10 +92,8 @@ export const moreActions = [
|
||||
return !models.some(model => ctx.matchModel(model, FrameBlockModel));
|
||||
},
|
||||
run(ctx) {
|
||||
const edgeless = getEdgelessWith(ctx);
|
||||
if (!edgeless) return;
|
||||
|
||||
edgeless.service.createGroupFromSelected();
|
||||
const service = ctx.std.get(EdgelessRootService);
|
||||
service.createGroupFromSelected();
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -34,18 +34,21 @@ import type {
|
||||
ToolbarModuleConfig,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { getMostCommonValue } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
createTextActions,
|
||||
getRootBlock,
|
||||
LINE_STYLE_LIST,
|
||||
renderMenu,
|
||||
} from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import { AddTextIcon, ShapeIcon } from '@blocksuite/icons/lit';
|
||||
import { html } from 'lit';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
|
||||
import { LINE_STYLE_LIST } from './consts';
|
||||
import {
|
||||
createMindmapLayoutActionMenu,
|
||||
createMindmapStyleActionMenu,
|
||||
} from './mindmap';
|
||||
import { createTextActions } from './text-common';
|
||||
import { getRootBlock, renderMenu } from './utils';
|
||||
|
||||
export const builtinShapeToolbarConfig = {
|
||||
actions: [
|
||||
|
||||
@@ -1,375 +0,0 @@
|
||||
import {
|
||||
EdgelessCRUDIdentifier,
|
||||
TextUtils,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
packColor,
|
||||
type PickColorEvent,
|
||||
} from '@blocksuite/affine-components/color-picker';
|
||||
import { EditorChevronDown } from '@blocksuite/affine-components/toolbar';
|
||||
import {
|
||||
DefaultTheme,
|
||||
FontFamily,
|
||||
FontStyle,
|
||||
FontWeight,
|
||||
resolveColor,
|
||||
type SurfaceTextModelMap,
|
||||
TextAlign,
|
||||
type TextStyleProps,
|
||||
} from '@blocksuite/affine-model';
|
||||
import type {
|
||||
ToolbarActions,
|
||||
ToolbarContext,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
getMostCommonResolvedValue,
|
||||
getMostCommonValue,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { GfxModel } from '@blocksuite/block-std/gfx';
|
||||
import {
|
||||
TextAlignCenterIcon,
|
||||
TextAlignLeftIcon,
|
||||
TextAlignRightIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { signal } from '@preact/signals-core';
|
||||
import { html } from 'lit';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import type { MenuItem } from './types';
|
||||
import { renderCurrentMenuItemWith, renderMenu } from './utils';
|
||||
|
||||
const FONT_WEIGHT_LIST = [
|
||||
{
|
||||
key: 'Light',
|
||||
value: FontWeight.Light,
|
||||
},
|
||||
{
|
||||
key: 'Regular',
|
||||
value: FontWeight.Regular,
|
||||
},
|
||||
{
|
||||
key: 'Semibold',
|
||||
value: FontWeight.SemiBold,
|
||||
},
|
||||
] as const satisfies MenuItem<FontWeight>[];
|
||||
|
||||
const FONT_STYLE_LIST = [
|
||||
{
|
||||
value: FontStyle.Normal,
|
||||
},
|
||||
{
|
||||
key: 'Italic',
|
||||
value: FontStyle.Italic,
|
||||
},
|
||||
] as const satisfies MenuItem<FontStyle>[];
|
||||
|
||||
const FONT_SIZE_LIST = [
|
||||
{ value: 16 },
|
||||
{ value: 24 },
|
||||
{ value: 32 },
|
||||
{ value: 40 },
|
||||
{ value: 64 },
|
||||
{ value: 128 },
|
||||
] as const satisfies MenuItem<number>[];
|
||||
|
||||
const TEXT_ALIGN_LIST = [
|
||||
{
|
||||
key: 'Left',
|
||||
value: TextAlign.Left,
|
||||
icon: TextAlignLeftIcon(),
|
||||
},
|
||||
{
|
||||
key: 'Center',
|
||||
value: TextAlign.Center,
|
||||
icon: TextAlignCenterIcon(),
|
||||
},
|
||||
{
|
||||
key: 'Right',
|
||||
value: TextAlign.Right,
|
||||
icon: TextAlignRightIcon(),
|
||||
},
|
||||
] as const satisfies MenuItem<TextAlign>[];
|
||||
|
||||
export function createTextActions<
|
||||
K extends abstract new (...args: any) => any,
|
||||
T extends keyof SurfaceTextModelMap,
|
||||
>(
|
||||
klass: K,
|
||||
type: T,
|
||||
update: (
|
||||
ctx: ToolbarContext,
|
||||
model: InstanceType<K>,
|
||||
props: Partial<TextStyleProps>
|
||||
) => void = (ctx, model, props) =>
|
||||
ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, props),
|
||||
mapInto: (model: InstanceType<K>) => TextStyleProps = model => model,
|
||||
stash: <P extends keyof TextStyleProps>(
|
||||
model: InstanceType<K>,
|
||||
type: 'stash' | 'pop',
|
||||
field: P
|
||||
) => void = (model, type, field) => model[type](field)
|
||||
) {
|
||||
return [
|
||||
{
|
||||
id: 'a.font',
|
||||
content(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(klass);
|
||||
if (!models.length) return null;
|
||||
const allowed = models.every(model =>
|
||||
isSurfaceTextModel(model, klass, type)
|
||||
);
|
||||
if (!allowed) return null;
|
||||
|
||||
const fontFamily =
|
||||
getMostCommonValue(models.map(mapInto), 'fontFamily') ??
|
||||
FontFamily.Inter;
|
||||
const styleInfo = { fontFamily: TextUtils.wrapFontFamily(fontFamily) };
|
||||
|
||||
const onPick = (fontFamily: FontFamily) => {
|
||||
let fontWeight =
|
||||
getMostCommonValue(models.map(mapInto), 'fontWeight') ??
|
||||
FontWeight.Regular;
|
||||
let fontStyle =
|
||||
getMostCommonValue(models.map(mapInto), 'fontStyle') ??
|
||||
FontStyle.Normal;
|
||||
|
||||
if (!TextUtils.isFontWeightSupported(fontFamily, fontWeight)) {
|
||||
fontWeight = FontWeight.Regular;
|
||||
}
|
||||
if (!TextUtils.isFontStyleSupported(fontFamily, fontStyle)) {
|
||||
fontStyle = FontStyle.Normal;
|
||||
}
|
||||
|
||||
for (const model of models) {
|
||||
update(ctx, model, { fontFamily, fontWeight, fontStyle });
|
||||
}
|
||||
};
|
||||
|
||||
return html`
|
||||
<editor-menu-button
|
||||
.contentPadding="${'8px'}"
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
aria-label="Font"
|
||||
.tooltip="${'Font'}"
|
||||
.justify="${'space-between'}"
|
||||
.iconContainerWidth="${'40px'}"
|
||||
>
|
||||
<span class="label padding0" style=${styleMap(styleInfo)}
|
||||
>Aa</span
|
||||
>
|
||||
${EditorChevronDown}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
>
|
||||
<edgeless-font-family-panel
|
||||
.value=${fontFamily}
|
||||
.onSelect=${onPick}
|
||||
></edgeless-font-family-panel>
|
||||
</editor-menu-button>
|
||||
`;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b.text-color',
|
||||
content(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(klass);
|
||||
if (!models.length) return null;
|
||||
const allowed = models.every(model =>
|
||||
isSurfaceTextModel(model, klass, type)
|
||||
);
|
||||
if (!allowed) return null;
|
||||
|
||||
const enableCustomColor = ctx.features.getFlag('enable_color_picker');
|
||||
const theme = ctx.theme.edgeless$.value;
|
||||
|
||||
const palettes =
|
||||
type === 'shape'
|
||||
? DefaultTheme.ShapeTextColorPalettes
|
||||
: DefaultTheme.Palettes;
|
||||
const defaultColor =
|
||||
type === 'shape'
|
||||
? DefaultTheme.shapeTextColor
|
||||
: DefaultTheme.textColor;
|
||||
|
||||
const field = 'color';
|
||||
const firstModel = models[0];
|
||||
const originalColor = mapInto(firstModel)[field];
|
||||
const color =
|
||||
getMostCommonResolvedValue(models, field, color =>
|
||||
resolveColor(color, theme)
|
||||
) ?? resolveColor(defaultColor, theme);
|
||||
|
||||
const onPick = (e: PickColorEvent) => {
|
||||
if (e.type === 'pick') {
|
||||
const color = e.detail.value;
|
||||
for (const model of models) {
|
||||
const props = packColor(field, color);
|
||||
update(ctx, model, props);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (const model of models) {
|
||||
stash(model, e.type === 'start' ? 'stash' : 'pop', field);
|
||||
}
|
||||
};
|
||||
|
||||
return html`
|
||||
<edgeless-color-picker-button
|
||||
class="text-color"
|
||||
.label="${'Text color'}"
|
||||
.pick=${onPick}
|
||||
.color=${color}
|
||||
.theme=${theme}
|
||||
.isText=${true}
|
||||
.hollowCircle=${true}
|
||||
.originalColor=${originalColor}
|
||||
.palettes=${palettes}
|
||||
.enableCustomColor=${enableCustomColor}
|
||||
>
|
||||
</edgeless-color-picker-button>
|
||||
`;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'c.font-style',
|
||||
content(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(klass);
|
||||
if (!models.length) return null;
|
||||
const allowed = models.every(model =>
|
||||
isSurfaceTextModel(model, klass, type)
|
||||
);
|
||||
if (!allowed) return null;
|
||||
|
||||
const fontFamily =
|
||||
getMostCommonValue(models.map(mapInto), 'fontFamily') ??
|
||||
FontFamily.Inter;
|
||||
const fontWeight =
|
||||
getMostCommonValue(models.map(mapInto), 'fontWeight') ??
|
||||
FontWeight.Regular;
|
||||
const fontStyle =
|
||||
getMostCommonValue(models.map(mapInto), 'fontStyle') ??
|
||||
FontStyle.Normal;
|
||||
const matchFontFaces = TextUtils.getFontFacesByFontFamily(fontFamily);
|
||||
const disabled =
|
||||
matchFontFaces.length === 1 &&
|
||||
matchFontFaces[0].style === fontStyle &&
|
||||
matchFontFaces[0].weight === fontWeight;
|
||||
|
||||
const onPick = (fontWeight: FontWeight, fontStyle: FontStyle) => {
|
||||
for (const model of models) {
|
||||
update(ctx, model, { fontWeight, fontStyle });
|
||||
}
|
||||
};
|
||||
|
||||
return html`
|
||||
<editor-menu-button
|
||||
.contentPadding="${'8px'}"
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
aria-label="Font style"
|
||||
.tooltip="${'Font style'}"
|
||||
.justify="${'space-between'}"
|
||||
.iconContainerWidth="${'90px'}"
|
||||
.disabled=${disabled}
|
||||
>
|
||||
<span class="label ellipsis">
|
||||
${renderCurrentMenuItemWith(
|
||||
FONT_WEIGHT_LIST,
|
||||
fontWeight,
|
||||
'key'
|
||||
)}
|
||||
${renderCurrentMenuItemWith(
|
||||
FONT_STYLE_LIST,
|
||||
fontStyle,
|
||||
'key'
|
||||
)}
|
||||
</span>
|
||||
${EditorChevronDown}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
>
|
||||
<edgeless-font-weight-and-style-panel
|
||||
.fontFamily=${fontFamily}
|
||||
.fontWeight=${fontWeight}
|
||||
.fontStyle=${fontStyle}
|
||||
.onSelect=${onPick}
|
||||
></edgeless-font-weight-and-style-panel>
|
||||
</editor-menu-button>
|
||||
`;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'd.font-size',
|
||||
when: type !== 'edgeless-text',
|
||||
content(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(klass);
|
||||
if (!models.length) return null;
|
||||
const allowed = models.every(model =>
|
||||
isSurfaceTextModel(model, klass, type)
|
||||
);
|
||||
if (!allowed) return null;
|
||||
|
||||
const fontSize$ = signal(
|
||||
Math.trunc(
|
||||
getMostCommonValue(models.map(mapInto), 'fontSize') ??
|
||||
FONT_SIZE_LIST[0].value
|
||||
)
|
||||
);
|
||||
|
||||
const onPick = (e: CustomEvent<number>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const fontSize = e.detail;
|
||||
|
||||
for (const model of models) {
|
||||
update(ctx, model, { fontSize });
|
||||
}
|
||||
};
|
||||
|
||||
return html`<affine-size-dropdown-menu
|
||||
@select=${onPick}
|
||||
.label="${'Font size'}"
|
||||
.sizes=${FONT_SIZE_LIST}
|
||||
.size$=${fontSize$}
|
||||
></affine-size-dropdown-menu>`;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'e.alignment',
|
||||
content(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(klass);
|
||||
if (!models.length) return null;
|
||||
const allowed = models.every(model =>
|
||||
isSurfaceTextModel(model, klass, type)
|
||||
);
|
||||
if (!allowed) return null;
|
||||
|
||||
const textAlign =
|
||||
getMostCommonValue(models.map(mapInto), 'textAlign') ??
|
||||
TextAlign.Left;
|
||||
|
||||
const onPick = (textAlign: TextAlign) => {
|
||||
for (const model of models) {
|
||||
update(ctx, model, { textAlign });
|
||||
}
|
||||
};
|
||||
|
||||
return renderMenu({
|
||||
label: 'Alignment',
|
||||
items: TEXT_ALIGN_LIST,
|
||||
currentValue: textAlign,
|
||||
onPick,
|
||||
});
|
||||
},
|
||||
},
|
||||
] as const satisfies ToolbarActions;
|
||||
}
|
||||
|
||||
function isSurfaceTextModel<
|
||||
K extends abstract new (...args: any) => any,
|
||||
T extends keyof SurfaceTextModelMap,
|
||||
>(model: GfxModel, klass: K, type: T): model is InstanceType<K> {
|
||||
return model instanceof klass || ('type' in model && model.type === type);
|
||||
}
|
||||
@@ -4,10 +4,9 @@ import {
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import { TextElementModel } from '@blocksuite/affine-model';
|
||||
import { type ToolbarModuleConfig } from '@blocksuite/affine-shared/services';
|
||||
import { createTextActions } from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
|
||||
import { createTextActions } from './text-common';
|
||||
|
||||
export const builtinTextToolbarConfig = {
|
||||
actions: createTextActions(TextElementModel, 'text', (ctx, model, props) => {
|
||||
// No need to adjust element bounds
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
export type MenuItem<T> = {
|
||||
key?: string;
|
||||
value: T;
|
||||
icon?: TemplateResult;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export type Menu<T> = {
|
||||
label: string;
|
||||
icon?: TemplateResult;
|
||||
tooltip?: string;
|
||||
items: MenuItem<T>[];
|
||||
currentValue: T;
|
||||
onPick: (value: T) => void;
|
||||
};
|
||||
@@ -1,69 +1,6 @@
|
||||
import { EditorChevronDown } from '@blocksuite/affine-components/toolbar';
|
||||
import type { ToolbarContext } from '@blocksuite/affine-shared/services';
|
||||
import type { BlockComponent } from '@blocksuite/block-std';
|
||||
import { html } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import { EdgelessRootBlockComponent } from '../..';
|
||||
import type { Menu, MenuItem } from './types';
|
||||
|
||||
export function renderCurrentMenuItemWith<T, F extends keyof MenuItem<T>>(
|
||||
items: MenuItem<T>[],
|
||||
currentValue: T,
|
||||
field: F
|
||||
) {
|
||||
return items.find(({ value }) => value === currentValue)?.[field];
|
||||
}
|
||||
|
||||
export function renderMenu<T>({
|
||||
label,
|
||||
tooltip,
|
||||
icon,
|
||||
items,
|
||||
currentValue,
|
||||
onPick,
|
||||
}: Menu<T>) {
|
||||
return html`
|
||||
<editor-menu-button
|
||||
aria-label="${`${label.toLowerCase()}-menu`}"
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
aria-label="${label}"
|
||||
.tooltip="${tooltip ?? label}"
|
||||
>
|
||||
${icon ?? renderCurrentMenuItemWith(items, currentValue, 'icon')}
|
||||
${EditorChevronDown}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
>
|
||||
${renderMenuItems(items, currentValue, onPick)}
|
||||
</editor-menu-button>
|
||||
`;
|
||||
}
|
||||
|
||||
export function renderMenuItems<T>(
|
||||
items: MenuItem<T>[],
|
||||
currentValue: T,
|
||||
onPick: (value: T) => void
|
||||
) {
|
||||
return repeat(
|
||||
items,
|
||||
item => item.value,
|
||||
({ key, value, icon, disabled }) => html`
|
||||
<editor-icon-button
|
||||
aria-label="${ifDefined(key)}"
|
||||
.disabled=${ifDefined(disabled)}
|
||||
.tooltip="${ifDefined(key)}"
|
||||
.active="${currentValue === value}"
|
||||
.activeMode="${'background'}"
|
||||
@click=${() => onPick(value)}
|
||||
>
|
||||
${icon}
|
||||
</editor-icon-button>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
// TODO(@fundon): it should be simple
|
||||
export function getEdgelessWith(ctx: ToolbarContext) {
|
||||
@@ -78,10 +15,3 @@ export function getEdgelessWith(ctx: ToolbarContext) {
|
||||
|
||||
return edgeless;
|
||||
}
|
||||
|
||||
export function getRootBlock(ctx: ToolbarContext): BlockComponent | null {
|
||||
const rootModel = ctx.store.root;
|
||||
if (!rootModel) return null;
|
||||
|
||||
return ctx.view.getBlock(rootModel.id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user