From 6454a592bd60e11ebc2ef6e8646967f15d62735d Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Mon, 9 Jun 2014 17:56:42 -0400 Subject: [PATCH] custom best_in_place that completes change on hitting enter for textaareas --- app/assets/javascripts/application.js | 2 +- app/assets/javascripts/bip.js | 744 ++++++++++++++++++++++++++ 2 files changed, 745 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/bip.js diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index d2b6fdba..ee3a517d 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -18,7 +18,7 @@ //= require jquery.lettering //= require jquery.textillate //= require jquery.roundabout.min -//= require best_in_place +//= require bip //= require jquery_ujs //= require_tree . diff --git a/app/assets/javascripts/bip.js b/app/assets/javascripts/bip.js new file mode 100644 index 00000000..2c270e1d --- /dev/null +++ b/app/assets/javascripts/bip.js @@ -0,0 +1,744 @@ +/* + 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(); + this.element.find("input[type='text']")[0].select(); + 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); + 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(); + } + } + }, + + "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(); + }, + + 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); + 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 + }, + + keyupHandler : function(event) { + if (event.keyCode == 27) { + event.data.editor.abortIfConfirm(); + } + else if (event.keyCode == 13 && !event.shiftKey) { + event.data.editor.update(); + } + } + } +}; + +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);