Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Implement :ok_button and :cancel_button options for inputs and textareas #55

Merged
merged 3 commits into from

3 participants

@bfalling

If OK button present, form no longer submits on blur. Cancel button does not use textarea confirmation dialog. Add new migrations for test app. Small clean-ups.

@bfalling bfalling Implement :ok_button and :cancel_button options for inputs and textareas
If OK button present, form no longer submits on blur. Cancel button does not use textarea confirmation dialog. Add new migrations for test app. Small clean-ups.
43d6abf
@rogercampos
Collaborator

Hi @bfalling, thanks for this work! It has been requested some time ago so I think it might be useful to some people. I've only noticed some small misbehaviors:

  • When using only a cancel button on text areas we have no way to submit the changes
  • If we click to edit "favorite color" and then click to edit some other best in place field we have two forms opened. We should avoid that always closing the form on blur. Then we submit changes or not depending on the presence of an 'ok button'

With these issues fixed I'll happily merge it, thanks!

@bfalling

Great feedback! Thanks. I'll get on it.

I didn't realize it was a requirement to only have 1 form open at a time. Makes sense though.

@bfalling

@rogercampos, I've been rethinking the "two forms opened" issue.

Do you really think it's necessary to prevent having two bip forms opened? Once we add explicit control of submitting and canceling via buttons, it seems like we could let the user decide when to explicitly submit/cancel a given form, rather than deciding ourselves via blur event. I can't think of any drawbacks to allowing this. (But maybe you can...?)

Also, another tricky point is that when someone clicks on the OK button, both a blur event on the text form and a click event on the button are generated, and either can happen first, so implementing the JS to intelligently handle these cases requires some messy workarounds, due to the fact that a blur without a click means abort, whereas a blur with a click means update. Most people get around these event issues with timers or mouse over events.

What do you think? Possible to disable the blur event when there's an OK button? (When there's just a Cancel button, the blur acts like the Cancel button, so it's not a problem, but we could remove the blur event in that case for consistency with the OK button.)

@bfalling

Hey @rogercampos, I went back in and changed the logic to work as you described:

  • Fixed the bug where there was no way to submit if only a Cancel button was present (now, it happens on blur).
  • Blur now always results in a form close, plus submit if no OK button is present.
  • I also made the confirmation dialog consistent for all text area abort cases.

LMK if you need anything else in there. Thanks!

@albertbellonch albertbellonch merged commit 4be3ff1 into bernat:master
@albertbellonch
Collaborator

Hi @bfalling, I just merged your commits to master. Good work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Dec 11, 2011
  1. @bfalling

    Implement :ok_button and :cancel_button options for inputs and textareas

    bfalling authored
    If OK button present, form no longer submits on blur. Cancel button does not use textarea confirmation dialog. Add new migrations for test app. Small clean-ups.
Commits on Dec 19, 2011
  1. @bfalling
  2. @bfalling

    Update button handling logic for text areas, including blur event tim…

    bfalling authored
    …ing and confirmation dialog
