Last active
January 25, 2023 23:17
-
-
Save poqudrof/0cf74465841f0fed0f6e4afe6ddc89e4 to your computer and use it in GitHub Desktop.
Best in place for Rails 6 and Turbo
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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 | |
import autosize from "autosize"; | |
import autosizeInput from "autosize-input"; | |
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('data-turbo', 'false') | |
.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(); | |
autosizeInput(this.element.find("input[type='text']")[0]); | |
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('data-turbo', 'false') | |
.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('data-turbo', 'false') | |
.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(); | |
// autosize(textarea_elt); // this.element.find("textarea")[0]); | |
// autosize(textarea_elt); | |
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; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Best in place is an awesome tool for content edition with a simple click. While it is based on JQuery, it can still survive and be quite useful with modern JS.
This snippet contains small updates:
I store it in my
javascript/vendor
folder in Rails.Project page: https://github.com/bernat/best_in_place
This version does not benefit the Rails 6.1 update yet.