From e5630e55e512e6673799ee79327a2657c685c25e Mon Sep 17 00:00:00 2001 From: Sam Beam Date: Wed, 20 Jan 2016 08:37:22 -0500 Subject: [PATCH 1/6] add multiple option for select when option "multiple" is true, will process original value as array (joined on comma for now for display), and then pass array of selected options directly back to controller. Always adds a button because the change event is often fired spuriously when interacting with a multiselect. --- lib/assets/javascripts/best_in_place.js | 64 ++++++++++++++++++--- lib/best_in_place/helper.rb | 12 +++- lib/best_in_place/test_helpers.rb | 13 +++++ spec/integration/js_spec.rb | 17 ++++++ spec/internal/app/helpers/users_helper.rb | 4 ++ spec/internal/app/models/user.rb | 3 + spec/internal/app/views/users/show.html.erb | 6 ++ spec/internal/db/schema.rb | 1 + 8 files changed, 109 insertions(+), 11 deletions(-) diff --git a/lib/assets/javascripts/best_in_place.js b/lib/assets/javascripts/best_in_place.js index db3f8bde..6f30e006 100644 --- a/lib/assets/javascripts/best_in_place.js +++ b/lib/assets/javascripts/best_in_place.js @@ -120,7 +120,14 @@ BestInPlaceEditor.prototype = { 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]); }); + if (this.selectMultiple) { + var selectedValues = this.values.map( function(v) { if (value.indexOf(v[0]) > -1) { return v[1] } }) + .filter( function(v) { return v != undefined } ) + editor.element.html(selectedValues.join(',')); + } + else { + $.each(this.values, function(index, arr){ if (String(arr[0]) === String(value)) editor.element.html(arr[1]); }); + } break; case "checkbox": @@ -189,6 +196,7 @@ BestInPlaceEditor.prototype = { 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.selectMultiple = self.element.data("bipSelectMultiple"); // Fix for default values of 0 if (self.element.data("bipPlaceholder") == null) { @@ -254,7 +262,17 @@ BestInPlaceEditor.prototype = { csrf_param = jQuery('meta[name=csrf-param]').attr('content'); var data = "_method=" + BestInPlaceEditor.defaults.ajaxMethod; - data += "&" + this.objectName + '[' + this.attributeName + ']=' + encodeURIComponent(this.getValue()); + + var value = this.getValue() + + if (jQuery.isArray(value)) { + for (var i in value) { + data += "&" + this.objectName + '[' + this.attributeName + '][]=' + encodeURIComponent(value[i]); + } + } + else { + data += "&" + this.objectName + '[' + this.attributeName + ']=' + encodeURIComponent(value); + } if (csrf_param !== undefined && csrf_token !== undefined) { data += "&" + csrf_param + "=" + encodeURIComponent(csrf_token); @@ -458,10 +476,12 @@ BestInPlaceEditor.forms = { .attr('style', 'display:inline'), selected = '', select_elt = jQuery(document.createElement('select')) - .attr('class', this.inner_class !== null ? this.inner_class : ''), + .attr('class', this.inner_class !== null ? this.inner_class : '') + .attr('multiple', this.selectMultiple), currentCollectionValue = this.collectionValue, key, value, a = this.values; + var _self = this; $.each(a, function(index, arr){ key = arr[0]; @@ -469,16 +489,35 @@ BestInPlaceEditor.forms = { var option_elt = jQuery(document.createElement('option')) .val(key) .html(value); - if (String(key) === String(currentCollectionValue)) option_elt.attr('selected', 'selected'); + + if (_self.selectMultiple) { + if (currentCollectionValue.indexOf(String(key)) > -1) option_elt.attr('selected', 'selected'); + } + else { + if (String(key) === String(currentCollectionValue)) option_elt.attr('selected', 'selected'); + } + select_elt.append(option_elt); }); output.append(select_elt); + if (this.selectMultiple) { + if (!this.okButton) this.okButton = 'submit' + this.placeButtons(output, this); + } + this.element.html(output); + + if (this.selectMultiple) { + this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.input.submitHandler) + } + else { + 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.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 @@ -495,7 +534,16 @@ BestInPlaceEditor.forms = { getValue: function () { 'use strict'; - return this.sanitizeValue(this.element.find("select").val()); + var val = this.element.find("select").val(); + if (this.selectMultiple) { + var _self = this; + return val.map( function(v) { + return _self.sanitizeValue(v); + }) + } + else { + return this.sanitizeValue(val); + } }, blurHandler: function (event) { diff --git a/lib/best_in_place/helper.rb b/lib/best_in_place/helper.rb index 74d5e6e7..94027d12 100644 --- a/lib/best_in_place/helper.rb +++ b/lib/best_in_place/helper.rb @@ -19,10 +19,14 @@ def best_in_place(object, field, opts = {}) if opts[:collection] or type == :checkbox collection = opts[:collection] - value = value.to_s collection = best_in_place_default_collection if collection.blank? collection = best_in_place_collection_builder(type, collection) - display_value = collection.flat_map{|a| a[0].to_s == value ? a[1] : nil }.compact[0] + if value.is_a? Array + display_value = collection.flat_map{|a| value.include?(a[0].to_s) ? a[1] : nil }.compact.join(',') + else + value = value.to_s + display_value = collection.flat_map{|a| a[0].to_s == value ? a[1] : nil }.compact[0] + end collection = collection.to_json options[:data]['bip-collection'] = html_escape(collection) end @@ -53,6 +57,8 @@ def best_in_place(object, field, opts = {}) options[:data]['bip-confirm'] = opts[:confirm].presence options[:data]['bip-value'] = html_escape(value).presence + options[:data]['bip-select-multiple'] = opts[:multiple].presence + if opts[:raw] options[:data]['bip-raw'] = 'true' end @@ -82,7 +88,7 @@ def pass_through_html_options(opts, options) :activator, :cancel_button, :cancel_button_class, :html_attrs, :inner_class, :nil, :object_name, :ok_button, :ok_button_class, :display_as, :display_with, :path, :value, :use_confirm, :confirm, :sanitize, :raw, :helper_options, :url, :place_holder, :class, - :as, :param, :container] + :as, :param, :container, :multiple] uknown_keys = opts.keys - known_keys uknown_keys.each { |key| options[key] = opts[key] } end diff --git a/lib/best_in_place/test_helpers.rb b/lib/best_in_place/test_helpers.rb index 68f47b3b..ae6b5486 100644 --- a/lib/best_in_place/test_helpers.rb +++ b/lib/best_in_place/test_helpers.rb @@ -35,6 +35,19 @@ def bip_select(model, attr, name) wait_for_ajax end + def bip_multi_select(model, attr, opts, unselect=[]) + id = BestInPlace::Utils.build_best_in_place_id model, attr + find("##{id}").trigger('click') + opts.each do |opt| + find("##{id}").select(opt) + end + unselect.each do |opt| + find("##{id}").unselect(opt) + end + find("##{id} input[type=submit]").trigger('click') + wait_for_ajax + end + def wait_for_ajax return unless respond_to?(:evaluate_script) wait_until { finished_all_ajax_requests? } diff --git a/spec/integration/js_spec.rb b/spec/integration/js_spec.rb index 5bbbbd4d..d0d673ae 100644 --- a/spec/integration/js_spec.rb +++ b/spec/integration/js_spec.rb @@ -213,6 +213,23 @@ expect(find('#country')).to have_content('France') end + it "should be able to use bip_multi_select to render and update a multi-select field" do + @user.favorite_snacks = %w(whoppers sun-chips) + @user.save! + visit user_path(@user) + + expect(find('#favorite_snacks')).to have_content('Whoppers,Sun Chips') + + bip_multi_select @user, :favorite_snacks, ['Twizzlers','Kettle Corn'], ['Sun Chips'] + + expect(find('#favorite_snacks')).to have_content('Twizzlers,Whoppers,Kettle Corn') + expect(find('#favorite_snacks')).not_to have_content('Sun Chips') + + visit user_path(@user) + + expect(find('#favorite_snacks')).to have_content('Twizzlers,Whoppers,Kettle Corn') + end + it "should apply the inner_class option to a select field" do @user.save! visit user_path(@user) diff --git a/spec/internal/app/helpers/users_helper.rb b/spec/internal/app/helpers/users_helper.rb index 007e8808..aa5557a4 100644 --- a/spec/internal/app/helpers/users_helper.rb +++ b/spec/internal/app/helpers/users_helper.rb @@ -26,4 +26,8 @@ def height_collection def bb(value) "#{value} €" end + + def snacks_options + Hash[ ["Twizzlers", "Whoppers", "Krispy Kremes", "Sun Chips", "Kit Kat", "Kettle Corn"].map { |o| [o.downcase.gsub(/\s+/, '-'), o] } ] + end end diff --git a/spec/internal/app/models/user.rb b/spec/internal/app/models/user.rb index 86365fd6..c435bf8e 100644 --- a/spec/internal/app/models/user.rb +++ b/spec/internal/app/models/user.rb @@ -21,6 +21,8 @@ class User < ActiveRecord::Base alias_attribute :receive_email_image, :receive_email alias_attribute :description_simple, :description + serialize :favorite_snacks + def address_format "addr => [#{address}]".html_safe end @@ -32,4 +34,5 @@ def markdown_desc def zip_format nil end + end diff --git a/spec/internal/app/views/users/show.html.erb b/spec/internal/app/views/users/show.html.erb index b10f3f8e..781dccaf 100644 --- a/spec/internal/app/views/users/show.html.erb +++ b/spec/internal/app/views/users/show.html.erb @@ -137,6 +137,12 @@ <%= best_in_place @user, :favorite_movie, opts %> + + Favorite snacks + + <%= best_in_place @user, :favorite_snacks, as: :select, multiple: true, collection: snacks_options %> + + Zero Field diff --git a/spec/internal/db/schema.rb b/spec/internal/db/schema.rb index 8b5deae4..b885679c 100644 --- a/spec/internal/db/schema.rb +++ b/spec/internal/db/schema.rb @@ -23,5 +23,6 @@ t.string "favorite_movie" t.string "favorite_locale" t.integer "zero_field" + t.string "favorite_snacks" end end From 4b8de2201da07243e753a30b80a91a0b2e139190 Mon Sep 17 00:00:00 2001 From: Sam Beam Date: Fri, 22 Jan 2016 18:20:49 -0500 Subject: [PATCH 2/6] fix fails because of asset precompilation and digests in 4.2 the block for Combustion.initialize! sets the old values for the test env that turn off digests, so the matcher for the image tag in "should be able to use bip_bool to change a boolean value using an image" works. also moving css files to standard application.css manifest avoids the complaints from rails about "Asset was not declared to be precompiled in production." for each of these. --- spec/internal/app/assets/stylesheets/application.css | 5 +++++ spec/internal/app/views/layouts/application.html.erb | 2 +- spec/rails_helper.rb | 8 ++++++-- 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 spec/internal/app/assets/stylesheets/application.css diff --git a/spec/internal/app/assets/stylesheets/application.css b/spec/internal/app/assets/stylesheets/application.css new file mode 100644 index 00000000..31dd104d --- /dev/null +++ b/spec/internal/app/assets/stylesheets/application.css @@ -0,0 +1,5 @@ +/* +*= require scaffold.css +*= require style.css +*= require jquery-ui-1.8.16.custom.css +*/ diff --git a/spec/internal/app/views/layouts/application.html.erb b/spec/internal/app/views/layouts/application.html.erb index 87b58033..984fd21b 100644 --- a/spec/internal/app/views/layouts/application.html.erb +++ b/spec/internal/app/views/layouts/application.html.erb @@ -2,7 +2,7 @@ Best In Place TEST APP <%= Rails::VERSION::STRING %> - <%= stylesheet_link_tag "scaffold", "style", "jquery-ui-1.8.16.custom" %> + <%= stylesheet_link_tag "application" %> <%= javascript_include_tag "application" %> <%= csrf_meta_tag %> diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 3f20dd08..a6eaad5f 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -12,8 +12,12 @@ require 'best_in_place' -Combustion.initialize! :active_record, :action_controller, - :action_view, :sprockets +Combustion.initialize! :active_record, :action_controller, :action_view, :sprockets do + config.assets.compile = true + config.assets.compress = false + config.assets.debug = false + config.assets.digest = false +end require 'rspec/rails' require 'capybara/rails' From e759e15b9e6f127815018692bbd889e05fcda178 Mon Sep 17 00:00:00 2001 From: Sam Beam Date: Mon, 25 Jan 2016 09:08:21 -0500 Subject: [PATCH 3/6] defend against nil values for given field --- lib/assets/javascripts/best_in_place.js | 2 +- spec/integration/js_spec.rb | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/assets/javascripts/best_in_place.js b/lib/assets/javascripts/best_in_place.js index 6f30e006..834cf6c5 100644 --- a/lib/assets/javascripts/best_in_place.js +++ b/lib/assets/javascripts/best_in_place.js @@ -491,7 +491,7 @@ BestInPlaceEditor.forms = { .html(value); if (_self.selectMultiple) { - if (currentCollectionValue.indexOf(String(key)) > -1) option_elt.attr('selected', 'selected'); + if (currentCollectionValue && currentCollectionValue.indexOf(String(key)) > -1) option_elt.attr('selected', 'selected'); } else { if (String(key) === String(currentCollectionValue)) option_elt.attr('selected', 'selected'); diff --git a/spec/integration/js_spec.rb b/spec/integration/js_spec.rb index d0d673ae..be658e75 100644 --- a/spec/integration/js_spec.rb +++ b/spec/integration/js_spec.rb @@ -224,12 +224,22 @@ expect(find('#favorite_snacks')).to have_content('Twizzlers,Whoppers,Kettle Corn') expect(find('#favorite_snacks')).not_to have_content('Sun Chips') - visit user_path(@user) expect(find('#favorite_snacks')).to have_content('Twizzlers,Whoppers,Kettle Corn') end + it "does not error when value is nil" do + @user.save! + visit user_path(@user) + expect(find('#favorite_snacks')).to have_content('-') + + bip_multi_select @user, :favorite_snacks, ['Sun Chips'] + expect(find('#favorite_snacks')).to have_content('Sun Chips') + visit user_path(@user) + expect(find('#favorite_snacks')).to have_content('Sun Chips') + end + it "should apply the inner_class option to a select field" do @user.save! visit user_path(@user) From 1d73ef9838b7199111cfa084bf5f68f70cbc45bc Mon Sep 17 00:00:00 2001 From: Sam Beam Date: Mon, 25 Jan 2016 09:50:48 -0500 Subject: [PATCH 4/6] travis kick From e2a6e9790ea8f4648660df9085350292b3985735 Mon Sep 17 00:00:00 2001 From: Sam Beam Date: Mon, 25 Jan 2016 10:10:48 -0500 Subject: [PATCH 5/6] smack travis From ee6112d4403628fe2efa41edf90179caa858aaf8 Mon Sep 17 00:00:00 2001 From: Sam Beam Date: Mon, 25 Jan 2016 13:17:11 -0500 Subject: [PATCH 6/6] travis kick harder