mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-20 19:46:32 +08:00
a5641ae608
Closes: [BS-1475](https://linear.app/affine-design/issue/BS-1475/颜色主题更新) [BS-1803](https://linear.app/affine-design/issue/BS-1803/fill-color色板影响的yuan素) [BS-1804](https://linear.app/affine-design/issue/BS-1804/border-color色板影响的yuan素) [BS-1815](https://linear.app/affine-design/issue/BS-1815/连线文字配色略瞎) ### What's Changed * refactor `EdgelessLineWidthPanel` component, the previous width is fixed and cannot be used in the new design * refactor `EdgelessColorPanel` and `EdgelessColorButton` components, make them simple and reusable * delete redundant `EdgelessOneRowColorPanel` component * unity and update color palette, if the previously set color is not in the latest color palette, the custom color button will be selected
512 lines
16 KiB
TypeScript
512 lines
16 KiB
TypeScript
import {
|
|
ConnectorUtils,
|
|
EdgelessCRUDIdentifier,
|
|
normalizeShapeBound,
|
|
TextUtils,
|
|
} from '@blocksuite/affine-block-surface';
|
|
import {
|
|
SmallArrowDownIcon,
|
|
TextAlignCenterIcon,
|
|
TextAlignLeftIcon,
|
|
TextAlignRightIcon,
|
|
} from '@blocksuite/affine-components/icons';
|
|
import { renderToolbarSeparator } from '@blocksuite/affine-components/toolbar';
|
|
import {
|
|
type ColorScheme,
|
|
ConnectorElementModel,
|
|
EdgelessTextBlockModel,
|
|
FontFamily,
|
|
FontStyle,
|
|
FontWeight,
|
|
PALETTES,
|
|
ShapeElementModel,
|
|
TextAlign,
|
|
TextElementModel,
|
|
type TextStyleProps,
|
|
} from '@blocksuite/affine-model';
|
|
import {
|
|
Bound,
|
|
countBy,
|
|
maxBy,
|
|
WithDisposable,
|
|
} from '@blocksuite/global/utils';
|
|
import { css, html, LitElement, nothing, type TemplateResult } from 'lit';
|
|
import { property, query } from 'lit/decorators.js';
|
|
import { choose } from 'lit/directives/choose.js';
|
|
import { join } from 'lit/directives/join.js';
|
|
import { when } from 'lit/directives/when.js';
|
|
|
|
import type {
|
|
EdgelessColorPickerButton,
|
|
PickColorEvent,
|
|
} from '../../edgeless/components/color-picker/index.js';
|
|
import {
|
|
packColor,
|
|
packColorsWithColorScheme,
|
|
} from '../../edgeless/components/color-picker/utils.js';
|
|
import {
|
|
type ColorEvent,
|
|
GET_DEFAULT_LINE_COLOR,
|
|
} from '../../edgeless/components/panel/color-panel.js';
|
|
import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js';
|
|
|
|
const FONT_SIZE_LIST = [
|
|
{ value: 16 },
|
|
{ value: 24 },
|
|
{ value: 32 },
|
|
{ value: 40 },
|
|
{ value: 64 },
|
|
{ value: 128 },
|
|
] as const;
|
|
|
|
const FONT_WEIGHT_CHOOSE: [FontWeight, () => string][] = [
|
|
[FontWeight.Light, () => 'Light'],
|
|
[FontWeight.Regular, () => 'Regular'],
|
|
[FontWeight.SemiBold, () => 'Semibold'],
|
|
] as const;
|
|
|
|
const FONT_STYLE_CHOOSE: [FontStyle, () => string | typeof nothing][] = [
|
|
[FontStyle.Normal, () => nothing],
|
|
[FontStyle.Italic, () => 'Italic'],
|
|
] as const;
|
|
|
|
const TEXT_ALIGN_CHOOSE: [TextAlign, () => TemplateResult<1>][] = [
|
|
[TextAlign.Left, () => TextAlignLeftIcon],
|
|
[TextAlign.Center, () => TextAlignCenterIcon],
|
|
[TextAlign.Right, () => TextAlignRightIcon],
|
|
] as const;
|
|
|
|
function countByField<K extends keyof Omit<TextStyleProps, 'color'>>(
|
|
elements: BlockSuite.EdgelessTextModelType[],
|
|
field: K
|
|
) {
|
|
return countBy(elements, element => extractField(element, field));
|
|
}
|
|
|
|
function extractField<K extends keyof Omit<TextStyleProps, 'color'>>(
|
|
element: BlockSuite.EdgelessTextModelType,
|
|
field: K
|
|
) {
|
|
//TODO: It's not a very good handling method.
|
|
// The edgeless-change-text-menu should be refactored into a widget to allow external registration of its own logic.
|
|
if (element instanceof EdgelessTextBlockModel) {
|
|
return field === 'fontSize'
|
|
? null
|
|
: (element[field as keyof EdgelessTextBlockModel] as TextStyleProps[K]);
|
|
}
|
|
return (
|
|
element instanceof ConnectorElementModel
|
|
? element.labelStyle[field]
|
|
: element[field]
|
|
) as TextStyleProps[K];
|
|
}
|
|
|
|
function getMostCommonValue<K extends keyof Omit<TextStyleProps, 'color'>>(
|
|
elements: BlockSuite.EdgelessTextModelType[],
|
|
field: K
|
|
) {
|
|
const values = countByField(elements, field);
|
|
return maxBy(Object.entries(values), ([_k, count]) => count);
|
|
}
|
|
|
|
function getMostCommonAlign(elements: BlockSuite.EdgelessTextModelType[]) {
|
|
const max = getMostCommonValue(elements, 'textAlign');
|
|
return max ? (max[0] as TextAlign) : TextAlign.Left;
|
|
}
|
|
|
|
function getMostCommonColor(
|
|
elements: BlockSuite.EdgelessTextModelType[],
|
|
colorScheme: ColorScheme
|
|
): string {
|
|
const colors = countBy(elements, (ele: BlockSuite.EdgelessTextModelType) => {
|
|
const color =
|
|
ele instanceof ConnectorElementModel ? ele.labelStyle.color : ele.color;
|
|
return typeof color === 'object'
|
|
? (color[colorScheme] ?? color.normal ?? null)
|
|
: color;
|
|
});
|
|
const max = maxBy(Object.entries(colors), ([_k, count]) => count);
|
|
return max ? (max[0] as string) : GET_DEFAULT_LINE_COLOR(colorScheme);
|
|
}
|
|
|
|
function getMostCommonFontFamily(elements: BlockSuite.EdgelessTextModelType[]) {
|
|
const max = getMostCommonValue(elements, 'fontFamily');
|
|
return max ? (max[0] as FontFamily) : FontFamily.Inter;
|
|
}
|
|
|
|
function getMostCommonFontSize(elements: BlockSuite.EdgelessTextModelType[]) {
|
|
const max = getMostCommonValue(elements, 'fontSize');
|
|
return max ? Number(max[0]) : FONT_SIZE_LIST[0].value;
|
|
}
|
|
|
|
function getMostCommonFontStyle(elements: BlockSuite.EdgelessTextModelType[]) {
|
|
const max = getMostCommonValue(elements, 'fontStyle');
|
|
return max ? (max[0] as FontStyle) : FontStyle.Normal;
|
|
}
|
|
|
|
function getMostCommonFontWeight(elements: BlockSuite.EdgelessTextModelType[]) {
|
|
const max = getMostCommonValue(elements, 'fontWeight');
|
|
return max ? (max[0] as FontWeight) : FontWeight.Regular;
|
|
}
|
|
|
|
function buildProps(
|
|
element: BlockSuite.EdgelessTextModelType,
|
|
props: { [K in keyof TextStyleProps]?: TextStyleProps[K] }
|
|
) {
|
|
if (element instanceof ConnectorElementModel) {
|
|
return {
|
|
labelStyle: {
|
|
...element.labelStyle,
|
|
...props,
|
|
},
|
|
};
|
|
}
|
|
|
|
return { ...props };
|
|
}
|
|
|
|
export class EdgelessChangeTextMenu extends WithDisposable(LitElement) {
|
|
static override styles = css`
|
|
:host {
|
|
display: inherit;
|
|
align-items: inherit;
|
|
justify-content: inherit;
|
|
gap: inherit;
|
|
height: 100%;
|
|
}
|
|
`;
|
|
|
|
get crud() {
|
|
return this.edgeless.std.get(EdgelessCRUDIdentifier);
|
|
}
|
|
|
|
private readonly _setFontFamily = (fontFamily: FontFamily) => {
|
|
const currentFontWeight = getMostCommonFontWeight(this.elements);
|
|
const fontWeight = TextUtils.isFontWeightSupported(
|
|
fontFamily,
|
|
currentFontWeight
|
|
)
|
|
? currentFontWeight
|
|
: FontWeight.Regular;
|
|
const currentFontStyle = getMostCommonFontStyle(this.elements);
|
|
const fontStyle = TextUtils.isFontStyleSupported(
|
|
fontFamily,
|
|
currentFontStyle
|
|
)
|
|
? currentFontStyle
|
|
: FontStyle.Normal;
|
|
|
|
const props = { fontFamily, fontWeight, fontStyle };
|
|
this.elements.forEach(element => {
|
|
this.crud.updateElement(element.id, buildProps(element, props));
|
|
this._updateElementBound(element);
|
|
});
|
|
};
|
|
|
|
private readonly _setFontSize = (fontSize: number) => {
|
|
const props = { fontSize };
|
|
this.elements.forEach(element => {
|
|
this.crud.updateElement(element.id, buildProps(element, props));
|
|
this._updateElementBound(element);
|
|
});
|
|
};
|
|
|
|
private readonly _setFontWeightAndStyle = (
|
|
fontWeight: FontWeight,
|
|
fontStyle: FontStyle
|
|
) => {
|
|
const props = { fontWeight, fontStyle };
|
|
this.elements.forEach(element => {
|
|
this.crud.updateElement(element.id, buildProps(element, props));
|
|
this._updateElementBound(element);
|
|
});
|
|
};
|
|
|
|
private readonly _setTextAlign = (textAlign: TextAlign) => {
|
|
const props = { textAlign };
|
|
this.elements.forEach(element => {
|
|
this.crud.updateElement(element.id, buildProps(element, props));
|
|
});
|
|
};
|
|
|
|
private readonly _setTextColor = ({ detail: color }: ColorEvent) => {
|
|
const props = { color };
|
|
this.elements.forEach(element => {
|
|
this.crud.updateElement(element.id, buildProps(element, props));
|
|
});
|
|
};
|
|
|
|
private readonly _updateElementBound = (
|
|
element: BlockSuite.EdgelessTextModelType
|
|
) => {
|
|
const elementType = this.elementType;
|
|
if (elementType === 'text' && element instanceof TextElementModel) {
|
|
// the change of font family will change the bound of the text
|
|
const {
|
|
text: yText,
|
|
fontFamily,
|
|
fontStyle,
|
|
fontSize,
|
|
fontWeight,
|
|
hasMaxWidth,
|
|
} = element;
|
|
const newBound = TextUtils.normalizeTextBound(
|
|
{
|
|
yText,
|
|
fontFamily,
|
|
fontStyle,
|
|
fontSize,
|
|
fontWeight,
|
|
hasMaxWidth,
|
|
},
|
|
Bound.fromXYWH(element.deserializedXYWH)
|
|
);
|
|
this.crud.updateElement(element.id, {
|
|
xywh: newBound.serialize(),
|
|
});
|
|
} else if (
|
|
elementType === 'connector' &&
|
|
ConnectorUtils.isConnectorWithLabel(element)
|
|
) {
|
|
const {
|
|
text,
|
|
labelXYWH,
|
|
labelStyle: { fontFamily, fontStyle, fontSize, fontWeight },
|
|
labelConstraints: { hasMaxWidth, maxWidth },
|
|
} = element as ConnectorElementModel;
|
|
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;
|
|
this.crud.updateElement(element.id, {
|
|
labelXYWH: bounds.toXYWH(),
|
|
});
|
|
} else if (
|
|
elementType === 'shape' &&
|
|
element instanceof ShapeElementModel
|
|
) {
|
|
const newBound = normalizeShapeBound(
|
|
element,
|
|
Bound.fromXYWH(element.deserializedXYWH)
|
|
);
|
|
this.crud.updateElement(element.id, {
|
|
xywh: newBound.serialize(),
|
|
});
|
|
}
|
|
// no need to update the bound of edgeless text block, which updates itself using ResizeObserver
|
|
};
|
|
|
|
pickColor = (event: PickColorEvent) => {
|
|
if (event.type === 'pick') {
|
|
this.elements.forEach(element => {
|
|
const props = packColor('color', { ...event.detail });
|
|
this.crud.updateElement(element.id, buildProps(element, props));
|
|
this._updateElementBound(element);
|
|
});
|
|
return;
|
|
}
|
|
|
|
const key = this.elementType === 'connector' ? 'labelStyle' : 'color';
|
|
this.elements.forEach(ele => {
|
|
ele[event.type === 'start' ? 'stash' : 'pop'](key as 'color');
|
|
});
|
|
};
|
|
|
|
get service() {
|
|
return this.edgeless.service;
|
|
}
|
|
|
|
override render() {
|
|
const colorScheme = this.edgeless.surface.renderer.getColorScheme();
|
|
const elements = this.elements;
|
|
const selectedAlign = getMostCommonAlign(elements);
|
|
const selectedColor = getMostCommonColor(elements, colorScheme);
|
|
const selectedFontFamily = getMostCommonFontFamily(elements);
|
|
const selectedFontSize = Math.trunc(getMostCommonFontSize(elements));
|
|
const selectedFontStyle = getMostCommonFontStyle(elements);
|
|
const selectedFontWeight = getMostCommonFontWeight(elements);
|
|
const matchFontFaces =
|
|
TextUtils.getFontFacesByFontFamily(selectedFontFamily);
|
|
const fontStyleBtnDisabled =
|
|
matchFontFaces.length === 1 &&
|
|
matchFontFaces[0].style === selectedFontStyle &&
|
|
matchFontFaces[0].weight === selectedFontWeight;
|
|
|
|
return join(
|
|
[
|
|
html`
|
|
<editor-menu-button
|
|
.contentPadding=${'8px'}
|
|
.button=${html`
|
|
<editor-icon-button
|
|
aria-label="Font"
|
|
.tooltip=${'Font'}
|
|
.justify=${'space-between'}
|
|
.labelHeight=${'20px'}
|
|
.iconContainerWidth=${'40px'}
|
|
>
|
|
<span
|
|
class="label padding0"
|
|
style=${`font-family: ${TextUtils.wrapFontFamily(selectedFontFamily)}`}
|
|
>Aa</span
|
|
>${SmallArrowDownIcon}
|
|
</editor-icon-button>
|
|
`}
|
|
>
|
|
<edgeless-font-family-panel
|
|
.value=${selectedFontFamily}
|
|
.onSelect=${this._setFontFamily}
|
|
></edgeless-font-family-panel>
|
|
</editor-menu-button>
|
|
`,
|
|
|
|
when(
|
|
this.edgeless.doc.awarenessStore.getFlag('enable_color_picker'),
|
|
() => {
|
|
const { type, colors } = packColorsWithColorScheme(
|
|
colorScheme,
|
|
selectedColor,
|
|
elements[0] instanceof ConnectorElementModel
|
|
? elements[0].labelStyle.color
|
|
: elements[0].color
|
|
);
|
|
|
|
return html`
|
|
<edgeless-color-picker-button
|
|
class="text-color"
|
|
.label=${'Text color'}
|
|
.pick=${this.pickColor}
|
|
.isText=${true}
|
|
.color=${selectedColor}
|
|
.colors=${colors}
|
|
.colorType=${type}
|
|
.palettes=${PALETTES}
|
|
>
|
|
</edgeless-color-picker-button>
|
|
`;
|
|
},
|
|
() => html`
|
|
<editor-menu-button
|
|
.contentPadding=${'8px'}
|
|
.button=${html`
|
|
<editor-icon-button
|
|
aria-label="Text color"
|
|
.tooltip=${'Text color'}
|
|
>
|
|
<edgeless-text-color-icon
|
|
.color=${selectedColor}
|
|
></edgeless-text-color-icon>
|
|
</editor-icon-button>
|
|
`}
|
|
>
|
|
<edgeless-color-panel
|
|
.value=${selectedColor}
|
|
.palettes=${PALETTES}
|
|
@select=${this._setTextColor}
|
|
></edgeless-color-panel>
|
|
</editor-menu-button>
|
|
`
|
|
),
|
|
|
|
html`
|
|
<editor-menu-button
|
|
.contentPadding=${'8px'}
|
|
.button=${html`
|
|
<editor-icon-button
|
|
aria-label="Font style"
|
|
.tooltip=${'Font style'}
|
|
.justify=${'space-between'}
|
|
.labelHeight=${'20px'}
|
|
.iconContainerWidth=${'90px'}
|
|
.disabled=${fontStyleBtnDisabled}
|
|
>
|
|
<span class="label ellipsis">
|
|
${choose(selectedFontWeight, FONT_WEIGHT_CHOOSE)}
|
|
${choose(selectedFontStyle, FONT_STYLE_CHOOSE)}
|
|
</span>
|
|
${SmallArrowDownIcon}
|
|
</editor-icon-button>
|
|
`}
|
|
>
|
|
<edgeless-font-weight-and-style-panel
|
|
.fontFamily=${selectedFontFamily}
|
|
.fontWeight=${selectedFontWeight}
|
|
.fontStyle=${selectedFontStyle}
|
|
.onSelect=${this._setFontWeightAndStyle}
|
|
></edgeless-font-weight-and-style-panel>
|
|
</editor-menu-button>
|
|
`,
|
|
|
|
this.elementType === 'edgeless-text'
|
|
? nothing
|
|
: html`
|
|
<editor-menu-button
|
|
.contentPadding=${'8px'}
|
|
.button=${html`
|
|
<editor-icon-button
|
|
aria-label="Font size"
|
|
.tooltip=${'Font size'}
|
|
.justify=${'space-between'}
|
|
.labelHeight=${'20px'}
|
|
.iconContainerWidth=${'60px'}
|
|
>
|
|
<span class="label">${selectedFontSize}</span>
|
|
${SmallArrowDownIcon}
|
|
</editor-icon-button>
|
|
`}
|
|
>
|
|
<edgeless-size-panel
|
|
data-type="check"
|
|
.size=${selectedFontSize}
|
|
.sizeList=${FONT_SIZE_LIST}
|
|
.onSelect=${this._setFontSize}
|
|
></edgeless-size-panel>
|
|
</editor-menu-button>
|
|
`,
|
|
|
|
html`
|
|
<editor-menu-button
|
|
.button=${html`
|
|
<editor-icon-button
|
|
aria-label="Alignment"
|
|
.tooltip=${'Alignment'}
|
|
>
|
|
${choose(selectedAlign, TEXT_ALIGN_CHOOSE)}${SmallArrowDownIcon}
|
|
</editor-icon-button>
|
|
`}
|
|
>
|
|
<edgeless-align-panel
|
|
.value=${selectedAlign}
|
|
.onSelect=${this._setTextAlign}
|
|
></edgeless-align-panel>
|
|
</editor-menu-button>
|
|
`,
|
|
].filter(b => b !== nothing),
|
|
renderToolbarSeparator
|
|
);
|
|
}
|
|
|
|
@property({ attribute: false })
|
|
accessor edgeless!: EdgelessRootBlockComponent;
|
|
|
|
@property({ attribute: false })
|
|
accessor elements!: BlockSuite.EdgelessTextModelType[];
|
|
|
|
@property({ attribute: false })
|
|
accessor elementType!: BlockSuite.EdgelessTextModelKeyType;
|
|
|
|
@query('edgeless-color-picker-button.text-color')
|
|
accessor textColorButton!: EdgelessColorPickerButton;
|
|
}
|