Merge pull request #9815 from WeMoveEU/CRM-19962
[civicrm-core.git] / js / jquery / jquery.crmEditable.js
1 // https://civicrm.org/licensing
2 (function($, _) {
3 "use strict";
4 /* jshint validthis: true */
5
6 // TODO: We'll need a way to clear this cache if options are edited.
7 // Maybe it should be stored in the CRM object so other parts of the app can use it.
8 // Note that if we do move it, we should also change the format of option lists to our standard sequential arrays
9 var optionsCache = {};
10
11 /**
12 * Helper fn to retrieve semantic data from markup
13 */
14 $.fn.crmEditableEntity = function() {
15 var
16 el = this[0],
17 ret = {},
18 $row = this.first().closest('.crm-entity');
19 ret.entity = $row.data('entity') || $row[0].id.split('-')[0];
20 ret.id = $row.data('id') || $row[0].id.split('-')[1];
21 ret.action = $row.data('action') || 'create';
22
23 if (!ret.entity || !ret.id) {
24 return false;
25 }
26 $('.crm-editable, [data-field]', $row).each(function() {
27 var fieldName = $(this).data('field') || this.className.match(/crmf-(\S*)/)[1];
28 if (fieldName) {
29 ret[fieldName] = $(this).text();
30 if (this === el) {
31 ret.field = fieldName;
32 }
33 }
34 });
35 return ret;
36 };
37
38 /**
39 * @see http://wiki.civicrm.org/confluence/display/CRMDOC/Structure+convention+for+automagic+edit+in+place
40 */
41 $.fn.crmEditable = function(options) {
42 function checkable() {
43 $(this).off('.crmEditable').on('change.crmEditable', function() {
44 var $el = $(this),
45 info = $el.crmEditableEntity();
46 if (!info.field) {
47 return false;
48 }
49 var params = {
50 sequential: 1,
51 id: info.id,
52 field: info.field,
53 value: $el.is(':checked') ? 1 : 0
54 };
55 CRM.api3(info.entity, info.action, params, true);
56 });
57 }
58
59 return this.each(function() {
60 var $i,
61 fieldName = "",
62 defaults = {
63 error: function(entity, field, value, data) {
64 restoreContainer();
65 $(this).html(originalValue || settings.placeholder).click();
66 var msg = $.isPlainObject(data) && data.error_message;
67 errorMsg = $(':input', this).first().crmError(msg || ts('Sorry an error occurred and your information was not saved'), ts('Error'));
68 },
69 success: function(entity, field, value, data, settings) {
70 restoreContainer();
71 if ($i.data('refresh')) {
72 CRM.refreshParent($i);
73 } else {
74 value = value === '' ? settings.placeholder : _.escape(value);
75 $i.html(value);
76 }
77 }
78 },
79 originalValue = '',
80 errorMsg,
81 editableSettings = $.extend({}, defaults, options);
82
83 if ($(this).hasClass('crm-editable-enabled')) {
84 return;
85 }
86
87 if (this.nodeName == "INPUT" && this.type == "checkbox") {
88 checkable.call(this, this);
89 return;
90 }
91
92 // Table cell needs something inside it to look right
93 if ($(this).is('td')) {
94 $(this)
95 .removeClass('crm-editable')
96 .wrapInner('<div class="crm-editable" />');
97 $i = $('div.crm-editable', this)
98 .data($(this).data());
99 var field = this.className.match(/crmf-(\S*)/);
100 if (field) {
101 $i.data('field', field[1]);
102 }
103 }
104 else {
105 $i = $(this);
106 }
107
108 var settings = {
109 tooltip: $i.data('tooltip') || ts('Click to edit'),
110 placeholder: $i.data('placeholder') || '<i class="crm-i fa-pencil crm-editable-placeholder"></i>',
111 onblur: 'cancel',
112 cancel: '<button type="cancel"><i class="crm-i fa-times"></i></button>',
113 submit: '<button type="submit"><i class="crm-i fa-check"></i></button>',
114 cssclass: 'crm-editable-form',
115 data: getData,
116 onreset: restoreContainer
117 };
118 if ($i.data('type')) {
119 settings.type = $i.data('type');
120 if (settings.type == 'boolean') {
121 settings.type = 'select';
122 $i.data('options', {'0': ts('No'), '1': ts('Yes')});
123 }
124 }
125 if (settings.type == 'textarea') {
126 $i.addClass('crm-editable-textarea-enabled');
127 }
128 $i.addClass('crm-editable-enabled');
129
130 function callback(value, settings) {
131 $i.addClass('crm-editable-saving');
132 var
133 info = $i.crmEditableEntity(),
134 $el = $($i),
135 params = {},
136 action = $i.data('action') || info.action;
137 if (!info.field) {
138 return false;
139 }
140 if (info.id && info.id !== 'new') {
141 params.id = info.id;
142 }
143 if (action === 'setvalue') {
144 params.field = info.field;
145 params.value = value;
146 }
147 else {
148 params[info.field] = value;
149 }
150 CRM.api3(info.entity, action, params, {error: null})
151 .done(function(data) {
152 if (data.is_error) {
153 return editableSettings.error.call($el[0], info.entity, info.field, value, data);
154 }
155 if ($el.data('options')) {
156 value = $el.data('options')[value] || '';
157 }
158 else if ($el.data('optionsHashKey')) {
159 var options = optionsCache[$el.data('optionsHashKey')];
160 value = options && options[value] ? options[value] : '';
161 }
162 $el.trigger('crmFormSuccess');
163 editableSettings.success.call($el[0], info.entity, info.field, value, data, settings);
164 })
165 .fail(function(data) {
166 editableSettings.error.call($el[0], info.entity, info.field, value, data);
167 });
168 }
169
170 CRM.loadScript(CRM.config.resourceBase + 'packages/jquery/plugins/jquery.jeditable.min.js').done(function() {
171 $i.editable(callback, settings);
172 });
173
174 // CRM-15759 - Workaround broken textarea handling in jeditable 1.7.1
175 $i.click(function() {
176 $('textarea', this).off()
177 // Fix cancel-on-blur
178 .on('blur', function(e) {
179 if (!e.relatedTarget || !$(e.relatedTarget).is('.crm-editable-form button')) {
180 $i.find('button[type=cancel]').click();
181 }
182 })
183 // Add support for ctrl-enter shortcut key
184 .on('keydown', function (e) {
185 if (e.ctrlKey && e.keyCode == 13) {
186 $i.find('button[type=submit]').click();
187 e.preventDefault();
188 }
189 });
190 });
191
192 function getData(value, settings) {
193 // Add css class to wrapper
194 // 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 :(
195 $i.addClass('crm-editable-editing');
196
197 originalValue = value;
198
199 if ($i.data('type') == 'select' || $i.data('type') == 'boolean') {
200 if ($i.data('options')) {
201 return formatOptions($i.data('options'));
202 }
203 var result,
204 info = $i.crmEditableEntity(),
205 // Strip extra id from multivalued custom fields
206 custom = info.field.match(/(custom_\d+)_\d+/),
207 field = custom ? custom[1] : info.field,
208 hash = info.entity + '.' + field,
209 params = {
210 field: field,
211 context: 'create'
212 };
213 $i.data('optionsHashKey', hash);
214 if (!optionsCache[hash]) {
215 $.ajax({
216 url: CRM.url('civicrm/ajax/rest'),
217 data: {entity: info.entity, action: 'getoptions', json: JSON.stringify(params)},
218 async: false, // jeditable lacks support for async options lookup
219 success: function(data) {optionsCache[hash] = data.values;}
220 });
221 }
222 return formatOptions(optionsCache[hash]);
223 }
224 // Unwrap contents then replace html special characters with plain text
225 return _.unescape(value.replace(/<(?:.|\n)*?>/gm, ''));
226 }
227
228 function formatOptions(options) {
229 if (typeof $i.data('emptyOption') === 'string') {
230 // Using 'null' because '' is broken in jeditable 1.7.1
231 return $.extend({'null': $i.data('emptyOption')}, options);
232 }
233 return options;
234 }
235
236 function restoreContainer() {
237 if (errorMsg && errorMsg.close) errorMsg.close();
238 $i.removeClass('crm-editable-saving crm-editable-editing');
239 }
240
241 });
242 };
243
244 })(jQuery, CRM._);