diff --git a/blocksuite/affine/blocks/surface/src/renderer/dom-renderer.ts b/blocksuite/affine/blocks/surface/src/renderer/dom-renderer.ts index 73c9c1009..b123a3124 100644 --- a/blocksuite/affine/blocks/surface/src/renderer/dom-renderer.ts +++ b/blocksuite/affine/blocks/surface/src/renderer/dom-renderer.ts @@ -11,7 +11,7 @@ import { getBoundWithRotation, intersects, } from '@blocksuite/global/gfx'; -import type { BlockStdScope } from '@blocksuite/std'; +import { type BlockStdScope, SurfaceSelection } from '@blocksuite/std'; import type { GfxCompatibleInterface, GridManager, @@ -390,6 +390,36 @@ export class DomRenderer { this.refresh(); }) ); + + // Workaround for the group rendering reactive update when selection changed + let lastSet = new Set(); + this._disposables.add( + this.std.selection.filter$(SurfaceSelection).subscribe(selections => { + const groupRelatedSelection = new Set( + selections.flatMap(s => + s.elements.flatMap(e => { + const element = surfaceModel.getElementById(e); + if ( + element && + (element.type === 'group' || element.groups.length !== 0) + ) { + return [element.id, ...element.groups.map(g => g.id)]; + } + return []; + }) + ) + ); + + if (lastSet.symmetricDifference(groupRelatedSelection).size !== 0) { + lastSet.union(groupRelatedSelection).forEach(g => { + this._markElementDirty(g, UpdateType.ELEMENT_UPDATED); + }); + this.refresh(); + } + + lastSet = groupRelatedSelection; + }) + ); } addOverlay = (overlay: Overlay) => { diff --git a/blocksuite/affine/gfx/connector/src/element-renderer/connector-dom.ts b/blocksuite/affine/gfx/connector/src/element-renderer/connector-dom.ts deleted file mode 100644 index 6b75e48ad..000000000 --- a/blocksuite/affine/gfx/connector/src/element-renderer/connector-dom.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { DomElementRendererExtension } from '@blocksuite/affine-block-surface'; - -import { connectorDomRenderer } from './connector-dom/index.js'; - -/** - * Extension to register the DOM-based renderer for 'connector' elements. - */ -export const ConnectorDomRendererExtension = DomElementRendererExtension( - 'connector', - connectorDomRenderer -); diff --git a/blocksuite/affine/gfx/connector/src/index.ts b/blocksuite/affine/gfx/connector/src/index.ts index b01e96786..7d87973a6 100644 --- a/blocksuite/affine/gfx/connector/src/index.ts +++ b/blocksuite/affine/gfx/connector/src/index.ts @@ -1,9 +1,8 @@ export * from './adapter'; export * from './connector-manager'; export * from './connector-tool'; -export * from './element-renderer'; -export { ConnectorDomRendererExtension } from './element-renderer/connector-dom'; export * from './element-transform'; +export * from './renderer'; export * from './text'; export * from './toolbar/config'; export * from './toolbar/quick-tool'; diff --git a/blocksuite/affine/gfx/connector/src/element-renderer/connector-dom/index.ts b/blocksuite/affine/gfx/connector/src/renderer/dom-renderer.ts similarity index 92% rename from blocksuite/affine/gfx/connector/src/element-renderer/connector-dom/index.ts rename to blocksuite/affine/gfx/connector/src/renderer/dom-renderer.ts index f8de1d12d..46a6a0711 100644 --- a/blocksuite/affine/gfx/connector/src/element-renderer/connector-dom/index.ts +++ b/blocksuite/affine/gfx/connector/src/renderer/dom-renderer.ts @@ -1,14 +1,18 @@ -import type { DomRenderer } from '@blocksuite/affine-block-surface'; +import { + DomElementRendererExtension, + type DomRenderer, +} from '@blocksuite/affine-block-surface'; import { type ConnectorElementModel, ConnectorMode, DefaultTheme, + type LocalConnectorElementModel, type PointStyle, } from '@blocksuite/affine-model'; import { PointLocation, SVGPathBuilder } from '@blocksuite/global/gfx'; -import { isConnectorWithLabel } from '../../connector-manager.js'; -import { DEFAULT_ARROW_SIZE } from '../utils.js'; +import { isConnectorWithLabel } from '../connector-manager'; +import { DEFAULT_ARROW_SIZE } from './utils'; interface PathBounds { minX: number; @@ -221,8 +225,8 @@ function renderConnectorLabel( * @param element - The HTMLElement to apply the connector's styles to. * @param renderer - The main DOMRenderer instance, providing access to viewport and color utilities. */ -export const connectorDomRenderer = ( - model: ConnectorElementModel, +export const connectorBaseDomRenderer = ( + model: ConnectorElementModel | LocalConnectorElementModel, element: HTMLElement, renderer: DomRenderer ): void => { @@ -358,7 +362,21 @@ export const connectorDomRenderer = ( element.style.height = `${model.h * zoom}px`; element.style.overflow = 'visible'; element.style.pointerEvents = 'none'; - - // Render label if present - renderConnectorLabel(model, element, renderer, zoom); }; + +export const connectorDomRenderer = ( + model: ConnectorElementModel, + element: HTMLElement, + renderer: DomRenderer +): void => { + connectorBaseDomRenderer(model, element, renderer); + renderConnectorLabel(model, element, renderer, renderer.viewport.zoom); +}; + +/** + * Extension to register the DOM-based renderer for 'connector' elements. + */ +export const ConnectorDomRendererExtension = DomElementRendererExtension( + 'connector', + connectorDomRenderer +); diff --git a/blocksuite/affine/gfx/connector/src/element-renderer/index.ts b/blocksuite/affine/gfx/connector/src/renderer/element-renderer.ts similarity index 98% rename from blocksuite/affine/gfx/connector/src/element-renderer/index.ts rename to blocksuite/affine/gfx/connector/src/renderer/element-renderer.ts index 397114fe4..6d6b1e205 100644 --- a/blocksuite/affine/gfx/connector/src/element-renderer/index.ts +++ b/blocksuite/affine/gfx/connector/src/renderer/element-renderer.ts @@ -25,7 +25,7 @@ import { } from '@blocksuite/global/gfx'; import { deltaInsertsToChunks } from '@blocksuite/std/inline'; -import { isConnectorWithLabel } from '../connector-manager.js'; +import { isConnectorWithLabel } from '../connector-manager'; import { DEFAULT_ARROW_SIZE, getArrowOptions, @@ -33,7 +33,7 @@ import { renderCircle, renderDiamond, renderTriangle, -} from './utils.js'; +} from './utils'; export const connector: ElementRenderer< ConnectorElementModel | LocalConnectorElementModel diff --git a/blocksuite/affine/gfx/connector/src/renderer/index.ts b/blocksuite/affine/gfx/connector/src/renderer/index.ts new file mode 100644 index 000000000..ad3ea2f1b --- /dev/null +++ b/blocksuite/affine/gfx/connector/src/renderer/index.ts @@ -0,0 +1,2 @@ +export * from './dom-renderer'; +export * from './element-renderer'; diff --git a/blocksuite/affine/gfx/connector/src/element-renderer/utils.ts b/blocksuite/affine/gfx/connector/src/renderer/utils.ts similarity index 100% rename from blocksuite/affine/gfx/connector/src/element-renderer/utils.ts rename to blocksuite/affine/gfx/connector/src/renderer/utils.ts diff --git a/blocksuite/affine/gfx/connector/src/view.ts b/blocksuite/affine/gfx/connector/src/view.ts index 2d372d00d..2353aa7b2 100644 --- a/blocksuite/affine/gfx/connector/src/view.ts +++ b/blocksuite/affine/gfx/connector/src/view.ts @@ -6,9 +6,11 @@ import { import { ConnectionOverlay } from './connector-manager'; import { ConnectorTool } from './connector-tool'; import { effects } from './effects'; -import { ConnectorElementRendererExtension } from './element-renderer'; -import { ConnectorDomRendererExtension } from './element-renderer/connector-dom'; import { ConnectorFilter } from './element-transform'; +import { + ConnectorDomRendererExtension, + ConnectorElementRendererExtension, +} from './renderer'; import { connectorToolbarExtension } from './toolbar/config'; import { connectorQuickTool } from './toolbar/quick-tool'; import { ConnectorElementView, ConnectorInteraction } from './view/view'; diff --git a/blocksuite/affine/gfx/group/src/index.ts b/blocksuite/affine/gfx/group/src/index.ts index 2b0228cfa..c59777851 100644 --- a/blocksuite/affine/gfx/group/src/index.ts +++ b/blocksuite/affine/gfx/group/src/index.ts @@ -1,6 +1,6 @@ export * from './adapter'; export * from './command'; -export * from './element-renderer'; export * from './element-view'; +export * from './renderer'; export * from './text/text'; export * from './toolbar/config'; diff --git a/blocksuite/affine/gfx/group/src/element-renderer/consts.ts b/blocksuite/affine/gfx/group/src/renderer/consts.ts similarity index 100% rename from blocksuite/affine/gfx/group/src/element-renderer/consts.ts rename to blocksuite/affine/gfx/group/src/renderer/consts.ts diff --git a/blocksuite/affine/gfx/group/src/renderer/dom-renderer.ts b/blocksuite/affine/gfx/group/src/renderer/dom-renderer.ts new file mode 100644 index 000000000..963716f9e --- /dev/null +++ b/blocksuite/affine/gfx/group/src/renderer/dom-renderer.ts @@ -0,0 +1,62 @@ +import { DomElementRendererExtension } from '@blocksuite/affine-block-surface'; +import { FontWeight, type GroupElementModel } from '@blocksuite/affine-model'; + +import { + GROUP_TITLE_FONT, + GROUP_TITLE_FONT_SIZE, + GROUP_TITLE_PADDING, +} from './consts'; +import { titleRenderParams } from './utils'; + +export const GroupDomRendererExtension = DomElementRendererExtension( + 'group', + (model: GroupElementModel, domElement, renderer) => { + const { zoom } = renderer.viewport; + const [, , w, h] = model.deserializedXYWH; + + const renderParams = titleRenderParams(model, zoom); + model.externalXYWH = renderParams.titleBound.serialize(); + + domElement.innerHTML = ''; + domElement.style.outlineColor = ''; + domElement.style.outlineWidth = ''; + domElement.style.outlineStyle = ''; + + const elements = renderer.provider.selectedElements?.() || []; + + const renderTitle = () => { + const { text } = renderParams; + const titleElement = document.createElement('div'); + titleElement.style.transform = `translate(0, -100%)`; + titleElement.style.fontFamily = GROUP_TITLE_FONT; + titleElement.style.fontWeight = `${FontWeight.Regular}`; + titleElement.style.fontStyle = 'normal'; + titleElement.style.fontSize = `${GROUP_TITLE_FONT_SIZE}px`; + titleElement.style.color = renderer.getPropertyValue('--affine-blue'); + titleElement.style.textAlign = 'left'; + titleElement.style.padding = `${GROUP_TITLE_PADDING[0]}px ${GROUP_TITLE_PADDING[1]}px`; + titleElement.textContent = text; + domElement.replaceChildren(titleElement); + }; + + if (elements.includes(model.id)) { + if (model.showTitle) { + renderTitle(); + } else { + domElement.style.outlineColor = + renderer.getPropertyValue('--affine-blue'); + domElement.style.outlineWidth = '2px'; + domElement.style.outlineStyle = 'solid'; + } + } else if (model.childElements.some(child => elements.includes(child.id))) { + domElement.style.outlineColor = '#8FD1FF'; + domElement.style.outlineWidth = '2px'; + domElement.style.outlineStyle = 'solid'; + } + + domElement.style.width = `${w * zoom}px`; + domElement.style.height = `${h * zoom}px`; + domElement.style.overflow = 'visible'; + domElement.style.pointerEvents = 'none'; + } +); diff --git a/blocksuite/affine/gfx/group/src/element-renderer/index.ts b/blocksuite/affine/gfx/group/src/renderer/element-renderer.ts similarity index 97% rename from blocksuite/affine/gfx/group/src/element-renderer/index.ts rename to blocksuite/affine/gfx/group/src/renderer/element-renderer.ts index 0d301c1bc..81ea684c1 100644 --- a/blocksuite/affine/gfx/group/src/element-renderer/index.ts +++ b/blocksuite/affine/gfx/group/src/renderer/element-renderer.ts @@ -6,7 +6,7 @@ import { import type { GroupElementModel } from '@blocksuite/affine-model'; import { Bound } from '@blocksuite/global/gfx'; -import { titleRenderParams } from './utils.js'; +import { titleRenderParams } from './utils'; export const group: ElementRenderer = ( model, diff --git a/blocksuite/affine/gfx/group/src/renderer/index.ts b/blocksuite/affine/gfx/group/src/renderer/index.ts new file mode 100644 index 000000000..ad3ea2f1b --- /dev/null +++ b/blocksuite/affine/gfx/group/src/renderer/index.ts @@ -0,0 +1,2 @@ +export * from './dom-renderer'; +export * from './element-renderer'; diff --git a/blocksuite/affine/gfx/group/src/element-renderer/utils.ts b/blocksuite/affine/gfx/group/src/renderer/utils.ts similarity index 98% rename from blocksuite/affine/gfx/group/src/element-renderer/utils.ts rename to blocksuite/affine/gfx/group/src/renderer/utils.ts index 7b4d8ae70..33a397b61 100644 --- a/blocksuite/affine/gfx/group/src/element-renderer/utils.ts +++ b/blocksuite/affine/gfx/group/src/renderer/utils.ts @@ -13,7 +13,7 @@ import { GROUP_TITLE_FONT_SIZE, GROUP_TITLE_OFFSET, GROUP_TITLE_PADDING, -} from './consts.js'; +} from './consts'; export function titleRenderParams(group: GroupElementModel, zoom: number) { let text = group.title.toString().trim(); diff --git a/blocksuite/affine/gfx/group/src/text/edgeless-group-title-editor.ts b/blocksuite/affine/gfx/group/src/text/edgeless-group-title-editor.ts index 8250dd1d8..a14f70894 100644 --- a/blocksuite/affine/gfx/group/src/text/edgeless-group-title-editor.ts +++ b/blocksuite/affine/gfx/group/src/text/edgeless-group-title-editor.ts @@ -21,7 +21,7 @@ import { GROUP_TITLE_FONT_SIZE, GROUP_TITLE_OFFSET, GROUP_TITLE_PADDING, -} from '../element-renderer/consts'; +} from '../renderer/consts'; export function mountGroupTitleEditor( group: GroupElementModel, diff --git a/blocksuite/affine/gfx/group/src/view.ts b/blocksuite/affine/gfx/group/src/view.ts index 8e6236758..2b07d76ab 100644 --- a/blocksuite/affine/gfx/group/src/view.ts +++ b/blocksuite/affine/gfx/group/src/view.ts @@ -4,9 +4,12 @@ import { } from '@blocksuite/affine-ext-loader'; import { effects } from './effects'; -import { GroupElementRendererExtension } from './element-renderer'; import { GroupElementView, GroupInteraction } from './element-view'; import { GroupInteractionExtension } from './interaction-ext'; +import { + GroupDomRendererExtension, + GroupElementRendererExtension, +} from './renderer'; import { groupToolbarExtension } from './toolbar/config'; export class GroupViewExtension extends ViewExtensionProvider { @@ -20,6 +23,7 @@ export class GroupViewExtension extends ViewExtensionProvider { override setup(context: ViewExtensionContext) { super.setup(context); context.register(GroupElementRendererExtension); + context.register(GroupDomRendererExtension); context.register(GroupElementView); if (this.isEdgeless(context.scope)) { context.register(groupToolbarExtension); diff --git a/blocksuite/affine/gfx/mindmap/src/index.ts b/blocksuite/affine/gfx/mindmap/src/index.ts index ea1d52ffa..20d1cda1e 100644 --- a/blocksuite/affine/gfx/mindmap/src/index.ts +++ b/blocksuite/affine/gfx/mindmap/src/index.ts @@ -1,7 +1,7 @@ export * from './adapter'; -export * from './element-renderer'; export * from './indicator-overlay'; export * from './interactivity'; +export * from './renderer'; export * from './toolbar/config'; export * from './toolbar/senior-tool'; export * from './utils'; diff --git a/blocksuite/affine/gfx/mindmap/src/renderer/dom-renderer.ts b/blocksuite/affine/gfx/mindmap/src/renderer/dom-renderer.ts new file mode 100644 index 000000000..17ce84478 --- /dev/null +++ b/blocksuite/affine/gfx/mindmap/src/renderer/dom-renderer.ts @@ -0,0 +1,65 @@ +import { DomElementRendererExtension } from '@blocksuite/affine-block-surface'; +import { + connectorBaseDomRenderer, + ConnectorPathGenerator, +} from '@blocksuite/affine-gfx-connector'; +import type { + MindmapElementModel, + MindmapNode, +} from '@blocksuite/affine-model'; +import type { GfxModel } from '@blocksuite/std/gfx'; + +export const MindmapDomRendererExtension = DomElementRendererExtension( + 'mindmap', + (model: MindmapElementModel, domElement, renderer) => { + const bound = model.elementBound; + + const { zoom } = renderer.viewport; + // Set element size and position + domElement.style.width = `${bound.w * zoom}px`; + domElement.style.height = `${bound.h * zoom}px`; + domElement.style.overflow = 'visible'; + domElement.style.pointerEvents = 'none'; + + const newChildren: HTMLDivElement[] = []; + + const traverse = (node: MindmapNode) => { + const connectors = model.getConnectors(node); + if (!connectors) return; + + connectors.reverse().forEach(result => { + const { connector, outdated } = result; + const elementGetter = (id: string) => + model.surface.getElementById(id) ?? + (model.surface.store.getModelById(id) as GfxModel); + + if (outdated) { + ConnectorPathGenerator.updatePath(connector, null, elementGetter); + } + + const connectorContainer = document.createElement('div'); + connectorContainer.style.position = 'absolute'; + connectorContainer.style.transformOrigin = 'top left'; + const geometricStyles = { + left: `${(connector.x - bound.x) * zoom}px`, + top: `${(connector.y - bound.y) * zoom}px`, + }; + const opacityStyle = { opacity: node.element.opacity }; + Object.assign(connectorContainer.style, geometricStyles, opacityStyle); + + connectorBaseDomRenderer(connector, connectorContainer, renderer); + newChildren.push(connectorContainer); + }); + + if (node.detail.collapsed) { + return; + } else { + node.children.forEach(traverse); + } + }; + + model.tree && traverse(model.tree); + + domElement.replaceChildren(...newChildren); + } +); diff --git a/blocksuite/affine/gfx/mindmap/src/element-renderer.ts b/blocksuite/affine/gfx/mindmap/src/renderer/element-renderer.ts similarity index 100% rename from blocksuite/affine/gfx/mindmap/src/element-renderer.ts rename to blocksuite/affine/gfx/mindmap/src/renderer/element-renderer.ts diff --git a/blocksuite/affine/gfx/mindmap/src/renderer/index.ts b/blocksuite/affine/gfx/mindmap/src/renderer/index.ts new file mode 100644 index 000000000..ad3ea2f1b --- /dev/null +++ b/blocksuite/affine/gfx/mindmap/src/renderer/index.ts @@ -0,0 +1,2 @@ +export * from './dom-renderer'; +export * from './element-renderer'; diff --git a/blocksuite/affine/gfx/mindmap/src/view.ts b/blocksuite/affine/gfx/mindmap/src/view.ts index 335fc1f1e..0d0e5abb5 100644 --- a/blocksuite/affine/gfx/mindmap/src/view.ts +++ b/blocksuite/affine/gfx/mindmap/src/view.ts @@ -4,9 +4,12 @@ import { } from '@blocksuite/affine-ext-loader'; import { effects } from './effects'; -import { MindmapElementRendererExtension } from './element-renderer'; import { MindMapIndicatorOverlay } from './indicator-overlay'; import { MindMapDragExtension } from './interactivity'; +import { + MindmapDomRendererExtension, + MindmapElementRendererExtension, +} from './renderer'; import { mindmapToolbarExtension, shapeMindmapToolbarExtension, @@ -25,6 +28,7 @@ export class MindmapViewExtension extends ViewExtensionProvider { override setup(context: ViewExtensionContext) { super.setup(context); context.register(MindmapElementRendererExtension); + context.register(MindmapDomRendererExtension); context.register(mindMapSeniorTool); context.register(mindmapToolbarExtension); context.register(shapeMindmapToolbarExtension); diff --git a/blocksuite/affine/gfx/shape/src/element-renderer/shape-dom/index.ts b/blocksuite/affine/gfx/shape/src/element-renderer/shape-dom/index.ts index bdcc807cc..749ad6766 100644 --- a/blocksuite/affine/gfx/shape/src/element-renderer/shape-dom/index.ts +++ b/blocksuite/affine/gfx/shape/src/element-renderer/shape-dom/index.ts @@ -1,4 +1,5 @@ import type { DomRenderer } from '@blocksuite/affine-block-surface'; +import { isRTL } from '@blocksuite/affine-gfx-text'; import type { ShapeElementModel } from '@blocksuite/affine-model'; import { DefaultTheme } from '@blocksuite/affine-model'; import { SVGShapeBuilder } from '@blocksuite/global/gfx'; @@ -99,6 +100,8 @@ export const shapeDomRenderer = ( const unscaledWidth = model.w; const unscaledHeight = model.h; + const newChildren: Element[] = []; + const fillColor = renderer.getColorValue( model.fillColor, DefaultTheme.shapeFillColor, @@ -170,8 +173,7 @@ export const shapeDomRenderer = ( } svg.append(polygon); - // Replace existing children to avoid memory leaks - element.replaceChildren(svg); + newChildren.push(svg); } else { // Standard rendering for other shapes (e.g., rect, ellipse) // innerHTML was already cleared by applyShapeSpecificStyles if necessary @@ -179,6 +181,41 @@ export const shapeDomRenderer = ( applyBorderStyles(model, element, strokeColor, zoom); // Uses standard CSS border } + if (model.textDisplay && model.text) { + const str = model.text.toString(); + const textElement = document.createElement('div'); + if (isRTL(str)) { + textElement.dir = 'rtl'; + } + textElement.style.position = 'absolute'; + textElement.style.inset = '0'; + textElement.style.display = 'flex'; + textElement.style.flexDirection = 'column'; + textElement.style.justifyContent = + model.textVerticalAlign === 'center' + ? 'center' + : model.textVerticalAlign === 'top' + ? 'flex-start' + : 'flex-end'; + textElement.style.whiteSpace = 'pre-wrap'; + textElement.style.wordBreak = 'break-word'; + textElement.style.textAlign = model.textAlign; + textElement.style.alignmentBaseline = 'alphabetic'; + textElement.style.fontFamily = model.fontFamily; + textElement.style.fontSize = `${model.fontSize * zoom}px`; + textElement.style.fontWeight = model.fontWeight; + textElement.style.color = renderer.getColorValue( + model.color, + DefaultTheme.shapeTextColor, + true + ); + textElement.textContent = str; + newChildren.push(textElement); + } + + // Replace existing children to avoid memory leaks + element.replaceChildren(...newChildren); + applyTransformStyles(model, element); manageClassNames(model, element);