diff --git a/src/display/data-schema/data.d.ts b/src/display/data-schema/data.d.ts index a5c50826..18adc3df 100644 --- a/src/display/data-schema/data.d.ts +++ b/src/display/data-schema/data.d.ts @@ -99,6 +99,7 @@ export interface Grid { label?: string; show?: boolean; // Default: true cells: (0 | 1 | string)[][]; + inactiveCellStrategy?: 'destroy' | 'hide'; // Default: 'destroy' gap?: Gap; item: { components?: Component[]; diff --git a/src/display/data-schema/element-schema.js b/src/display/data-schema/element-schema.js index 16d7dd1b..9c5e4670 100644 --- a/src/display/data-schema/element-schema.js +++ b/src/display/data-schema/element-schema.js @@ -31,6 +31,7 @@ export const groupSchema = Base.extend({ export const gridSchema = Base.extend({ type: z.literal('grid'), cells: z.array(z.array(z.union([z.literal(0), z.literal(1), z.string()]))), + inactiveCellStrategy: z.enum(['destroy', 'hide']).default('destroy'), gap: Gap, item: z.object({ components: componentArraySchema.default([]), diff --git a/src/display/elements/Relations.js b/src/display/elements/Relations.js index 89a1495a..52e95c7b 100644 --- a/src/display/elements/Relations.js +++ b/src/display/elements/Relations.js @@ -20,7 +20,9 @@ export class Relations extends ComposedRelations { this.path = this.initPath(); } - apply(changes, options) { + apply(_changes, options) { + const changes = structuredClone(_changes); + // Filter out duplicates that already exist in the current props. if (options?.mergeStrategy === 'merge') { const existingLinks = this.props?.links; diff --git a/src/display/mixins/Cellsable.js b/src/display/mixins/Cellsable.js index d5e6524a..daa26118 100644 --- a/src/display/mixins/Cellsable.js +++ b/src/display/mixins/Cellsable.js @@ -1,14 +1,13 @@ import { newElement } from '../elements/creator'; import { UPDATE_STAGES } from './constants'; -const KEYS = ['cells']; +const KEYS = ['cells', 'inactiveCellStrategy']; export const Cellsable = (superClass) => { const MixedClass = class extends superClass { _applyCells(relevantChanges) { - const { cells } = relevantChanges; - - const { gap, item: itemProps } = this.props; + const cells = relevantChanges.cells ?? this.props.cells; + const { gap, item: itemProps, inactiveCellStrategy } = this.props; const requiredItemIds = new Set(); const childrenMap = new Map( @@ -17,11 +16,13 @@ export const Cellsable = (superClass) => { cells.forEach((row, rowIndex) => { row.forEach((col, colIndex) => { - if (!col) return; + const isInactive = !col; + if (isInactive && inactiveCellStrategy !== 'hide') return; + const id = `${this.id}.${rowIndex}.${colIndex}`; + const label = String(col); requiredItemIds.add(id); - const label = typeof col === 'string' ? col : ''; const existingItem = childrenMap.get(id); if (!existingItem) { const attrs = { @@ -30,10 +31,17 @@ export const Cellsable = (superClass) => { y: rowIndex * (itemProps.size.height + gap.y), }; const item = newElement('item', this.context); - item.apply({ type: 'item', id, ...itemProps, label, attrs }); + item.apply({ + type: 'item', + id, + ...itemProps, + label, + attrs, + show: !isInactive, + }); this.addChild(item); } else { - existingItem.apply({ label }); + existingItem.apply({ label, show: !isInactive }); } }); }); diff --git a/src/events/states/SelectionState.js b/src/events/states/SelectionState.js index 91e61d98..a37e1f63 100644 --- a/src/events/states/SelectionState.js +++ b/src/events/states/SelectionState.js @@ -86,6 +86,7 @@ export default class SelectionState extends State { 'onpointermove', 'onpointerup', 'onpointerover', + 'onpointerleave', 'onclick', 'rightclick', ]; @@ -236,6 +237,10 @@ export default class SelectionState extends State { }); } + onpointerleave(e) { + this.onpointerup(e); + } + #processClick(e, callback) { const currentPoint = this.viewport.toWorld(e.global); const isActuallyMoved =