/* BestInPlace (for jQuery) version: 0.1.0 (01/01/2011) @requires jQuery >= v1.4 @requires jQuery.purr to display pop-up windows By Bernat Farrero based on the work of Jan Varwig. Examples at http://bernatfarrero.com Licensed under the MIT: http://www.opensource.org/licenses/mit-license.php Usage: Attention. The format of the JSON object given to the select inputs is the following: [["key", "value"],["key", "value"]] The format of the JSON object given to the checkbox inputs is the following: ["falseValue", "trueValue"] */ function BestInPlaceEditor(e) { this.element = e; this.initOptions(); this.bindForm(); this.initNil(); jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); } BestInPlaceEditor.prototype = { // Public Interface Functions ////////////////////////////////////////////// activate : function() { var to_display = ""; if (this.isNil()) { to_display = ""; } else if (this.original_content) { to_display = this.original_content; } else { if (this.sanitize) { to_display = this.element.text(); } else { to_display = this.element.html(); } } this.oldValue = this.isNil() ? "" : this.element.html(); this.display_value = to_display; jQuery(this.activator).unbind("click", this.clickHandler); this.activateForm(); this.element.trigger(jQuery.Event("best_in_place:activate")); }, abort : function() { this.activateText(this.oldValue); jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); this.element.trigger(jQuery.Event("best_in_place:abort")); this.element.trigger(jQuery.Event("best_in_place:deactivate")); }, abortIfConfirm : function () { if (!this.useConfirm) { this.abort(); return; } if (confirm("Are you sure you want to discard your changes?")) { this.abort(); } }, update : function() { var editor = this; if (this.formType in {"input":1, "textarea":1} && this.getValue() == this.oldValue) { // Avoid request if no change is made this.abort(); return true; } editor.ajax({ "type" : "post", "dataType" : "text", "data" : editor.requestData(), "success" : function(data){ editor.loadSuccessCallback(data); }, "error" : function(request, error){ editor.loadErrorCallback(request, error); } }); if (this.formType == "select") { var value = this.getValue(); this.previousCollectionValue = value; jQuery.each(this.values, function(i, v) { if (value == v[0]) { editor.element.html(v[1]); } } ); } else if (this.formType == "checkbox") { editor.element.html(this.getValue() ? this.values[1] : this.values[0]); } else { if (this.getValue() !== "") { editor.element.text(this.getValue()); } else { editor.element.html(this.nil); } } editor.element.trigger(jQuery.Event("best_in_place:update")); }, activateForm : function() { alert("The form was not properly initialized. activateForm is unbound"); }, activateText : function(value){ this.element.html(value); if(this.isNil()) this.element.html(this.nil); }, // Helper Functions //////////////////////////////////////////////////////// initOptions : function() { // Try parent supplied info var self = this; self.element.parents().each(function(){ $parent = jQuery(this); self.url = self.url || $parent.attr("data-url"); self.collection = self.collection || $parent.attr("data-collection"); self.formType = self.formType || $parent.attr("data-type"); self.objectName = self.objectName || $parent.attr("data-object"); self.attributeName = self.attributeName || $parent.attr("data-attribute"); self.activator = self.activator || $parent.attr("data-activator"); self.okButton = self.okButton || $parent.attr("data-ok-button"); self.okButtonClass = self.okButtonClass || $parent.attr("data-ok-button-class"); self.cancelButton = self.cancelButton || $parent.attr("data-cancel-button"); self.cancelButtonClass = self.cancelButtonClass || $parent.attr("data-cancel-button-class"); self.nil = self.nil || $parent.attr("data-nil"); self.inner_class = self.inner_class || $parent.attr("data-inner-class"); self.html_attrs = self.html_attrs || $parent.attr("data-html-attrs"); self.original_content = self.original_content || $parent.attr("data-original-content"); self.collectionValue = self.collectionValue || $parent.attr("data-value"); }); // Try Rails-id based if parents did not explicitly supply something self.element.parents().each(function(){ var res = this.id.match(/^(\w+)_(\d+)$/i); if (res) { self.objectName = self.objectName || res[1]; } }); // Load own attributes (overrides all others) self.url = self.element.attr("data-url") || self.url || document.location.pathname; self.collection = self.element.attr("data-collection") || self.collection; self.formType = self.element.attr("data-type") || self.formtype || "input"; self.objectName = self.element.attr("data-object") || self.objectName; self.attributeName = self.element.attr("data-attribute") || self.attributeName; self.activator = self.element.attr("data-activator") || self.element; self.okButton = self.element.attr("data-ok-button") || self.okButton; self.okButtonClass = self.element.attr("data-ok-button-class") || self.okButtonClass || ""; self.cancelButton = self.element.attr("data-cancel-button") || self.cancelButton; self.cancelButtonClass = self.element.attr("data-cancel-button-class") || self.cancelButtonClass || ""; self.nil = self.element.attr("data-nil") || self.nil || "—"; self.inner_class = self.element.attr("data-inner-class") || self.inner_class || null; self.html_attrs = self.element.attr("data-html-attrs") || self.html_attrs; self.original_content = self.element.attr("data-original-content") || self.original_content; self.collectionValue = self.element.attr("data-value") || self.collectionValue; if (!self.element.attr("data-sanitize")) { self.sanitize = true; } else { self.sanitize = (self.element.attr("data-sanitize") == "true"); } if (!self.element.attr("data-use-confirm")) { self.useConfirm = true; } else { self.useConfirm = (self.element.attr("data-use-confirm") != "false"); } if ((self.formType == "select" || self.formType == "checkbox") && self.collection !== null) { self.values = jQuery.parseJSON(self.collection); } }, bindForm : function() { this.activateForm = BestInPlaceEditor.forms[this.formType].activateForm; this.getValue = BestInPlaceEditor.forms[this.formType].getValue; }, initNil: function() { if (this.element.html() === "") { this.element.html(this.nil); } }, isNil: function() { // TODO: It only work when form is deactivated. // Condition will fail when form is activated return this.element.html() === "" || this.element.html() === this.nil; }, getValue : function() { alert("The form was not properly initialized. getValue is unbound"); }, // Trim and Strips HTML from text sanitizeValue : function(s) { return jQuery.trim(s); }, /* Generate the data sent in the POST request */ requestData : function() { // To prevent xss attacks, a csrf token must be defined as a meta attribute csrf_token = jQuery('meta[name=csrf-token]').attr('content'); csrf_param = jQuery('meta[name=csrf-param]').attr('content'); var data = "_method=put"; data += "&" + this.objectName + '[' + this.attributeName + ']=' + encodeURIComponent(this.getValue()); if (csrf_param !== undefined && csrf_token !== undefined) { data += "&" + csrf_param + "=" + encodeURIComponent(csrf_token); } return data; }, ajax : function(options) { options.url = this.url; options.beforeSend = function(xhr){ xhr.setRequestHeader("Accept", "application/json"); }; return jQuery.ajax(options); }, // Handlers //////////////////////////////////////////////////////////////// loadSuccessCallback : function(data) { data = jQuery.trim(data); if(data && data!=""){ var response = jQuery.parseJSON(jQuery.trim(data)); if (response !== null && response.hasOwnProperty("display_as")) { this.element.attr("data-original-content", this.element.text()); this.original_content = this.element.text(); this.element.html(response["display_as"]); } this.element.trigger(jQuery.Event("best_in_place:success"), data); this.element.trigger(jQuery.Event("ajax:success"), data); } else { this.element.trigger(jQuery.Event("best_in_place:success")); this.element.trigger(jQuery.Event("ajax:success")); } // Binding back after being clicked jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); this.element.trigger(jQuery.Event("best_in_place:deactivate")); if (this.collectionValue !== null && this.formType == "select") { this.collectionValue = this.previousCollectionValue; this.previousCollectionValue = null; } }, loadErrorCallback : function(request, error) { this.activateText(this.oldValue); this.element.trigger(jQuery.Event("best_in_place:error"), [request, error]); this.element.trigger(jQuery.Event("ajax:error"), request, error); // Binding back after being clicked jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); this.element.trigger(jQuery.Event("best_in_place:deactivate")); }, clickHandler : function(event) { event.preventDefault(); event.data.editor.activate(); }, setHtmlAttributes : function() { var formField = this.element.find(this.formType); if(this.html_attrs){ var attrs = jQuery.parseJSON(this.html_attrs); for(var key in attrs){ formField.attr(key, attrs[key]); } } } }; // Button cases: // If no buttons, then blur saves, ESC cancels // If just Cancel button, then blur saves, ESC or clicking Cancel cancels (careful of blur event!) // If just OK button, then clicking OK saves (careful of blur event!), ESC or blur cancels // If both buttons, then clicking OK saves, ESC or clicking Cancel or blur cancels BestInPlaceEditor.forms = { "input" : { activateForm : function() { var output = jQuery(document.createElement('form')) .addClass('form_in_place') .attr('action', 'javascript:void(0);') .attr('style', 'display:inline'); var input_elt = jQuery(document.createElement('input')) .attr('type', 'text') .attr('name', this.attributeName) .val(this.display_value); if(this.inner_class !== null) { input_elt.addClass(this.inner_class); } output.append(input_elt); if(this.okButton) { output.append( jQuery(document.createElement('input')) .attr('type', 'submit') .attr('class', this.okButtonClass) .attr('value', this.okButton) ) } if(this.cancelButton) { output.append( jQuery(document.createElement('input')) .attr('type', 'button') .attr('class', this.cancelButtonClass) .attr('value', this.cancelButton) ) } this.element.html(output); this.setHtmlAttributes(); // START METAMAPS CODE //this.element.find("input[type='text']")[0].select(); this.element.find("input[type='text']")[0].focus(); // END METAMAPS CODE this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.input.submitHandler); if (this.cancelButton) { this.element.find("input[type='button']").bind('click', {editor: this}, BestInPlaceEditor.forms.input.cancelButtonHandler); } this.element.find("input[type='text']").bind('blur', {editor: this}, BestInPlaceEditor.forms.input.inputBlurHandler); // START METAMAPS CODE this.element.find("input[type='text']").bind('keydown', {editor: this}, BestInPlaceEditor.forms.input.keydownHandler); // END METAMAPS CODE this.element.find("input[type='text']").bind('keyup', {editor: this}, BestInPlaceEditor.forms.input.keyupHandler); this.blurTimer = null; this.userClicked = false; }, getValue : function() { return this.sanitizeValue(this.element.find("input").val()); }, // When buttons are present, use a timer on the blur event to give precedence to clicks inputBlurHandler : function(event) { if (event.data.editor.okButton) { event.data.editor.blurTimer = setTimeout(function () { if (!event.data.editor.userClicked) { event.data.editor.abort(); } }, 500); } else { if (event.data.editor.cancelButton) { event.data.editor.blurTimer = setTimeout(function () { if (!event.data.editor.userClicked) { event.data.editor.update(); } }, 500); } else { event.data.editor.update(); } } }, submitHandler : function(event) { event.data.editor.userClicked = true; clearTimeout(event.data.editor.blurTimer); event.data.editor.update(); }, cancelButtonHandler : function(event) { event.data.editor.userClicked = true; clearTimeout(event.data.editor.blurTimer); event.data.editor.abort(); event.stopPropagation(); // Without this, click isn't handled }, keyupHandler : function(event) { if (event.keyCode == 27) { event.data.editor.abort(); } // START METAMAPS CODE else if (event.keyCode == 13 && !event.shiftKey) { event.data.editor.update(); } // END METAMAPS CODE } }, "date" : { activateForm : function() { var that = this, output = jQuery(document.createElement('form')) .addClass('form_in_place') .attr('action', 'javascript:void(0);') .attr('style', 'display:inline'), input_elt = jQuery(document.createElement('input')) .attr('type', 'text') .attr('name', this.attributeName) .attr('value', this.sanitizeValue(this.display_value)); if(this.inner_class !== null) { input_elt.addClass(this.inner_class); } output.append(input_elt) this.element.html(output); this.setHtmlAttributes(); this.element.find('input')[0].select(); this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.input.submitHandler); this.element.find("input").bind('keyup', {editor: this}, BestInPlaceEditor.forms.input.keyupHandler); this.element.find('input') .datepicker({ onClose: function() { that.update(); } }) .datepicker('show'); }, getValue : function() { return this.sanitizeValue(this.element.find("input").val()); }, submitHandler : function(event) { event.data.editor.update(); }, // START METAMAPS CODE keydownHandler : function(event) { if (event.keyCode == 13 && !event.shiftKey) { event.preventDefault(); event.stopPropagation(); return false; } }, // END METAMAPS CODE keyupHandler : function(event) { if (event.keyCode == 27) { event.data.editor.abort(); } } }, "select" : { activateForm : function() { var output = jQuery(document.createElement('form')) .attr('action', 'javascript:void(0)') .attr('style', 'display:inline'); selected = '', oldValue = this.oldValue, select_elt = jQuery(document.createElement('select')) .attr('class', this.inned_class !== null ? this.inner_class : '' ), currentCollectionValue = this.collectionValue; jQuery.each(this.values, function (index, value) { var option_elt = jQuery(document.createElement('option')) // .attr('value', value[0]) .val(value[0]) .html(value[1]); if(value[0] == currentCollectionValue) { option_elt.attr('selected', 'selected'); } select_elt.append(option_elt); }); output.append(select_elt); this.element.html(output); this.setHtmlAttributes(); this.element.find("select").bind('change', {editor: this}, BestInPlaceEditor.forms.select.blurHandler); this.element.find("select").bind('blur', {editor: this}, BestInPlaceEditor.forms.select.blurHandler); this.element.find("select").bind('keyup', {editor: this}, BestInPlaceEditor.forms.select.keyupHandler); this.element.find("select")[0].focus(); }, getValue : function() { return this.sanitizeValue(this.element.find("select").val()); // return this.element.find("select").val(); }, blurHandler : function(event) { event.data.editor.update(); }, keyupHandler : function(event) { if (event.keyCode == 27) event.data.editor.abort(); } }, "checkbox" : { activateForm : function() { this.collectionValue = !this.getValue(); this.setHtmlAttributes(); this.update(); }, getValue : function() { return this.collectionValue; } }, "textarea" : { activateForm : function() { // grab width and height of text width = this.element.css('width'); height = this.element.css('height'); // construct form var output = jQuery(document.createElement('form')) .attr('action', 'javascript:void(0)') .attr('style', 'display:inline') .append(jQuery(document.createElement('textarea')) .val(this.sanitizeValue(this.display_value))); if(this.okButton) { output.append( jQuery(document.createElement('input')) .attr('type', 'submit') .attr('value', this.okButton) ); } if(this.cancelButton) { output.append( jQuery(document.createElement('input')) .attr('type', 'button') .attr('value', this.cancelButton) ) } this.element.html(output); this.setHtmlAttributes(); // set width and height of textarea jQuery(this.element.find("textarea")[0]).css({ 'min-width': width, 'min-height': height }); jQuery(this.element.find("textarea")[0]).elastic(); this.element.find("textarea")[0].focus(); this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.textarea.submitHandler); if (this.cancelButton) { this.element.find("input[type='button']").bind('click', {editor: this}, BestInPlaceEditor.forms.textarea.cancelButtonHandler); } this.element.find("textarea").bind('blur', {editor: this}, BestInPlaceEditor.forms.textarea.blurHandler); // START METAMAPS CODE this.element.find("textarea").bind('keydown', {editor: this}, BestInPlaceEditor.forms.textarea.keydownHandler); // END METAMAPS CODE this.element.find("textarea").bind('keyup', {editor: this}, BestInPlaceEditor.forms.textarea.keyupHandler); this.blurTimer = null; this.userClicked = false; }, getValue : function() { return this.sanitizeValue(this.element.find("textarea").val()); }, // When buttons are present, use a timer on the blur event to give precedence to clicks blurHandler : function(event) { if (event.data.editor.okButton) { event.data.editor.blurTimer = setTimeout(function () { if (!event.data.editor.userClicked) { event.data.editor.abortIfConfirm(); } }, 500); } else { if (event.data.editor.cancelButton) { event.data.editor.blurTimer = setTimeout(function () { if (!event.data.editor.userClicked) { event.data.editor.update(); } }, 500); } else { event.data.editor.update(); } } }, submitHandler : function(event) { event.data.editor.userClicked = true; clearTimeout(event.data.editor.blurTimer); event.data.editor.update(); }, cancelButtonHandler : function(event) { event.data.editor.userClicked = true; clearTimeout(event.data.editor.blurTimer); event.data.editor.abortIfConfirm(); event.stopPropagation(); // Without this, click isn't handled }, // START METAMAPS CODE keydownHandler : function(event) { if (event.keyCode == 13 && !event.shiftKey) { event.preventDefault(); event.stopPropagation(); return false; } }, // END METAMAPS CODE keyupHandler : function(event) { if (event.keyCode == 27) { event.data.editor.abortIfConfirm(); } // START METAMAPS CODE else if (event.keyCode == 13 && !event.shiftKey) { event.data.editor.update(); } // END METAMAPS CODE } } }; jQuery.fn.best_in_place = function() { function setBestInPlace(element) { if (!element.data('bestInPlaceEditor')) { element.data('bestInPlaceEditor', new BestInPlaceEditor(element)); return true; } } jQuery(this.context).delegate(this.selector, 'click', function () { var el = jQuery(this); if (setBestInPlace(el)) el.click(); }); this.each(function () { setBestInPlace(jQuery(this)); }); return this; }; /** * @name Elastic * @descripton Elastic is Jquery plugin that grow and shrink your textareas automaticliy * @version 1.6.5 * @requires Jquery 1.2.6+ * * @author Jan Jarfalk * @author-email jan.jarfalk@unwrongest.com * @author-website http://www.unwrongest.com * * @licens MIT License - http://www.opensource.org/licenses/mit-license.php */ (function(jQuery){ if (typeof jQuery.fn.elastic !== 'undefined') return; jQuery.fn.extend({ elastic: function() { // We will create a div clone of the textarea // by copying these attributes from the textarea to the div. var mimics = [ 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', 'fontSize', 'lineHeight', 'fontFamily', 'width', 'fontWeight']; return this.each( function() { // Elastic only works on textareas if ( this.type != 'textarea' ) { return false; } var $textarea = jQuery(this), $twin = jQuery('
').css({'position': 'absolute','display':'none','word-wrap':'break-word'}), lineHeight = parseInt($textarea.css('line-height'),10) || parseInt($textarea.css('font-size'),'10'), minheight = parseInt($textarea.css('height'),10) || lineHeight*3, maxheight = parseInt($textarea.css('max-height'),10) || Number.MAX_VALUE, goalheight = 0, i = 0; // Opera returns max-height of -1 if not set if (maxheight < 0) { maxheight = Number.MAX_VALUE; } // Append the twin to the DOM // We are going to meassure the height of this, not the textarea. $twin.appendTo($textarea.parent()); // Copy the essential styles (mimics) from the textarea to the twin i = mimics.length; while(i--){ $twin.css(mimics[i].toString(),$textarea.css(mimics[i].toString())); } // Sets a given height and overflow state on the textarea function setHeightAndOverflow(height, overflow){ curratedHeight = Math.floor(parseInt(height,10)); if($textarea.height() != curratedHeight){ $textarea.css({'height': curratedHeight + 'px','overflow':overflow}); } } // This function will update the height of the textarea if necessary function update() { // Get curated content from the textarea. var textareaContent = $textarea.val().replace(/&/g,'&').replace(/ /g, ' ').replace(/<|>/g, '>').replace(/\n/g, '
'); // Compare curated content with curated twin. var twinContent = $twin.html().replace(/
/ig,'
'); if(textareaContent+' ' != twinContent){ // Add an extra white space so new rows are added when you are at the end of a row. $twin.html(textareaContent+' '); // Change textarea height if twin plus the height of one line differs more than 3 pixel from textarea height if(Math.abs($twin.height() + lineHeight - $textarea.height()) > 3){ var goalheight = $twin.height()+lineHeight; if(goalheight >= maxheight) { setHeightAndOverflow(maxheight,'auto'); } else if(goalheight <= minheight) { setHeightAndOverflow(minheight,'hidden'); } else { setHeightAndOverflow(goalheight,'hidden'); } } } } // Hide scrollbars $textarea.css({'overflow':'hidden'}); // Update textarea size on keyup, change, cut and paste $textarea.bind('keyup change cut paste', function(){ update(); }); // Compact textarea on blur // Lets animate this.... $textarea.bind('blur',function(){ if($twin.height() < maxheight){ if($twin.height() > minheight) { $textarea.height($twin.height()); } else { $textarea.height(minheight); } } }); // And this line is to catch the browser paste event $textarea.on("input paste", function(e){ setTimeout( update, 250); }); // Run update once when elastic is initialized update(); }); } }); })(jQuery);