2 * jQuery plugin that makes elements editable
4 * @author Victor Jonsson (http://victorjonsson.se/)
5 * @website https://github.com/victorjonsson/jquery-editable/
6 * @license GPLv2 http://www.gnu.org/licenses/gpl-2.0.html
8 * @donations http://victorjonsson.se/donations/
10 (function($, window) {
14 var $win = $(window), // Reference to window
16 // Reference to textarea
19 // Reference to currently edit element
20 $currentlyEdited = false,
23 EVENT_ATTR = 'data-edit-event',
24 IS_EDITING_ATTR = 'data-is-editing',
25 EMPTY_ATTR = 'data-is-empty',
26 DBL_TAP_EVENT = 'dbltap',
27 SUPPORTS_TOUCH = 'ontouchend' in window,
28 TINYMCE_INSTALLED = 'tinyMCE' in window && typeof window.tinyMCE.init == 'function',
30 // reference to old is function
31 oldjQueryIs = $.fn.is,
34 * Function responsible of triggering double tap event
38 var now = new Date().getTime();
39 if( (now-lastTap) < 250 ) {
40 $(this).trigger(DBL_TAP_EVENT);
46 * Event listener that largens font size
48 keyHandler = function(e) {
49 if( e.keyCode == 13 && e.data.closeOnEnter ) {
50 $currentlyEdited.editable('close');
52 else if( e.keyCode == 27 ) {
53 $textArea.val($currentlyEdited.attr('orig-text'));
54 $currentlyEdited.editable('close');
56 else if( e.data.toggleFontSize && (e.metaKey && (e.keyCode == 38 || e.keyCode == 40)) ) {
57 var fontSize = parseInt($textArea.css('font-size'), 10);
58 fontSize += e.keyCode == 40 ? -1 : 1;
59 $textArea.css('font-size', fontSize+'px');
65 * Adjusts the height of the textarea to remove scroll
66 * @todo This way of doing it does not make the textarea smaller when the number of text lines gets smaller
68 adjustTextAreaHeight = function() {
69 if( $textArea[0].scrollHeight !== parseInt($textArea.attr('data-scroll'), 10) ) {
70 $textArea.css('height', $textArea[0].scrollHeight +'px');
71 $textArea.attr('data-scroll', $textArea[0].scrollHeight);
77 * @param {String} newText
79 resetElement = function($el, newText, emptyMessage) {
80 $el.removeAttr(IS_EDITING_ATTR);
82 if (newText.length == 0 && emptyMessage) {
83 $el.html(emptyMessage);
84 $el.attr(EMPTY_ATTR, 'empty');
87 $el.removeAttr(EMPTY_ATTR);
94 * Function creating editor
96 elementEditor = function($el, opts) {
98 if( $el.is(':editing') )
101 $currentlyEdited = $el;
102 $el.attr(IS_EDITING_ATTR, '1');
104 if ($el.is(':empty')) {
105 $el.removeAttr(EMPTY_ATTR);
109 var defaultText = $.trim( $el.html() ),
110 defaultFontSize = $el.css('font-size'),
111 elementHeight = $el.height(),
112 textareaStyle = 'width: 96%; padding:0; margin:0; border:0; background:none;'+
113 'font-family: '+$el.css('font-family')+'; font-size: '+$el.css('font-size')+';'+
114 'font-weight: '+$el.css('font-weight')+';';
116 $el.attr('orig-text', defaultText);
117 if( opts.lineBreaks ) {
118 defaultText = defaultText.replace(/<br( |)(|\/)>/g, '\n');
121 $textArea = $('<textarea></textarea>');
124 if( navigator.userAgent.match(/webkit/i) !== null ) {
125 textareaStyle = document.defaultView.getComputedStyle($el.get(0), "").cssText;
128 // The editor should always be static
129 textareaStyle += 'position: static';
134 if( opts.tinyMCE !== false ) {
135 var id = 'editable-area-'+(new Date().getTime());
141 if( typeof opts.tinyMCE != 'object' )
144 opts.tinyMCE.mode = 'exact';
145 opts.tinyMCE.elements = id;
146 opts.tinyMCE.width = $el.innerWidth();
147 opts.tinyMCE.height = $el.height() + 200;
148 opts.tinyMCE.theme_advanced_resize_vertical = true;
150 opts.tinyMCE.setup = function (ed) {
151 ed.onInit.add(function(editor, evt) {
152 var editorWindow = editor.getWin();
153 var hasPressedKey = false;
154 var editorBlur = function() {
156 var newText = $(editor.getDoc()).find('body').html();
157 if( $(newText).get(0).nodeName == $el.get(0).nodeName ) {
158 newText = $(newText).html();
161 // Update element and remove editor
162 resetElement($el, newText, opts.emptyMessage);
165 $win.unbind('click', editorBlur);
166 $currentlyEdited = false;
169 if( typeof opts.callback == 'function' ) {
171 content : newText == defaultText || !hasPressedKey ? false : newText,
176 else if (newText != defaultText) {
177 var route = $($el).attr('route');
178 var name = $($el).attr('name');
179 var item_id = $($el).attr('item_id');
180 if (typeof(route) == 'string' && typeof(item_id) == 'string')
188 // Blur editor when user clicks outside the editor
189 setTimeout(function() {
190 $win.bind('click', editorBlur);
193 // Create a dummy textarea that will called upon when
194 // programmatically interacting with the editor
195 $textArea = $('<textarea></textarea>');
196 $textArea.bind('blur', editorBlur);
198 editorWindow.onkeydown = function() {
199 hasPressedKey = true;
202 editorWindow.focus();
206 tinyMCE.init(opts.tinyMCE);
214 if( opts.toggleFontSize || opts.closeOnEnter ) {
215 $win.bind('keydown', opts, keyHandler);
217 $win.bind('keyup', adjustTextAreaHeight);
223 $currentlyEdited = false;
225 // Get new text and font size
226 var newText = $.trim( $textArea.val() ),
227 newFontSize = $textArea.css('font-size');
228 if( opts.lineBreaks ) {
229 newText = newText.replace(new RegExp('\n','g'), '<br />');
233 resetElement($el, newText, opts.emptyMessage);
234 if( newFontSize != defaultFontSize ) {
235 $el.css('font-size', newFontSize);
238 // remove textarea and size toggles
239 $win.unbind('keydown', keyHandler);
240 $win.unbind('keyup', adjustTextAreaHeight);
243 if( typeof opts.callback == 'function' ) {
245 content : newText == defaultText ? false : newText,
246 fontSize : newFontSize == defaultFontSize ? false : newFontSize,
250 else if (newText != defaultText) {
251 var route = $($el).attr('route');
252 var name = $($el).attr('name');
253 var item_id = $($el).attr('item_id');
254 if (typeof(route) == 'string' && typeof(item_id) == 'string')
261 .attr('style', textareaStyle)
266 height : elementHeight +'px',
269 .css(opts.editorStyle)
273 adjustTextAreaHeight();
277 $el.trigger('edit', [$textArea]);
283 editEvent = function(event) {
285 if( $currentlyEdited !== false && !$currentlyEdited.children("textarea").is(clickedElement)) {
286 // Not closing the currently open editor before opening a new
287 // editor makes things go crazy
288 $currentlyEdited.editable('close');
289 elementEditor($(this), event.data);
292 elementEditor($(this), event.data);
298 * Jquery plugin that makes elments editable
299 * @param {Object|String} [opts] Either callback function or the string 'destroy' if wanting to remove the editor event
300 * @return {jQuery|Boolean}
302 $.fn.editable = function(opts) {
304 if(typeof opts == 'string') {
306 if( this.is(':editable') ) {
310 if( !this.is(':editing') ) {
311 this.trigger(this.attr(EVENT_ATTR));
315 if( this.is(':editing') ) {
316 $textArea.trigger('blur');
320 if( this.is(':editing') ) {
321 $textArea.trigger('blur');
323 this.unbind(this.attr(EVENT_ATTR));
324 this.removeAttr(EVENT_ATTR);
327 console.warn('Unknown command "'+opts+'" for jquery.editable');
331 console.error('Calling .editable() on an element that is not editable, call .editable() first');
336 if( this.is(':editable') ) {
337 console.warn('Making an already editable element editable, call .editable("destroy") first');
338 this.editable('destroy');
345 toggleFontSize : true,
346 closeOnEnter : false,
347 emptyMessage : false,
352 if( opts.tinyMCE !== false && !TINYMCE_INSTALLED ) {
353 console.warn('Trying to use tinyMCE as editor but id does not seem to be installed');
354 opts.tinyMCE = false;
357 if( SUPPORTS_TOUCH && opts.touch ) {
358 opts.event = DBL_TAP_EVENT;
359 this.unbind('touchend', tapper);
360 this.bind('touchend', tapper);
363 opts.event += '.textEditor';
366 this.bind(opts.event, opts, editEvent);
367 this.attr(EVENT_ATTR, opts.event);
369 // If it is empty to start with, apply the empty message
370 if (this.html().length == 0 && opts.emptyMessage) {
371 this.html(opts.emptyMessage);
372 this.attr(EMPTY_ATTR, 'empty');
374 this.removeAttr(EMPTY_ATTR);
382 * Add :editable :editing to $.is()
383 * @param {Object} statement
386 $.fn.is = function(statement) {
387 if( typeof statement == 'string' && statement.indexOf(':') === 0) {
388 if( statement == ':editable' ) {
389 return this.attr(EVENT_ATTR) !== undefined;
390 } else if( statement == ':editing' ) {
391 return this.attr(IS_EDITING_ATTR) !== undefined;
392 } else if( statement == ':empty' ) {
393 return this.attr(EMPTY_ATTR) !== undefined;
396 return oldjQueryIs.apply(this, arguments);
399 // The latest element clicked
401 $(document).mousedown(function(e) {
402 clickedElement = $(e.target);