Add Promise queue to prevent overlapping dialogue boxes #841 (#1002)

This commit is contained in:
Jaifroid 2023-05-11 20:39:54 +01:00 committed by GitHub
parent cdcbc9f046
commit 3277f85d37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 135 additions and 86 deletions

View File

@ -24,14 +24,14 @@
// DEV: Put your RequireJS definition in the rqDef array below, and any function exports in the function parenthesis of the define statement // DEV: Put your RequireJS definition in the rqDef array below, and any function exports in the function parenthesis of the define statement
// We need to do it this way in order to load WebP polyfills conditionally. The WebP polyfills are only needed by a few old browsers, so loading them // We need to do it this way in order to load WebP polyfills conditionally. The WebP polyfills are only needed by a few old browsers, so loading them
// only if needed saves approximately 1MB of memory. // only if needed saves approximately 1MB of memory.
var rqDef = ['settingsStore']; var rqDef = ['settingsStore', 'util'];
// Add WebP polyfill only if webpHero was loaded in init.js // Add WebP polyfill only if webpHero was loaded in init.js
if (webpMachine) { if (webpMachine) {
rqDef.push('webpHeroBundle'); rqDef.push('webpHeroBundle');
} }
define(rqDef, function(settingsStore) { define(rqDef, function(settingsStore, util) {
/** /**
* Displays a Bootstrap alert or confirm dialog box depending on the options provided * Displays a Bootstrap alert or confirm dialog box depending on the options provided
@ -49,95 +49,97 @@ define(rqDef, function(settingsStore) {
approveConfirmLabel = approveConfirmLabel || 'Confirm'; approveConfirmLabel = approveConfirmLabel || 'Confirm';
closeMessageLabel = closeMessageLabel || 'Okay'; closeMessageLabel = closeMessageLabel || 'Okay';
label = label || (isConfirm ? 'Confirmation' : 'Message'); label = label || (isConfirm ? 'Confirmation' : 'Message');
return new Promise(function (resolve, reject) { return util.PromiseQueue.enqueue(function () {
if (!message) reject('Missing body message'); return new Promise(function (resolve, reject) {
// Set the text to the modal and its buttons if (!message) reject('Missing body message');
document.getElementById('approveConfirm').textContent = approveConfirmLabel; // Set the text to the modal and its buttons
document.getElementById('declineConfirm').textContent = declineConfirmLabel; document.getElementById('approveConfirm').textContent = approveConfirmLabel;
document.getElementById('closeMessage').textContent = closeMessageLabel; document.getElementById('declineConfirm').textContent = declineConfirmLabel;
document.getElementById('modalLabel').textContent = label; document.getElementById('closeMessage').textContent = closeMessageLabel;
// Using innerHTML to set the message to allow HTML formatting document.getElementById('modalLabel').textContent = label;
document.getElementById('modalText').innerHTML = message; // Using innerHTML to set the message to allow HTML formatting
// Display buttons acc to the type of alert document.getElementById('modalText').innerHTML = message;
document.getElementById('approveConfirm').style.display = isConfirm ? 'inline' : 'none'; // Display buttons acc to the type of alert
document.getElementById('declineConfirm').style.display = isConfirm ? 'inline' : 'none'; document.getElementById('approveConfirm').style.display = isConfirm ? 'inline' : 'none';
document.getElementById('closeMessage').style.display = isConfirm ? 'none' : 'inline'; document.getElementById('declineConfirm').style.display = isConfirm ? 'inline' : 'none';
// Display the modal document.getElementById('closeMessage').style.display = isConfirm ? 'none' : 'inline';
const modal = document.querySelector('#alertModal'); // Display the modal
const backdrop = document.createElement('div'); const modal = document.querySelector('#alertModal');
backdrop.classList.add('modal-backdrop'); const backdrop = document.createElement('div');
document.body.appendChild(backdrop); backdrop.classList.add('modal-backdrop');
document.body.appendChild(backdrop);
// Show the modal // Show the modal
document.body.classList.add('modal-open'); document.body.classList.add('modal-open');
modal.classList.add('show'); modal.classList.add('show');
modal.style.display = 'block'; modal.style.display = 'block';
backdrop.classList.add('show'); backdrop.classList.add('show');
// Set the ARIA attributes for the modal // Set the ARIA attributes for the modal
modal.setAttribute('aria-hidden', 'false'); modal.setAttribute('aria-hidden', 'false');
modal.setAttribute('aria-modal', 'true'); modal.setAttribute('aria-modal', 'true');
modal.setAttribute('role', 'dialog'); modal.setAttribute('role', 'dialog');
// Hide modal handlers // Hide modal handlers
var closeModalHandler = function () { var closeModalHandler = function () {
document.body.classList.remove('modal-open'); document.body.classList.remove('modal-open');
modal.classList.remove('show'); modal.classList.remove('show');
modal.style.display = 'none'; modal.style.display = 'none';
backdrop.classList.remove('show'); backdrop.classList.remove('show');
if(Array.from(document.body.children).indexOf(backdrop)>=0){ if(Array.from(document.body.children).indexOf(backdrop)>=0){
document.body.removeChild(backdrop); document.body.removeChild(backdrop);
}
//remove event listeners
document.getElementById('modalCloseBtn').removeEventListener('click', close);
document.getElementById('declineConfirm').removeEventListener('click', close);
document.getElementById('closeMessage').removeEventListener('click', close);
document.getElementById('approveConfirm').removeEventListener('click', closeConfirm);
modal.removeEventListener('click', close);
document.getElementsByClassName('modal-dialog')[0].removeEventListener('click', stopOutsideModalClick);
modal.removeEventListener('keyup', keyHandler);
};
// function to call when modal is closed
var close = function () {
closeModalHandler();
resolve(false);
};
var closeConfirm = function () {
closeModalHandler();
resolve(true);
};
var stopOutsideModalClick = function (e) {
e.stopPropagation();
};
var keyHandler = function (e) {
if (/Enter/.test(e.key)) {
// We need to focus before clicking the button, because the handler above is based on document.activeElement
if (isConfirm) {
document.getElementById('approveConfirm').focus();
document.getElementById('approveConfirm').click();
} else {
document.getElementById('closeMessage').focus();
document.getElementById('closeMessage').click();
} }
} else if (/Esc/.test(e.key)) { //remove event listeners
document.getElementById('modalCloseBtn').focus(); document.getElementById('modalCloseBtn').removeEventListener('click', close);
document.getElementById('modalCloseBtn').click(); document.getElementById('declineConfirm').removeEventListener('click', close);
} document.getElementById('closeMessage').removeEventListener('click', close);
}; document.getElementById('approveConfirm').removeEventListener('click', closeConfirm);
modal.removeEventListener('click', close);
document.getElementsByClassName('modal-dialog')[0].removeEventListener('click', stopOutsideModalClick);
modal.removeEventListener('keyup', keyHandler);
};
// When hide modal is called, resolve promise with true if hidden using approve button, false otherwise // function to call when modal is closed
document.getElementById('modalCloseBtn').addEventListener('click', close); var close = function () {
document.getElementById('declineConfirm').addEventListener('click', close); closeModalHandler();
document.getElementById('closeMessage').addEventListener('click', close); resolve(false);
document.getElementById('approveConfirm').addEventListener('click', closeConfirm); };
var closeConfirm = function () {
modal.addEventListener('click', close); closeModalHandler();
document.getElementsByClassName('modal-dialog')[0].addEventListener('click', stopOutsideModalClick); resolve(true);
};
var stopOutsideModalClick = function (e) {
e.stopPropagation();
};
var keyHandler = function (e) {
if (/Enter/.test(e.key)) {
// We need to focus before clicking the button, because the handler above is based on document.activeElement
if (isConfirm) {
document.getElementById('approveConfirm').focus();
document.getElementById('approveConfirm').click();
} else {
document.getElementById('closeMessage').focus();
document.getElementById('closeMessage').click();
}
} else if (/Esc/.test(e.key)) {
document.getElementById('modalCloseBtn').focus();
document.getElementById('modalCloseBtn').click();
}
};
modal.addEventListener('keyup', keyHandler); // When hide modal is called, resolve promise with true if hidden using approve button, false otherwise
// Set focus to the first focusable element inside the modal document.getElementById('modalCloseBtn').addEventListener('click', close);
modal.focus(); document.getElementById('declineConfirm').addEventListener('click', close);
document.getElementById('closeMessage').addEventListener('click', close);
document.getElementById('approveConfirm').addEventListener('click', closeConfirm);
modal.addEventListener('click', close);
document.getElementsByClassName('modal-dialog')[0].addEventListener('click', stopOutsideModalClick);
modal.addEventListener('keyup', keyHandler);
// Set focus to the first focusable element inside the modal
modal.focus();
});
}); });
} }

View File

@ -194,6 +194,52 @@ define([], function() {
} }
/** /**
* Queues Promise Factories* to be resolved or rejected sequentially. This helps to avoid overlapping Promise functions.
* Primarily used by uiUtil.systemAlert, to prevent alerts showing while others are being displayed.
*
* *A Promise Factory is merely a Promise wrapped in a function to prevent it from executing immediately. E.g. to use
* this function with a Promise, call it like this (or, more likely, use your own pre-wrapped Promise):
*
* return util.PromiseQueue.enqueue(function () {
* return new Promise(function (resolve, reject) { ... });
* });
*
* Adapted from https://medium.com/@karenmarkosyan/how-to-manage-promises-into-dynamic-queue-with-vanilla-javascript-9d0d1f8d4df5
*
* @type {Object} PromiseQueue
* @property {Function} enqueue Queues a Promise Factory. Call this function repeatedly to queue Promises sequentially
* @param {Function<Promise>} promiseFactory A Promise wrapped in an ordinary function
* @returns {Promise} A Promise that resolves or rejects with the resolved/rejected value of the Promise Factory
*/
var PromiseQueue = {
_queue: [],
_working: false,
enqueue: function (promiseFactory) {
var that = this;
return new Promise(function (resolve, reject) {
that._queue.push({promise: promiseFactory, resolve: resolve, reject: reject});
if (!that._working) that._dequeue();
});
},
_dequeue: function () {
this._working = true;
var that = this;
var deferred = this._queue.shift();
if (!deferred) {
this._working = false;
return false;
}
return deferred.promise().then(function (val) {
deferred.resolve(val);
}).catch(function (err) {
deferred.reject(err);
}).finally(function () {
return that._dequeue();
});
}
};
/*
* Functions and classes exposed by this module * Functions and classes exposed by this module
*/ */
return { return {
@ -203,6 +249,7 @@ define([], function() {
readFloatFrom4Bytes: readFloatFrom4Bytes, readFloatFrom4Bytes: readFloatFrom4Bytes,
readFileSlice: readFileSlice, readFileSlice: readFileSlice,
binarySearch: binarySearch, binarySearch: binarySearch,
leftShift: leftShift leftShift: leftShift,
PromiseQueue: PromiseQueue
}; };
}); });