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,
178 // Blur editor when user clicks outside the editor
179 setTimeout(function() {
180 $win.bind('click', editorBlur);
183 // Create a dummy textarea that will called upon when
184 // programmatically interacting with the editor
185 $textArea = $('<textarea></textarea>');
186 $textArea.bind('blur', editorBlur);
188 editorWindow.onkeydown = function() {
189 hasPressedKey = true;
192 editorWindow.focus();
196 tinyMCE.init(opts.tinyMCE);
204 if( opts.toggleFontSize || opts.closeOnEnter ) {
205 $win.bind('keydown', opts, keyHandler);
207 $win.bind('keyup', adjustTextAreaHeight);
213 $currentlyEdited = false;
215 // Get new text and font size
216 var newText = $.trim( $textArea.val() ),
217 newFontSize = $textArea.css('font-size');
218 if( opts.lineBreaks ) {
219 newText = newText.replace(new RegExp('\n','g'), '<br />');
223 resetElement($el, newText, opts.emptyMessage);
224 if( newFontSize != defaultFontSize ) {
225 $el.css('font-size', newFontSize);
228 // remove textarea and size toggles
229 $win.unbind('keydown', keyHandler);
230 $win.unbind('keyup', adjustTextAreaHeight);
233 if( typeof opts.callback == 'function' ) {
235 content : newText == defaultText ? false : newText,
236 fontSize : newFontSize == defaultFontSize ? false : newFontSize,
241 .attr('style', textareaStyle)
246 height : elementHeight +'px',
249 .css(opts.editorStyle)
253 adjustTextAreaHeight();
257 $el.trigger('edit', [$textArea]);
263 editEvent = function(event) {
265 if( $currentlyEdited !== false && !$currentlyEdited.children("textarea").is(clickedElement)) {
266 // Not closing the currently open editor before opening a new
267 // editor makes things go crazy
268 $currentlyEdited.editable('close');
269 elementEditor($(this), event.data);
272 elementEditor($(this), event.data);
278 * Jquery plugin that makes elments editable
279 * @param {Object|String} [opts] Either callback function or the string 'destroy' if wanting to remove the editor event
280 * @return {jQuery|Boolean}
282 $.fn.editable = function(opts) {
284 if(typeof opts == 'string') {
286 if( this.is(':editable') ) {
290 if( !this.is(':editing') ) {
291 this.trigger(this.attr(EVENT_ATTR));
295 if( this.is(':editing') ) {
296 $textArea.trigger('blur');
300 if( this.is(':editing') ) {
301 $textArea.trigger('blur');
303 this.unbind(this.attr(EVENT_ATTR));
304 this.removeAttr(EVENT_ATTR);
307 console.warn('Unknown command "'+opts+'" for jquery.editable');
311 console.error('Calling .editable() on an element that is not editable, call .editable() first');
316 if( this.is(':editable') ) {
317 console.warn('Making an already editable element editable, call .editable("destroy") first');
318 this.editable('destroy');
325 toggleFontSize : true,
326 closeOnEnter : false,
327 emptyMessage : false,
332 if( opts.tinyMCE !== false && !TINYMCE_INSTALLED ) {
333 console.warn('Trying to use tinyMCE as editor but id does not seem to be installed');
334 opts.tinyMCE = false;
337 if( SUPPORTS_TOUCH && opts.touch ) {
338 opts.event = DBL_TAP_EVENT;
339 this.unbind('touchend', tapper);
340 this.bind('touchend', tapper);
343 opts.event += '.textEditor';
346 this.bind(opts.event, opts, editEvent);
347 this.attr(EVENT_ATTR, opts.event);
349 // If it is empty to start with, apply the empty message
350 if (this.html().length == 0 && opts.emptyMessage) {
351 this.html(opts.emptyMessage);
352 this.attr(EMPTY_ATTR, 'empty');
354 this.removeAttr(EMPTY_ATTR);
362 * Add :editable :editing to $.is()
363 * @param {Object} statement
366 $.fn.is = function(statement) {
367 if( typeof statement == 'string' && statement.indexOf(':') === 0) {
368 if( statement == ':editable' ) {
369 return this.attr(EVENT_ATTR) !== undefined;
370 } else if( statement == ':editing' ) {
371 return this.attr(IS_EDITING_ATTR) !== undefined;
372 } else if( statement == ':empty' ) {
373 return this.attr(EMPTY_ATTR) !== undefined;
376 return oldjQueryIs.apply(this, arguments);
379 // The latest element clicked
381 $(document).mousedown(function(e) {
382 clickedElement = $(e.target);