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