Skip to content

Commit 5c5b260

Browse files
authored
Simplify canvas widgets placement at one or multiple specific layers: (#77)
* Add `CanvasPlaceAt` component to render a widget at specified non-viewport canvas layer; - Now it is possible to place widgets at `underlay` and `overLinkGeometry` layers; * Add `changeTransform` event to `CanvasApi.events` which triggers on `CanvasApi.metrics.getTransform()` changes; * Add `--reactodia-z-index-base` CSS property to override base z-index level for workspace components with a `z-index` set; * Fix `HaloLink` and visual authoring path highlight being rendered on top on elements by placing it onto `overLinkGeometry` layer; * Deprecate `canvasWidgets` prop on `DefaultWorkspace` and `ClassicWorkspace` in favor of passing widgets directly as children; * **[Breaking]** Remove `defineCanvasWidget()` and `SharedCanvasState.setCanvasWidget()`: - Canvas children are always assumed to be viewport widgets; - Use child `CanvasPlaceAt` components to render different parts at other layers instead. * **[Breaking]** Canvas widgets are not re-rendered when parent canvas is rendered and require explicit subscriptions: - Subscribe to canvas `changeTransform` event when using `CanvasApi.metrics` to convert between coordinates; - Subscribe to canvas `resize` event to track viewport size; - Subscribe to `changeCells` event from `DiagramModel` to track graph content changes.
1 parent 963c1d2 commit 5c5b260

31 files changed

Lines changed: 492 additions & 433 deletions

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,28 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
55

66
## [Unreleased]
77
#### 🚀 New Features
8+
- Simplify canvas widgets placement at one or multiple layers:
9+
* Canvas children are always assumed to be viewport widgets;
10+
* Add `CanvasPlaceAt` component to render its children at specified non-viewport canvas layer instead;
11+
* Support new placement layers: `underlay` layer to place components under all canvas content, `overLinkGeometry` layer to place components above link geometry (connections) but under link labels;
12+
* **[💥Breaking]** Remove `defineCanvasWidget()` and `SharedCanvasState.setCanvasWidget()` (use `CanvasPlaceAt` to display components at canvas layers instead).
813
- Add `EditorController.applyAuthoringChanges()` method to apply current authoring changes to the diagram (i.e. change entity data, delete relations, etc) and reset the change state to be empty.
914

15+
#### ⏱ Performance
16+
- **[💥Breaking]** Canvas widgets are not automatically updated when parent canvas is rendered to reduce unnecessary re-renders, and now require explicit subscriptions:
17+
* Subscribe to canvas `changeTransform` event when using `CanvasApi.metrics` to convert between coordinates;
18+
* Subscribe to canvas `resize` event to track viewport size;
19+
* Subscribe to `changeCells` event from `DiagramModel` to track graph content changes.
20+
1021
#### 💅 Polish
1122
- Make dialogs fill the available viewport when the viewport width is small:
1223
* This is controlled by new CSS property `--reactodia-dialog-viewport-breakpoint-s` with default value `600px` which makes dialog fill the viewport if the available width is less or equal to that value.
24+
- Allow to override base z-index level for workspace components with a set z-index value via `--reactodia-z-index-base` CSS property;
25+
- Add `changeTransform` event to `CanvasApi.events` which triggers on `CanvasApi.metrics.getTransform()` changes, i.e. when coordinate mapping changes due to scale or canvas size is re-adjusted.
26+
- Deprecate `canvasWidgets` prop on `DefaultWorkspace` and `ClassicWorkspace` in favor of passing widgets directly as children.
27+
28+
#### 🐛 Fixed
29+
- Fix `HaloLink` and visual authoring link path highlight being rendered on top on elements by placing it onto `overLinkGeometry` widget layer instead.
1330

1431
## [0.30.1] - 2025-06-27
1532
#### 🐛 Fixed

examples/sparql.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,6 @@ function SparqlExample() {
6262
defaultLayout={defaultLayout}>
6363
<Reactodia.DefaultWorkspace
6464
menu={<ExampleToolbarMenu />}
65-
canvasWidgets={[
66-
<Reactodia.Toolbar key='sparql-settings'
67-
dock='sw'
68-
dockOffsetY={40}>
69-
<SparqlConnectionAction settings={connectionSettings}
70-
applySettings={applyConnectionSettings}
71-
/>
72-
</Reactodia.Toolbar>
73-
]}
7465
languages={[
7566
{code: 'de', label: 'Deutsch'},
7667
{code: 'en', label: 'English'},
@@ -82,8 +73,14 @@ function SparqlExample() {
8273
{code: 'pt', label: 'português'},
8374
{code: 'ru', label: 'Русский'},
8475
{code: 'zh', label: '汉语'},
85-
]}
86-
/>
76+
]}>
77+
<Reactodia.Toolbar dock='sw'
78+
dockOffsetY={40}>
79+
<SparqlConnectionAction settings={connectionSettings}
80+
applySettings={applyConnectionSettings}
81+
/>
82+
</Reactodia.Toolbar>
83+
</Reactodia.DefaultWorkspace>
8784
</Reactodia.Workspace>
8885
);
8986
}

