@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
JavaScript
/*
* 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(/&/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';
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;
};