5a920362 |
1 | /* |
2 | * Jeditable - jQuery in place edit plugin |
3 | * |
4 | * Copyright (c) 2006-2009 Mika Tuupola, Dylan Verheul |
5 | * |
6 | * Licensed under the MIT license: |
7 | * http://www.opensource.org/licenses/mit-license.php |
8 | * |
9 | * Project home: |
10 | * http://www.appelsiini.net/projects/jeditable |
11 | * |
12 | * Based on editable by Dylan Verheul <dylan_at_dyve.net>: |
13 | * http://www.dyve.net/jquery/?editable |
14 | * |
15 | */ |
16 | |
17 | /** |
18 | * Version 1.7.1 |
19 | * |
20 | * ** means there is basic unit tests for this parameter. |
21 | * |
22 | * @name Jeditable |
23 | * @type jQuery |
24 | * @param String target (POST) URL or function to send edited content to ** |
25 | * @param Hash options additional options |
26 | * @param String options[method] method to use to send edited content (POST or PUT) ** |
27 | * @param Function options[callback] Function to run after submitting edited content ** |
28 | * @param String options[name] POST parameter name of edited content |
29 | * @param String options[id] POST parameter name of edited div id |
30 | * @param Hash options[submitdata] Extra parameters to send when submitting edited content. |
31 | * @param String options[type] text, textarea or select (or any 3rd party input type) ** |
32 | * @param Integer options[rows] number of rows if using textarea ** |
33 | * @param Integer options[cols] number of columns if using textarea ** |
34 | * @param Mixed options[height] 'auto', 'none' or height in pixels ** |
35 | * @param Mixed options[width] 'auto', 'none' or width in pixels ** |
36 | * @param String options[loadurl] URL to fetch input content before editing ** |
37 | * @param String options[loadtype] Request type for load url. Should be GET or POST. |
38 | * @param String options[loadtext] Text to display while loading external content. |
39 | * @param Mixed options[loaddata] Extra parameters to pass when fetching content before editing. |
40 | * @param Mixed options[data] Or content given as paramameter. String or function.** |
41 | * @param String options[indicator] indicator html to show when saving |
42 | * @param String options[tooltip] optional tooltip text via title attribute ** |
43 | * @param String options[event] jQuery event such as 'click' of 'dblclick' ** |
44 | * @param String options[submit] submit button value, empty means no button ** |
45 | * @param String options[cancel] cancel button value, empty means no button ** |
46 | * @param String options[cssclass] CSS class to apply to input form. 'inherit' to copy from parent. ** |
47 | * @param String options[style] Style to apply to input form 'inherit' to copy from parent. ** |
48 | * @param String options[select] true or false, when true text is highlighted ?? |
49 | * @param String options[placeholder] Placeholder text or html to insert when element is empty. ** |
50 | * @param String options[onblur] 'cancel', 'submit', 'ignore' or function ?? |
51 | * |
52 | * @param Function options[onsubmit] function(settings, original) { ... } called before submit |
53 | * @param Function options[onreset] function(settings, original) { ... } called before reset |
54 | * @param Function options[onerror] function(settings, original, xhr) { ... } called on error |
55 | * |
56 | * @param Hash options[ajaxoptions] jQuery Ajax options. See docs.jquery.com. |
57 | * |
58 | */ |
59 | |
60 | (function($) { |
61 | |
62 | $.fn.editable = function(target, options) { |
63 | |
64 | if ('disable' == target) { |
65 | $(this).data('disabled.editable', true); |
66 | return; |
67 | } |
68 | if ('enable' == target) { |
69 | $(this).data('disabled.editable', false); |
70 | return; |
71 | } |
72 | if ('destroy' == target) { |
73 | $(this) |
74 | .unbind($(this).data('event.editable')) |
75 | .removeData('disabled.editable') |
76 | .removeData('event.editable'); |
77 | return; |
78 | } |
79 | |
80 | var settings = $.extend({}, $.fn.editable.defaults, {target:target}, options); |
81 | |
82 | /* setup some functions */ |
83 | var plugin = $.editable.types[settings.type].plugin || function() { }; |
84 | var submit = $.editable.types[settings.type].submit || function() { }; |
85 | var buttons = $.editable.types[settings.type].buttons |
86 | || $.editable.types['defaults'].buttons; |
87 | var content = $.editable.types[settings.type].content |
88 | || $.editable.types['defaults'].content; |
89 | var element = $.editable.types[settings.type].element |
90 | || $.editable.types['defaults'].element; |
91 | var reset = $.editable.types[settings.type].reset |
92 | || $.editable.types['defaults'].reset; |
93 | var callback = settings.callback || function() { }; |
94 | var onedit = settings.onedit || function() { }; |
95 | var onsubmit = settings.onsubmit || function() { }; |
96 | var onreset = settings.onreset || function() { }; |
97 | var onerror = settings.onerror || reset; |
98 | |
99 | /* show tooltip */ |
100 | if (settings.tooltip) { |
101 | $(this).attr('title', settings.tooltip); |
102 | } |
103 | |
104 | settings.autowidth = 'auto' == settings.width; |
105 | settings.autoheight = 'auto' == settings.height; |
106 | |
107 | return this.each(function() { |
108 | |
109 | /* save this to self because this changes when scope changes */ |
110 | var self = this; |
111 | |
112 | /* inlined block elements lose their width and height after first edit */ |
113 | /* save them for later use as workaround */ |
114 | var savedwidth = $(self).width(); |
115 | var savedheight = $(self).height(); |
116 | |
117 | /* save so it can be later used by $.editable('destroy') */ |
118 | $(this).data('event.editable', settings.event); |
119 | |
120 | /* if element is empty add something clickable (if requested) */ |
121 | if (!$.trim($(this).html())) { |
122 | $(this).html(settings.placeholder); |
123 | } |
124 | |
125 | $(this).bind(settings.event, function(e) { |
126 | |
127 | /* abort if disabled for this element */ |
128 | if (true === $(this).data('disabled.editable')) { |
129 | return; |
130 | } |
131 | |
132 | /* prevent throwing an exeption if edit field is clicked again */ |
133 | if (self.editing) { |
134 | return; |
135 | } |
136 | |
137 | /* abort if onedit hook returns false */ |
138 | if (false === onedit.apply(this, [settings, self])) { |
139 | return; |
140 | } |
141 | |
142 | /* prevent default action and bubbling */ |
143 | e.preventDefault(); |
144 | e.stopPropagation(); |
145 | |
146 | /* remove tooltip */ |
147 | if (settings.tooltip) { |
148 | $(self).removeAttr('title'); |
149 | } |
150 | |
151 | /* figure out how wide and tall we are, saved width and height */ |
152 | /* are workaround for http://dev.jquery.com/ticket/2190 */ |
153 | if (0 == $(self).width()) { |
154 | //$(self).css('visibility', 'hidden'); |
155 | settings.width = savedwidth; |
156 | settings.height = savedheight; |
157 | } else { |
158 | if (settings.width != 'none') { |
159 | settings.width = |
160 | settings.autowidth ? $(self).width() : settings.width; |
161 | } |
162 | if (settings.height != 'none') { |
163 | settings.height = |
164 | settings.autoheight ? $(self).height() : settings.height; |
165 | } |
166 | } |
167 | //$(this).css('visibility', ''); |
168 | |
169 | /* remove placeholder text, replace is here because of IE */ |
170 | if ($(this).html().toLowerCase().replace(/(;|")/g, '') == |
171 | settings.placeholder.toLowerCase().replace(/(;|")/g, '')) { |
172 | $(this).html(''); |
173 | } |
174 | |
175 | self.editing = true; |
176 | self.revert = $(self).html(); |
177 | $(self).html(''); |
178 | |
179 | /* create the form object */ |
180 | var form = $('<form />'); |
181 | |
182 | /* apply css or style or both */ |
183 | if (settings.cssclass) { |
184 | if ('inherit' == settings.cssclass) { |
185 | form.attr('class', $(self).attr('class')); |
186 | } else { |
187 | form.attr('class', settings.cssclass); |
188 | } |
189 | } |
190 | |
191 | if (settings.style) { |
192 | if ('inherit' == settings.style) { |
193 | form.attr('style', $(self).attr('style')); |
194 | /* IE needs the second line or display wont be inherited */ |
195 | form.css('display', $(self).css('display')); |
196 | } else { |
197 | form.attr('style', settings.style); |
198 | } |
199 | } |
200 | |
201 | /* add main input element to form and store it in input */ |
202 | var input = element.apply(form, [settings, self]); |
203 | |
204 | /* set input content via POST, GET, given data or existing value */ |
205 | var input_content; |
206 | |
207 | if (settings.loadurl) { |
208 | var t = setTimeout(function() { |
209 | input.disabled = true; |
210 | content.apply(form, [settings.loadtext, settings, self]); |
211 | }, 100); |
212 | |
213 | var loaddata = {}; |
214 | loaddata[settings.id] = self.id; |
215 | if ($.isFunction(settings.loaddata)) { |
216 | $.extend(loaddata, settings.loaddata.apply(self, [self.revert, settings])); |
217 | } else { |
218 | $.extend(loaddata, settings.loaddata); |
219 | } |
220 | $.ajax({ |
221 | type : settings.loadtype, |
222 | url : settings.loadurl, |
223 | data : loaddata, |
224 | async : false, |
225 | success: function(result) { |
226 | window.clearTimeout(t); |
227 | input_content = result; |
228 | input.disabled = false; |
229 | } |
230 | }); |
231 | } else if (settings.data) { |
232 | input_content = settings.data; |
233 | if ($.isFunction(settings.data)) { |
234 | input_content = settings.data.apply(self, [self.revert, settings]); |
235 | } |
236 | } else { |
237 | input_content = self.revert; |
238 | } |
239 | content.apply(form, [input_content, settings, self]); |
240 | |
241 | input.attr('name', settings.name); |
242 | |
243 | /* add buttons to the form */ |
244 | buttons.apply(form, [settings, self]); |
245 | |
246 | /* add created form to self */ |
247 | $(self).append(form); |
248 | |
249 | /* attach 3rd party plugin if requested */ |
250 | plugin.apply(form, [settings, self]); |
251 | |
252 | /* focus to first visible form element */ |
253 | $(':input:visible:enabled:first', form).focus(); |
254 | |
255 | /* highlight input contents when requested */ |
256 | if (settings.select) { |
257 | input.select(); |
258 | } |
259 | |
260 | /* discard changes if pressing esc */ |
261 | input.keydown(function(e) { |
262 | if (e.keyCode == 27) { |
263 | e.preventDefault(); |
264 | //self.reset(); |
265 | reset.apply(form, [settings, self]); |
266 | } |
267 | }); |
268 | |
269 | /* discard, submit or nothing with changes when clicking outside */ |
270 | /* do nothing is usable when navigating with tab */ |
271 | var t; |
272 | if ('cancel' == settings.onblur) { |
273 | input.blur(function(e) { |
274 | /* prevent canceling if submit was clicked */ |
275 | t = setTimeout(function() { |
276 | reset.apply(form, [settings, self]); |
277 | }, 500); |
278 | }); |
279 | } else if ('submit' == settings.onblur) { |
280 | input.blur(function(e) { |
281 | /* prevent double submit if submit was clicked */ |
282 | t = setTimeout(function() { |
283 | form.submit(); |
284 | }, 200); |
285 | }); |
286 | } else if ($.isFunction(settings.onblur)) { |
287 | input.blur(function(e) { |
288 | settings.onblur.apply(self, [input.val(), settings]); |
289 | }); |
290 | } else { |
291 | input.blur(function(e) { |
292 | /* TODO: maybe something here */ |
293 | }); |
294 | } |
295 | |
296 | form.submit(function(e) { |
297 | |
298 | if (t) { |
299 | clearTimeout(t); |
300 | } |
301 | |
302 | /* do no submit */ |
303 | e.preventDefault(); |
304 | |
305 | /* call before submit hook. */ |
306 | /* if it returns false abort submitting */ |
307 | if (false !== onsubmit.apply(form, [settings, self])) { |
308 | /* custom inputs call before submit hook. */ |
309 | /* if it returns false abort submitting */ |
310 | if (false !== submit.apply(form, [settings, self])) { |
311 | |
312 | /* check if given target is function */ |
313 | if ($.isFunction(settings.target)) { |
314 | var str = settings.target.apply(self, [input.val(), settings]); |
315 | $(self).html(str); |
316 | self.editing = false; |
317 | callback.apply(self, [self.innerHTML, settings]); |
318 | /* TODO: this is not dry */ |
319 | if (!$.trim($(self).html())) { |
320 | $(self).html(settings.placeholder); |
321 | } |
322 | } else { |
323 | /* add edited content and id of edited element to POST */ |
324 | var submitdata = {}; |
325 | submitdata[settings.name] = input.val(); |
326 | submitdata[settings.id] = self.id; |
327 | /* add extra data to be POST:ed */ |
328 | if ($.isFunction(settings.submitdata)) { |
329 | $.extend(submitdata, settings.submitdata.apply(self, [self.revert, settings])); |
330 | } else { |
331 | $.extend(submitdata, settings.submitdata); |
332 | } |
333 | |
334 | /* quick and dirty PUT support */ |
335 | if ('PUT' == settings.method) { |
336 | submitdata['_method'] = 'put'; |
337 | } |
338 | |
339 | /* show the saving indicator */ |
340 | $(self).html(settings.indicator); |
341 | |
342 | /* defaults for ajaxoptions */ |
343 | var ajaxoptions = { |
344 | type : 'POST', |
345 | data : submitdata, |
346 | dataType: 'html', |
347 | url : settings.target, |
348 | success : function(result, status) { |
349 | if (ajaxoptions.dataType == 'html') { |
350 | $(self).html(result); |
351 | } |
352 | self.editing = false; |
353 | callback.apply(self, [result, settings]); |
354 | if (!$.trim($(self).html())) { |
355 | $(self).html(settings.placeholder); |
356 | } |
357 | }, |
358 | error : function(xhr, status, error) { |
359 | onerror.apply(form, [settings, self, xhr]); |
360 | } |
361 | }; |
362 | |
363 | /* override with what is given in settings.ajaxoptions */ |
364 | $.extend(ajaxoptions, settings.ajaxoptions); |
365 | $.ajax(ajaxoptions); |
366 | |
367 | } |
368 | } |
369 | } |
370 | |
371 | /* show tooltip again */ |
372 | $(self).attr('title', settings.tooltip); |
373 | |
374 | return false; |
375 | }); |
376 | }); |
377 | |
378 | /* privileged methods */ |
379 | this.reset = function(form) { |
380 | /* prevent calling reset twice when blurring */ |
381 | if (this.editing) { |
382 | /* before reset hook, if it returns false abort reseting */ |
383 | if (false !== onreset.apply(form, [settings, self])) { |
384 | $(self).html(self.revert); |
385 | self.editing = false; |
386 | if (!$.trim($(self).html())) { |
387 | $(self).html(settings.placeholder); |
388 | } |
389 | /* show tooltip again */ |
390 | if (settings.tooltip) { |
391 | $(self).attr('title', settings.tooltip); |
392 | } |
393 | } |
394 | } |
395 | }; |
396 | }); |
397 | |
398 | }; |
399 | |
400 | |
401 | $.editable = { |
402 | types: { |
403 | defaults: { |
404 | element : function(settings, original) { |
405 | var input = $('<input type="hidden"></input>'); |
406 | $(this).append(input); |
407 | return(input); |
408 | }, |
409 | content : function(string, settings, original) { |
410 | $(':input:first', this).val(string); |
411 | }, |
412 | reset : function(settings, original) { |
413 | original.reset(this); |
414 | }, |
415 | buttons : function(settings, original) { |
416 | var form = this; |
417 | if (settings.submit) { |
418 | /* if given html string use that */ |
419 | if (settings.submit.match(/>$/)) { |
420 | var submit = $(settings.submit).click(function() { |
421 | if (submit.attr("type") != "submit") { |
422 | form.submit(); |
423 | } |
424 | }); |
425 | /* otherwise use button with given string as text */ |
426 | } else { |
427 | var submit = $('<button type="submit" />'); |
428 | submit.html(settings.submit); |
429 | } |
430 | $(this).append(submit); |
431 | } |
432 | if (settings.cancel) { |
433 | /* if given html string use that */ |
434 | if (settings.cancel.match(/>$/)) { |
435 | var cancel = $(settings.cancel); |
436 | /* otherwise use button with given string as text */ |
437 | } else { |
438 | var cancel = $('<button type="cancel" />'); |
439 | cancel.html(settings.cancel); |
440 | } |
441 | $(this).append(cancel); |
442 | |
443 | $(cancel).click(function(event) { |
444 | //original.reset(); |
445 | if ($.isFunction($.editable.types[settings.type].reset)) { |
446 | var reset = $.editable.types[settings.type].reset; |
447 | } else { |
448 | var reset = $.editable.types['defaults'].reset; |
449 | } |
450 | reset.apply(form, [settings, original]); |
451 | return false; |
452 | }); |
453 | } |
454 | } |
455 | }, |
456 | text: { |
457 | element : function(settings, original) { |
458 | var input = $('<input />'); |
459 | if (settings.width != 'none') { input.width(settings.width); } |
460 | if (settings.height != 'none') { input.height(settings.height); } |
461 | /* https://bugzilla.mozilla.org/show_bug.cgi?id=236791 */ |
462 | //input[0].setAttribute('autocomplete','off'); |
463 | input.attr('autocomplete','off'); |
464 | $(this).append(input); |
465 | return(input); |
466 | } |
467 | }, |
468 | textarea: { |
469 | element : function(settings, original) { |
470 | var textarea = $('<textarea />'); |
471 | if (settings.rows) { |
472 | textarea.attr('rows', settings.rows); |
473 | } else if (settings.height != "none") { |
474 | textarea.height(settings.height); |
475 | } |
476 | if (settings.cols) { |
477 | textarea.attr('cols', settings.cols); |
478 | } else if (settings.width != "none") { |
479 | textarea.width(settings.width); |
480 | } |
481 | $(this).append(textarea); |
482 | return(textarea); |
483 | } |
484 | }, |
485 | select: { |
486 | element : function(settings, original) { |
487 | var select = $('<select />'); |
488 | $(this).append(select); |
489 | return(select); |
490 | }, |
491 | content : function(data, settings, original) { |
492 | /* If it is string assume it is json. */ |
493 | if (String == data.constructor) { |
494 | eval ('var json = ' + data); |
495 | } else { |
496 | /* Otherwise assume it is a hash already. */ |
497 | var json = data; |
498 | } |
499 | for (var key in json) { |
500 | if (!json.hasOwnProperty(key)) { |
501 | continue; |
502 | } |
503 | if ('selected' == key) { |
504 | continue; |
505 | } |
506 | var option = $('<option />').val(key).append(json[key]); |
507 | $('select', this).append(option); |
508 | } |
509 | /* Loop option again to set selected. IE needed this... */ |
510 | $('select', this).children().each(function() { |
511 | if ($(this).val() == json['selected'] || |
512 | $(this).text() == $.trim(original.revert)) { |
513 | $(this).attr('selected', 'selected'); |
514 | } |
515 | }); |
516 | } |
517 | } |
518 | }, |
519 | |
520 | /* Add new input type */ |
521 | addInputType: function(name, input) { |
522 | $.editable.types[name] = input; |
523 | } |
524 | }; |
525 | |
526 | // publicly accessible defaults |
527 | $.fn.editable.defaults = { |
528 | name : 'value', |
529 | id : 'id', |
530 | type : 'text', |
531 | width : 'auto', |
532 | height : 'auto', |
533 | event : 'click.editable', |
534 | onblur : 'cancel', |
535 | loadtype : 'GET', |
536 | loadtext : 'Loading...', |
537 | placeholder: 'Click to edit', |
538 | loaddata : {}, |
539 | submitdata : {}, |
540 | ajaxoptions: {} |
541 | }; |
542 | |
543 | })(jQuery); |