mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-18 18:41:52 +08:00
refactor(editor): edgeless text toolbar config extension (#10811)
This commit is contained in:
@@ -63,14 +63,14 @@ export const builtinBrushToolbarConfig = {
|
|||||||
.getFlag('enable_color_picker');
|
.getFlag('enable_color_picker');
|
||||||
const theme = ctx.themeProvider.edgelessTheme;
|
const theme = ctx.themeProvider.edgelessTheme;
|
||||||
|
|
||||||
|
const field = 'color';
|
||||||
const firstModel = models[0];
|
const firstModel = models[0];
|
||||||
|
const originalColor = firstModel[field];
|
||||||
const color =
|
const color =
|
||||||
getMostCommonResolvedValue(models, 'color', color =>
|
getMostCommonResolvedValue(models, field, color =>
|
||||||
resolveColor(color, theme)
|
resolveColor(color, theme)
|
||||||
) ?? resolveColor(DefaultTheme.black, theme);
|
) ?? resolveColor(DefaultTheme.black, theme);
|
||||||
const onPick = (e: PickColorEvent) => {
|
const onPick = (e: PickColorEvent) => {
|
||||||
const field = 'color';
|
|
||||||
|
|
||||||
if (e.type === 'pick') {
|
if (e.type === 'pick') {
|
||||||
const color = e.detail.value;
|
const color = e.detail.value;
|
||||||
for (const model of models) {
|
for (const model of models) {
|
||||||
@@ -94,7 +94,7 @@ export const builtinBrushToolbarConfig = {
|
|||||||
.pick=${onPick}
|
.pick=${onPick}
|
||||||
.color=${color}
|
.color=${color}
|
||||||
.theme=${theme}
|
.theme=${theme}
|
||||||
.originalColor=${firstModel.color}
|
.originalColor=${originalColor}
|
||||||
.enableCustomColor=${enableCustomColor}
|
.enableCustomColor=${enableCustomColor}
|
||||||
>
|
>
|
||||||
</edgeless-color-picker-button>
|
</edgeless-color-picker-button>
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface';
|
import {
|
||||||
|
ConnectorUtils,
|
||||||
|
EdgelessCRUDIdentifier,
|
||||||
|
TextUtils,
|
||||||
|
} from '@blocksuite/affine-block-surface';
|
||||||
import {
|
import {
|
||||||
packColor,
|
packColor,
|
||||||
type PickColorEvent,
|
type PickColorEvent,
|
||||||
@@ -20,12 +24,14 @@ import {
|
|||||||
import {
|
import {
|
||||||
FeatureFlagService,
|
FeatureFlagService,
|
||||||
type ToolbarContext,
|
type ToolbarContext,
|
||||||
|
type ToolbarGenericAction,
|
||||||
type ToolbarModuleConfig,
|
type ToolbarModuleConfig,
|
||||||
} from '@blocksuite/affine-shared/services';
|
} from '@blocksuite/affine-shared/services';
|
||||||
import {
|
import {
|
||||||
getMostCommonResolvedValue,
|
getMostCommonResolvedValue,
|
||||||
getMostCommonValue,
|
getMostCommonValue,
|
||||||
} from '@blocksuite/affine-shared/utils';
|
} from '@blocksuite/affine-shared/utils';
|
||||||
|
import { Bound } from '@blocksuite/global/gfx';
|
||||||
import {
|
import {
|
||||||
AddTextIcon,
|
AddTextIcon,
|
||||||
ConnectorCIcon,
|
ConnectorCIcon,
|
||||||
@@ -48,6 +54,7 @@ import { styleMap } from 'lit/directives/style-map.js';
|
|||||||
import { EdgelessRootBlockComponent } from '../..';
|
import { EdgelessRootBlockComponent } from '../..';
|
||||||
import { mountConnectorLabelEditor } from '../../utils/text';
|
import { mountConnectorLabelEditor } from '../../utils/text';
|
||||||
import { LINE_STYLE_LIST } from './consts';
|
import { LINE_STYLE_LIST } from './consts';
|
||||||
|
import { createTextActions } from './text-common';
|
||||||
import type { MenuItem } from './types';
|
import type { MenuItem } from './types';
|
||||||
import { renderMenu } from './utils';
|
import { renderMenu } from './utils';
|
||||||
|
|
||||||
@@ -129,19 +136,18 @@ export const builtinConnectorToolbarConfig = {
|
|||||||
.getFlag('enable_color_picker');
|
.getFlag('enable_color_picker');
|
||||||
const theme = ctx.themeProvider.edgelessTheme;
|
const theme = ctx.themeProvider.edgelessTheme;
|
||||||
|
|
||||||
|
const field = 'stroke';
|
||||||
const firstModel = models[0];
|
const firstModel = models[0];
|
||||||
const strokeWidth =
|
const strokeWidth =
|
||||||
getMostCommonValue(models, 'strokeWidth') ?? LineWidth.Four;
|
getMostCommonValue(models, 'strokeWidth') ?? LineWidth.Four;
|
||||||
const strokeStyle =
|
const strokeStyle =
|
||||||
getMostCommonValue(models, 'strokeStyle') ?? StrokeStyle.Solid;
|
getMostCommonValue(models, 'strokeStyle') ?? StrokeStyle.Solid;
|
||||||
const stroke =
|
const stroke =
|
||||||
getMostCommonResolvedValue(models, 'stroke', stroke =>
|
getMostCommonResolvedValue(models, field, stroke =>
|
||||||
resolveColor(stroke, theme)
|
resolveColor(stroke, theme)
|
||||||
) ?? resolveColor(DefaultTheme.connectorColor, theme);
|
) ?? resolveColor(DefaultTheme.connectorColor, theme);
|
||||||
|
|
||||||
const onPickColor = (e: PickColorEvent) => {
|
const onPickColor = (e: PickColorEvent) => {
|
||||||
const field = 'stroke';
|
|
||||||
|
|
||||||
if (e.type === 'pick') {
|
if (e.type === 'pick') {
|
||||||
const color = e.detail.value;
|
const color = e.detail.value;
|
||||||
for (const model of models) {
|
for (const model of models) {
|
||||||
@@ -341,14 +347,60 @@ export const builtinConnectorToolbarConfig = {
|
|||||||
mountConnectorLabelEditor(model, edgeless);
|
mountConnectorLabelEditor(model, edgeless);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
// id: `g.text`
|
||||||
id: 'g.text',
|
...createTextActions(
|
||||||
|
ConnectorElementModel,
|
||||||
|
'connector',
|
||||||
|
(ctx, model, props) => {
|
||||||
|
if (!ConnectorUtils.isConnectorWithLabel(model)) return;
|
||||||
|
|
||||||
|
const labelStyle = { ...model.labelStyle, ...props };
|
||||||
|
|
||||||
|
// No need to adjust element bounds
|
||||||
|
if (props['textAlign']) {
|
||||||
|
ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, {
|
||||||
|
labelStyle,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { fontFamily, fontStyle, fontSize, fontWeight } = labelStyle;
|
||||||
|
const {
|
||||||
|
text,
|
||||||
|
labelXYWH,
|
||||||
|
labelConstraints: { hasMaxWidth, maxWidth },
|
||||||
|
} = model;
|
||||||
|
const prevBounds = Bound.fromXYWH(labelXYWH || [0, 0, 16, 16]);
|
||||||
|
const center = prevBounds.center;
|
||||||
|
const bounds = TextUtils.normalizeTextBound(
|
||||||
|
{
|
||||||
|
yText: text!,
|
||||||
|
fontFamily,
|
||||||
|
fontStyle,
|
||||||
|
fontSize,
|
||||||
|
fontWeight,
|
||||||
|
hasMaxWidth,
|
||||||
|
maxWidth,
|
||||||
|
},
|
||||||
|
prevBounds
|
||||||
|
);
|
||||||
|
bounds.center = center;
|
||||||
|
|
||||||
|
ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, {
|
||||||
|
labelStyle,
|
||||||
|
labelXYWH: bounds.toXYWH(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
model => model.labelStyle,
|
||||||
|
(model, type, _) => model[type]('labelStyle')
|
||||||
|
).map<ToolbarGenericAction>(action => ({
|
||||||
|
...action,
|
||||||
|
id: `g.text-${action.id}`,
|
||||||
when(ctx) {
|
when(ctx) {
|
||||||
const models = ctx.getSurfaceModelsByType(ConnectorElementModel);
|
const models = ctx.getSurfaceModelsByType(ConnectorElementModel);
|
||||||
return models.length > 0 && !models.some(model => !model.text);
|
return models.length > 0 && models.every(model => model.hasLabel());
|
||||||
},
|
},
|
||||||
// TODO(@fundon): text actoins
|
})),
|
||||||
},
|
|
||||||
],
|
],
|
||||||
|
|
||||||
when: ctx => ctx.getSurfaceModelsByType(ConnectorElementModel).length > 0,
|
when: ctx => ctx.getSurfaceModelsByType(ConnectorElementModel).length > 0,
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { EdgelessTextBlockModel } from '@blocksuite/affine-model';
|
||||||
|
import { type ToolbarModuleConfig } from '@blocksuite/affine-shared/services';
|
||||||
|
|
||||||
|
import { createTextActions } from './text-common';
|
||||||
|
|
||||||
|
export const builtinEdgelessTextToolbarConfig = {
|
||||||
|
// No need to adjust element bounds, which updates itself using ResizeObserver
|
||||||
|
actions: createTextActions(EdgelessTextBlockModel, 'edgeless-text'),
|
||||||
|
|
||||||
|
when: ctx => ctx.getSurfaceModelsByType(EdgelessTextBlockModel).length > 0,
|
||||||
|
} as const satisfies ToolbarModuleConfig;
|
||||||
@@ -139,22 +139,20 @@ const builtinSurfaceToolbarConfig = {
|
|||||||
const models = ctx.getSurfaceModelsByType(FrameBlockModel);
|
const models = ctx.getSurfaceModelsByType(FrameBlockModel);
|
||||||
if (!models.length) return null;
|
if (!models.length) return null;
|
||||||
|
|
||||||
|
const theme = ctx.themeProvider.edgelessTheme;
|
||||||
const enableCustomColor = ctx.std
|
const enableCustomColor = ctx.std
|
||||||
.get(FeatureFlagService)
|
.get(FeatureFlagService)
|
||||||
.getFlag('enable_color_picker');
|
.getFlag('enable_color_picker');
|
||||||
const theme = ctx.themeProvider.edgelessTheme;
|
|
||||||
|
|
||||||
|
const field = 'background';
|
||||||
const firstModel = models[0];
|
const firstModel = models[0];
|
||||||
const background =
|
const background =
|
||||||
getMostCommonResolvedValue(
|
getMostCommonResolvedValue(
|
||||||
models.map(model => model.props),
|
models.map(model => model.props),
|
||||||
'background',
|
field,
|
||||||
background => resolveColor(background, theme)
|
background => resolveColor(background, theme)
|
||||||
) ?? DefaultTheme.transparent;
|
) ?? DefaultTheme.transparent;
|
||||||
|
|
||||||
const onPick = (e: PickColorEvent) => {
|
const onPick = (e: PickColorEvent) => {
|
||||||
const field = 'background';
|
|
||||||
|
|
||||||
if (e.type === 'pick') {
|
if (e.type === 'pick') {
|
||||||
const color = e.detail.value;
|
const color = e.detail.value;
|
||||||
for (const model of models) {
|
for (const model of models) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { ExtensionType } from '@blocksuite/store';
|
|||||||
|
|
||||||
import { builtinBrushToolbarConfig } from './brush';
|
import { builtinBrushToolbarConfig } from './brush';
|
||||||
import { builtinConnectorToolbarConfig } from './connector';
|
import { builtinConnectorToolbarConfig } from './connector';
|
||||||
|
import { builtinEdgelessTextToolbarConfig } from './edgeless-text';
|
||||||
import { createFrameToolbarConfig } from './frame';
|
import { createFrameToolbarConfig } from './frame';
|
||||||
import { builtinGroupToolbarConfig } from './group';
|
import { builtinGroupToolbarConfig } from './group';
|
||||||
import { builtinMindmapToolbarConfig } from './mindmap';
|
import { builtinMindmapToolbarConfig } from './mindmap';
|
||||||
@@ -36,13 +37,18 @@ export const EdgelessElementToolbarExtension: ExtensionType[] = [
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
ToolbarModuleExtension({
|
ToolbarModuleExtension({
|
||||||
id: BlockFlavourIdentifier('affine:surface:shape'),
|
id: BlockFlavourIdentifier('affine:surface:text'),
|
||||||
config: builtinShapeToolbarConfig,
|
config: builtinTextToolbarConfig,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
ToolbarModuleExtension({
|
ToolbarModuleExtension({
|
||||||
id: BlockFlavourIdentifier('affine:surface:text'),
|
id: BlockFlavourIdentifier('affine:surface:edgeless-text'),
|
||||||
config: builtinTextToolbarConfig,
|
config: builtinEdgelessTextToolbarConfig,
|
||||||
|
}),
|
||||||
|
|
||||||
|
ToolbarModuleExtension({
|
||||||
|
id: BlockFlavourIdentifier('affine:surface:shape'),
|
||||||
|
config: builtinShapeToolbarConfig,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
ToolbarModuleExtension({
|
ToolbarModuleExtension({
|
||||||
|
|||||||
@@ -0,0 +1,377 @@
|
|||||||
|
import {
|
||||||
|
EdgelessCRUDIdentifier,
|
||||||
|
TextUtils,
|
||||||
|
} from '@blocksuite/affine-block-surface';
|
||||||
|
import {
|
||||||
|
packColor,
|
||||||
|
type PickColorEvent,
|
||||||
|
} from '@blocksuite/affine-components/color-picker';
|
||||||
|
import {
|
||||||
|
DefaultTheme,
|
||||||
|
FontFamily,
|
||||||
|
FontStyle,
|
||||||
|
FontWeight,
|
||||||
|
resolveColor,
|
||||||
|
type SurfaceTextModelMap,
|
||||||
|
TextAlign,
|
||||||
|
type TextStyleProps,
|
||||||
|
} from '@blocksuite/affine-model';
|
||||||
|
import {
|
||||||
|
FeatureFlagService,
|
||||||
|
type ToolbarActions,
|
||||||
|
type ToolbarContext,
|
||||||
|
} from '@blocksuite/affine-shared/services';
|
||||||
|
import {
|
||||||
|
getMostCommonResolvedValue,
|
||||||
|
getMostCommonValue,
|
||||||
|
} from '@blocksuite/affine-shared/utils';
|
||||||
|
import type { GfxModel } from '@blocksuite/block-std/gfx';
|
||||||
|
import {
|
||||||
|
ArrowDownSmallIcon,
|
||||||
|
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
|
||||||
|
>${ArrowDownSmallIcon()}
|
||||||
|
</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.std
|
||||||
|
.get(FeatureFlagService)
|
||||||
|
.getFlag('enable_color_picker');
|
||||||
|
const theme = ctx.themeProvider.edgelessTheme;
|
||||||
|
|
||||||
|
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>
|
||||||
|
${ArrowDownSmallIcon()}
|
||||||
|
</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);
|
||||||
|
}
|
||||||
@@ -1,11 +1,49 @@
|
|||||||
import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services';
|
import {
|
||||||
|
EdgelessCRUDIdentifier,
|
||||||
|
TextUtils,
|
||||||
|
} from '@blocksuite/affine-block-surface';
|
||||||
|
import { TextElementModel } from '@blocksuite/affine-model';
|
||||||
|
import { type ToolbarModuleConfig } from '@blocksuite/affine-shared/services';
|
||||||
|
import { Bound } from '@blocksuite/global/gfx';
|
||||||
|
|
||||||
|
import { createTextActions } from './text-common';
|
||||||
|
|
||||||
export const builtinTextToolbarConfig = {
|
export const builtinTextToolbarConfig = {
|
||||||
actions: [
|
actions: createTextActions(TextElementModel, 'text', (ctx, model, props) => {
|
||||||
{
|
// No need to adjust element bounds
|
||||||
id: 'a.test',
|
if (props['textAlign']) {
|
||||||
label: 'Text',
|
ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, props);
|
||||||
run() {},
|
return;
|
||||||
},
|
}
|
||||||
],
|
|
||||||
|
const { text: yText, hasMaxWidth } = model;
|
||||||
|
const textStyle = {
|
||||||
|
fontFamily: model.fontFamily,
|
||||||
|
fontStyle: model.fontStyle,
|
||||||
|
fontSize: model.fontSize,
|
||||||
|
fontWeight: model.fontWeight,
|
||||||
|
...props,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { fontFamily, fontStyle, fontSize, fontWeight } = textStyle;
|
||||||
|
|
||||||
|
const bounds = TextUtils.normalizeTextBound(
|
||||||
|
{
|
||||||
|
yText,
|
||||||
|
fontFamily,
|
||||||
|
fontStyle,
|
||||||
|
fontSize,
|
||||||
|
fontWeight,
|
||||||
|
hasMaxWidth,
|
||||||
|
},
|
||||||
|
Bound.fromXYWH(model.deserializedXYWH)
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, {
|
||||||
|
...textStyle,
|
||||||
|
xywh: bounds.serialize(),
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
when: ctx => ctx.getSurfaceModelsByType(TextElementModel).length > 0,
|
||||||
} as const satisfies ToolbarModuleConfig;
|
} as const satisfies ToolbarModuleConfig;
|
||||||
|
|||||||
@@ -137,16 +137,18 @@ abstract class ToolbarContextBase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSurfaceModels() {
|
||||||
|
if (this.hasSelectedSurfaceModels) {
|
||||||
|
const elements = this.elementsMap$.peek().get(this.flavour$.peek());
|
||||||
|
return elements ?? [];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
getSurfaceModelsByType<T extends abstract new (...args: any) => any>(
|
getSurfaceModelsByType<T extends abstract new (...args: any) => any>(
|
||||||
klass: T
|
klass: T
|
||||||
) {
|
) {
|
||||||
if (this.hasSelectedSurfaceModels) {
|
return this.getSurfaceModels().filter(e => this.matchModel(e, klass));
|
||||||
const elements = this.elementsMap$.peek().get(this.flavour$.peek());
|
|
||||||
if (elements?.length) {
|
|
||||||
return elements.filter(e => this.matchModel(e, klass));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSurfaceBlocksByType<T extends abstract new (...args: any) => any>(
|
getSurfaceBlocksByType<T extends abstract new (...args: any) => any>(
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ export function getMostCommonValue<T, F extends keyof T>(
|
|||||||
return record?.[field];
|
return record?.[field];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMostCommonResolvedValue<
|
export function getMostCommonResolvedValue<T, F extends keyof T, U>(
|
||||||
T,
|
records: T[],
|
||||||
F extends Exclude<keyof T, symbol>,
|
field: F,
|
||||||
U,
|
resolve: (value: T[F]) => U
|
||||||
>(records: T[], field: F, resolve: (value: T[F]) => U) {
|
) {
|
||||||
return getMostCommonValue(
|
return getMostCommonValue(
|
||||||
records.map(record => ({ [field]: resolve(record[field]) })),
|
records.map(record => ({ [field]: resolve(record[field]) })),
|
||||||
field
|
String(field)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user