Merge remote-tracking branch 'upstream/4.5' into 4.5-master-2015-01-05-23-28-33
[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 });
54 }
55
56 return this.each(function() {
57 var $i,
58 fieldName = "",
59 defaults = {
60 error: function(entity, field, value, data) {
61 restoreContainer();
62 $(this).html(originalValue || settings.placeholder).click();
63 var msg = $.isPlainObject(data) && data.error_message;
64 errorMsg = $(':input', this).first().crmError(msg || ts('Sorry an error occurred and your information was not saved'), ts('Error'));
65 },
66 success: function(entity, field, value, data, settings) {
67 restoreContainer();
68 if ($i.data('refresh')) {
69 CRM.refreshParent($i);
70 } else {
71 value = value === '' ? settings.placeholder : value;
72 $i.html(value);
73 }
74 }
75 },
76 originalValue = '',
77 errorMsg,
78 editableSettings = $.extend({}, defaults, options);
79
80 if ($(this).hasClass('crm-editable-enabled')) {
81 return;
82 }
83
84 if (this.nodeName == "INPUT" && this.type == "checkbox") {
85 checkable.call(this, this);
86 return;
87 }
88
89 // Table cell needs something inside it to look right
90 if ($(this).is('td')) {
91 $(this)
92 .removeClass('crm-editable')
93 .wrapInner('<div class="crm-editable" />');
94 $i = $('div.crm-editable', this)
95 .data($(this).data());
96 var field = this.className.match(/crmf-(\S*)/);
97 if (field) {
98 $i.data('field', field[1]);
99 }
100 }
101 else {
102 $i = $(this);
103 }
104
105 var settings = {
106 tooltip: $i.data('tooltip') || ts('Click to edit'),
107 placeholder: $i.data('placeholder') || '<span class="crm-editable-placeholder">' + ts('Click to edit') + '</span>',
108 onblur: 'cancel',
109 cancel: '<button type="cancel"><span class="ui-icon ui-icon-closethick"></span></button>',
110 submit: '<button type="submit"><span class="ui-icon ui-icon-check"></span></button>',
111 cssclass: 'crm-editable-form',
112 data: getData,
113 onreset: restoreContainer
114 };
115 if ($i.data('type')) {
116 settings.type = $i.data('type');
117 if (settings.type == 'boolean') {
118 settings.type = 'select';
119 $i.data('options', {'0': ts('No'), '1': ts('Yes')});
120 }
121 }
122 if (settings.type == 'textarea') {
123 $i.addClass('crm-editable-textarea-enabled');
124 }
125 $i.addClass('crm-editable-enabled');
126
127 $i.editable(function(value, settings) {
128 $i.addClass('crm-editable-saving');
129 var
130 info = $i.crmEditableEntity(),
131 $el = $($i),
132 params = {},
133 action = $i.data('action') || info.action;
134 if (!info.field) {
135 return false;
136 }
137 if (info.id && info.id !== 'new') {
138 params.id = info.id;
139 }
140 if (action === 'setvalue') {
141 params.field = info.field;
142 params.value = value;
143 }
144 else {
145 params[info.field] = value;
146 }
147 CRM.api3(info.entity, action, params, {error: null})
148 .done(function(data) {
149 if (data.is_error) {
150 return editableSettings.error.call($el[0], info.entity, info.field, value, data);
151 }
152 if ($el.data('options')) {
153 value = $el.data('options')[value] || '';
154 }
155 else if ($el.data('optionsHashKey')) {
156 var options = optionsCache[$el.data('optionsHashKey')];
157 value = options && options[value] ? options[value] : '';
158 }
159 $el.trigger('crmFormSuccess');
160 editableSettings.success.call($el[0], info.entity, info.field, value, data, settings);
161 })
162 .fail(function(data) {
163 editableSettings.error.call($el[0], info.entity, info.field, value, data);
164 });
165 }, settings);
166
167 // CRM-15759 - Workaround broken textarea handling in jeditable 1.7.1
168 $i.click(function() {
169 $('textarea', this).off()
170 // Fix cancel-on-blur
171 .on('blur', function(e) {
172 if (!e.relatedTarget || !$(e.relatedTarget).is('.crm-editable-form button')) {
173 $i.find('button[type=cancel]').click();
174 }
175 })
176 // Add support for ctrl-enter shortcut key
177 .on('keydown', function (e) {
178 if (e.ctrlKey && e.keyCode == 13) {
179 $i.find('button[type=submit]').click();
180 e.preventDefault();
181 }
182 });
183 });
184
185 function getData(value, settings) {
186 // Add css class to wrapper
187 // 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 :(
188 $i.addClass('crm-editable-editing');
189
190 originalValue = value;
191
192 if ($i.data('type') == 'select' || $i.data('type') == 'boolean') {
193 if ($i.data('options')) {
194 return formatOptions($i.data('options'));
195 }
196 var result,
197 info = $i.crmEditableEntity(),
198 hash = info.entity + '.' + info.field,
199 params = {
200 field: info.field,
201 context: 'create'
202 };
203 $i.data('optionsHashKey', hash);
204 if (!optionsCache[hash]) {
205 $.ajax({
206 url: CRM.url('civicrm/ajax/rest'),
207 data: {entity: info.entity, action: 'getoptions', json: JSON.stringify(params)},
208 async: false, // jeditable lacks support for async options lookup
209 success: function(data) {optionsCache[hash] = data.values;}
210 });
211 }
212 return formatOptions(optionsCache[hash]);
213
214 }
215 return value.replace(/<(?:.|\n)*?>/gm, '');
216 }
217
218 function formatOptions(options) {
219 if (typeof $i.data('emptyOption') === 'string') {
220 // Using 'null' because '' is broken in jeditable 1.7.1
221 return $.extend({'null': $i.data('emptyOption')}, options);
222 }
223 return options;
224 }
225
226 function restoreContainer() {
227 errorMsg && errorMsg.close && errorMsg.close();
228 $i.removeClass('crm-editable-saving crm-editable-editing');
229 }
230
231 });
232 };
233
234 $(document).on('crmLoad', function(e) {
235 $('.crm-editable', e.target).crmEditable();
236 })
237
238 })(jQuery);