diff --git a/CHANGELOG.md b/CHANGELOG.md index 137c245a..40c21c93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p * 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. - Allow to override base z-index level for workspace components with a set z-index value via `--reactodia-z-index-base` CSS property; - 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. +- Add `DiagramModel.cellsVersion` property which updates on every element or link addition/removal/reordering to be able to subscribe to `changeCells` event with `useSyncStore()` hook. - Deprecate `canvasWidgets` prop on `DefaultWorkspace` and `ClassicWorkspace` in favor of passing widgets directly as children. #### 🐛 Fixed diff --git a/examples/styleCustomization.tsx b/examples/styleCustomization.tsx index 25afbca8..965c7f71 100644 --- a/examples/styleCustomization.tsx +++ b/examples/styleCustomization.tsx @@ -71,12 +71,13 @@ function StyleCustomizationExample() { function BookDecorations() { const {model} = Reactodia.useCanvas(); - const [, forceUpdate] = React.useState({}); - React.useEffect(() => { - const listener = new Reactodia.EventObserver(); - listener.listen(model.events, 'changeCells', () => forceUpdate({})); - return () => listener.stopListening(); - }); + // Update decorations when graph content changes + Reactodia.useSyncStore( + Reactodia.useFrameDebouncedStore( + Reactodia.useEventStore(model.events, 'changeCells') + ), + () => model.cellsVersion + ); return model.elements .filter(element => element instanceof Reactodia.EntityElement) diff --git a/src/diagram/graph.ts b/src/diagram/graph.ts index cfff7196..42c83370 100644 --- a/src/diagram/graph.ts +++ b/src/diagram/graph.ts @@ -38,6 +38,7 @@ export class Graph { private readonly source = new EventSource(); readonly events: Events = this.source; + private cellsVersion = 1; private readonly elements = new OrderedMap(); private readonly links = new OrderedMap(); private readonly elementLinks = new WeakMap(); @@ -45,6 +46,10 @@ export class Graph { private readonly linkTypeVisibility = new Map(); + getCellsVersion(): number { + return this.cellsVersion; + } + getElements() { return this.elements.items; } getLinks() { return this.links.items; } @@ -86,6 +91,7 @@ export class Graph { reorderElements(compare: (a: Element, b: Element) => number) { this.elements.reorder(compare); + this.incrementCellsVersion(); } getElement(elementId: string): Element | undefined { @@ -98,6 +104,7 @@ export class Graph { } element.events.onAny(this.onElementEvent); this.elements.push(element.id, element); + this.incrementCellsVersion(); this.source.trigger('changeCells', {updateAll: false, changedElement: element}); } @@ -116,6 +123,7 @@ export class Graph { } this.elements.delete(elementId); element.events.offAny(this.onElementEvent); + this.incrementCellsVersion(); this.source.trigger('changeCells', {updateAll: false, changedElement: element, changedLinks}); } } @@ -151,6 +159,7 @@ export class Graph { link.events.onAny(this.onLinkEvent); this.links.push(link.id, link); + this.incrementCellsVersion(); this.source.trigger('changeCells', {updateAll: false, changedLinks: [link]}); } @@ -163,6 +172,7 @@ export class Graph { if (link) { link.events.offAny(this.onLinkEvent); this.removeLinkReferences(link); + this.incrementCellsVersion(); if (!(options && options.silent)) { this.source.trigger('changeCells', {updateAll: false, changedLinks: [link]}); } @@ -193,6 +203,13 @@ export class Graph { } } + private incrementCellsVersion(): void { + this.cellsVersion = this.cellsVersion + 1; + if (this.cellsVersion >= Number.MAX_SAFE_INTEGER) { + this.cellsVersion = 1; + } + } + get linkVisibility(): ReadonlyMap { return this.linkTypeVisibility; } diff --git a/src/diagram/model.ts b/src/diagram/model.ts index c3d4cca8..785a0658 100644 --- a/src/diagram/model.ts +++ b/src/diagram/model.ts @@ -63,6 +63,11 @@ export interface GraphStructure { * to create RDF terms for identifiers and property values. */ get factory(): Rdf.DataFactory; + /** + * Graph content (elements and links) version number which changes on every cell change + * (when element or link added/removed/reordered, see {@link DiagramModelEvents.changeCells}). + */ + get cellsVersion(): number; /** * All elements (nodes) in the graph. */ @@ -205,6 +210,10 @@ export class DiagramModel implements GraphStructure { return this.getTermFactory(); } + get cellsVersion(): number { + return this.graph.getCellsVersion(); + } + get elements(): ReadonlyArray { return this.graph.getElements(); } diff --git a/src/widgets/visualAuthoring/visualAuthoring.tsx b/src/widgets/visualAuthoring/visualAuthoring.tsx index 55ca194b..1356b57f 100644 --- a/src/widgets/visualAuthoring/visualAuthoring.tsx +++ b/src/widgets/visualAuthoring/visualAuthoring.tsx @@ -1,7 +1,9 @@ import * as React from 'react'; import { EventObserver } from '../../coreUtils/events'; -import { useObservedProperty } from '../../coreUtils/hooks'; +import { + useEventStore, useFrameDebouncedStore, useObservedProperty, useSyncStore, +} from '../../coreUtils/hooks'; import { Debouncer } from '../../coreUtils/scheduler'; import type { ElementModel, LinkModel } from '../../data/model'; @@ -312,25 +314,19 @@ function EntityDecoratorsInner(props: { const {inlineActions} = props; const {model, editor} = useWorkspace(); - const inAuthoringMode = useObservedProperty(editor.events, 'changeMode', () => editor.inAuthoringMode); - - const [version, setVersion] = React.useState(0); - React.useEffect(() => { - const debouncer = new Debouncer(); - const listener = new EventObserver(); - const scheduleUpdate = () => setVersion(v => v + 1); - listener.listen(model.events, 'changeCells', () => { - debouncer.call(scheduleUpdate); - }); - return () => { - listener.stopListening(); - debouncer.dispose(); - }; - }, []); + const inAuthoringMode = useObservedProperty( + editor.events, + 'changeMode', + () => editor.inAuthoringMode + ); + const cellsVersion = useSyncStore( + useFrameDebouncedStore(useEventStore(model.events, 'changeCells')), + () => model.cellsVersion + ); const cachedDecorators = React.useMemo( () => new WeakMap(), - [inlineActions, version] + [inlineActions, cellsVersion] ); const decorators: React.ReactNode[] = [];