Skip to content

Commit 5ae0e61

Browse files
committed
fix(workspace): revalidate jsPlumb links after element resize in general and master views
1 parent fa7db99 commit 5ae0e61

6 files changed

Lines changed: 274 additions & 54 deletions

File tree

client/src/features/general-recipe/ui/workspace/GeneralWorkspaceContent.vue

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
</template>
3535

3636
<script setup>
37-
import { onMounted, ref, computed, watch, nextTick } from 'vue';
37+
import { onBeforeUnmount, onMounted, ref, computed, watch, nextTick } from 'vue';
3838
import { newInstance, ready, EVENT_DRAG_STOP } from "@jsplumb/browser-ui";
3939
import { downloadTextFile } from "@/services/common/fileDownload";
4040
import {
@@ -46,6 +46,7 @@ import {
4646
import { buildGeneralWorkspaceHierarchy } from "@/services/workspace/mapping/generalWorkspaceHierarchy";
4747
import { normalizeConnection } from "@/services/workspace/core/connectionUtils";
4848
import { createDefaultDotEndpointDefinition } from "@/services/workspace/core/jsPlumbEndpointUtils";
49+
import { createJsPlumbElementLayoutObserver } from "@/services/workspace/core/jsPlumbLayoutObserverUtils";
4950
import { reconcileMaterialEndpoints } from "@/services/workspace/core/generalMaterialEndpointUtils";
5051
import {
5152
ensureParallelIndicatorDefaults,
@@ -78,6 +79,9 @@ const workspaceContentRef = ref(null);
7879
const jsplumbInstance = ref(null);
7980
const jsplumbElements = ref([]);
8081
const managedElements = ref({});
82+
const layoutObserver = createJsPlumbElementLayoutObserver({
83+
getInstance: () => jsplumbInstance.value,
84+
});
8185
const zoomLevel = ref(1);
8286
const initialWorkspaceWidth = 10000;
8387
const initialWorkspaceHeight = 10000;
@@ -117,7 +121,7 @@ onMounted(async () => {
117121
emit('update:workspace_items', updated);
118122
});
119123
120-
watch(computedWorkspaceItems, createUpdateItemListHandler(jsplumbInstance, jsplumbElements, managedElements), { deep: true });
124+
watch(computedWorkspaceItems, createUpdateItemListHandler(jsplumbInstance, jsplumbElements, managedElements, layoutObserver), { deep: true });
121125
watch(
122126
() => getMaterialTypeSignatures(computedWorkspaceItems.value),
123127
syncMaterialTypeChanges,
@@ -132,6 +136,11 @@ onMounted(async () => {
132136
});
133137
});
134138
139+
onBeforeUnmount(() => {
140+
stopPanning();
141+
layoutObserver.disconnect();
142+
});
143+
135144
const computedWorkspaceItems = computed({
136145
get: () => props.workspace_items,
137146
set: (newValue) => {
@@ -381,6 +390,7 @@ function syncMaterialEndpoints(instance, elementRef, item) {
381390
if (changed) {
382391
instance.revalidate?.(elementRef);
383392
instance.repaintEverything?.();
393+
layoutObserver.schedule(elementRef);
384394
}
385395
}
386396
@@ -600,6 +610,7 @@ function rebuildParallelIndicatorEndpoints(item) {
600610
jsplumbInstance.value.deleteConnectionsForElement(elementRef);
601611
jsplumbInstance.value.removeAllEndpoints(elementRef);
602612
addJsPlumbEndpoints(jsplumbInstance.value, elementRef, item);
613+
layoutObserver.observe(elementRef);
603614
jsplumbInstance.value.revalidate?.(elementRef);
604615
605616
connectionsToRestore.forEach((connection) => {
@@ -609,7 +620,7 @@ function rebuildParallelIndicatorEndpoints(item) {
609620
jsplumbInstance.value.repaintEverything?.();
610621
}
611622
612-
function createUpdateItemListHandler(instance, jsplumbElementsRef, managedElementsRef) {
623+
function createUpdateItemListHandler(instance, jsplumbElementsRef, managedElementsRef, layoutObserverRef) {
613624
return async () => {
614625
await nextTick();
615626
await nextTick();
@@ -623,12 +634,17 @@ function createUpdateItemListHandler(instance, jsplumbElementsRef, managedElemen
623634
(element) => element.id === item.id
624635
);
625636
if (!elementRef || managedElementsRef.value[item.id] === true) {
637+
if (elementRef) {
638+
layoutObserverRef.observe(elementRef);
639+
}
626640
return;
627641
}
628642
643+
layoutObserverRef.observe(elementRef);
629644
elementRef.style.left = `${item.x}px`;
630645
elementRef.style.top = `${item.y}px`;
631646
addJsPlumbEndpoints(instance.value, elementRef, item);
647+
layoutObserverRef.schedule(elementRef);
632648
managedElementsRef.value[item.id] = true;
633649
});
634650
};
@@ -752,6 +768,7 @@ async function clearWorkspace() {
752768
(element) => element.id === item.id
753769
);
754770
if (elementRef !== undefined) {
771+
layoutObserver.unobserve(elementRef);
755772
jsplumbInstance.value?.removeAllEndpoints(elementRef);
756773
}
757774
}
@@ -817,6 +834,8 @@ async function addElements(list) {
817834
elementRef.style.top = `${element.y}px`;
818835
await nextTick();
819836
await addJsPlumbEndpoints(jsplumbInstance.value, elementRef, element);
837+
layoutObserver.observe(elementRef);
838+
layoutObserver.schedule(elementRef);
820839
managedElements.value[element.id] = true;
821840
}
822841
}
@@ -828,6 +847,7 @@ function deleteElement(item) {
828847
(element) => element.id === item.id
829848
);
830849
if (elementRef !== undefined) {
850+
layoutObserver.unobserve(elementRef);
831851
jsplumbInstance.value.removeAllEndpoints(elementRef);
832852
jsplumbInstance.value.deleteConnectionsForElement(elementRef);
833853
elementRef.remove();

client/src/features/master-recipe/ui/workspace/MasterWorkspaceContent.vue

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
</template>
3737

3838
<script setup>
39-
import { onMounted, ref, computed, watch, nextTick } from 'vue';
39+
import { onBeforeUnmount, onMounted, ref, computed, watch, nextTick } from 'vue';
4040
import { newInstance, ready, EVENT_DRAG_STOP } from "@jsplumb/browser-ui";
4141
import { downloadTextFile } from "@/services/common/fileDownload";
4242
import {
@@ -45,6 +45,7 @@ import {
4545
importWorkspaceFile,
4646
} from "@/services/workspace";
4747
import { createDefaultDotEndpointDefinition } from "@/services/workspace/core/jsPlumbEndpointUtils";
48+
import { createJsPlumbElementLayoutObserver } from "@/services/workspace/core/jsPlumbLayoutObserverUtils";
4849
import { stringifyConditionGroup } from "@/services/recipe/master-recipe/conditions/conditionGroupUtils";
4950
const props = defineProps({
5051
main_workspace_items: Array,
@@ -62,6 +63,9 @@ const workspaceContentRef = ref(null)
6263
const jsplumbInstance = ref(null) //the jsplumb instance, this is a library which handles the drag and drop as well as the connections
6364
const jsplumbElements = ref([])
6465
const managedElements = ref({}) //object to mark to which elements Endpoints where already added. That why when detecting a change in workspace elemets we know which items are new
66+
const layoutObserver = createJsPlumbElementLayoutObserver({
67+
getInstance: () => jsplumbInstance.value,
68+
})
6569
const zoomLevel = ref(1)
6670
const initialWorkspaceWidth = 10000
6771
const initialWorkspaceHeight = 10000
@@ -100,12 +104,17 @@ onMounted(async () => {
100104
});
101105
102106
// Set up a watcher to handle further changes in the workspace items
103-
watch(computedWorkspaceItems, createUpdateItemListHandler(jsplumbInstance, jsplumbElements, managedElements), { deep: true });
107+
watch(computedWorkspaceItems, createUpdateItemListHandler(jsplumbInstance, jsplumbElements, managedElements, layoutObserver), { deep: true });
104108
watch(computedWorkspaceItems, updateWorkspaceBounds, { deep: true, immediate: true });
105109
});
106110
}
107111
});
108112
113+
onBeforeUnmount(() => {
114+
stopPanning();
115+
layoutObserver.disconnect();
116+
});
117+
109118
110119
// Create a computed property that represents the entire selectedElement
111120
// this is recommended solution to achieve two way binding between the parent and this child component
@@ -446,7 +455,7 @@ function checkEndpoints(instance, elementRef, item) {
446455
}
447456
}
448457
449-
function createUpdateItemListHandler(instance, jsplumbElements, managedElements) {
458+
function createUpdateItemListHandler(instance, jsplumbElements, managedElements, layoutObserverRef) {
450459
return async (newItems) => {
451460
console.debug("workspace_items updated, watcher triggered");
452461
await nextTick(); //wait one tick otherwise the new workspace item is not yet in jsplumbElements
@@ -465,16 +474,19 @@ function createUpdateItemListHandler(instance, jsplumbElements, managedElements)
465474
return; // onoly returns the pushedItems.forEach function, effectively working as a continue
466475
}
467476
477+
layoutObserverRef.observe(elementRef)
468478
if (managedElements.value[pushedItem.id] === true) {
469479
console.debug("pushed element already managed: ", pushedItem);
470480
checkEndpoints(instance.value, elementRef, pushedItem)
481+
layoutObserverRef.schedule(elementRef)
471482
return;
472483
}
473484
474485
console.debug("changed element not managed yet, placing in workspace and adding endpoints:", pushedItem);
475486
elementRef.style.left = pushedItem.x + "px";
476487
elementRef.style.top = pushedItem.y + "px";
477488
addJsPlumbEndpoints(instance.value, elementRef, pushedItem);
489+
layoutObserverRef.schedule(elementRef)
478490
managedElements.value[pushedItem.id] = true;
479491
});
480492
};
@@ -591,6 +603,7 @@ async function clearWorkspace() {
591603
(element) => element.id === item.id
592604
);
593605
if (elementRef !== undefined) {
606+
layoutObserver.unobserve(elementRef);
594607
jsplumbInstance.value.removeAllEndpoints(elementRef);
595608
}
596609
}
@@ -644,6 +657,8 @@ async function addElements(list) {
644657
645658
// Add the necessary jsPlumb endpoints for this element
646659
await addJsPlumbEndpoints(jsplumbInstance.value, elementRef, element);
660+
layoutObserver.observe(elementRef);
661+
layoutObserver.schedule(elementRef);
647662
}
648663
}
649664
}
@@ -657,6 +672,7 @@ function deleteElement(item) {
657672
(element) => element.id === item.id
658673
);
659674
if (elementRef !== undefined) {
675+
layoutObserver.unobserve(elementRef);
660676
jsplumbInstance.value.removeAllEndpoints(elementRef);
661677
jsplumbInstance.value.deleteConnectionsForElement(elementRef)
662678
elementRef.remove();
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
export function createJsPlumbElementLayoutObserver({ getInstance } = {}) {
2+
const observedElements = new Set();
3+
const pendingElements = new Set();
4+
let resizeObserver = null;
5+
let flushTimer = null;
6+
7+
function flushPendingElements() {
8+
flushTimer = null;
9+
10+
if (pendingElements.size === 0) {
11+
return;
12+
}
13+
14+
const instance = getInstance?.();
15+
const elements = Array.from(pendingElements);
16+
pendingElements.clear();
17+
18+
if (!instance) {
19+
return;
20+
}
21+
22+
elements.forEach((element) => {
23+
if (!element?.isConnected) {
24+
return;
25+
}
26+
instance.revalidate?.(element);
27+
});
28+
29+
instance.repaintEverything?.();
30+
}
31+
32+
function schedule(element) {
33+
if (!element) {
34+
return;
35+
}
36+
37+
pendingElements.add(element);
38+
if (flushTimer !== null) {
39+
return;
40+
}
41+
42+
flushTimer = setTimeout(flushPendingElements, 0);
43+
}
44+
45+
function ensureResizeObserver() {
46+
if (resizeObserver || typeof ResizeObserver !== "function") {
47+
return;
48+
}
49+
50+
resizeObserver = new ResizeObserver((entries) => {
51+
entries.forEach((entry) => schedule(entry.target));
52+
});
53+
}
54+
55+
function observe(element) {
56+
if (!element || observedElements.has(element)) {
57+
return;
58+
}
59+
60+
observedElements.add(element);
61+
ensureResizeObserver();
62+
resizeObserver?.observe(element);
63+
schedule(element);
64+
}
65+
66+
function unobserve(element) {
67+
if (!element || !observedElements.has(element)) {
68+
return;
69+
}
70+
71+
observedElements.delete(element);
72+
pendingElements.delete(element);
73+
resizeObserver?.unobserve(element);
74+
}
75+
76+
function disconnect() {
77+
if (flushTimer !== null) {
78+
clearTimeout(flushTimer);
79+
flushTimer = null;
80+
}
81+
82+
pendingElements.clear();
83+
observedElements.clear();
84+
resizeObserver?.disconnect();
85+
resizeObserver = null;
86+
}
87+
88+
return {
89+
observe,
90+
unobserve,
91+
schedule,
92+
disconnect,
93+
};
94+
}

0 commit comments

Comments
 (0)