diff --git a/LesionTracker/client/components/app/app.js b/LesionTracker/client/components/app/app.js index 4e1cd0f0e..6f795067a 100644 --- a/LesionTracker/client/components/app/app.js +++ b/LesionTracker/client/components/app/app.js @@ -1,3 +1,5 @@ +import { OHIF } from 'meteor/ohif:core'; + const studylistContentId = 'studylistTab'; const viewerContentId = 'viewerTab'; @@ -19,11 +21,21 @@ Template.app.events({ 'click .js-toggle-studyList'() { const contentId = Session.get('activeContentId'); - if (contentId !== studylistContentId) { - switchToTab(studylistContentId); - } else { - switchToTab(viewerContentId); - } + OHIF.ui.unsavedChanges.checkBeforeAction('viewer.*', function(shouldProceed, hasChanges) { + if (shouldProceed) { + // Drop signaled unsaved changes if any... + if (hasChanges) { + OHIF.ui.unsavedChanges.clear('viewer.*'); + } + + if (contentId !== studylistContentId) { + switchToTab(studylistContentId); + } else { + switchToTab(viewerContentId); + } + } + }); + } }); diff --git a/Packages/ohif-core/client/ui/unsavedChanges/unsavedChanges.js b/Packages/ohif-core/client/ui/unsavedChanges/unsavedChanges.js index a52383551..9db53b180 100644 --- a/Packages/ohif-core/client/ui/unsavedChanges/unsavedChanges.js +++ b/Packages/ohif-core/client/ui/unsavedChanges/unsavedChanges.js @@ -1,10 +1,12 @@ import { OHIF } from 'meteor/ohif:core'; import { _ } from 'meteor/underscore'; -OHIF.ui.unsavedChanges = (function() { +OHIF.ui.unsavedChanges = (function(OHIF, _) { + // Root of the internal Namespace tree. const rootTree = {}; + // Create an unattached namespace node be later appended to the Namespace tree. function createNode(name, hasChildren) { let node = { name }; @@ -21,13 +23,14 @@ OHIF.ui.unsavedChanges = (function() { } + // Attach a new node (identified by a namespace string) to the Namespace tree or increment its internal signal count. function addNode(tree, path) { let node, result = false, name = path.shift(); - if (name !== '*') { // "*" is a special name which means "all children" + if (name !== '*') { // "*" is a special name which means "all children" and thus CANNOT be used. if (name in tree) { node = tree[name]; if (path.length > 0) { @@ -49,6 +52,8 @@ OHIF.ui.unsavedChanges = (function() { } + // Detach a node (identified by a namespace string) from the internal Namespace tree consequently clearing its internal signal count. + // ... The supplied namespace can be a wildcard, in which case all sub-nodes are removed as well. function removeNode(tree, path) { let result = false, @@ -78,6 +83,8 @@ OHIF.ui.unsavedChanges = (function() { } + // Count the amount of signals registered for a given node. + // ... The supplied namespace can be a wildcard, in which case the resulting value will consider the counter of all sub-nodes. function probeNode(tree, path) { let node, @@ -115,29 +122,89 @@ OHIF.ui.unsavedChanges = (function() { // return the exposed interface of UnsavedChanges object return { + + /** + * Signal an unsaved change for a given namespace. + * @param {String} name A string (e.g., "viewer.studyViewer.measurements.targets") that identifies the namespace of the signaled changes. + * @return {Boolean} Returns false if the signal could not be saved or the supplied namespace is invalid. Otherwise, true is returned. + */ set: function(name) { return typeof name === 'string' ? addNode(rootTree, name.split('.')) : false; }, + + /** + * Clear all signaled unsaved changes for a given namespace. If the supplied namespace is a wildcard, all signals below that namespace + * are cleared. + * @param {String} name A string that identifies the namespace of the signaled changes (e.g., "viewer.studyViewer.measurements.targets" + * for clearing the "targets" item of the "viewer.studyViewer.measurements" namespace or "viewer.studyViewer.*" to specify all signaled + * changes for the "viewer.studyViewer" namespace). + * @return {Boolean} Returns false if the signal could not be removed or the supplied namespace is invalid. Otherwise, true is returned. + */ clear: function(name) { return typeof name === 'string' ? removeNode(rootTree, name.split('.')) : false; }, + + /** + * Count the amount of signaled unsaved changes for a given namespace. If the supplied namespace is a wildcard, all signals below that + * namespace will also be accounted. + * @param {String} name A string that identifies the namespace of the signaled changes (e.g., "viewer.studyViewer.measurements.targets" + * for counting the amount of signals for the "targets" item of the "viewer.studyViewer.measurements" namespace or "viewer.studyViewer.*" + * to count all signaled changes for the "viewer.studyViewer" namespace). + * @return {Number} Returns the amount of signaled changes for a given namespace. If the supplied namespace is a wildcard, the sum of all + * changes for that namespace are returned. + */ probe: function(name) { return typeof name === 'string' ? probeNode(rootTree, name.split('.')) : 0; }, - check: function(name, options) { - return new Promise((resolve, reject) => { - let probe = this.probe(name); - if (probe > 0) { - let dialogOptions = _.extend({ - title: 'You have unsaved changes!', - message: "Your changes will be lost if you don't save them... Are you sure you want to proceed?" - }, options); - OHIF.ui.showFormDialog('dialogConfirm', dialogOptions).then(resolve, reject); - } else { - resolve(); - } - }); + + /** + * UI utility that presents a confirmation dialog to the user if any unsaved changes where sinaled for the given namespace. + * @param {String} name A string that identifies the namespace of the signaled changes (e.g., "viewer.studyViewer.measurements.targets" + * for considering only the signals for the "targets" item of the "viewer.studyViewer.measurements" namespace or "viewer.studyViewer.*" + * to consider all signaled changes for the "viewer.studyViewer" namespace). + * @param {Function} callback A callback function (e.g, function(shouldProceed, hasChanges) { ... }) that will be executed after assessment. + * Upon execution, the callback will receive two boolean arguments (shouldProceed and hasChanges) indicating if the action can be performed + * or not and if changes that need to be cleared exist. + * @param {Object} options (Optional) An object with UI presentation options. + * @param {String} options.title The string that will be used as a title for confirmation dialog. + * @param {String} options.message The string that will be used as a message for confirmation dialog. + * @return {void} No value is returned. + */ + checkBeforeAction: function(name, callback, options) { + + let probe, hasChanges, shouldProceed; + + if (typeof callback !== 'function') { + // nothing to do if no callback function is supplied... + return; + } + + probe = this.probe(name); + if (probe > 0) { + // Unsaved changes exist... + hasChanges = true; + let dialogOptions = _.extend({ + title: 'You have unsaved changes!', + message: "Your changes will be lost if you don't save them before leaving the current page... Are you sure you want to proceed?" + }, options); + OHIF.ui.showFormDialog('dialogConfirm', dialogOptions).then(function() { + // Unsaved changes exist but user confirms action... + shouldProceed = true; + callback.call(null, shouldProceed, hasChanges); + }, function() { + // Unsaved changes exist and user does NOT confirm action... + shouldProceed = false; + callback.call(null, shouldProceed, hasChanges); + }); + } else { + // No unsaved changes, action can be performed... + hasChanges = false; + shouldProceed = true; + callback.call(null, shouldProceed, hasChanges); + } + } + }; -}()); +}(OHIF, _)); diff --git a/Packages/ohif-measurements/client/components/caseProgress/caseProgress.js b/Packages/ohif-measurements/client/components/caseProgress/caseProgress.js index c88b02c2b..1442b0495 100644 --- a/Packages/ohif-measurements/client/components/caseProgress/caseProgress.js +++ b/Packages/ohif-measurements/client/components/caseProgress/caseProgress.js @@ -132,12 +132,15 @@ Template.caseProgress.events({ return; } - switchToTab('studylistTab'); - const timepointApi = instance.data.timepointApi; const timepoints = timepointApi.all(); OHIF.log.info('Saving Measurements for timepoints:') OHIF.log.info(timepoints); instance.data.measurementApi.storeMeasurements(timepoints); + + // Clear signaled unsaved changes... + OHIF.ui.unsavedChanges.clear('viewer.studyViewer.measurements.*'); + + switchToTab('studylistTab'); } }); diff --git a/Packages/ohif-measurements/client/lib/MeasurementHandlers.js b/Packages/ohif-measurements/client/lib/MeasurementHandlers.js index 145f1a914..d96978374 100644 --- a/Packages/ohif-measurements/client/lib/MeasurementHandlers.js +++ b/Packages/ohif-measurements/client/lib/MeasurementHandlers.js @@ -81,6 +81,9 @@ class MeasurementHandlers { // Insert the new measurement into the collection measurementData._id = Collection.insert(measurement); + // Signal unsaved changes + OHIF.ui.unsavedChanges.set('viewer.studyViewer.measurements.' + measurementToolConfiguration.id); + // Update the Overall Measurement Numbers for all Measurements if (timepointApi) { const baseline = timepointApi.baseline(); @@ -125,6 +128,9 @@ class MeasurementHandlers { Collection.update(measurementId, { $set: measurement }); + + // Signal unsaved changes + OHIF.ui.unsavedChanges.set('viewer.studyViewer.measurements.' + measurementToolConfiguration.id); } static onRemoved(e, instance, eventData) { @@ -146,6 +152,9 @@ class MeasurementHandlers { Collection.remove(measurementData._id); + // Signal unsaved changes + OHIF.ui.unsavedChanges.set('viewer.studyViewer.measurements.' + measurementToolConfiguration.id); + // Update the Overall Measurement Numbers for all Measurements const timepointApi = instance.data.timepointApi; if (timepointApi) { diff --git a/Packages/ohif-viewerbase/client/components/viewer/viewerMain/viewerMain.js b/Packages/ohif-viewerbase/client/components/viewer/viewerMain/viewerMain.js index 307752cee..ccea34560 100644 --- a/Packages/ohif-viewerbase/client/components/viewer/viewerMain/viewerMain.js +++ b/Packages/ohif-viewerbase/client/components/viewer/viewerMain/viewerMain.js @@ -1,4 +1,5 @@ import { OHIF } from 'meteor/ohif:core'; +import { unloadHandlers } from '../../../lib/unloadHandlers.js'; Template.viewerMain.onCreated(() => { // Attach the Window resize listener @@ -9,6 +10,9 @@ Template.viewerMain.onCreated(() => { // See cineDialog instance.setResizeHandler function window.addEventListener('resize', handleResize); + // Add beforeUnload event handler to check for unsaved changes + window.addEventListener('beforeunload', unloadHandlers.beforeUnload); + // Create the synchronizer used to update reference lines OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer('CornerstoneNewImage', cornerstoneTools.updateImageSynchronizer); }); @@ -92,6 +96,9 @@ Template.viewerMain.onDestroyed(() => { // Remove the Window resize listener window.removeEventListener('resize', handleResize); + // Remove beforeUnload event handler... + window.removeEventListener('beforeunload', unloadHandlers.beforeUnload); + // Destroy the synchronizer used to update reference lines OHIF.viewer.updateImageSynchronizer.destroy(); diff --git a/Packages/ohif-viewerbase/client/lib/unloadHandlers.js b/Packages/ohif-viewerbase/client/lib/unloadHandlers.js new file mode 100644 index 000000000..f84f21b2c --- /dev/null +++ b/Packages/ohif-viewerbase/client/lib/unloadHandlers.js @@ -0,0 +1,12 @@ +import { OHIF } from 'meteor/ohif:core'; + +export const unloadHandlers = { + beforeUnload: function(event) { + // Check for any unsaved changes on viewer namespace... + if (OHIF.ui.unsavedChanges.probe('viewer.*') > 0) { + let confirmationMessage = 'You have unsaved changes!'; + event.returnValue = confirmationMessage; + return confirmationMessage; + } + } +}; diff --git a/Packages/ohif-viewerbase/package.js b/Packages/ohif-viewerbase/package.js index a7bfc8955..f4fecad22 100644 --- a/Packages/ohif-viewerbase/package.js +++ b/Packages/ohif-viewerbase/package.js @@ -166,6 +166,7 @@ Package.onUse(function(api) { api.addFiles('client/lib/isImage.js', 'client'); api.addFiles('client/lib/sopClassDictionary.js', 'client'); api.addFiles('client/lib/debugReactivity.js', 'client'); + api.addFiles('client/lib/unloadHandlers.js', 'client'); api.export('resizeViewportElements', 'client'); api.export('handleResize', 'client');