This page is out of date. Refresh to see the latest.
View
5 README.md
@@ -24,6 +24,7 @@ The editor works by PUTting the updated value to the server and GETting the upda
- Sanitize HTML and trim spaces of user's input on user's choice
- Displays server-side **validation** errors
- Allows external activator
+- Allows optional, configurable OK and Cancel buttons for inputs and textareas
- ESC key destroys changes (requires user confirmation)
- Autogrowing textarea
- Helper for generating the best_in_place field only if a condition is satisfied
@@ -49,6 +50,8 @@ Options:
- **:nil**: The nil param defines the content displayed in case no value is defined for that field. It can be something like "click me to edit".
If not defined it will show *"-"*.
- **:activator**: Is the DOM object that can activate the field. If not defined the user will making editable by clicking on it.
+- **:ok_button**: (Inputs and textareas only) If set to a string, then an OK button will be shown with the string as its label, replacing save on blur.
+- **:cancel_button**: (Inputs and textareas only) If set to a string, then a Cancel button will be shown with the string as its label.
- **:sanitize**: True by default. If set to false the input/textarea will accept html tags.
- **:html_attrs**: Hash of html arguments, such as maxlength, default-value etc.
- **:inner_class**: Class that is set to the rendered form.
@@ -95,6 +98,8 @@ Examples (code in the views):
<%= best_in_place @user, :description, :type => :textarea %>
+ <%= best_in_place @user, :favorite_books, :type => :textarea, :ok_button => 'Save', :cancel_button => 'Cancel' %>
+
### Select
<%= best_in_place @user, :country, :type => :select, :collection => [[1, "Spain"], [2, "Italy"], [3, "Germany"], [4, "France"]] %>
View
137 lib/assets/javascripts/best_in_place.js
@@ -55,6 +55,12 @@ BestInPlaceEditor.prototype = {
$(this.activator).bind('click', {editor: this}, this.clickHandler);
},
+ abortIfConfirm : function () {
+ 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)
@@ -100,6 +106,9 @@ BestInPlaceEditor.prototype = {
self.formType = self.formType || jQuery(this).attr("data-type");
self.objectName = self.objectName || jQuery(this).attr("data-object");
self.attributeName = self.attributeName || jQuery(this).attr("data-attribute");
+ self.activator = self.activator || jQuery(this).attr("data-activator");
+ self.okButton = self.okButton || jQuery(this).attr("data-ok-button");
+ self.cancelButton = self.cancelButton || jQuery(this).attr("data-cancel-button");
self.nil = self.nil || jQuery(this).attr("data-nil");
self.inner_class = self.inner_class || jQuery(this).attr("data-inner-class");
self.html_attrs = self.html_attrs || jQuery(this).attr("data-html-attrs");
@@ -115,15 +124,17 @@ BestInPlaceEditor.prototype = {
});
// 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.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.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.cancelButton = self.element.attr("data-cancel-button") || self.cancelButton;
+ 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;
if (!self.element.attr("data-sanitize")) {
@@ -229,6 +240,11 @@ BestInPlaceEditor.prototype = {
};
+// 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() {
@@ -237,27 +253,65 @@ BestInPlaceEditor.forms = {
if (this.inner_class != null) {
output += ' class="' + this.inner_class + '"';
}
- output += '></form>'
+ output += '>';
+ if (this.okButton) {
+ output += '<input type="submit" value="' + this.okButton + '" />'
+ }
+ if (this.cancelButton) {
+ output += '<input type="button" value="' + this.cancelButton + '" />'
+ }
+ output += '</form>';
this.element.html(output);
this.setHtmlAttributes();
- this.element.find('input')[0].select();
+ this.element.find("input[type='text']")[0].select();
this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.input.submitHandler);
- this.element.find("input").bind('blur', {editor: this}, BestInPlaceEditor.forms.input.inputBlurHandler);
- this.element.find("input").bind('keyup', {editor: this}, BestInPlaceEditor.forms.input.keyupHandler);
+ 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() {
+ 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) {
- event.data.editor.update();
+ 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();
@@ -319,7 +373,14 @@ BestInPlaceEditor.forms = {
// construct the form
var output = '<form action="javascript:void(0)" style="display:inline;"><textarea>';
output += this.sanitizeValue(this.display_value);
- output += '</textarea></form>';
+ output += '</textarea>';
+ if (this.okButton) {
+ output += '<input type="submit" value="' + this.okButton + '" />'
+ }
+ if (this.cancelButton) {
+ output += '<input type="button" value="' + this.cancelButton + '" />'
+ }
+ output += '</form>';
this.element.html(output);
this.setHtmlAttributes();
@@ -328,27 +389,57 @@ BestInPlaceEditor.forms = {
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();
},
- keyupHandler : function(event) {
- if (event.keyCode == 27) {
- BestInPlaceEditor.forms.textarea.abort(event.data.editor);
- }
+ 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
},
- abort : function(editor) {
- if (confirm("Are you sure you want to discard your changes?")) {
- editor.abort();
+ keyupHandler : function(event) {
+ if (event.keyCode == 27) {
+ event.data.editor.abortIfConfirm();
}
}
}
View
2  lib/best_in_place/helper.rb
@@ -29,6 +29,8 @@ def best_in_place(object, field, opts = {})
out << " data-collection='#{collection.gsub(/'/, "&#39;")}'" unless collection.blank?
out << " data-attribute='#{field}'"
out << " data-activator='#{opts[:activator]}'" unless opts[:activator].blank?
+ out << " data-ok-button='#{opts[:ok_button]}'" unless opts[:ok_button].blank?
+ out << " data-cancel-button='#{opts[:cancel_button]}'" unless opts[:cancel_button].blank?
out << " data-nil='#{opts[:nil]}'" unless opts[:nil].blank?
out << " data-type='#{opts[:type]}'"
out << " data-inner-class='#{opts[:inner_class]}'" if opts[:inner_class]
View
22 spec/helpers/best_in_place_spec.rb
@@ -56,6 +56,14 @@
@span.attribute("data-activator").should be_nil
end
+ it "should have no OK button text by default" do
+ @span.attribute("data-ok-button").should be_nil
+ end
+
+ it "should have no Cancel button text by default" do
+ @span.attribute("data-cancel-button").should be_nil
+ end
+
it "should have no inner_class by default" do
@span.attribute("data-inner-class").should be_nil
end
@@ -124,6 +132,20 @@
span.attribute("data-activator").value.should == "awesome"
end
+ it "should have the given OK button text" do
+ out = helper.best_in_place @user, :name, :ok_button => "okay"
+ nk = Nokogiri::HTML.parse(out)
+ span = nk.css("span")
+ span.attribute("data-ok-button").value.should == "okay"
+ end
+
+ it "should have the given Cancel button text" do
+ out = helper.best_in_place @user, :name, :cancel_button => "nasty"
+ nk = Nokogiri::HTML.parse(out)
+ span = nk.css("span")
+ span.attribute("data-cancel-button").value.should == "nasty"
+ end
+
describe "display_as" do
it "should render the address with a custom renderer" do
@user.should_receive(:address_format).and_return("the result")
View
180 spec/integration/js_spec.rb
@@ -10,6 +10,8 @@
:zip => "25123",
:country => "2",
:receive_email => false,
+ :favorite_color => 'Red',
+ :favorite_books => "The City of Gold and Lead",
:description => "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus a lectus et lacus ultrices auctor. Morbi aliquet convallis tincidunt. Praesent enim libero, iaculis at commodo nec, fermentum a dolor. Quisque eget eros id felis lacinia faucibus feugiat et ante. Aenean justo nisi, aliquam vel egestas vel, porta in ligula. Etiam molestie, lacus eget tincidunt accumsan, elit justo rhoncus urna, nec pretium neque mi et lorem. Aliquam posuere, dolor quis pulvinar luctus, felis dolor tincidunt leo, eget pretium orci purus ac nibh. Ut enim sem, suscipit ac elementum vitae, sodales vel sem."
end
@@ -90,7 +92,7 @@
end
end
- it "should be able to use bil_select to change a select field" do
+ it "should be able to use bip_select to change a select field" do
@user.save!
visit user_path(@user)
within("#country") do
@@ -121,6 +123,182 @@
end
end
+ it "should correctly use an OK submit button when so configured for an input" do
+ @user.save!
+ visit user_path(@user)
+
+ within("#favorite_color") do
+ page.should have_content('Red')
+ end
+
+ id = BestInPlace::Utils.build_best_in_place_id @user, :favorite_color
+ page.execute_script <<-JS
+ $("##{id}").click();
+ $("##{id} input[name='favorite_color']").val('Blue');
+ $("##{id} input[type='submit']").click();
+ JS
+
+ visit user_path(@user)
+ within("#favorite_color") do
+ page.should have_content('Blue')
+ end
+ end
+
+ it "should correctly use a Cancel button when so configured for an input" do
+ @user.save!
+ visit user_path(@user)
+
+ within("#favorite_color") do
+ page.should have_content('Red')
+ end
+
+ id = BestInPlace::Utils.build_best_in_place_id @user, :favorite_color
+ page.execute_script <<-JS
+ $("##{id}").click();
+ $("##{id} input[name='favorite_color']").val('Blue');
+ $("##{id} input[type='button']").click();
+ JS
+
+ visit user_path(@user)
+ within("#favorite_color") do
+ page.should have_content('Red')
+ end
+ end
+
+ it "should not submit input on blur if there's an OK button present" do
+ @user.save!
+ visit user_path(@user)
+
+ within("#favorite_color") do
+ page.should have_content('Red')
+ end
+
+ id = BestInPlace::Utils.build_best_in_place_id @user, :favorite_color
+ page.execute_script <<-JS
+ $("##{id}").click();
+ $("##{id} input[name='favorite_color']").val('Blue');
+ $("##{id} input[name='favorite_color']").blur();
+ JS
+ sleep 1 # Increase if browser is slow
+
+ visit user_path(@user)
+ within("#favorite_color") do
+ page.should have_content('Red')
+ end
+ end
+
+ it "should still submit input on blur if there's only a Cancel button present" do
+ @user.save!
+ visit user_path(@user, :suppress_ok_button => 1)
+
+ within("#favorite_color") do
+ page.should have_content('Red')
+ end
+
+ id = BestInPlace::Utils.build_best_in_place_id @user, :favorite_color
+ page.execute_script %{$("##{id}").click();}
+ page.should have_no_css("##{id} input[type='submit']")
+ page.execute_script <<-JS
+ $("##{id} input[name='favorite_color']").val('Blue');
+ $("##{id} input[name='favorite_color']").blur();
+ JS
+ sleep 1 # Increase if browser is slow
+
+ visit user_path(@user)
+ within("#favorite_color") do
+ page.should have_content('Blue')
+ end
+ end
+
+ it "should correctly use an OK submit button when so configured for a text area" do
+ @user.save!
+ visit user_path(@user)
+
+ within("#favorite_books") do
+ page.should have_content('The City of Gold and Lead')
+ end
+
+ id = BestInPlace::Utils.build_best_in_place_id @user, :favorite_books
+ page.execute_script <<-JS
+ $("##{id}").click();
+ $("##{id} textarea").val('1Q84');
+ $("##{id} input[type='submit']").click();
+ JS
+
+ visit user_path(@user)
+ within("#favorite_books") do
+ page.should have_content('1Q84')
+ end
+ end
+
+ it "should correctly use a Cancel button when so configured for a text area" do
+ @user.save!
+ visit user_path(@user)
+
+ within("#favorite_books") do
+ page.should have_content('The City of Gold and Lead')
+ end
+
+ id = BestInPlace::Utils.build_best_in_place_id @user, :favorite_books
+ page.execute_script <<-JS
+ $("##{id}").click();
+ $("##{id} textarea").val('1Q84');
+ $("##{id} input[type='button']").click();
+ JS
+ page.driver.browser.switch_to.alert.accept
+
+ visit user_path(@user)
+ within("#favorite_books") do
+ page.should have_content('The City of Gold and Lead')
+ end
+ end
+
+ it "should not submit text area on blur if there's an OK button present" do
+ @user.save!
+ visit user_path(@user)
+
+ within("#favorite_books") do
+ page.should have_content('The City of Gold and Lead')
+ end
+
+ id = BestInPlace::Utils.build_best_in_place_id @user, :favorite_books
+ page.execute_script <<-JS
+ $("##{id}").click();
+ $("##{id} textarea").val('1Q84');
+ $("##{id} textarea").blur();
+ JS
+ sleep 1 # Increase if browser is slow
+ page.driver.browser.switch_to.alert.accept
+
+ visit user_path(@user)
+ within("#favorite_books") do
+ page.should have_content('The City of Gold and Lead')
+ end
+ end
+
+ it "should still submit text area on blur if there's only a Cancel button present" do
+ @user.save!
+ visit user_path(@user, :suppress_ok_button => 1)
+
+ within("#favorite_books") do
+ page.should have_content('The City of Gold and Lead')
+ end
+
+ id = BestInPlace::Utils.build_best_in_place_id @user, :favorite_books
+ page.execute_script %{$("##{id}").click();}
+ page.should have_no_css("##{id} input[type='submit']")
+ page.execute_script <<-JS
+ $("##{id} textarea").val('1Q84');
+ $("##{id} textarea").blur();
+ JS
+ sleep 1 # Increase if browser is slow
+
+ visit user_path(@user)
+ within("#favorite_books") do
+ page.should have_content('1Q84')
+ end
+ end
+
it "should show validation errors" do
@user.save!
visit user_path(@user)
View
2  test_app/app/assets/stylesheets/style.css
@@ -17,7 +17,7 @@ table th {
input {
width: 80%;
}
-input[type=submit] {
+input[type=submit], input[type=button] {
width: 5em;
}
input[type=checkbox] {
View
17 test_app/app/views/users/show.html.erb
@@ -47,10 +47,27 @@
</td>
</tr>
<tr>
+ <td>Favorite color</td>
+ <td id="favorite_color">
+ <%- opts = { :ok_button => 'Do it!', :cancel_button => 'Nope' } %>
+ <%- opts.delete(:ok_button) if params[:suppress_ok_button] %>
+ <%= best_in_place @user, :favorite_color, opts %>
+ </td>
+ </tr>
+ <tr>
+ <td>Favorite books</td>
+ <td id="favorite_books">
+ <%- opts = { :type => :textarea, :ok_button => 'Save', :cancel_button => 'Cancel' } %>
+ <%- opts.delete(:ok_button) if params[:suppress_ok_button] %>
+ <%= best_in_place @user, :favorite_books, opts %>
+ </td>
+ </tr>
+ <tr>
<td>User description</td>
<td id="description">
<%= best_in_place @user, :description, :display_as => :markdown_desc, :type => :textarea, :sanitize => false %>
</td>
+ </tr>
</table>
<br />
<hr />
View
5 test_app/db/migrate/20111210084202_add_favorite_color_to_users.rb
@@ -0,0 +1,5 @@
+class AddFavoriteColorToUsers < ActiveRecord::Migration
+ def change
+ add_column :users, :favorite_color, :string
+ end
+end
View
5 test_app/db/migrate/20111210084251_add_favorite_books_to_users.rb
@@ -0,0 +1,5 @@
+class AddFavoriteBooksToUsers < ActiveRecord::Migration
+ def change
+ add_column :users, :favorite_books, :text
+ end
+end
View
6 test_app/db/schema.rb
@@ -11,19 +11,21 @@
#
# It's strongly recommended to check this file into your version control system.
-ActiveRecord::Schema.define(:version => 20110115204441) do
+ActiveRecord::Schema.define(:version => 20111210084251) do
create_table "users", :force => true do |t|
t.string "name"
t.string "last_name"
t.string "address"
- t.string "email", :null => false
+ t.string "email", :null => false
t.string "zip"
t.string "country"
t.datetime "created_at"
t.datetime "updated_at"
t.boolean "receive_email"
t.text "description"
+ t.string "favorite_color"
+ t.text "favorite_books"
end
end
Something went wrong with that request. Please try again.