[Edit] Add mct-before-unload directive

Add directive for exposing expressions which should be
evaluated for the browser's onbeforeunload event, to
prevent user-initiated navigation from causing a loss
of unsaved changes. WTD-1035.
This commit is contained in:
Victor Woeltjen
2015-03-16 17:28:08 -07:00
parent d86e27504f
commit 783d2f332b
6 changed files with 110 additions and 3 deletions

View File

@ -23,6 +23,13 @@
"depends": [ "$scope" ] "depends": [ "$scope" ]
} }
], ],
"directives": [
{
"key": "mctBeforeUnload",
"implementation": "directives/MCTBeforeUnload.js",
"depends": [ "$window" ]
}
],
"actions": [ "actions": [
{ {
"key": "edit", "key": "edit",

View File

@ -1,8 +1,9 @@
<div content="jquery-wrapper" <div content="jquery-wrapper"
class="abs holder-all edit-mode" class="abs holder-all edit-mode"
ng-controller="EditController"> ng-controller="EditController as editMode"
mct-before-unload="editMode.getUnloadWarning()">
<mct-representation key="'edit-object'" mct-object="navigatedObject"> <mct-representation key="'edit-object'" mct-object="editMode.navigatedObject()">
</mct-representation> </mct-representation>
<mct-include key="'bottombar'"></mct-include> <mct-include key="'bottombar'"></mct-include>

View File

@ -88,6 +88,13 @@ define(
*/ */
cancel: function () { cancel: function () {
return resolvePromise(undefined); return resolvePromise(undefined);
},
/**
* Check if there are any unsaved changes.
* @returns {boolean} true if there are unsaved changes
*/
dirty: function () {
return cache.dirty();
} }
}; };
}; };

View File

@ -15,10 +15,12 @@ define(
* @constructor * @constructor
*/ */
function EditController($scope, navigationService) { function EditController($scope, navigationService) {
var navigatedObject;
function setNavigation(domainObject) { function setNavigation(domainObject) {
// Wrap the domain object such that all mutation is // Wrap the domain object such that all mutation is
// confined to edit mode (until Save) // confined to edit mode (until Save)
$scope.navigatedObject = navigatedObject =
domainObject && new EditableDomainObject(domainObject); domainObject && new EditableDomainObject(domainObject);
} }
@ -27,6 +29,21 @@ define(
$scope.$on("$destroy", function () { $scope.$on("$destroy", function () {
navigationService.removeListener(setNavigation); navigationService.removeListener(setNavigation);
}); });
return {
navigatedObject: function () {
return navigatedObject;
},
getUnloadWarning: function () {
var editorCapability = navigatedObject &&
navigatedObject.getCapability("editor"),
hasChanges = editorCapability && editorCapability.dirty();
return hasChanges ?
"Unsaved changes will be lost if you leave this page." :
undefined;
}
};
} }
return EditController; return EditController;

View File

@ -0,0 +1,68 @@
/*global define*/
define(
[],
function () {
"use strict";
/**
* Defines the `mct-before-unload` directive. The expression bound
* to this attribute will be evaluated during page navigation events
* and, if it returns a truthy value, will be used to populate a
* prompt to the user to confirm this navigation.
* @constructor
* @param $window the window
*/
function MCTBeforeUnload($window) {
var unloads = [],
oldBeforeUnload = $window.onbeforeunload;
// Run all unload functions, returning the first returns truthily.
function checkUnloads() {
var result;
unloads.forEach(function (unload) {
result = result || unload();
});
return result;
}
// Link function for an mct-before-unload directive usage
function link(scope, element, attrs) {
// Invoke the
function unload() {
return scope.$eval(attrs.mctBeforeUnload);
}
// Stop using this unload expression
function removeUnload() {
unloads = unloads.filter(function (callback) {
return callback !== unload;
});
if (unloads.length === 0) {
$window.onbeforeunload = oldBeforeUnload;
}
}
// Include this instance of the directive's unload function
unloads.push(unload);
// If this is the first active instance of this directive,
// register as the window's beforeunload handler
$window.onbeforeunload = checkUnloads;
// Remove it when the scope is destroyed
scope.$on("$destroy", removeUnload);
}
return {
// Applicable as an attribute
restrict: "A",
// Link with the provided function
link: link
};
}
return MCTBeforeUnload;
}
);

View File

@ -91,6 +91,13 @@ define(
// Invoke its save behavior // Invoke its save behavior
object.getCapability('editor').save(); object.getCapability('editor').save();
} }
},
/**
* Check if any objects have been marked dirty in this cache.
* @returns {boolean} true if objects are dirty
*/
dirty: function () {
return Object.keys(dirty).length > 0;
} }
}; };
} }