/* * jQuery plugin that makes elements editable * * @author Victor Jonsson (http://victorjonsson.se/) * @website https://github.com/victorjonsson/jquery-editable/ * @license GPLv2 http://www.gnu.org/licenses/gpl-2.0.html * @version 1.3.6.dev * @donations http://victorjonsson.se/donations/ */ (function($, window) { 'use strict'; var $win = $(window), // Reference to window // Reference to textarea $textArea = false, // Reference to currently edit element $currentlyEdited = false, // Some constants EVENT_ATTR = 'data-edit-event', IS_EDITING_ATTR = 'data-is-editing', EMPTY_ATTR = 'data-is-empty', DBL_TAP_EVENT = 'dbltap', SUPPORTS_TOUCH = 'ontouchend' in window, TINYMCE_INSTALLED = 'tinyMCE' in window && typeof window.tinyMCE.init == 'function', // reference to old is function oldjQueryIs = $.fn.is, /* * Function responsible of triggering double tap event */ lastTap = 0, tapper = function() { var now = new Date().getTime(); if( (now-lastTap) < 250 ) { $(this).trigger(DBL_TAP_EVENT); } lastTap = now; }, /** * Event listener that largens font size */ keyHandler = function(e) { if( e.keyCode == 13 && e.data.closeOnEnter ) { $currentlyEdited.editable('close'); } else if( e.data.toggleFontSize && (e.metaKey && (e.keyCode == 38 || e.keyCode == 40)) ) { var fontSize = parseInt($textArea.css('font-size'), 10); fontSize += e.keyCode == 40 ? -1 : 1; $textArea.css('font-size', fontSize+'px'); return false; } }, /** * Adjusts the height of the textarea to remove scroll * @todo This way of doing it does not make the textarea smaller when the number of text lines gets smaller */ adjustTextAreaHeight = function() { if( $textArea[0].scrollHeight !== parseInt($textArea.attr('data-scroll'), 10) ) { $textArea.css('height', $textArea[0].scrollHeight +'px'); $textArea.attr('data-scroll', $textArea[0].scrollHeight); } }, /** * @param {jQuery} $el * @param {String} newText */ resetElement = function($el, newText, emptyMessage) { $el.removeAttr(IS_EDITING_ATTR); if (newText.length == 0 && emptyMessage) { $el.html(emptyMessage); $el.attr(EMPTY_ATTR, 'empty'); } else { $el.html( newText ); $el.removeAttr(EMPTY_ATTR); } $textArea.remove(); }, /** * Function creating editor */ elementEditor = function($el, opts) { if( $el.is(':editing') ) return; $currentlyEdited = $el; $el.attr(IS_EDITING_ATTR, '1'); if ($el.is(':empty')) { $el.removeAttr(EMPTY_ATTR); $el.html(''); } var defaultText = $.trim( $el.html() ), defaultFontSize = $el.css('font-size'), elementHeight = $el.height(), textareaStyle = 'width: 96%; padding:0; margin:0; border:0; background:none;'+ 'font-family: '+$el.css('font-family')+'; font-size: '+$el.css('font-size')+';'+ 'font-weight: '+$el.css('font-weight')+';'; if( opts.lineBreaks ) { defaultText = defaultText.replace(//g, '\n'); } $textArea = $(''); $el.text(''); if( navigator.userAgent.match(/webkit/i) !== null ) { textareaStyle = document.defaultView.getComputedStyle($el.get(0), "").cssText; } // The editor should always be static textareaStyle += 'position: static'; /* TINYMCE EDITOR */ if( opts.tinyMCE !== false ) { var id = 'editable-area-'+(new Date().getTime()); $textArea .val(defaultText) .appendTo($el) .attr('id', id); if( typeof opts.tinyMCE != 'object' ) opts.tinyMCE = {}; opts.tinyMCE.mode = 'exact'; opts.tinyMCE.elements = id; opts.tinyMCE.width = $el.innerWidth(); opts.tinyMCE.height = $el.height() + 200; opts.tinyMCE.theme_advanced_resize_vertical = true; opts.tinyMCE.setup = function (ed) { ed.onInit.add(function(editor, evt) { var editorWindow = editor.getWin(); var hasPressedKey = false; var editorBlur = function() { var newText = $(editor.getDoc()).find('body').html(); if( $(newText).get(0).nodeName == $el.get(0).nodeName ) { newText = $(newText).html(); } // Update element and remove editor resetElement($el, newText, opts.emptyMessage); editor.remove(); $textArea = false; $win.unbind('click', editorBlur); $currentlyEdited = false; // Run callback if( typeof opts.callback == 'function' ) { opts.callback({ content : newText == defaultText || !hasPressedKey ? false : newText, fontSize : false, $el : $el }); } }; // Blur editor when user clicks outside the editor setTimeout(function() { $win.bind('click', editorBlur); }, 500); // Create a dummy textarea that will called upon when // programmatically interacting with the editor $textArea = $(''); $textArea.bind('blur', editorBlur); editorWindow.onkeydown = function() { hasPressedKey = true; }; editorWindow.focus(); }); }; tinyMCE.init(opts.tinyMCE); } /* TEXTAREA EDITOR */ else { if( opts.toggleFontSize || opts.closeOnEnter ) { $win.bind('keydown', opts, keyHandler); } $win.bind('keyup', adjustTextAreaHeight); $textArea .val(defaultText) .blur(function() { $currentlyEdited = false; // Get new text and font size var newText = $.trim( $textArea.val() ), newFontSize = $textArea.css('font-size'); if( opts.lineBreaks ) { newText = newText.replace(new RegExp('\n','g'), '
'); } // Update element resetElement($el, newText, opts.emptyMessage); if( newFontSize != defaultFontSize ) { $el.css('font-size', newFontSize); } // remove textarea and size toggles $win.unbind('keydown', keyHandler); $win.unbind('keyup', adjustTextAreaHeight); // Run callback if( typeof opts.callback == 'function' ) { opts.callback({ content : newText == defaultText ? false : newText, fontSize : newFontSize == defaultFontSize ? false : newFontSize, $el : $el }); } }) .attr('style', textareaStyle) .appendTo($el) .css({ margin: 0, padding: 0, height : elementHeight +'px', overflow : 'hidden' }) .css(opts.editorStyle) .focus() .get(0).select(); adjustTextAreaHeight(); } $el.trigger('edit', [$textArea]); }, /** * Event listener */ editEvent = function(event) { if( $currentlyEdited !== false ) { // Not closing the currently open editor before opening a new // editor makes things go crazy $currentlyEdited.editable('close'); elementEditor($(this), event.data); } else { elementEditor($(this), event.data); } return false; }; /** * Jquery plugin that makes elments editable * @param {Object|String} [opts] Either callback function or the string 'destroy' if wanting to remove the editor event * @return {jQuery|Boolean} */ $.fn.editable = function(opts) { if(typeof opts == 'string') { if( this.is(':editable') ) { switch (opts) { case 'open': if( !this.is(':editing') ) { this.trigger(this.attr(EVENT_ATTR)); } break; case 'close': if( this.is(':editing') ) { $textArea.trigger('blur'); } break; case 'destroy': if( this.is(':editing') ) { $textArea.trigger('blur'); } this.unbind(this.attr(EVENT_ATTR)); this.removeAttr(EVENT_ATTR); break; default: console.warn('Unknown command "'+opts+'" for jquery.editable'); } } else { console.error('Calling .editable() on an element that is not editable, call .editable() first'); } } else { if( this.is(':editable') ) { console.warn('Making an already editable element editable, call .editable("destroy") first'); this.editable('destroy'); } opts = $.extend({ event : 'dblclick', touch : true, lineBreaks : true, toggleFontSize : true, closeOnEnter : false, emptyMessage : false, tinyMCE : false, editorStyle : {} }, opts); if( opts.tinyMCE !== false && !TINYMCE_INSTALLED ) { console.warn('Trying to use tinyMCE as editor but id does not seem to be installed'); opts.tinyMCE = false; } if( SUPPORTS_TOUCH && opts.touch ) { opts.event = DBL_TAP_EVENT; this.unbind('touchend', tapper); this.bind('touchend', tapper); } else { opts.event += '.textEditor'; } this.bind(opts.event, opts, editEvent); this.attr(EVENT_ATTR, opts.event); // If it is empty to start with, apply the empty message if (this.html().length == 0 && opts.emptyMessage) { this.html(opts.emptyMessage); this.attr(EMPTY_ATTR, 'empty'); } else { this.removeAttr(EMPTY_ATTR); } } return this; }; /** * Add :editable :editing to $.is() * @param {Object} statement * @return {*} */ $.fn.is = function(statement) { if( typeof statement == 'string' && statement.indexOf(':') === 0) { if( statement == ':editable' ) { return this.attr(EVENT_ATTR) !== undefined; } else if( statement == ':editing' ) { return this.attr(IS_EDITING_ATTR) !== undefined; } else if( statement == ':empty' ) { return this.attr(EMPTY_ATTR) !== undefined; } } return oldjQueryIs.apply(this, arguments); } })(jQuery, window);