From ac0533ae58e6cdb219522cf260f5e93596e3ca55 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Tue, 30 Dec 2025 11:53:58 +0900 Subject: [PATCH 1/4] feat: add inactiveCellStrategy to Grid to support hiding inactive cells --- src/display/data-schema/element-schema.js | 1 + src/display/mixins/Cellsable.js | 25 +++++++++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) 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/mixins/Cellsable.js b/src/display/mixins/Cellsable.js index d5e6524a..9c486efe 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,14 @@ export const Cellsable = (superClass) => { cells.forEach((row, rowIndex) => { row.forEach((col, colIndex) => { - if (!col) return; const id = `${this.id}.${rowIndex}.${colIndex}`; + const isInactive = !col; + const label = String(col); + + if (isInactive && inactiveCellStrategy !== 'hide') return; + requiredItemIds.add(id); - const label = typeof col === 'string' ? col : ''; const existingItem = childrenMap.get(id); if (!existingItem) { const attrs = { @@ -30,10 +32,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 }); } }); }); From c58a26346c52706496ae4e8cc2c0ba5bba81a3e1 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Tue, 30 Dec 2025 11:54:51 +0900 Subject: [PATCH 2/4] fix data.d.ts --- src/display/data-schema/data.d.ts | 1 + 1 file changed, 1 insertion(+) 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[]; From 81b7a08732fcf6da1ae44710dcaccee84ac20887 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Tue, 30 Dec 2025 12:10:16 +0900 Subject: [PATCH 3/4] fix --- src/display/mixins/Cellsable.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/display/mixins/Cellsable.js b/src/display/mixins/Cellsable.js index 9c486efe..daa26118 100644 --- a/src/display/mixins/Cellsable.js +++ b/src/display/mixins/Cellsable.js @@ -16,12 +16,11 @@ export const Cellsable = (superClass) => { cells.forEach((row, rowIndex) => { row.forEach((col, colIndex) => { - const id = `${this.id}.${rowIndex}.${colIndex}`; const isInactive = !col; - const label = String(col); - if (isInactive && inactiveCellStrategy !== 'hide') return; + const id = `${this.id}.${rowIndex}.${colIndex}`; + const label = String(col); requiredItemIds.add(id); const existingItem = childrenMap.get(id); From 568fa6987f10bf375ba5615a77d4c5c963fe34c2 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Tue, 30 Dec 2025 16:35:20 +0900 Subject: [PATCH 4/4] fix bug --- src/display/elements/Relations.js | 4 +++- src/events/states/SelectionState.js | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) 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/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 =