refactor(editor): move shape toolbar config and components to its package (#11082)

This commit is contained in:
Saul-Mirone
2025-03-21 16:47:39 +00:00
parent 909426c644
commit f762797772
13 changed files with 410 additions and 386 deletions

View File

@@ -1,360 +0,0 @@
import {
CanvasElementType,
EdgelessCRUDIdentifier,
} from '@blocksuite/affine-block-surface';
import {
ellipseSvg,
roundedSvg,
ShapeTool,
triangleSvg,
} from '@blocksuite/affine-gfx-shape';
import {
getShapeRadius,
getShapeType,
ShapeType,
} from '@blocksuite/affine-model';
import {
EditPropsStore,
TelemetryProvider,
ThemeProvider,
ViewportElementProvider,
} from '@blocksuite/affine-shared/services';
import {
EdgelessDraggableElementController,
EdgelessToolbarToolMixin,
} from '@blocksuite/affine-widget-edgeless-toolbar';
import { SignalWatcher } from '@blocksuite/global/lit';
import { css, html, LitElement, nothing } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import type { DraggableShape } from './utils.js';
import { buildVariablesObject } from './utils.js';
const shapes: DraggableShape[] = [];
// to move shapes together
const oy = -2;
const ox = 0;
shapes.push({
name: 'roundedRect',
svg: roundedSvg,
style: {
default: { x: -9, y: 6 },
hover: { y: -5, z: 1 },
next: { y: 60 },
},
});
shapes.push({
name: ShapeType.Ellipse,
svg: ellipseSvg,
style: {
default: { x: -20, y: 31 },
hover: { y: 15, z: 1 },
next: { y: 64 },
},
});
shapes.push({
name: ShapeType.Triangle,
svg: triangleSvg,
style: {
default: { x: 18, y: 25 },
hover: { y: 7, z: 1 },
next: { y: 64 },
},
});
shapes.forEach(s => {
Object.values(s.style).forEach(style => {
if (style.y) (style.y as number) += oy;
if (style.x) (style.x as number) += ox;
});
});
export class EdgelessToolbarShapeDraggable extends EdgelessToolbarToolMixin(
SignalWatcher(LitElement)
) {
static override styles = css`
:host {
display: flex;
justify-content: center;
align-items: flex-end;
}
.edgeless-shape-draggable {
/* avoid shadow clipping */
--shadow-safe-area: 10px;
box-sizing: border-box;
flex-shrink: 0;
width: calc(100% + 2 * var(--shadow-safe-area));
height: calc(100% + var(--shadow-safe-area));
padding-top: var(--shadow-safe-area);
padding-left: var(--shadow-safe-area);
padding-right: var(--shadow-safe-area);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
position: relative;
pointer-events: none;
}
.shape {
width: fit-content;
height: fit-content;
position: absolute;
transition:
transform 0.3s,
z-index 0.1s;
transform: translateX(var(--default-x, 0)) translateY(var(--default-y, 0))
scale(var(--default-s, 1));
z-index: var(--default-z, 0);
pointer-events: none;
}
.shape svg {
display: block;
}
.shape svg path,
.shape svg circle,
.shape svg rect {
pointer-events: auto;
cursor: grab;
}
.shape:hover,
.shape.cancel {
transform: translateX(var(--hover-x, 0)) translateY(var(--hover-y, 0))
scale(var(--hover-s, 1));
z-index: var(--hover-z, 0);
}
.shape.next {
transition: all 0.5s cubic-bezier(0.39, 0.28, 0.09, 0.95);
pointer-events: none;
transform: translateX(var(--next-x, 0)) translateY(var(--next-y, 0))
scale(var(--next-s, 1));
}
.shape.next.coming {
transform: translateX(var(--default-x, 0)) translateY(var(--default-y, 0))
scale(var(--default-s, 1));
}
`;
draggableController!: EdgelessDraggableElementController<DraggableShape>;
draggingShape: DraggableShape['name'] = 'roundedRect';
override type = 'shape' as const;
get crud() {
return this.edgeless.std.get(EdgelessCRUDIdentifier);
}
get shapeShadow() {
return this.theme === 'dark'
? '0 0 7px rgba(0, 0, 0, .22)'
: '0 0 5px rgba(0, 0, 0, .2)';
}
private _setShapeOverlayLock(lock: boolean) {
const controller = this.gfx.tool.currentTool$.peek();
if (controller instanceof ShapeTool) {
controller.setDisableOverlay(lock);
}
}
initDragController() {
if (!this.edgeless || !this.toolbarContainer) return;
if (this.draggableController) return;
this.draggableController = new EdgelessDraggableElementController(this, {
edgeless: this.edgeless,
scopeElement: this.toolbarContainer,
standardWidth: 100,
clickToDrag: true,
onOverlayCreated: (overlay, element) => {
const shapeName =
this.draggableController.states.draggingElement?.data.name;
if (!shapeName) return;
this.setEdgelessTool({
type: 'shape',
shapeName,
});
const controller = this.gfx.tool.currentTool$.peek();
if (controller instanceof ShapeTool) {
controller.clearOverlay();
}
overlay.element.style.filter = `drop-shadow(${this.shapeShadow})`;
this.readyToDrop = true;
this.draggingShape = element.data.name;
},
onDrop: (el, bound) => {
const xywh = bound.serialize();
const shape = el.data;
const id = this.crud.addElement(CanvasElementType.SHAPE, {
shapeType: getShapeType(shape.name),
xywh,
radius: getShapeRadius(shape.name),
});
if (!id) return;
this.edgeless.std
.getOptional(TelemetryProvider)
?.track('CanvasElementAdded', {
control: 'toolbar:dnd',
page: 'whiteboard editor',
module: 'toolbar',
segment: 'toolbar',
type: 'shape',
other: {
shapeType: getShapeType(shape.name),
},
});
this._setShapeOverlayLock(false);
this.readyToDrop = false;
this.gfx.tool.setTool('default');
this.gfx.selection.set({
elements: [id],
editing: false,
});
},
onCanceled: () => {
this._setShapeOverlayLock(false);
this.readyToDrop = false;
},
onElementClick: el => {
this.onShapeClick?.(el.data);
this._setShapeOverlayLock(true);
},
onEnterOrLeaveScope: (overlay, isOutside) => {
overlay.element.style.filter = isOutside
? 'none'
: `drop-shadow(${this.shapeShadow})`;
},
});
this.edgeless.bindHotKey(
{
s: ctx => {
// `page.keyboard.press('Shift+s')` in playwright will also trigger this 's' key event
if (ctx.get('keyboardState').raw.shiftKey) return;
const locked = this.gfx.viewport.locked;
const selection = this.gfx.selection;
if (locked || selection.editing) return;
if (this.readyToDrop) {
const activeIndex = shapes.findIndex(
s => s.name === this.draggingShape
);
const nextIndex = (activeIndex + 1) % shapes.length;
const next = shapes[nextIndex];
this.draggingShape = next.name;
this.draggableController.cancelWithoutAnimation();
}
const el = this.shapeContainer.querySelector(
`.shape.${this.draggingShape}`
) as HTMLElement;
if (!el) {
console.error('Edgeless toolbar Shape element not found');
return;
}
const { x, y } = this.gfx.tool.lastMousePos$.peek();
const { viewport } = this.edgeless.std.get(ViewportElementProvider);
const { left, top } = viewport;
const clientPos = { x: x + left, y: y + top };
this.draggableController.clickToDrag(el, clientPos);
},
},
{ global: true }
);
}
override render() {
const { cancelled, dragOut, draggingElement } =
this.draggableController?.states || {};
const draggingShape = draggingElement?.data;
return html`<div class="edgeless-shape-draggable">
${repeat(
shapes,
s => s.name,
shape => {
const isBeingDragged = draggingShape?.name === shape.name;
const { fillColor, strokeColor } =
this.edgeless.std.get(EditPropsStore).lastProps$.value[
`shape:${shape.name}`
] || {};
const color = this.edgeless.std
.get(ThemeProvider)
.generateColorProperty(fillColor);
const stroke = this.edgeless.std
.get(ThemeProvider)
.generateColorProperty(strokeColor);
const baseStyle = {
...buildVariablesObject(shape.style),
filter: `drop-shadow(${this.shapeShadow})`,
color,
stroke,
};
const currStyle = styleMap({
...baseStyle,
opacity: isBeingDragged ? 0 : 1,
});
const nextStyle = styleMap(baseStyle);
return html`${isBeingDragged
? html`<div
style=${nextStyle}
class=${classMap({
shape: true,
next: true,
coming: !!dragOut && !cancelled,
})}
>
${shape.svg}
</div>`
: nothing}
<div
style=${currStyle}
class=${classMap({
shape: true,
[shape.name]: true,
cancel: isBeingDragged && !dragOut,
})}
@mousedown=${(e: MouseEvent) =>
this.draggableController.onMouseDown(e, {
data: shape,
preview: shape.svg,
})}
@touchstart=${(e: TouchEvent) =>
this.draggableController.onTouchStart(e, {
data: shape,
preview: shape.svg,
})}
@click=${(e: MouseEvent) => e.stopPropagation()}
>
${shape.svg}
</div>`;
}
)}
</div>`;
}
override updated(_changedProperties: Map<PropertyKey, unknown>) {
const controllerRequiredProps = ['edgeless', 'toolbarContainer'] as const;
if (
controllerRequiredProps.some(p => _changedProperties.has(p)) &&
!this.draggableController
) {
this.initDragController();
}
}
@property({ attribute: false })
accessor onShapeClick: (shape: DraggableShape) => void = () => {};
@state()
accessor readyToDrop = false;
@query('.edgeless-shape-draggable')
accessor shapeContainer!: HTMLDivElement;
}

View File

@@ -1,90 +0,0 @@
import { ShapeTool } from '@blocksuite/affine-gfx-shape';
import { type ShapeName, ShapeType } from '@blocksuite/affine-model';
import { EdgelessToolbarToolMixin } from '@blocksuite/affine-widget-edgeless-toolbar';
import { SignalWatcher } from '@blocksuite/global/lit';
import { css, html, LitElement } from 'lit';
import type { DraggableShape } from './utils.js';
export class EdgelessShapeToolButton extends EdgelessToolbarToolMixin(
SignalWatcher(LitElement)
) {
static override styles = css`
:host {
display: block;
width: 100%;
height: 100%;
}
edgeless-toolbar-button,
.shapes {
width: 100%;
height: 64px;
}
`;
private readonly _handleShapeClick = (shape: DraggableShape) => {
this.setEdgelessTool(this.type, {
shapeName: shape.name,
});
if (!this.popper) this._toggleMenu();
};
private readonly _handleWrapperClick = () => {
if (this.tryDisposePopper()) return;
this.setEdgelessTool(this.type, {
shapeName: ShapeType.Rect,
});
if (!this.popper) this._toggleMenu();
};
override type = 'shape' as const;
private _toggleMenu() {
this.createPopper('edgeless-shape-menu', this, {
setProps: ele => {
ele.edgeless = this.edgeless;
ele.onChange = (shapeName: ShapeName) => {
this.setEdgelessTool(this.type, {
shapeName,
});
this._updateOverlay();
};
},
});
}
private _updateOverlay() {
const controller = this.gfx.tool.currentTool$.peek();
if (controller instanceof ShapeTool) {
controller.createOverlay();
}
}
override render() {
const { active } = this;
return html`
<edgeless-toolbar-button
class="edgeless-shape-button"
.tooltip=${this.popper
? ''
: html`<affine-tooltip-content-with-shortcut
data-tip="${'Shape'}"
data-shortcut="${'S'}"
></affine-tooltip-content-with-shortcut>`}
.tooltipOffset=${5}
.active=${active}
>
<edgeless-toolbar-shape-draggable
.edgeless=${this.edgeless}
.toolbarContainer=${this.toolbarContainer}
class="shapes"
@click=${this._handleWrapperClick}
.onShapeClick=${this._handleShapeClick}
>
</edgeless-toolbar-shape-draggable>
</edgeless-toolbar-button>
`;
}
}

View File

@@ -1,150 +0,0 @@
import type { ShapeToolOption } from '@blocksuite/affine-gfx-shape';
import { render, type TemplateResult } from 'lit';
type TransformState = {
/** horizental offset base on center */
x?: number | string;
/** vertical offset base on center */
y?: number | string;
/** scale */
s?: number;
/** z-index */
z?: number;
};
export type DraggableShape = {
name: ShapeToolOption['shapeName'];
svg: TemplateResult;
style: {
default?: TransformState;
hover?: TransformState;
/**
* The next shape when previous shape is dragged outside toolbar
*/
next?: TransformState;
};
};
/**
* Helper function to build the CSS variables object for the shape
* @returns
*/
export const buildVariablesObject = (style: DraggableShape['style']) => {
const states: Array<keyof DraggableShape['style']> = [
'default',
'hover',
'next',
];
const variables: Array<keyof TransformState> = ['x', 'y', 's', 'z'];
const resolveValue = (
variable: keyof TransformState,
value: string | number
) => {
if (['x', 'y'].includes(variable)) {
return typeof value === 'number' ? `${value}px` : value;
}
return value;
};
return states.reduce((acc, state) => {
return {
...acc,
...variables.reduce((acc, variable) => {
const defaultValue = style.default?.[variable];
const value = style[state]?.[variable] ?? defaultValue;
if (value === undefined) return acc;
return {
...acc,
[`--${state}-${variable}`]: resolveValue(variable, value),
};
}, {}),
};
}, {});
};
// drag helper
export type ShapeDragEvent = {
inputType: 'mouse' | 'touch';
x: number;
y: number;
el: HTMLElement;
originalEvent: MouseEvent | TouchEvent;
};
export const touchResolver = (event: TouchEvent) =>
({
inputType: 'touch',
x: event.touches[0].clientX,
y: event.touches[0].clientY,
el: event.currentTarget as HTMLElement,
originalEvent: event,
}) satisfies ShapeDragEvent;
export const mouseResolver = (event: MouseEvent) =>
({
inputType: 'mouse',
x: event.clientX,
y: event.clientY,
el: event.currentTarget as HTMLElement,
originalEvent: event,
}) satisfies ShapeDragEvent;
// overlay helper
export const defaultDraggingInfo = {
startPos: { x: 0, y: 0 },
toolbarRect: {} as DOMRect,
edgelessRect: {} as DOMRect,
shapeRectOriginal: {} as DOMRect,
shapeEl: null as unknown as HTMLElement,
parentToMount: null as unknown as HTMLElement,
moved: false,
shape: null as unknown as DraggableShape,
style: {} as CSSStyleDeclaration,
};
export type DraggingInfo = typeof defaultDraggingInfo;
export const createShapeDraggingOverlay = (info: DraggingInfo) => {
const { edgelessRect, parentToMount } = info;
const overlay = document.createElement('div');
Object.assign(overlay.style, {
position: 'absolute',
top: '0',
left: '0',
width: edgelessRect.width + 'px',
// always clip
// height: toolbarRect.bottom - edgelessRect.top + 'px',
height: edgelessRect.height + 'px',
overflow: 'hidden',
zIndex: '9999',
// for debug purpose
// background: 'rgba(255, 0, 0, 0.1)',
});
const shape = document.createElement('div');
const shapeScaleWrapper = document.createElement('div');
Object.assign(shapeScaleWrapper.style, {
transform: 'scale(var(--s, 1))',
transition: 'transform 0.1s',
transformOrigin: 'var(--o, center)',
});
render(info.shape.svg, shapeScaleWrapper);
Object.assign(shape.style, {
position: 'absolute',
color: info.style.color,
stroke: info.style.stroke,
filter: `var(--shape-filter, ${info.style.filter})`,
transform: 'translate(var(--x, 0), var(--y, 0))',
left: 'var(--left, 0)',
top: 'var(--top, 0)',
cursor: 'grabbing',
transition: 'inherit',
});
shape.append(shapeScaleWrapper);
overlay.append(shape);
parentToMount.append(overlay);
return overlay;
};

View File

@@ -1,383 +1,72 @@
import { shapeToolbarConfig } from '@blocksuite/affine-gfx-shape';
import {
EdgelessCRUDIdentifier,
normalizeShapeBound,
} from '@blocksuite/affine-block-surface';
import {
packColor,
type PickColorEvent,
} from '@blocksuite/affine-components/color-picker';
import type { LineDetailType } from '@blocksuite/affine-components/edgeless-line-styles-panel';
import {
mountShapeTextEditor,
ShapeComponentConfig,
type ShapeToolOption,
} from '@blocksuite/affine-gfx-shape';
import {
type Color,
DefaultTheme,
FontFamily,
getShapeName,
getShapeRadius,
getShapeType,
isTransparent,
LineWidth,
MindmapElementModel,
resolveColor,
ShapeElementModel,
type ShapeName,
ShapeStyle,
ShapeType,
StrokeStyle,
} from '@blocksuite/affine-model';
import type {
ToolbarGenericAction,
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 type { ToolbarActions } from '@blocksuite/affine-shared/services';
import {
createMindmapLayoutActionMenu,
createMindmapStyleActionMenu,
} from './mindmap';
const mindmapActions = [
{
id: 'a.mindmap-style',
when(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
return models.some(hasGrouped);
},
content(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
if (!models.length) return null;
let mindmaps = models
.map(model => model.group)
.filter(model => ctx.matchModel(model, MindmapElementModel));
if (!mindmaps.length) return null;
// Not displayed when there is both a normal shape and a mindmap shape.
if (models.length !== mindmaps.length) return null;
mindmaps = Array.from(new Set(mindmaps));
return createMindmapStyleActionMenu(ctx, mindmaps);
},
},
{
id: 'b.mindmap-layout',
when(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
return models.some(hasGrouped);
},
content(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
if (!models.length) return null;
let mindmaps = models
.map(model => model.group)
.filter(model => ctx.matchModel(model, MindmapElementModel));
if (!mindmaps.length) return null;
// Not displayed when there is both a normal shape and a mindmap shape.
if (models.length !== mindmaps.length) return null;
mindmaps = Array.from(new Set(mindmaps));
// It's a sub node.
if (models.length === 1 && mindmaps[0].tree.element !== models[0])
return null;
return createMindmapLayoutActionMenu(ctx, mindmaps);
},
},
] as const satisfies ToolbarActions;
export const builtinShapeToolbarConfig = {
actions: [
{
id: 'a.mindmap-style',
when(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
return models.some(hasGrouped);
},
content(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
if (!models.length) return null;
let mindmaps = models
.map(model => model.group)
.filter(model => ctx.matchModel(model, MindmapElementModel));
if (!mindmaps.length) return null;
// Not displayed when there is both a normal shape and a mindmap shape.
if (models.length !== mindmaps.length) return null;
mindmaps = Array.from(new Set(mindmaps));
return createMindmapStyleActionMenu(ctx, mindmaps);
},
},
{
id: 'b.mindmap-layout',
when(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
return models.some(hasGrouped);
},
content(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
if (!models.length) return null;
let mindmaps = models
.map(model => model.group)
.filter(model => ctx.matchModel(model, MindmapElementModel));
if (!mindmaps.length) return null;
// Not displayed when there is both a normal shape and a mindmap shape.
if (models.length !== mindmaps.length) return null;
mindmaps = Array.from(new Set(mindmaps));
// It's a sub node.
if (models.length === 1 && mindmaps[0].tree.element !== models[0])
return null;
return createMindmapLayoutActionMenu(ctx, mindmaps);
},
},
{
id: 'c.switch-type',
when(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
return models.length > 0 && models.every(model => !hasGrouped(model));
},
content(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
if (!models.length) return null;
const shapeName =
getMostCommonValue<ShapeToolOption, 'shapeName'>(
models.map(model => ({
shapeName: getShapeName(model.shapeType, model.radius),
})),
'shapeName'
) ?? ShapeType.Rect;
const onPick = (shapeName: ShapeName) => {
const shapeType = getShapeType(shapeName);
const radius = getShapeRadius(shapeName);
ctx.std.store.captureSync();
for (const model of models) {
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, { shapeType, radius });
}
};
return renderMenu({
icon: ShapeIcon(),
label: 'Switch type',
items: ShapeComponentConfig.map(item => ({
key: item.tooltip,
value: item.name,
// TODO(@fundon): should add a feature flag to switch style
icon: item.generalIcon,
disabled: item.disabled,
})),
currentValue: shapeName,
onPick,
});
},
},
{
id: 'd.style',
// TODO(@fundon): should add a feature flag
when: false,
content(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
if (!models.length) return null;
const field = 'shapeStyle';
const shapeStyle =
getMostCommonValue(models, field) ?? ShapeStyle.General;
const onPick = (value: boolean) => {
const shapeStyle = value ? ShapeStyle.Scribbled : ShapeStyle.General;
const fontFamily = value ? FontFamily.Kalam : FontFamily.Inter;
for (const model of models) {
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, { shapeStyle, fontFamily });
}
};
return renderMenu({
label: 'Style',
items: LINE_STYLE_LIST,
currentValue: shapeStyle === ShapeStyle.Scribbled,
onPick,
});
},
},
{
id: 'e.color',
when(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
return models.length > 0 && models.every(model => !hasGrouped(model));
},
content(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
if (!models.length) return null;
const enableCustomColor = ctx.features.getFlag('enable_color_picker');
const theme = ctx.theme.edgeless$.value;
const firstModel = models[0];
const originalFillColor = firstModel.fillColor;
const originalStrokeColor = firstModel.strokeColor;
const mapped = models.map(
({ filled, fillColor, strokeColor, strokeWidth, strokeStyle }) => ({
fillColor: filled
? resolveColor(fillColor, theme)
: DefaultTheme.transparent,
strokeColor: resolveColor(strokeColor, theme),
strokeWidth,
strokeStyle,
})
);
const fillColor =
getMostCommonValue(mapped, 'fillColor') ??
resolveColor(DefaultTheme.shapeFillColor, theme);
const strokeColor =
getMostCommonValue(mapped, 'strokeColor') ??
resolveColor(DefaultTheme.shapeStrokeColor, theme);
const strokeWidth =
getMostCommonValue(mapped, 'strokeWidth') ?? LineWidth.Four;
const strokeStyle =
getMostCommonValue(mapped, 'strokeStyle') ?? StrokeStyle.Solid;
const onPickFillColor = (e: CustomEvent<PickColorEvent>) => {
e.stopPropagation();
const d = e.detail;
const field = 'fillColor';
if (d.type === 'pick') {
const value = d.detail.value;
const filled = isTransparent(value);
for (const model of models) {
const props = packColor(field, value);
// If `filled` can be set separately, this logic can be removed
if (field && !model.filled) {
const color = getTextColor(value, filled);
Object.assign(props, { filled, color });
}
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, props);
}
return;
}
for (const model of models) {
model[d.type === 'start' ? 'stash' : 'pop'](field);
}
};
const onPickStrokeColor = (e: CustomEvent<PickColorEvent>) => {
e.stopPropagation();
const d = e.detail;
const field = 'strokeColor';
if (d.type === 'pick') {
const value = d.detail.value;
for (const model of models) {
const props = packColor(field, value);
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, props);
}
return;
}
for (const model of models) {
model[d.type === 'start' ? 'stash' : 'pop'](field);
}
};
const onPickStrokeStyle = (e: CustomEvent<LineDetailType>) => {
e.stopPropagation();
const { type, value } = e.detail;
if (type === 'size') {
const strokeWidth = value;
for (const model of models) {
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, { strokeWidth });
}
return;
}
const strokeStyle = value;
for (const model of models) {
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, { strokeStyle });
}
};
return html`
<edgeless-shape-color-picker
@pickFillColor=${onPickFillColor}
@pickStrokeColor=${onPickStrokeColor}
@pickStrokeStyle=${onPickStrokeStyle}
.payload=${{
fillColor,
strokeColor,
strokeWidth,
strokeStyle,
originalFillColor,
originalStrokeColor,
theme,
enableCustomColor,
}}
>
</edgeless-shape-color-picker>
`;
},
},
{
id: 'f.text',
tooltip: 'Add text',
icon: AddTextIcon(),
when(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
return models.length === 1 && !hasGrouped(models[0]) && !models[0].text;
},
run(ctx) {
const model = ctx.getCurrentModelByType(ShapeElementModel);
if (!model) return;
const rootBlock = getRootBlock(ctx);
if (!rootBlock) return;
mountShapeTextEditor(model, rootBlock);
},
},
// id: `g.text`
...createTextActions(ShapeElementModel, 'shape', (ctx, model, props) => {
// No need to adjust element bounds
if (props['textAlign']) {
ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, props);
return;
}
const xywh = normalizeShapeBound(
model,
Bound.fromXYWH(model.deserializedXYWH)
).serialize();
ctx.std
.get(EdgelessCRUDIdentifier)
.updateElement(model.id, { ...props, xywh });
}).map<ToolbarGenericAction>(action => ({
...action,
id: `g.text-${action.id}`,
when(ctx) {
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
return (
models.length > 0 &&
models.every(model => !hasGrouped(model) && model.text)
);
},
})),
],
when: ctx => ctx.getSurfaceModelsByType(ShapeElementModel).length > 0,
} as const satisfies ToolbarModuleConfig;
// When the shape is filled with black color, the text color should be white.
// When the shape is transparent, the text color should be set according to the theme.
// Otherwise, the text color should be black.
function getTextColor(fillColor: Color, isNotTransparent = false) {
if (isNotTransparent) {
if (isEqual(fillColor, DefaultTheme.black)) {
return DefaultTheme.white;
} else if (isEqual(fillColor, DefaultTheme.white)) {
return DefaultTheme.black;
} else if (isEqual(fillColor, DefaultTheme.pureBlack)) {
return DefaultTheme.pureWhite;
} else if (isEqual(fillColor, DefaultTheme.pureWhite)) {
return DefaultTheme.pureBlack;
}
}
// aka `DefaultTheme.pureBlack`
return DefaultTheme.shapeTextColor;
}
...shapeToolbarConfig,
actions: [...mindmapActions, ...shapeToolbarConfig.actions],
};
function hasGrouped(model: ShapeElementModel) {
return model.group instanceof MindmapElementModel;

View File

@@ -51,8 +51,6 @@ import { EdgelessFrameOrderMenu } from './edgeless/components/toolbar/present/fr
import { EdgelessNavigatorSettingButton } from './edgeless/components/toolbar/present/navigator-setting-button.js';
import { EdgelessPresentButton } from './edgeless/components/toolbar/present/present-button.js';
import { PresentationToolbar } from './edgeless/components/toolbar/presentation-toolbar.js';
import { EdgelessToolbarShapeDraggable } from './edgeless/components/toolbar/shape/shape-draggable.js';
import { EdgelessShapeToolButton } from './edgeless/components/toolbar/shape/shape-tool-button.js';
import { OverlayScrollbar } from './edgeless/components/toolbar/template/overlay-scrollbar.js';
import { AffineTemplateLoading } from './edgeless/components/toolbar/template/template-loading.js';
import { EdgelessTemplatePanel } from './edgeless/components/toolbar/template/template-panel.js';
@@ -168,7 +166,6 @@ function registerEdgelessToolbarComponents() {
EdgelessMindmapToolButton
);
customElements.define('edgeless-note-tool-button', EdgelessNoteToolButton);
customElements.define('edgeless-shape-tool-button', EdgelessShapeToolButton);
customElements.define('edgeless-template-button', EdgelessTemplateButton);
// Menus
@@ -181,10 +178,6 @@ function registerEdgelessToolbarComponents() {
customElements.define('edgeless-slide-menu', EdgelessSlideMenu);
// Toolbar components
customElements.define(
'edgeless-toolbar-shape-draggable',
EdgelessToolbarShapeDraggable
);
customElements.define('toolbar-arrow-up-icon', ToolbarArrowUpIcon);
// Frame order components
@@ -322,8 +315,6 @@ declare global {
'edgeless-frame-order-menu': EdgelessFrameOrderMenu;
'edgeless-navigator-setting-button': EdgelessNavigatorSettingButton;
'edgeless-present-button': EdgelessPresentButton;
'edgeless-toolbar-shape-draggable': EdgelessToolbarShapeDraggable;
'edgeless-shape-tool-button': EdgelessShapeToolButton;
'overlay-scrollbar': OverlayScrollbar;
'affine-template-loading': AffineTemplateLoading;
'edgeless-templates-panel': EdgelessTemplatePanel;