UNPKG

@didww/best_in_place

Version:

BestInPlace is a jQuery script and a Rails helper that provide the method best_in_place to display any object field easily editable for the user by just clicking on it. It supports input data, text data, boolean data and custom dropdown data. It works wit

748 lines (639 loc) 26.9 kB
/* * BestInPlace (for jQuery) * version: 3.0.0.alpha (2014) * * 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 * * @requires jQuery * * 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"] */ import 'jquery-autosize'; import '../vendor/jquery.purr'; function BestInPlaceEditor(e) { 'use strict'; this.element = e; this.initOptions(); this.bindForm(); this.initPlaceHolder(); jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); } BestInPlaceEditor.prototype = { // Public Interface Functions ////////////////////////////////////////////// activate: function () { 'use strict'; var to_display; if (this.isPlaceHolder()) { to_display = ""; } else if (this.original_content) { to_display = this.original_content; } else { switch (this.formType) { case 'input': case 'textarea': if (this.display_raw) { to_display = this.element.html().replace(/&amp;/gi, '&'); } else { var value = this.element.data('bipValue'); if (typeof value === 'undefined') { to_display = ''; } else if (typeof value === 'string') { to_display = this.element.data('bipValue').replace(/&amp;/gi, '&'); } else { to_display = this.element.data('bipValue'); } } break; case 'select': to_display = this.element.html(); } } this.oldValue = this.isPlaceHolder() ? "" : 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 () { 'use strict'; 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 () { 'use strict'; if (!this.useConfirm) { this.abort(); return; } if (confirm(BestInPlaceEditor.defaults.locales[''].confirmMessage)) { this.abort(); } }, update: function () { 'use strict'; this.element.trigger(jQuery.Event("best_in_place:before-update")); var editor = this, value = this.getValue(); // Avoid request if no change is made if (this.formType in {"input": 1, "textarea": 1} && value === this.oldValue) { this.abort(); return true; } editor.ajax({ "type": this.requestMethod(), "dataType": BestInPlaceEditor.defaults.ajaxDataType, "data": editor.requestData(), "success": function (data, status, xhr) { editor.loadSuccessCallback(data, status, xhr); }, "error": function (request, error) { editor.loadErrorCallback(request, error); } }); switch (this.formType) { case "select": this.previousCollectionValue = value; // search for the text for the span $.each(this.values, function(index, arr){ if (String(arr[0]) === String(value)) editor.element.html(arr[1]); }); break; case "checkbox": $.each(this.values, function(index, arr){ if (String(arr[0]) === String(value)) editor.element.html(arr[1]); }); break; default: if (value !== "") { if (this.display_raw) { editor.element.html(value); } else { editor.element.text(value); } } else { editor.element.html(this.placeHolder); } } editor.element.data('bipValue', value); editor.element.attr('data-bip-value', value); editor.element.trigger(jQuery.Event("best_in_place:update")); }, activateForm: function () { 'use strict'; alert(BestInPlaceEditor.defaults.locales[''].uninitializedForm); }, activateText: function (value) { 'use strict'; this.element.html(value); if (this.isPlaceHolder()) { this.element.html(this.placeHolder); } }, // Helper Functions //////////////////////////////////////////////////////// initOptions: function () { // Try parent supplied info 'use strict'; var self = this; self.element.parents().each(function () { var $parent = jQuery(this); self.url = self.url || $parent.data("bipUrl"); self.activator = self.activator || $parent.data("bipActivator"); self.okButton = self.okButton || $parent.data("bipOkButton"); self.okButtonClass = self.okButtonClass || $parent.data("bipOkButtonClass"); self.cancelButton = self.cancelButton || $parent.data("bipCancelButton"); self.cancelButtonClass = self.cancelButtonClass || $parent.data("bipCancelButtonClass"); self.skipBlur = self.skipBlur || $parent.data("bipSkipBlur"); }); // Load own attributes (overrides all others) self.url = self.element.data("bipUrl") || self.url || document.location.pathname; self.collection = self.element.data("bipCollection") || self.collection; self.formType = self.element.data("bipType") || "input"; self.objectName = self.element.data("bipObject") || self.objectName; self.attributeName = self.element.data("bipAttribute") || self.attributeName; self.activator = self.element.data("bipActivator") || self.element; self.okButton = self.element.data("bipOkButton") || self.okButton; self.okButtonClass = self.element.data("bipOkButtonClass") || self.okButtonClass || BestInPlaceEditor.defaults.okButtonClass; self.cancelButton = self.element.data("bipCancelButton") || self.cancelButton; self.cancelButtonClass = self.element.data("bipCancelButtonClass") || self.cancelButtonClass || BestInPlaceEditor.defaults.cancelButtonClass; self.skipBlur = self.element.data("bipSkipBlur") || self.skipBlur || BestInPlaceEditor.defaults.skipBlur; self.isNewObject = self.element.data("bipNewObject"); self.dataExtraPayload = self.element.data("bipExtraPayload"); // Fix for default values of 0 if (self.element.data("bipPlaceholder") == null) { self.placeHolder = BestInPlaceEditor.defaults.locales[''].placeHolder; } else { self.placeHolder = self.element.data("bipPlaceholder"); } self.inner_class = self.element.data("bipInnerClass"); self.html_attrs = self.element.data("bipHtmlAttrs"); self.original_content = self.element.data("bipOriginalContent") || self.original_content; // if set the input won't be satinized self.display_raw = self.element.data("bip-raw"); self.useConfirm = self.element.data("bip-confirm"); if (self.formType === "select" || self.formType === "checkbox") { self.values = self.collection; self.collectionValue = self.element.data("bipValue") || self.collectionValue; } }, bindForm: function () { 'use strict'; this.activateForm = BestInPlaceEditor.forms[this.formType].activateForm; this.getValue = BestInPlaceEditor.forms[this.formType].getValue; }, initPlaceHolder: function () { 'use strict'; // TODO add placeholder for select and checkbox if (this.element.html() === "") { this.element.addClass('bip-placeholder'); this.element.html(this.placeHolder); } }, isPlaceHolder: function () { 'use strict'; // TODO: It only work when form is deactivated. // Condition will fail when form is activated return this.element.html() === "" || this.element.html() === this.placeHolder; }, getValue: function () { 'use strict'; alert(BestInPlaceEditor.defaults.locales[''].uninitializedForm); }, // Trim and Strips HTML from text sanitizeValue: function (s) { 'use strict'; return jQuery.trim(s); }, requestMethod: function() { 'use strict'; return this.isNewObject ? 'post' : BestInPlaceEditor.defaults.ajaxMethod; }, /* Generate the data sent in the POST request */ requestData: function () { 'use strict'; // To prevent xss attacks, a csrf token must be defined as a meta attribute var csrf_token = jQuery('meta[name=csrf-token]').attr('content'), csrf_param = jQuery('meta[name=csrf-param]').attr('content'); var data = {} data['_method'] = this.requestMethod() data[this.objectName] = this.dataExtraPayload || {} data[this.objectName][this.attributeName] = this.getValue() if (csrf_param !== undefined && csrf_token !== undefined) { data[csrf_param] = csrf_token } return jQuery.param(data); }, ajax: function (options) { 'use strict'; options.url = this.url; options.beforeSend = function (xhr) { xhr.setRequestHeader("Accept", "application/json"); }; return jQuery.ajax(options); }, // Handlers //////////////////////////////////////////////////////////////// loadSuccessCallback: function (data, status, xhr) { 'use strict'; data = jQuery.trim(data); //Update original content with current text. if (this.display_raw) { this.original_content = this.element.html(); } else { this.original_content = this.element.text(); } if (data && data !== "") { var response = jQuery.parseJSON(data); if (response !== null && response.hasOwnProperty("display_as")) { this.element.data('bip-original-content', this.element.text()); this.element.html(response.display_as); } if (this.isNewObject && response && response[this.objectName]) { if (response[this.objectName]["id"]) { this.isNewObject = false this.url += "/" + response[this.objectName]["id"] // in REST a POST /thing url should become PUT /thing/123 } } } this.element.toggleClass('bip-placeholder', this.isPlaceHolder()); this.element.trigger(jQuery.Event("best_in_place:success"), [data, status, xhr]); this.element.trigger(jQuery.Event("ajax:success"), [data, status, xhr]); // 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) { 'use strict'; 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) { 'use strict'; event.preventDefault(); event.data.editor.activate(); }, setHtmlAttributes: function () { 'use strict'; var formField = this.element.find(this.formType); if (this.html_attrs) { var attrs = this.html_attrs; $.each(attrs, function (key, val) { formField.attr(key, val); }); } }, placeButtons: function (output, field) { 'use strict'; if (field.okButton) { output.append( jQuery(document.createElement('input')) .attr('type', 'submit') .attr('class', field.okButtonClass) .attr('value', field.okButton) ); } if (field.cancelButton) { output.append( jQuery(document.createElement('input')) .attr('type', 'button') .attr('class', field.cancelButtonClass) .attr('value', field.cancelButton) ); } } }; // 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 () { 'use strict'; 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); // Add class to form input if (this.inner_class) { input_elt.addClass(this.inner_class); } output.append(input_elt); this.placeButtons(output, this); 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); } if (!this.okButton) { 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 () { 'use strict'; 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) { 'use strict'; 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) { 'use strict'; event.data.editor.userClicked = true; clearTimeout(event.data.editor.blurTimer); event.data.editor.update(); }, cancelButtonHandler: function (event) { 'use strict'; 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) { 'use strict'; if (event.keyCode === 27) { event.data.editor.abort(); event.stopImmediatePropagation(); } } }, "select": { activateForm: function () { 'use strict'; var output = jQuery(document.createElement('form')) .attr('action', 'javascript:void(0)') .attr('style', 'display:inline'), selected = '', select_elt = jQuery(document.createElement('select')) .attr('class', this.inner_class !== null ? this.inner_class : ''), currentCollectionValue = this.collectionValue, key, value, a = this.values; $.each(a, function(index, arr){ key = arr[0]; value = arr[1]; var option_elt = jQuery(document.createElement('option')) .val(key) .html(value); if (currentCollectionValue) { if (String(key) === String(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(); // automatically click on the select so you // don't have to click twice try { var e = document.createEvent("MouseEvents"); e.initMouseEvent("mousedown", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); this.element.find("select")[0].dispatchEvent(e); } catch(e) { // browser doesn't support this, e.g. IE8 } }, getValue: function () { 'use strict'; return this.sanitizeValue(this.element.find("select").val()); }, blurHandler: function (event) { 'use strict'; event.data.editor.update(); }, keyupHandler: function (event) { 'use strict'; if (event.keyCode === 27) { event.data.editor.abort(); } } }, "checkbox": { activateForm: function () { 'use strict'; this.collectionValue = !this.getValue(); this.setHtmlAttributes(); this.update(); }, getValue: function () { 'use strict'; return this.collectionValue; } }, "textarea": { activateForm: function () { 'use strict'; // grab width and height of text var width = this.element.css('width'); var height = this.element.css('height'); // construct form var output = jQuery(document.createElement('form')) .addClass('form_in_place') .attr('action', 'javascript:void(0);') .attr('style', 'display:inline'); var textarea_elt = jQuery(document.createElement('textarea')) .attr('name', this.attributeName) .val(this.sanitizeValue(this.display_value)); if (this.inner_class !== null) { textarea_elt.addClass(this.inner_class); } output.append(textarea_elt); this.placeButtons(output, this); 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]).autosize(); 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); } if (!this.skipBlur) { 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 () { 'use strict'; 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) { 'use strict'; 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) { 'use strict'; event.data.editor.userClicked = true; clearTimeout(event.data.editor.blurTimer); event.data.editor.update(); }, cancelButtonHandler: function (event) { 'use strict'; 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) { 'use strict'; if (event.keyCode === 27) { event.data.editor.abortIfConfirm(); } } } }; BestInPlaceEditor.defaults = { locales: {}, ajaxMethod: "put", //TODO Change to patch when support to 3.2 is dropped ajaxDataType: 'text', okButtonClass: '', cancelButtonClass: '', skipBlur: false }; // Default locale BestInPlaceEditor.defaults.locales[''] = { confirmMessage: "Are you sure you want to discard your changes?", uninitializedForm: "The form was not properly initialized. getValue is unbound", placeHolder: '-' }; BestInPlaceEditor.forms.date = { activateForm: function () { 'use strict'; 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 () { 'use strict'; return this.sanitizeValue(this.element.find("input").val()); }, submitHandler: function (event) { 'use strict'; event.data.editor.update(); }, keyupHandler: function (event) { 'use strict'; if (event.keyCode === 27) { event.data.editor.abort(); } } } BestInPlaceEditor.defaults.purrErrorContainer = "<span class='bip-flash-error'></span>"; jQuery(document).on('best_in_place:error', function (event, request, error) { 'use strict'; // Display all error messages from server side validation jQuery.each(jQuery.parseJSON(request.responseText), function (index, value) { if (typeof value === "object") {value = index + " " + value.toString(); } var container = jQuery(BestInPlaceEditor.defaults.purrErrorContainer).html(value); container.purr(); }); }); jQuery.fn.best_in_place = function () { 'use strict'; 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; };