2 * Jeditable - jQuery in place edit plugin
4 * Copyright (c) 2006-2009 Mika Tuupola, Dylan Verheul
6 * Licensed under the MIT license:
7 * http://www.opensource.org/licenses/mit-license.php
10 * http://www.appelsiini.net/projects/jeditable
12 * Based on editable by Dylan Verheul <dylan_at_dyve.net>:
13 * http://www.dyve.net/jquery/?editable
20 * ** means there is basic unit tests for this parameter.
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 ??
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
56 * @param Hash options[ajaxoptions] jQuery Ajax options. See docs.jquery.com.
62 $.fn
.editable = function(target
, options
) {
64 if ('disable' == target
) {
65 $(this).data('disabled.editable', true);
68 if ('enable' == target
) {
69 $(this).data('disabled.editable', false);
72 if ('destroy' == target
) {
74 .unbind($(this).data('event.editable'))
75 .removeData('disabled.editable')
76 .removeData('event.editable');
80 var settings
= $.extend({}, $.fn
.editable
.defaults
, {target
:target
}, options
);
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
;
100 if (settings
.tooltip
) {
101 $(this).attr('title', settings
.tooltip
);
104 settings
.autowidth
= 'auto' == settings
.width
;
105 settings
.autoheight
= 'auto' == settings
.height
;
107 return this.each(function() {
109 /* save this to self because this changes when scope changes */
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();
117 /* save so it can be later used by $.editable('destroy') */
118 $(this).data('event.editable', settings
.event
);
120 /* if element is empty add something clickable (if requested) */
121 if (!$.trim($(this).html())) {
122 $(this).html(settings
.placeholder
);
125 $(this).bind(settings
.event
, function(e
) {
127 /* abort if disabled for this element */
128 if (true === $(this).data('disabled.editable')) {
132 /* prevent throwing an exeption if edit field is clicked again */
137 /* abort if onedit hook returns false */
138 if (false === onedit
.apply(this, [settings
, self
])) {
142 /* prevent default action and bubbling */
147 if (settings
.tooltip
) {
148 $(self
).removeAttr('title');
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;
158 if (settings.width != 'none') {
160 settings.autowidth ? $(self).width() : settings.width;
162 if (settings.height != 'none') {
164 settings.autoheight ? $(self).height() : settings.height;
167 //$(this).css('visibility', '');
169 /* remove placeholder text, replace is here because of IE */
170 if ($(this).html().toLowerCase().replace(/(;|")/g, '') ==
171 settings
.placeholder
.toLowerCase().replace(/(;|")/g, '')) {
176 self
.revert
= $(self
).html();
179 /* create the form object */
180 var form
= $('<form />');
182 /* apply css or style or both */
183 if (settings
.cssclass
) {
184 if ('inherit' == settings
.cssclass
) {
185 form
.attr('class', $(self
).attr('class'));
187 form
.attr('class', settings
.cssclass
);
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'));
197 form
.attr('style', settings
.style
);
201 /* add main input element to form and store it in input */
202 var input
= element
.apply(form
, [settings
, self
]);
204 /* set input content via POST, GET, given data or existing value */
207 if (settings
.loadurl
) {
208 var t
= setTimeout(function() {
209 input
.disabled
= true;
210 content
.apply(form
, [settings
.loadtext
, settings
, self
]);
214 loaddata
[settings
.id
] = self
.id
;
215 if ($.isFunction(settings
.loaddata
)) {
216 $.extend(loaddata
, settings
.loaddata
.apply(self
, [self
.revert
, settings
]));
218 $.extend(loaddata
, settings
.loaddata
);
221 type
: settings
.loadtype
,
222 url
: settings
.loadurl
,
225 success: function(result
) {
226 window
.clearTimeout(t
);
227 input_content
= result
;
228 input
.disabled
= false;
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
]);
237 input_content
= self
.revert
;
239 content
.apply(form
, [input_content
, settings
, self
]);
241 input
.attr('name', settings
.name
);
243 /* add buttons to the form */
244 buttons
.apply(form
, [settings
, self
]);
246 /* add created form to self */
247 $(self
).append(form
);
249 /* attach 3rd party plugin if requested */
250 plugin
.apply(form
, [settings
, self
]);
252 /* focus to first visible form element */
253 $(':input:visible:enabled:first', form
).focus();
255 /* highlight input contents when requested */
256 if (settings
.select
) {
260 /* discard changes if pressing esc */
261 input
.keydown(function(e
) {
262 if (e
.keyCode
== 27) {
265 reset
.apply(form
, [settings
, self
]);
269 /* discard, submit or nothing with changes when clicking outside */
270 /* do nothing is usable when navigating with tab */
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
]);
279 } else if ('submit' == settings
.onblur
) {
280 input
.blur(function(e
) {
281 /* prevent double submit if submit was clicked */
282 t
= setTimeout(function() {
286 } else if ($.isFunction(settings
.onblur
)) {
287 input
.blur(function(e
) {
288 settings
.onblur
.apply(self
, [input
.val(), settings
]);
291 input
.blur(function(e
) {
292 /* TODO: maybe something here */
296 form
.submit(function(e
) {
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
])) {
312 /* check if given target is function */
313 if ($.isFunction(settings
.target
)) {
314 var str
= settings
.target
.apply(self
, [input
.val(), settings
]);
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
);
323 /* add edited content and id of edited element to POST */
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
]));
331 $.extend(submitdata
, settings
.submitdata
);
334 /* quick and dirty PUT support */
335 if ('PUT' == settings
.method
) {
336 submitdata
['_method'] = 'put';
339 /* show the saving indicator */
340 $(self
).html(settings
.indicator
);
342 /* defaults for ajaxoptions */
347 url
: settings
.target
,
348 success : function(result
, status
) {
349 if (ajaxoptions
.dataType
== 'html') {
350 $(self
).html(result
);
352 self
.editing
= false;
353 callback
.apply(self
, [result
, settings
]);
354 if (!$.trim($(self
).html())) {
355 $(self
).html(settings
.placeholder
);
358 error : function(xhr
, status
, error
) {
359 onerror
.apply(form
, [settings
, self
, xhr
]);
363 /* override with what is given in settings.ajaxoptions */
364 $.extend(ajaxoptions
, settings
.ajaxoptions
);
371 /* show tooltip again */
372 $(self
).attr('title', settings
.tooltip
);
378 /* privileged methods */
379 this.reset = function(form
) {
380 /* prevent calling reset twice when blurring */
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
);
389 /* show tooltip again */
390 if (settings
.tooltip
) {
391 $(self
).attr('title', settings
.tooltip
);
404 element : function(settings
, original
) {
405 var input
= $('<input type="hidden"></input>');
406 $(this).append(input
);
409 content : function(string
, settings
, original
) {
410 $(':input:first', this).val(string
);
412 reset : function(settings
, original
) {
413 original
.reset(this);
415 buttons : function(settings
, original
) {
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") {
425 /* otherwise use button with given string as text */
427 var submit
= $('<button type="submit" />');
428 submit
.html(settings
.submit
);
430 $(this).append(submit
);
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 */
438 var cancel
= $('<button type="cancel" />');
439 cancel
.html(settings
.cancel
);
441 $(this).append(cancel
);
443 $(cancel
).click(function(event
) {
445 if ($.isFunction($.editable
.types
[settings
.type
].reset
)) {
446 var reset
= $.editable
.types
[settings
.type
].reset
;
448 var reset
= $.editable
.types
['defaults'].reset
;
450 reset
.apply(form
, [settings
, original
]);
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
);
469 element : function(settings
, original
) {
470 var textarea
= $('<textarea />');
472 textarea
.attr('rows', settings
.rows
);
473 } else if (settings
.height
!= "none") {
474 textarea
.height(settings
.height
);
477 textarea
.attr('cols', settings
.cols
);
478 } else if (settings
.width
!= "none") {
479 textarea
.width(settings
.width
);
481 $(this).append(textarea
);
486 element : function(settings
, original
) {
487 var select
= $('<select />');
488 $(this).append(select
);
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
);
496 /* Otherwise assume it is a hash already. */
499 for (var key
in json
) {
500 if (!json
.hasOwnProperty(key
)) {
503 if ('selected' == key
) {
506 var option
= $('<option />').val(key
).append(json
[key
]);
507 $('select', this).append(option
);
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');
520 /* Add new input type */
521 addInputType: function(name
, input
) {
522 $.editable
.types
[name
] = input
;
526 // publicly accessible defaults
527 $.fn
.editable
.defaults
= {
533 event
: 'click.editable',
536 loadtext
: 'Loading...',
537 placeholder
: 'Click to edit',