+ * Configure a model class to track whether a model has unsaved changes.
+ *
+ * Methods:
+ * - setModified() - flag the model as modified/dirty
+ * - isSaved() - return true if there have been no changes to the data since the last fetch or save
+ * Events:
+ * - saved(object model, bool is_saved) - triggered whenever isSaved() value would change
+ *
+ * Note: You should not directly call isSaved() within the context of the success/error/sync callback;
+ * I haven't found a way to make isSaved() behave correctly within these callbacks without patching
+ * Backbone. Instead, attach an event listener to the 'saved' event.
+ *
+ * @param ModelClass
+ */
+ CRM.Backbone.trackSaved = function(ModelClass) {
+ // Retain references to some of the original class's functions
+ var Parent = _.pick(ModelClass.prototype, 'initialize', 'save', 'fetch');
+
+ // Private callback
+ var onSyncSuccess = function() {
+ this._modified = false;
+ if (this._oldModified.length > 0) {
+ this._oldModified.pop();
+ }
+ this.trigger('saved', this, this.isSaved());
+ };
+ var onSaveError = function() {
+ if (this._oldModified.length > 0) {
+ this._modified = this._oldModified.pop();
+ this.trigger('saved', this, this.isSaved());
+ }
+ };
+
+ // Defaults - if specified in ModelClass, preserve
+ _.defaults(ModelClass.prototype, {
+ isSaved: function() {
+ var result = !this.isNew() && !this.isModified();
+ return result;
+ },
+ isModified: function() {
+ return this._modified;
+ },
+ _saved_onchange: function(model, options) {
+ if (options.parse) return;
+ // console.log('change', model.changedAttributes(), model.previousAttributes());
+ this.setModified();
+ },
+ setModified: function() {
+ var oldModified = this._modified;
+ this._modified = true;
+ if (!oldModified) {
+ this.trigger('saved', this, this.isSaved());
+ }
+ }
+ });
+
+ // Overrides - if specified in ModelClass, replace
+ _.extend(ModelClass.prototype, {
+ initialize: function(options) {
+ this._modified = false;
+ this._oldModified = [];
+ this.listenTo(this, 'change', this._saved_onchange);
+ this.listenTo(this, 'error', onSaveError);
+ this.listenTo(this, 'sync', onSyncSuccess);
+ if (Parent.initialize) {
+ return Parent.initialize.apply(this, arguments);
+ }
+ },
+ save: function() {
+ // we'll assume success
+ this._oldModified.push(this._modified);
+ return Parent.save.apply(this, arguments);
+ },
+ fetch: function() {
+ this._oldModified.push(this._modified);
+ return Parent.fetch.apply(this, arguments);
+ }
+ });
+ };
+
+ /**
+ * Configure a model class to support client-side soft deletion.
+ * One can call "model.setDeleted(BOOLEAN)" to flag an entity for
+ * deletion (or not) -- however, deletion will be deferred until save()
+ * is called.
+ *
+ * Methods:
+ * setSoftDeleted(boolean) - flag the model as deleted (or not-deleted)
+ * isSoftDeleted() - determine whether model has been soft-deleted
+ * Events:
+ * softDelete(model, is_deleted) -- change value of is_deleted
+ *
+ * @param ModelClass
+ */
+ CRM.Backbone.trackSoftDelete = function(ModelClass) {
+ // Retain references to some of the original class's functions
+ var Parent = _.pick(ModelClass.prototype, 'save');
+
+ // Defaults - if specified in ModelClass, preserve
+ _.defaults(ModelClass.prototype, {
+ is_soft_deleted: false,
+ setSoftDeleted: function(is_deleted) {
+ if (this.is_soft_deleted != is_deleted) {
+ this.is_soft_deleted = is_deleted;
+ this.trigger('softDelete', this, is_deleted);
+ if (this.setModified) this.setModified(); // FIXME: ugly interaction, trackSoftDelete-trackSaved
+ }
+ },
+ isSoftDeleted: function() {
+ return this.is_soft_deleted;
+ }
+ });
+
+ // Overrides - if specified in ModelClass, replace
+ _.extend(ModelClass.prototype, {
+ save: function(attributes, options) {
+ if (this.isSoftDeleted()) {
+ return this.destroy(options);
+ } else {
+ return Parent.save.apply(this, arguments);
+ }
+ }
+ });
+ };
+
+ /**