| 1 | /// crmAutosave |
| 2 | (function(angular, $, _) { |
| 3 | |
| 4 | angular.module('crmAutosave', CRM.angRequires('crmAutosave')); |
| 5 | |
| 6 | // usage: |
| 7 | // var autosave = new CrmAutosaveCtrl({ |
| 8 | // save: function -- A function to handle saving. Should return a promise. |
| 9 | // If it's not a promise, then we'll assume that it completes successfully. |
| 10 | // saveIf: function -- Only allow autosave when conditional returns true. Default: !form.$invalid |
| 11 | // model: object|function -- (Re)schedule saves based on observed changes to object. We perform deep |
| 12 | // inspection on the model object. This could be a performance issue you |
| 13 | // had many concurrent autosave forms or a particularly large model, but |
| 14 | // it should be fine with typical usage. |
| 15 | // interval: object -- Interval spec. Default: {poll: 250, save: 5000} |
| 16 | // form: object|function -- FormController or its getter |
| 17 | // }); |
| 18 | // autosave.start(); |
| 19 | // $scope.$on('$destroy', autosave.stop); |
| 20 | // Note: if the save operation itself |
| 21 | angular.module('crmAutosave').service('CrmAutosaveCtrl', function($interval, $timeout, $q) { |
| 22 | function CrmAutosaveCtrl(options) { |
| 23 | var intervals = angular.extend({poll: 250, save: 5000}, options.interval); |
| 24 | var jobs = {poll: null, save: null}; // job handles used ot cancel/reschedule timeouts/intervals |
| 25 | var lastSeenModel = null; |
| 26 | var saving = false; |
| 27 | |
| 28 | // Determine if model has changed; (re)schedule the save. |
| 29 | // This is a bit expensive and doesn't need to be continuous, so we use polling instead of watches. |
| 30 | function checkChanges() { |
| 31 | if (saving) { |
| 32 | return; |
| 33 | } |
| 34 | var currentModel = _.isFunction(options.model) ? options.model() : options.model; |
| 35 | if (!angular.equals(currentModel, lastSeenModel)) { |
| 36 | lastSeenModel = angular.copy(currentModel); |
| 37 | if (jobs.save) { |
| 38 | $timeout.cancel(jobs.save); |
| 39 | } |
| 40 | jobs.save = $timeout(doAutosave, intervals.save); |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | function doAutosave() { |
| 45 | jobs.save = null; |
| 46 | if (saving) { |
| 47 | return; |
| 48 | } |
| 49 | |
| 50 | var form = _.isFunction(options.form) ? options.form() : options.form; |
| 51 | |
| 52 | if (options.saveIf) { |
| 53 | if (!options.saveIf()) { |
| 54 | return; |
| 55 | } |
| 56 | } |
| 57 | else if (form && form.$invalid) { |
| 58 | return; |
| 59 | } |
| 60 | |
| 61 | saving = true; |
| 62 | lastSeenModel = angular.copy(_.isFunction(options.model) ? options.model() : options.model); |
| 63 | |
| 64 | // Set to pristine before saving -- not after saving. |
| 65 | // If an eager user continues editing concurrent with the |
| 66 | // save process, then the form should become dirty again. |
| 67 | if (form) { |
| 68 | form.$setPristine(); |
| 69 | } |
| 70 | var res = options.save(); |
| 71 | if (res && res.then) { |
| 72 | res.then( |
| 73 | function() { |
| 74 | saving = false; |
| 75 | }, |
| 76 | function() { |
| 77 | saving = false; |
| 78 | if (form) { |
| 79 | form.$setDirty(); |
| 80 | } |
| 81 | } |
| 82 | ); |
| 83 | } |
| 84 | else { |
| 85 | saving = false; |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | var self = this; |
| 90 | |
| 91 | this.start = function() { |
| 92 | if (!jobs.poll) { |
| 93 | lastSeenModel = angular.copy(_.isFunction(options.model) ? options.model() : options.model); |
| 94 | jobs.poll = $interval(checkChanges, intervals.poll); |
| 95 | } |
| 96 | }; |
| 97 | |
| 98 | this.stop = function() { |
| 99 | if (jobs.poll) { |
| 100 | $interval.cancel(jobs.poll); |
| 101 | jobs.poll = null; |
| 102 | } |
| 103 | if (jobs.save) { |
| 104 | $timeout.cancel(jobs.save); |
| 105 | jobs.save = null; |
| 106 | } |
| 107 | }; |
| 108 | |
| 109 | this.suspend = function(p) { |
| 110 | self.stop(); |
| 111 | return p.finally(self.start); |
| 112 | }; |
| 113 | } |
| 114 | |
| 115 | return CrmAutosaveCtrl; |
| 116 | }); |
| 117 | |
| 118 | })(angular, CRM.$, CRM._); |