refactor(editor): add dom renderer entry for canvas element (#12149)

This commit is contained in:
Yifeng Wang
2025-05-14 16:22:16 +08:00
committed by GitHub
parent 6358249aea
commit 9cabe03386
20 changed files with 911 additions and 42 deletions

View File

@@ -0,0 +1,11 @@
import { DomElementRendererExtension } from '@blocksuite/affine-block-surface';
import { shapeDomRenderer } from './shape-dom/index.js';
/**
* Extension to register the DOM-based renderer for 'shape' elements.
*/
export const ShapeDomRendererExtension = DomElementRendererExtension(
'shape',
shapeDomRenderer
);

View File

@@ -0,0 +1,100 @@
import type { DomRenderer } from '@blocksuite/affine-block-surface';
import type { ShapeElementModel } from '@blocksuite/affine-model';
import { DefaultTheme } from '@blocksuite/affine-model';
import { manageClassNames, setStyles } from './utils';
function applyShapeSpecificStyles(
model: ShapeElementModel,
element: HTMLElement
) {
if (model.shapeType === 'rect') {
element.style.borderRadius = `${model.radius ?? 0}px`;
} else if (model.shapeType === 'ellipse') {
element.style.borderRadius = '50%';
} else {
element.style.borderRadius = '';
}
}
function applyBorderStyles(
model: ShapeElementModel,
element: HTMLElement,
strokeColor: string
) {
element.style.border =
model.strokeStyle !== 'none'
? `${model.strokeWidth}px ${model.strokeStyle === 'dash' ? 'dashed' : 'solid'} ${strokeColor}`
: 'none';
}
function applyTransformStyles(model: ShapeElementModel, element: HTMLElement) {
if (model.rotate && model.rotate !== 0) {
setStyles(element, {
transform: `rotate(${model.rotate}deg)`,
transformOrigin: 'center',
});
} else {
setStyles(element, {
transform: '',
transformOrigin: '',
});
}
}
function applyShadowStyles(
model: ShapeElementModel,
element: HTMLElement,
renderer: DomRenderer
) {
if (model.shadow) {
const { offsetX, offsetY, blur, color } = model.shadow;
setStyles(element, {
boxShadow: `${offsetX}px ${offsetY}px ${blur}px ${renderer.getColorValue(color)}`,
});
} else {
setStyles(element, { boxShadow: '' });
}
}
/**
* Renders a ShapeElementModel to a given HTMLElement using DOM properties.
* This function is intended to be registered via the DomElementRendererExtension.
*
* @param model - The shape element model containing rendering properties.
* @param element - The HTMLElement to apply the shape's styles to.
* @param renderer - The main DOMRenderer instance, providing access to viewport and color utilities.
*/
export const shapeDomRenderer = (
model: ShapeElementModel,
element: HTMLElement,
renderer: DomRenderer
): void => {
const { zoom } = renderer.viewport;
const fillColor = renderer.getColorValue(
model.fillColor,
DefaultTheme.shapeFillColor,
true
);
const strokeColor = renderer.getColorValue(
model.strokeColor,
DefaultTheme.shapeStrokeColor,
true
);
element.style.width = `${model.w * zoom}px`;
element.style.height = `${model.h * zoom}px`;
applyShapeSpecificStyles(model, element);
element.style.backgroundColor = model.filled ? fillColor : 'transparent';
applyBorderStyles(model, element, strokeColor);
applyTransformStyles(model, element);
element.style.boxSizing = 'border-box';
element.style.zIndex = renderer.layerManager.getZIndex(model).toString();
manageClassNames(model, element);
applyShadowStyles(model, element, renderer);
};

View File

@@ -0,0 +1,59 @@
import type { ShapeElementModel } from '@blocksuite/affine-model';
/**
* Utility to manage a class on an element, tracking the previous class via dataset.
* If the new class is different from the previous one (stored in dataset),
* the previous class is removed. The new class is added if not already present.
* The dataset is then updated with the new class name.
*
* @param element The HTMLElement to update.
* @param newClassName The new class name to apply.
* @param datasetKeyForPreviousClass The key in `element.dataset` used to store the previous class name.
*/
function updateClass(
element: HTMLElement,
newClassName: string,
datasetKeyForPreviousClass: string
): void {
const previousClassName = element.dataset[datasetKeyForPreviousClass];
if (previousClassName && previousClassName !== newClassName) {
element.classList.remove(previousClassName);
}
if (!element.classList.contains(newClassName)) {
element.classList.add(newClassName);
}
element.dataset[datasetKeyForPreviousClass] = newClassName;
}
/**
* Utility to set multiple CSS styles on an HTMLElement.
*
* @param element The HTMLElement to apply styles to.
* @param styles An object where keys are camelCased CSS property names and values are their string values.
*/
export function setStyles(
element: HTMLElement,
styles: Record<string, string>
): void {
for (const property in styles) {
if (Object.prototype.hasOwnProperty.call(styles, property)) {
// Using `any` for `element.style` index is a common practice for dynamic style assignment.
// Assumes `property` is a valid camelCased CSS property.
(element.style as any)[property] = styles[property];
}
}
}
export function manageClassNames(
model: ShapeElementModel,
element: HTMLElement
) {
const currentShapeTypeClass = `shape-${model.shapeType}`;
const currentShapeStyleClass = `shape-style-${model.shapeStyle.toLowerCase()}`;
updateClass(element, currentShapeTypeClass, 'prevShapeTypeClass');
updateClass(element, currentShapeStyleClass, 'prevShapeStyleClass');
}

View File

@@ -8,6 +8,7 @@ import {
HighlighterElementRendererExtension,
ShapeElementRendererExtension,
} from './element-renderer';
import { ShapeDomRendererExtension } from './element-renderer/shape-dom';
import { ShapeElementView, ShapeViewInteraction } from './element-view';
import { ShapeTool } from './shape-tool';
import { shapeSeniorTool, shapeToolbarExtension } from './toolbar';
@@ -25,6 +26,7 @@ export class ShapeViewExtension extends ViewExtensionProvider {
if (this.isEdgeless(context.scope)) {
context.register(HighlighterElementRendererExtension);
context.register(ShapeElementRendererExtension);
context.register(ShapeDomRendererExtension);
context.register(ShapeElementView);
context.register(ShapeTool);
context.register(shapeSeniorTool);