examples/styleCustomization.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,24 +61,28 @@ function StyleCustomizationExample() {
6161
},
6262
linkTemplateResolver: type => DoubleArrowLinkTemplate,
6363
}}
64-
canvasWidgets={[
65-
<BookDecorations key='book-decorations' />
66-
]}
67-
menu={<ExampleToolbarMenu />}
68-
/>
64+
menu={<ExampleToolbarMenu />}>
65+
<BookDecorations />
66+
</Reactodia.DefaultWorkspace>
6967
</Reactodia.Workspace>
7068
);
7169
}
7270

7371
function BookDecorations() {
7472
const {model} = Reactodia.useCanvas();
73+
74+
const [, forceUpdate] = React.useState({});
75+
React.useEffect(() => {
76+
const listener = new Reactodia.EventObserver();
77+
listener.listen(model.events, 'changeCells', () => forceUpdate({}));
78+
return () => listener.stopListening();
79+
});
80+
7581
return model.elements
7682
.filter(element => element instanceof Reactodia.EntityElement)
7783
.map(element => <BookDecoration key={element.id} target={element} />);
7884
}
7985

80-
Reactodia.defineCanvasWidget(BookDecorations, element => ({element, attachment: 'viewport'}));
81-
8286
function BookDecoration(props: { target: Reactodia.EntityElement }) {
8387
const {target} = props;
8488

src/diagram/canvasApi.ts

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,10 @@ export interface CanvasEvents {
212212
* Triggered on {@link CanvasApi.getScale} property change.
213213
*/
214214
changeScale: PropertyChange<CanvasApi, number>;
215+
/**
216+
* Triggered on {@link CanvasApi.metrics.getTransform()} property change.
217+
*/
218+
changeTransform: PropertyChange<CanvasApi, PaperTransform>;
215219
}
216220

217221
/**
@@ -551,31 +555,6 @@ export interface ExportSvgOptions {
551555
*/
552556
export interface ExportRasterOptions extends ExportSvgOptions, ToDataURLOptions {}
553557

554-
/**
555-
* Canvas widget layer to render widget:
556-
* - `viewport` - topmost layer, uses client (viewport) coordinates and
557-
* does not scale or scroll with the diagram;
558-
* - `overElements` - displayed over both elements and links, uses paper coordinates,
559-
* scales and scrolls with the diagram;
560-
* - `overLinks` - displayed under elements but over links, uses paper coordinates,
561-
* scales and scrolls with the diagram.
562-
*/
563-
export type CanvasWidgetAttachment = 'viewport' | 'overElements' | 'overLinks';
564-
565-
/**
566-
* Describes canvas widget element to render on the specific widget layer.
567-
*/
568-
export interface CanvasWidgetDescription {
569-
/**
570-
* Canvas widget element to render.
571-
*/
572-
element: React.ReactElement;
573-
/**
574-
* Canvas widget layer to render widget on.
575-
*/
576-
attachment: CanvasWidgetAttachment;
577-
}
578-
579558
/**
580559
* Represents a context for everything rendered inside the canvas,
581560
* including diagram content and widgets.
Lines changed: 1 addition & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,54 +2,9 @@ import * as React from 'react';
22

33
import { HotkeyAst, type HotkeyString, parseHotkey, formatHotkey } from '../coreUtils/hotkey';
44

5-
import { type CanvasWidgetDescription, useCanvas } from './canvasApi';
5+
import { useCanvas } from './canvasApi';
66
import { type MutableRenderingState } from './renderingState';
77

8-
const GET_WIDGET_METADATA: unique symbol = Symbol('getWidgetMetadata');
9-
10-
interface WithMetadata {
11-
[GET_WIDGET_METADATA]?: (element: React.ReactElement) => CanvasWidgetDescription;
12-
}
13-
14-
/**
15-
* Defines the React component to be a canvas widget.
16-
*
17-
* A component cannot be rendered by canvas as widget unless explicitly
18-
* defined as such using this function.
19-
*
20-
* **Example**:
21-
* ```jsx
22-
* function MyWidget(props) {
23-
* ...
24-
* }
25-
*
26-
* defineCanvasWidget(MyWidget, element => ({
27-
* element,
28-
* attachment: 'viewport'
29-
* }));
30-
* ```
31-
*
32-
* @category Core
33-
*/
34-
export function defineCanvasWidget<P>(
35-
type: React.ComponentType<P>,
36-
metadataOf: (element: React.ReactElement<P>) => CanvasWidgetDescription
37-
): void {
38-
const typeWithMetadata = type as WithMetadata;
39-
typeWithMetadata[GET_WIDGET_METADATA] = metadataOf as WithMetadata[typeof GET_WIDGET_METADATA];
40-
}
41-
42-
export function extractCanvasWidget(
43-
element: React.ReactElement
44-
): CanvasWidgetDescription | undefined {
45-
const typeWithMetadata = element.type as WithMetadata;
46-
const metadataOf = typeWithMetadata[GET_WIDGET_METADATA];
47-
if (metadataOf) {
48-
return metadataOf(element);
49-
}
50-
return undefined;
51-
}
52-
538
/**
549
* Represents a registered canvas hotkey.
5510
*

0 commit comments

Comments
 (0)