Merge pull request #8101 from colemanw/CRM-18379
[civicrm-core.git] / js / jquery / jquery.crmeditable.js
index 4cea8fb43c46f01e9bcb702fef34dbdfe0dbc83c..453c4969320ff3e64d3c9e7acefd13ba394795c7 100644 (file)
@@ -1,5 +1,13 @@
 // https://civicrm.org/licensing
-(function($) {
+(function($, _) {
+  "use strict";
+  /* jshint validthis: true */
+
+  // TODO: We'll need a way to clear this cache if options are edited.
+  // Maybe it should be stored in the CRM object so other parts of the app can use it.
+  // Note that if we do move it, we should also change the format of option lists to our standard sequential arrays
+  var optionsCache = {};
+
   /**
    * Helper fn to retrieve semantic data from markup
    */
@@ -10,7 +18,7 @@
       $row = this.first().closest('.crm-entity');
       ret.entity = $row.data('entity') || $row[0].id.split('-')[0];
       ret.id = $row.data('id') || $row[0].id.split('-')[1];
-      ret.action = $row.data('action') || 'setvalue';
+      ret.action = $row.data('action') || 'create';
 
     if (!ret.entity || !ret.id) {
       return false;
@@ -31,8 +39,8 @@
    * @see http://wiki.civicrm.org/confluence/display/CRMDOC/Structure+convention+for+automagic+edit+in+place
    */
   $.fn.crmEditable = function(options) {
-    var checkable = function() {
-      $(this).change(function() {
+    function checkable() {
+      $(this).off('.crmEditable').on('change.crmEditable', function() {
         var $el = $(this),
           info = $el.crmEditableEntity();
         if (!info.field) {
           field: info.field,
           value: $el.is(':checked') ? 1 : 0
         };
-        CRM.api3(info.entity, info.action, params, true)
-          .fail(function(data) {
-            editableSettings.error.call($el[0], info.entity, info.field, checked, data);
-          })
-          .done(function(data) {
-            editableSettings.success.call($el[0], info.entity, info.field, checked, data);
-          });
+        CRM.api3(info.entity, info.action, params, true);
       });
-    };
-
-    var defaults = {
-      form: {},
-      callBack: function(data) {
-        if (data.is_error) {
-          editableSettings.error.call(this, data);
-        } else {
-          return editableSettings.success.call(this, data);
-        }
-      },
-      error: function(entity, field, value, data) {
-        $(this).crmError(data.error_message, ts('Error'));
-        $(this).removeClass('crm-editable-saving');
-      },
-      success: function(entity, field, value, data, settings) {
-        var $i = $(this);
-        $i.removeClass('crm-editable-saving crm-error');
-        value = value === '' ? settings.placeholder : value;
-        $i.html(value);
-      }
-    };
+    }
 
-    var editableSettings = $.extend({}, defaults, options);
     return this.each(function() {
       var $i,
-        fieldName = "";
+        fieldName = "",
+        defaults = {
+          error: function(entity, field, value, data) {
+            restoreContainer();
+            $(this).html(originalValue || settings.placeholder).click();
+            var msg = $.isPlainObject(data) && data.error_message;
+            errorMsg = $(':input', this).first().crmError(msg || ts('Sorry an error occurred and your information was not saved'), ts('Error'));
+          },
+          success: function(entity, field, value, data, settings) {
+            restoreContainer();
+            if ($i.data('refresh')) {
+              CRM.refreshParent($i);
+            } else {
+              value = value === '' ? settings.placeholder : _.escape(value);
+              $i.html(value);
+            }
+          }
+        },
+        originalValue = '',
+        errorMsg,
+        editableSettings = $.extend({}, defaults, options);
+
+      if ($(this).hasClass('crm-editable-enabled')) {
+        return;
+      }
 
       if (this.nodeName == "INPUT" && this.type == "checkbox") {
         checkable.call(this, this);
 
       var settings = {
         tooltip: $i.data('tooltip') || ts('Click to edit'),
-        placeholder: $i.data('placeholder') || '<span class="crm-editable-placeholder">' + ts('Click to edit') + '</span>',
+        placeholder: $i.data('placeholder') || '<i class="crm-i fa-pencil crm-editable-placeholder"></i>',
         onblur: 'cancel',
-        cancel: '<button type="cancel"><span class="ui-icon ui-icon-closethick"></span></button>',
-        submit: '<button type="submit"><span class="ui-icon ui-icon-check"></span></button>',
+        cancel: '<button type="cancel"><i class="crm-i fa-times"></i></button>',
+        submit: '<button type="submit"><i class="crm-i fa-check"></i></button>',
         cssclass: 'crm-editable-form',
-        data: function(value, settings) {
-          return value.replace(/<(?:.|\n)*?>/gm, '');
-        }
+        data: getData,
+        onreset: restoreContainer
       };
       if ($i.data('type')) {
         settings.type = $i.data('type');
-      }
-      if ($i.data('options')) {
-        settings.data = $i.data('options');
+        if (settings.type == 'boolean') {
+          settings.type = 'select';
+          $i.data('options', {'0': ts('No'), '1': ts('Yes')});
+        }
       }
       if (settings.type == 'textarea') {
         $i.addClass('crm-editable-textarea-enabled');
       }
-      else {
-        $i.addClass('crm-editable-enabled');
-      }
+      $i.addClass('crm-editable-enabled');
 
       $i.editable(function(value, settings) {
         $i.addClass('crm-editable-saving');
         else {
           params[info.field] = value;
         }
-        CRM.api3(info.entity, action, params, true)
+        CRM.api3(info.entity, action, params, {error: null})
           .done(function(data) {
+            if (data.is_error) {
+              return editableSettings.error.call($el[0], info.entity, info.field, value, data);
+            }
             if ($el.data('options')) {
-              value = $el.data('options')[value];
+              value = $el.data('options')[value] || '';
+            }
+            else if ($el.data('optionsHashKey')) {
+              var options = optionsCache[$el.data('optionsHashKey')];
+              value = options && options[value] ? options[value] : '';
             }
             $el.trigger('crmFormSuccess');
             editableSettings.success.call($el[0], info.entity, info.field, value, data, settings);
       // CRM-15759 - Workaround broken textarea handling in jeditable 1.7.1
       $i.click(function() {
         $('textarea', this).off()
-          .on('blur', function() {
-            $i.find('button[type=cancel]').click();
+          // Fix cancel-on-blur
+          .on('blur', function(e) {
+            if (!e.relatedTarget || !$(e.relatedTarget).is('.crm-editable-form button')) {
+              $i.find('button[type=cancel]').click();
+            }
           })
+          // Add support for ctrl-enter shortcut key
           .on('keydown', function (e) {
             if (e.ctrlKey && e.keyCode == 13) {
-              // Ctrl-Enter pressed
               $i.find('button[type=submit]').click();
               e.preventDefault();
             }
           });
       });
+
+      function getData(value, settings) {
+        // Add css class to wrapper
+        // FIXME: This should be a response to an event instead of coupled with this function but jeditable 1.7.1 doesn't trigger any events :(
+        $i.addClass('crm-editable-editing');
+
+        originalValue = value;
+
+        if ($i.data('type') == 'select' || $i.data('type') == 'boolean') {
+          if ($i.data('options')) {
+            return formatOptions($i.data('options'));
+          }
+          var result,
+            info = $i.crmEditableEntity(),
+            // Strip extra id from multivalued custom fields
+            custom = info.field.match(/(custom_\d+)_\d+/),
+            field = custom ? custom[1] : info.field,
+            hash = info.entity + '.' + field,
+            params = {
+              field: field,
+              context: 'create'
+            };
+          $i.data('optionsHashKey', hash);
+          if (!optionsCache[hash]) {
+            $.ajax({
+              url: CRM.url('civicrm/ajax/rest'),
+              data: {entity: info.entity, action: 'getoptions', json: JSON.stringify(params)},
+              async: false, // jeditable lacks support for async options lookup
+              success: function(data) {optionsCache[hash] = data.values;}
+            });
+          }
+          return formatOptions(optionsCache[hash]);
+        }
+        // Unwrap contents then replace html special characters with plain text
+        return _.unescape(value.replace(/<(?:.|\n)*?>/gm, ''));
+      }
+
+      function formatOptions(options) {
+        if (typeof $i.data('emptyOption') === 'string') {
+          // Using 'null' because '' is broken in jeditable 1.7.1
+          return $.extend({'null': $i.data('emptyOption')}, options);
+        }
+        return options;
+      }
+
+      function restoreContainer() {
+        if (errorMsg && errorMsg.close) errorMsg.close();
+        $i.removeClass('crm-editable-saving crm-editable-editing');
+      }
+
     });
   };
 
-})(jQuery);
+  $(document).on('crmLoad', function(e) {
+    $('.crm-editable', e.target).not('thead *').crmEditable();
+  });
+
+})(jQuery, CRM._);