Skip to content

Commit 902fee4

Browse files
committed
Fixed PrimeVue's BlockUI's mask handling so overlay appears once and never gets stuck.
1 parent 84ab25b commit 902fee4

File tree

2 files changed

+99
-32
lines changed

2 files changed

+99
-32
lines changed

src/renderer/src/components/OpenCOR.vue

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
2-
<BlockUI ref="blockUiRef" class="opencor overflow-hidden h-full" :class="isFullWebApp ? 'with-main-menu' : ''"
3-
:blocked="blockUiBlocked"
2+
<SafeBlockUI ref="safeBlockUiRef" class="opencor overflow-hidden h-full" :class="isFullWebApp ? 'with-main-menu' : ''"
3+
:blocked="compBlockUiEnabled"
44
@click="activateInstance"
55
@focus="activateInstance"
66
@focusin="activateInstance"
@@ -98,7 +98,7 @@
9898
@close="webUpdateAvailableVisible = false"
9999
/>
100100
</div>
101-
</BlockUI>
101+
</SafeBlockUI>
102102
</template>
103103

104104
<script setup lang="ts">
@@ -110,7 +110,6 @@ import firebase from 'firebase/compat/app';
110110
import 'firebase/compat/auth';
111111
import { Octokit } from 'octokit';
112112
*/
113-
import BlockUI from 'primevue/blockui';
114113
import primeVueConfig from 'primevue/config';
115114
import primeVueConfirmationService from 'primevue/confirmationservice';
116115
import primeVueToastService from 'primevue/toastservice';
@@ -122,7 +121,7 @@ import type { IOpenCOREmits, IOpenCORProps } from '../../index.ts';
122121
import '../assets/app.css';
123122
import '../assets/primeicons-assets.ts';
124123
import * as common from '../common/common.ts';
125-
import { FULL_URI_SCHEME, LONG_DELAY, NO_DELAY, SHORT_DELAY, TOAST_LIFE } from '../common/constants.ts';
124+
import { FULL_URI_SCHEME, LONG_DELAY, SHORT_DELAY, TOAST_LIFE } from '../common/constants.ts';
126125
import * as dependencies from '../common/dependencies.ts';
127126
import { electronApi } from '../common/electronApi.ts';
128127
/* TODO: enable once our GitHub integration is fully ready.
@@ -136,6 +135,7 @@ import ContentsComponent from '../components/ContentsComponent.vue';
136135
import * as locApi from '../libopencor/locApi.ts';
137136
138137
import { provideDialogState } from './dialogs/BaseDialog.vue';
138+
import SafeBlockUI from './widgets/SafeBlockUI.vue';
139139
import MainMenu from './MainMenu.vue';
140140
141141
const props = defineProps<IOpenCORProps>();
@@ -191,7 +191,7 @@ const emitSimulationData = (): void => {
191191
192192
const { isDialogActive } = provideDialogState();
193193
194-
const blockUiRef = vue.ref<InstanceType<typeof BlockUI> | null>(null);
194+
const safeBlockUiRef = vue.ref<InstanceType<typeof SafeBlockUI> | null>(null);
195195
const mainMenuRef = vue.ref<InstanceType<typeof MainMenu> | null>(null);
196196
const filesRef = vue.ref<HTMLElement | null>(null);
197197
const contentsRef = vue.ref<InstanceType<typeof ContentsComponent> | null>(null);
@@ -247,35 +247,25 @@ const compUiEnabled = vue.computed<boolean>(() => {
247247
return !compBlockUiEnabled.value && !isDialogActive.value;
248248
});
249249
250-
// Block/unblock the UI with "no" (i.e. a bit of a) delay to ensure that it doesn't happen too quickly (e.g., when
251-
// loading a model from a COMBINE archive, we don't want the UI to be blocked for a split second while the file is
252-
// being processed and the Simulation Experiment view is being shown in isolation).
253-
254-
const blockUiBlocked = vue.ref<boolean>(true);
255-
let blockUiBlockedTimeoutId: number | undefined;
250+
// Remove any leftover SafeBlockUI masks when unblocking.
251+
// Note: this is to ensure that we don't end up with leftover masks if the block event is emitted multiple times before
252+
// the unblock event is emitted, which can lead to multiple masks being created and not properly removed.
256253
257254
vue.watch(
258255
compBlockUiEnabled,
259256
(newCompBlockUiEnabled: boolean) => {
260-
if (blockUiBlockedTimeoutId !== undefined) {
261-
window.clearTimeout(blockUiBlockedTimeoutId);
257+
if (!newCompBlockUiEnabled) {
258+
const safeBlockUi = safeBlockUiRef.value as unknown as InstanceType<typeof SafeBlockUI> | null;
259+
const safeBlockUiRoot = safeBlockUi?.$el as HTMLElement | undefined;
262260
263-
blockUiBlockedTimeoutId = undefined;
261+
for (const mask of safeBlockUiRoot?.querySelectorAll('.p-blockui-mask') ?? []) {
262+
mask.remove();
263+
}
264264
}
265-
266-
blockUiBlockedTimeoutId = window.setTimeout(() => {
267-
blockUiBlocked.value = newCompBlockUiEnabled;
268-
}, NO_DELAY);
269265
},
270266
{ immediate: true }
271267
);
272268
273-
vue.onUnmounted(() => {
274-
if (blockUiBlockedTimeoutId !== undefined) {
275-
window.clearTimeout(blockUiBlockedTimeoutId);
276-
}
277-
});
278-
279269
// Determine whether we are running the full Web app (i.e. not in isolation).
280270
281271
const isFullWebApp = vue.computed<boolean>(() => {
@@ -898,7 +888,7 @@ electronApi?.onSelect((filePath: string) => {
898888
// A few things that can only be done when the component is mounted.
899889
900890
vue.onMounted(() => {
901-
const blockUiElement = (blockUiRef.value as unknown as { $el: HTMLElement })?.$el;
891+
const safeBlockUiElement = (safeBlockUiRef.value as unknown as { $el: HTMLElement })?.$el;
902892
903893
// Make ourselves the active instance.
904894
@@ -911,8 +901,8 @@ vue.onMounted(() => {
911901
setTimeout(() => {
912902
const toastElement = document.getElementById(toastId.value);
913903
914-
if (toastElement && blockUiElement && toastElement.parentElement !== blockUiElement) {
915-
blockUiElement.appendChild(toastElement);
904+
if (toastElement && safeBlockUiElement && toastElement.parentElement !== safeBlockUiElement) {
905+
safeBlockUiElement.appendChild(toastElement);
916906
}
917907
}, SHORT_DELAY);
918908
});
@@ -928,10 +918,14 @@ vue.onMounted(() => {
928918
929919
void vue.nextTick(() => {
930920
const mainMenuElement = (mainMenuRef.value as unknown as { $el: HTMLElement })?.$el;
931-
const blockUiElement = (blockUiRef.value as unknown as { $el: HTMLElement })?.$el;
932-
933-
if (mainMenuElement && blockUiElement) {
934-
stopTrackingMainMenuHeight = vueCommon.trackElementHeight(mainMenuElement, blockUiElement, '--main-menu-height');
921+
const safeBlockUiElement = (safeBlockUiRef.value as unknown as { $el: HTMLElement })?.$el;
922+
923+
if (mainMenuElement && safeBlockUiElement) {
924+
stopTrackingMainMenuHeight = vueCommon.trackElementHeight(
925+
mainMenuElement,
926+
safeBlockUiElement,
927+
'--main-menu-height'
928+
);
935929
}
936930
});
937931
});
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<script lang="ts">
2+
import { ZIndex } from '@primeuix/utils/zindex';
3+
4+
import BlockUI from 'primevue/blockui';
5+
import * as vue from 'vue';
6+
7+
// This component is a wrapper around PrimeVue's BlockUI. It provides a safe implementation of the block and unblock
8+
// methods that ensures that we don't attempt to block if we are already blocked, and that we properly remove the mask
9+
// when unblocking. This is necessary because PrimeVue's BlockUI does not account for the case where multiple block
10+
// events are emitted before the unblock event is emitted, which can lead to multiple masks being created and not
11+
// properly removed.
12+
13+
export default vue.defineComponent({
14+
extends: BlockUI as unknown as vue.ComponentOptions<typeof BlockUI>,
15+
setup: () => {
16+
const instance = vue.getCurrentInstance();
17+
const self = instance?.proxy as unknown as vue.ComponentOptions<typeof BlockUI>;
18+
19+
const block = (): void => {
20+
// Don't attempt to block if we are already blocked.
21+
// Note: this can happen if the block event is emitted multiple times before the unblock event is emitted. In
22+
// that case, we want to ignore subsequent block events until the unblock event is emitted.
23+
24+
if (self.isBlocked) {
25+
return;
26+
}
27+
28+
// Create and attach mask as PrimeVue does.
29+
// Note: we don't account for the case where we are in full screen mode since that is not a use case for us.
30+
31+
const mask = document.createElement('div');
32+
33+
mask.className = 'p-blockui-mask p-overlay-mask p-overlay-mask-enter';
34+
35+
(self.$refs.container as HTMLElement | null)?.appendChild(mask);
36+
37+
ZIndex.set('modal', mask, self.baseZIndex + self.$primevue.config.zIndex.modal);
38+
39+
self.mask = mask;
40+
self.isBlocked = true;
41+
42+
self.$emit('block');
43+
};
44+
45+
const removeMask = (): void => {
46+
// Don't attempt to remove the mask if it doesn't exist.
47+
// Note: this can happen if the unblock event is emitted multiple times before the block event is emitted, which
48+
// can lead to multiple unblock events being emitted without a corresponding block event.
49+
50+
if (self.mask == null) {
51+
return;
52+
}
53+
54+
// Remove the mask as PrimeVue does.
55+
// Note: we don't account for the case where we are in full screen mode since that is not a use case for us.
56+
57+
self.mask.remove();
58+
59+
ZIndex.clear(self.mask);
60+
61+
self.mask = null;
62+
self.isBlocked = false;
63+
64+
self.$emit('unblock');
65+
};
66+
67+
return {
68+
block,
69+
removeMask
70+
};
71+
}
72+
});
73+
</script>

0 commit comments

Comments
 (0)