/* * 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"] */ //= require jquery.autosize 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(/&/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(/&/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'; 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: '-' }; 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; };