mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-12-02 13:24:20 +01:00
349 lines
12 KiB
JavaScript
349 lines
12 KiB
JavaScript
'use strict';
|
|
/*jshint globalstrict: true*/
|
|
/*jshint undef:false */
|
|
|
|
angular.module('unsavedChanges', ['resettable'])
|
|
|
|
.provider('unsavedWarningsConfig', function() {
|
|
|
|
var _this = this;
|
|
|
|
// defaults
|
|
var logEnabled = false;
|
|
var useTranslateService = true;
|
|
var routeEvent = ['$locationChangeStart', '$stateChangeStart'];
|
|
var navigateMessage = 'You will lose unsaved changes if you leave this page';
|
|
var reloadMessage = 'You will lose unsaved changes if you reload this page';
|
|
|
|
Object.defineProperty(_this, 'navigateMessage', {
|
|
get: function() {
|
|
return navigateMessage;
|
|
},
|
|
set: function(value) {
|
|
navigateMessage = value;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(_this, 'reloadMessage', {
|
|
get: function() {
|
|
return reloadMessage;
|
|
},
|
|
set: function(value) {
|
|
reloadMessage = value;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(_this, 'useTranslateService', {
|
|
get: function() {
|
|
return useTranslateService;
|
|
},
|
|
set: function(value) {
|
|
useTranslateService = !! (value);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(_this, 'routeEvent', {
|
|
get: function() {
|
|
return routeEvent;
|
|
},
|
|
set: function(value) {
|
|
if (typeof value === 'string') value = [value];
|
|
routeEvent = value;
|
|
}
|
|
});
|
|
Object.defineProperty(_this, 'logEnabled', {
|
|
get: function() {
|
|
return logEnabled;
|
|
},
|
|
set: function(value) {
|
|
logEnabled = !! (value);
|
|
}
|
|
});
|
|
|
|
this.$get = ['$injector',
|
|
function($injector) {
|
|
|
|
function translateIfAble(message) {
|
|
if ($injector.has('$translate') && useTranslateService) {
|
|
return $injector.get('$translate').instant(message);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
var publicInterface = {
|
|
// log function that accepts any number of arguments
|
|
// @see http://stackoverflow.com/a/7942355/1738217
|
|
log: function() {
|
|
if (console.log && logEnabled && arguments.length) {
|
|
var newarr = [].slice.call(arguments);
|
|
if (typeof console.log === 'object') {
|
|
log.apply.call(console.log, console, newarr);
|
|
} else {
|
|
console.log.apply(console, newarr);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
Object.defineProperty(publicInterface, 'useTranslateService', {
|
|
get: function() {
|
|
return useTranslateService;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(publicInterface, 'reloadMessage', {
|
|
get: function() {
|
|
return translateIfAble(reloadMessage) || reloadMessage;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(publicInterface, 'navigateMessage', {
|
|
get: function() {
|
|
return translateIfAble(navigateMessage) || navigateMessage;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(publicInterface, 'routeEvent', {
|
|
get: function() {
|
|
return routeEvent;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(publicInterface, 'logEnabled', {
|
|
get: function() {
|
|
return logEnabled;
|
|
}
|
|
});
|
|
|
|
return publicInterface;
|
|
}
|
|
];
|
|
})
|
|
|
|
.service('unsavedWarningSharedService', ['$rootScope', 'unsavedWarningsConfig', '$injector', '$window',
|
|
function($rootScope, unsavedWarningsConfig, $injector, $window) {
|
|
|
|
// Controller scopped variables
|
|
var _this = this;
|
|
var allForms = [];
|
|
var areAllFormsClean = true;
|
|
var removeFunctions = [];
|
|
|
|
// @note only exposed for testing purposes.
|
|
this.allForms = function() {
|
|
return allForms;
|
|
};
|
|
|
|
// Check all registered forms
|
|
// if any one is dirty function will return true
|
|
|
|
function allFormsClean() {
|
|
areAllFormsClean = true;
|
|
angular.forEach(allForms, function(item, idx) {
|
|
unsavedWarningsConfig.log('Form : ' + item.$name + ' dirty : ' + item.$dirty);
|
|
if (item.$dirty) {
|
|
areAllFormsClean = false;
|
|
}
|
|
});
|
|
return areAllFormsClean; // no dirty forms were found
|
|
}
|
|
|
|
// adds form controller to registered forms array
|
|
// this array will be checked when user navigates away from page
|
|
this.init = function(form) {
|
|
if (allForms.length === 0) setup();
|
|
unsavedWarningsConfig.log("Registering form", form);
|
|
allForms.push(form);
|
|
};
|
|
|
|
this.removeForm = function(form) {
|
|
var idx = allForms.indexOf(form);
|
|
|
|
// this form is not present array
|
|
// @todo needs test coverage
|
|
if (idx === -1) return;
|
|
|
|
allForms.splice(idx, 1);
|
|
unsavedWarningsConfig.log("Removing form from watch list", form);
|
|
|
|
if (allForms.length === 0) tearDown();
|
|
};
|
|
|
|
function tearDown() {
|
|
unsavedWarningsConfig.log('No more forms, tearing down');
|
|
angular.forEach(removeFunctions, function(fn) {
|
|
fn();
|
|
});
|
|
removeFunctions = [];
|
|
$window.onbeforeunload = null;
|
|
}
|
|
|
|
// Function called when user tries to close the window
|
|
this.confirmExit = function() {
|
|
if (!allFormsClean()) return unsavedWarningsConfig.reloadMessage;
|
|
$rootScope.$broadcast('resetResettables');
|
|
tearDown();
|
|
};
|
|
|
|
// bind to window close
|
|
// @todo investigate new method for listening as discovered in previous tests
|
|
|
|
function setup() {
|
|
unsavedWarningsConfig.log('Setting up');
|
|
|
|
$window.onbeforeunload = _this.confirmExit;
|
|
|
|
var eventsToWatchFor = unsavedWarningsConfig.routeEvent;
|
|
|
|
angular.forEach(eventsToWatchFor, function(aEvent) {
|
|
//calling this function later will unbind this, acting as $off()
|
|
var removeFn = $rootScope.$on(aEvent, function(event, next, current) {
|
|
unsavedWarningsConfig.log("user is moving with " + aEvent);
|
|
// @todo this could be written a lot cleaner!
|
|
if (!allFormsClean()) {
|
|
unsavedWarningsConfig.log("a form is dirty");
|
|
if (!confirm(unsavedWarningsConfig.navigateMessage)) {
|
|
unsavedWarningsConfig.log("user wants to cancel leaving");
|
|
event.preventDefault(); // user clicks cancel, wants to stay on page
|
|
} else {
|
|
unsavedWarningsConfig.log("user doesn't care about loosing stuff");
|
|
$rootScope.$broadcast('resetResettables');
|
|
}
|
|
} else {
|
|
unsavedWarningsConfig.log("all forms are clean");
|
|
}
|
|
|
|
});
|
|
removeFunctions.push(removeFn);
|
|
});
|
|
}
|
|
}
|
|
])
|
|
|
|
.directive('unsavedWarningClear', ['unsavedWarningSharedService',
|
|
function(unsavedWarningSharedService) {
|
|
return {
|
|
scope: {},
|
|
require: '^form',
|
|
priority: 10,
|
|
link: function(scope, element, attrs, formCtrl) {
|
|
element.bind('click', function(event) {
|
|
formCtrl.$setPristine();
|
|
});
|
|
|
|
}
|
|
};
|
|
}
|
|
])
|
|
|
|
.directive('unsavedWarningForm', ['unsavedWarningSharedService', '$rootScope',
|
|
function(unsavedWarningSharedService, $rootScope) {
|
|
return {
|
|
scope: {},
|
|
require: '^form',
|
|
link: function(scope, formElement, attrs, formCtrl) {
|
|
|
|
// @todo refactor, temp fix for issue #22
|
|
// where user might use form on element inside a form
|
|
// we shouldnt need isolate scope on this, but it causes the tests to fail
|
|
// traverse up parent elements to find the form.
|
|
// we need a form element since we bind to form events: submit, reset
|
|
var count = 0;
|
|
while(formElement[0].tagName !== 'FORM' && count < 3) {
|
|
count++;
|
|
formElement = formElement.parent();
|
|
}
|
|
if(count >= 3) {
|
|
throw('unsavedWarningForm must be inside a form element');
|
|
}
|
|
|
|
// register this form
|
|
unsavedWarningSharedService.init(formCtrl);
|
|
|
|
// bind to form submit, this makes the typical submit button work
|
|
// in addition to the ability to bind to a seperate button which clears warning
|
|
formElement.bind('submit', function(event) {
|
|
if (formCtrl.$valid) {
|
|
formCtrl.$setPristine();
|
|
}
|
|
});
|
|
|
|
// bind to form submit
|
|
// developers can hook into resetResettables to do
|
|
// do things like reset validation, present messages, etc.
|
|
formElement.bind('reset', function(event) {
|
|
event.preventDefault();
|
|
|
|
// trigger resettables within this form or element
|
|
var resettables = angular.element(formElement[0].querySelector('[resettable]'));
|
|
if(resettables.length) {
|
|
scope.$apply(resettables.triggerHandler('resetResettables'));
|
|
}
|
|
|
|
// sets for back to valid and pristine states
|
|
formCtrl.$setPristine();
|
|
});
|
|
|
|
// @todo check destroy on clear button too?
|
|
scope.$on('$destroy', function() {
|
|
unsavedWarningSharedService.removeForm(formCtrl);
|
|
});
|
|
}
|
|
};
|
|
}
|
|
]);
|
|
|
|
|
|
/**
|
|
* --------------------------------------------
|
|
* resettable models adapted from vitalets lazy model
|
|
* @see https://github.com/vitalets/lazy-model/
|
|
*
|
|
* The main difference is that we DO set the model value
|
|
* as the user changes the inputs. However we provide a hook
|
|
* to reset the model to original value. This we can then
|
|
* broadcast on from reset which triggers resettable to revert
|
|
* to original value.
|
|
* --------------------------------------------
|
|
*
|
|
* @note we don't create a seperate scope so the model value
|
|
* is still available onChange within the controller scope.
|
|
* This fixes https://github.com/facultymatt/angular-unsavedChanges/issues/19
|
|
*
|
|
*/
|
|
angular.module('resettable', [])
|
|
|
|
.directive('resettable', ['$parse', '$compile', '$rootScope',
|
|
function($parse, $compile, $rootScope) {
|
|
|
|
return {
|
|
restrict: 'A',
|
|
link: function postLink(scope, elem, attr, ngModelCtrl) {
|
|
|
|
var setter, getter, originalValue;
|
|
|
|
// save getters and setters and store the original value.
|
|
attr.$observe('ngModel', function(newValue) {
|
|
getter = $parse(attr.ngModel);
|
|
setter = getter.assign;
|
|
originalValue = getter(scope);
|
|
});
|
|
|
|
// reset our form to original value
|
|
var resetFn = function() {
|
|
setter(scope, originalValue);
|
|
};
|
|
|
|
elem.on('resetResettables', resetFn);
|
|
|
|
// @note this doesn't work if called using
|
|
// $rootScope.on() and $rootScope.$emit() pattern
|
|
var removeListenerFn = scope.$on('resetResettables', resetFn);
|
|
scope.$on('$destroy', removeListenerFn);
|
|
|
|
}
|
|
};
|
|
}
|
|
]);
|