bootstrap_form_legacy 4.0.1.dev
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.travis.yml +29 -0
- data/CHANGELOG.md +207 -0
- data/CONTRIBUTING.md +57 -0
- data/Dangerfile +54 -0
- data/Gemfile +21 -0
- data/LICENSE.txt +20 -0
- data/README.md +647 -0
- data/Rakefile +27 -0
- data/app/assets/stylesheets/rails_bootstrap_forms.css +10 -0
- data/bootstrap_form.gemspec +26 -0
- data/demo/README.md +17 -0
- data/demo/Rakefile +6 -0
- data/demo/app/controllers/application_controller.rb +2 -0
- data/demo/app/controllers/bootstrap_controller.rb +16 -0
- data/demo/app/helpers/bootstrap_helper.rb +23 -0
- data/demo/app/models/address.rb +3 -0
- data/demo/app/models/application_record.rb +3 -0
- data/demo/app/models/faux_user.rb +9 -0
- data/demo/app/models/super_user.rb +2 -0
- data/demo/app/models/user.rb +9 -0
- data/demo/app/views/bootstrap/form.html.erb +53 -0
- data/demo/app/views/layouts/application.html.erb +65 -0
- data/demo/bin/bundle +3 -0
- data/demo/bin/rails +4 -0
- data/demo/bin/rake +4 -0
- data/demo/bin/setup +36 -0
- data/demo/bin/update +31 -0
- data/demo/bin/yarn +11 -0
- data/demo/config.ru +5 -0
- data/demo/config/application.rb +24 -0
- data/demo/config/boot.rb +5 -0
- data/demo/config/database.yml +21 -0
- data/demo/config/environment.rb +5 -0
- data/demo/config/environments/development.rb +60 -0
- data/demo/config/environments/test.rb +48 -0
- data/demo/config/initializers/application_controller_renderer.rb +8 -0
- data/demo/config/initializers/assets.rb +14 -0
- data/demo/config/initializers/backtrace_silencers.rb +7 -0
- data/demo/config/initializers/cookies_serializer.rb +5 -0
- data/demo/config/initializers/filter_parameter_logging.rb +4 -0
- data/demo/config/initializers/inflections.rb +16 -0
- data/demo/config/initializers/mime_types.rb +4 -0
- data/demo/config/initializers/wrap_parameters.rb +14 -0
- data/demo/config/locales/en.yml +33 -0
- data/demo/config/puma.rb +56 -0
- data/demo/config/routes.rb +5 -0
- data/demo/config/spring.rb +6 -0
- data/demo/config/storage.yml +35 -0
- data/demo/db/schema.rb +24 -0
- data/demo/log/.keep +0 -0
- data/demo/package.json +5 -0
- data/demo/public/favicon.ico +0 -0
- data/lib/bootstrap_form.rb +13 -0
- data/lib/bootstrap_form/aliasing.rb +35 -0
- data/lib/bootstrap_form/form_builder.rb +564 -0
- data/lib/bootstrap_form/helper.rb +59 -0
- data/lib/bootstrap_form/helpers/bootstrap.rb +99 -0
- data/lib/bootstrap_form/version.rb +3 -0
- metadata +120 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
Rails.application.configure do
|
2
|
+
# Settings specified here will take precedence over those in config/application.rb.
|
3
|
+
|
4
|
+
# In the development environment your application's code is reloaded on
|
5
|
+
# every request. This slows down response time but is perfect for development
|
6
|
+
# since you don't have to restart the web server when you make code changes.
|
7
|
+
config.cache_classes = false
|
8
|
+
|
9
|
+
# Do not eager load code on boot.
|
10
|
+
config.eager_load = false
|
11
|
+
|
12
|
+
# Show full error reports.
|
13
|
+
config.consider_all_requests_local = true
|
14
|
+
|
15
|
+
# Enable/disable caching. By default caching is disabled.
|
16
|
+
# Run rails dev:cache to toggle caching.
|
17
|
+
if Rails.root.join('tmp/caching-dev.txt').exist?
|
18
|
+
config.action_controller.perform_caching = true
|
19
|
+
|
20
|
+
config.cache_store = :memory_store
|
21
|
+
config.public_file_server.headers = {
|
22
|
+
'Cache-Control' => "public, max-age=#{2.days.to_i}"
|
23
|
+
}
|
24
|
+
else
|
25
|
+
config.action_controller.perform_caching = false
|
26
|
+
|
27
|
+
config.cache_store = :null_store
|
28
|
+
end
|
29
|
+
|
30
|
+
# Store uploaded files on the local file system (see config/storage.yml for options)
|
31
|
+
if config.respond_to?(:active_storage)
|
32
|
+
config.active_storage.service = :local
|
33
|
+
end
|
34
|
+
|
35
|
+
# Don't care if the mailer can't send.
|
36
|
+
config.action_mailer.raise_delivery_errors = false
|
37
|
+
|
38
|
+
config.action_mailer.perform_caching = false
|
39
|
+
|
40
|
+
# Print deprecation notices to the Rails logger.
|
41
|
+
config.active_support.deprecation = :log
|
42
|
+
|
43
|
+
# Raise an error on page load if there are pending migrations.
|
44
|
+
config.active_record.migration_error = :page_load
|
45
|
+
|
46
|
+
# Debug mode disables concatenation and preprocessing of assets.
|
47
|
+
# This option may cause significant delays in view rendering with a large
|
48
|
+
# number of complex assets.
|
49
|
+
config.assets.debug = true
|
50
|
+
|
51
|
+
# Suppress logger output for asset requests.
|
52
|
+
config.assets.quiet = true
|
53
|
+
|
54
|
+
# Raises error for missing translations
|
55
|
+
# config.action_view.raise_on_missing_translations = true
|
56
|
+
|
57
|
+
# Use an evented file watcher to asynchronously detect changes in source code,
|
58
|
+
# routes, locales, etc. This feature depends on the listen gem.
|
59
|
+
# config.file_watcher = ActiveSupport::EventedFileUpdateChecker
|
60
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
Rails.application.configure do
|
2
|
+
# Settings specified here will take precedence over those in config/application.rb.
|
3
|
+
|
4
|
+
# The test environment is used exclusively to run your application's
|
5
|
+
# test suite. You never need to work with it otherwise. Remember that
|
6
|
+
# your test database is "scratch space" for the test suite and is wiped
|
7
|
+
# and recreated between test runs. Don't rely on the data there!
|
8
|
+
config.cache_classes = true
|
9
|
+
|
10
|
+
# Do not eager load code on boot. This avoids loading your whole application
|
11
|
+
# just for the purpose of running a single test. If you are using a tool that
|
12
|
+
# preloads Rails for running tests, you may have to set it to true.
|
13
|
+
config.eager_load = false
|
14
|
+
|
15
|
+
# Configure public file server for tests with Cache-Control for performance.
|
16
|
+
config.public_file_server.enabled = true
|
17
|
+
config.public_file_server.headers = {
|
18
|
+
'Cache-Control' => "public, max-age=#{1.hour.to_i}"
|
19
|
+
}
|
20
|
+
|
21
|
+
# Show full error reports and disable caching.
|
22
|
+
config.consider_all_requests_local = true
|
23
|
+
config.action_controller.perform_caching = false
|
24
|
+
|
25
|
+
# Raise exceptions instead of rendering exception templates.
|
26
|
+
config.action_dispatch.show_exceptions = false
|
27
|
+
|
28
|
+
# Disable request forgery protection in test environment.
|
29
|
+
config.action_controller.allow_forgery_protection = false
|
30
|
+
|
31
|
+
# Store uploaded files on the local file system in a temporary directory
|
32
|
+
if config.respond_to?(:active_storage)
|
33
|
+
config.active_storage.service = :test
|
34
|
+
end
|
35
|
+
|
36
|
+
config.action_mailer.perform_caching = false
|
37
|
+
|
38
|
+
# Tell Action Mailer not to deliver emails to the real world.
|
39
|
+
# The :test delivery method accumulates sent emails in the
|
40
|
+
# ActionMailer::Base.deliveries array.
|
41
|
+
config.action_mailer.delivery_method = :test
|
42
|
+
|
43
|
+
# Print deprecation notices to the stderr.
|
44
|
+
config.active_support.deprecation = :stderr
|
45
|
+
|
46
|
+
# Raises error for missing translations
|
47
|
+
# config.action_view.raise_on_missing_translations = true
|
48
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# Be sure to restart your server when you modify this file.
|
2
|
+
|
3
|
+
# Version of your assets, change this if you want to expire all your assets.
|
4
|
+
Rails.application.config.assets.version = '1.0'
|
5
|
+
|
6
|
+
# Add additional assets to the asset load path.
|
7
|
+
# Rails.application.config.assets.paths << Emoji.images_path
|
8
|
+
# Add Yarn node_modules folder to the asset load path.
|
9
|
+
Rails.application.config.assets.paths << Rails.root.join('node_modules')
|
10
|
+
|
11
|
+
# Precompile additional assets.
|
12
|
+
# application.js, application.css, and all non-JS/CSS in the app/assets
|
13
|
+
# folder are already added.
|
14
|
+
# Rails.application.config.assets.precompile += %w( admin.js admin.css )
|
@@ -0,0 +1,7 @@
|
|
1
|
+
# Be sure to restart your server when you modify this file.
|
2
|
+
|
3
|
+
# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
|
4
|
+
# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
|
5
|
+
|
6
|
+
# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
|
7
|
+
# Rails.backtrace_cleaner.remove_silencers!
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# Be sure to restart your server when you modify this file.
|
2
|
+
|
3
|
+
# Add new inflection rules using the following format. Inflections
|
4
|
+
# are locale specific, and you may define rules for as many different
|
5
|
+
# locales as you wish. All of these examples are active by default:
|
6
|
+
# ActiveSupport::Inflector.inflections(:en) do |inflect|
|
7
|
+
# inflect.plural /^(ox)$/i, '\1en'
|
8
|
+
# inflect.singular /^(ox)en/i, '\1'
|
9
|
+
# inflect.irregular 'person', 'people'
|
10
|
+
# inflect.uncountable %w( fish sheep )
|
11
|
+
# end
|
12
|
+
|
13
|
+
# These inflection rules are supported but not enabled by default:
|
14
|
+
# ActiveSupport::Inflector.inflections(:en) do |inflect|
|
15
|
+
# inflect.acronym 'RESTful'
|
16
|
+
# end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# Be sure to restart your server when you modify this file.
|
2
|
+
|
3
|
+
# This file contains settings for ActionController::ParamsWrapper which
|
4
|
+
# is enabled by default.
|
5
|
+
|
6
|
+
# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
|
7
|
+
ActiveSupport.on_load(:action_controller) do
|
8
|
+
wrap_parameters format: [:json]
|
9
|
+
end
|
10
|
+
|
11
|
+
# To enable root element in JSON for ActiveRecord objects.
|
12
|
+
# ActiveSupport.on_load(:active_record) do
|
13
|
+
# self.include_root_in_json = true
|
14
|
+
# end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Files in the config/locales directory are used for internationalization
|
2
|
+
# and are automatically loaded by Rails. If you want to use locales other
|
3
|
+
# than English, add the necessary files in this directory.
|
4
|
+
#
|
5
|
+
# To use the locales, use `I18n.t`:
|
6
|
+
#
|
7
|
+
# I18n.t 'hello'
|
8
|
+
#
|
9
|
+
# In views, this is aliased to just `t`:
|
10
|
+
#
|
11
|
+
# <%= t('hello') %>
|
12
|
+
#
|
13
|
+
# To use a different locale, set it with `I18n.locale`:
|
14
|
+
#
|
15
|
+
# I18n.locale = :es
|
16
|
+
#
|
17
|
+
# This would use the information in config/locales/es.yml.
|
18
|
+
#
|
19
|
+
# The following keys must be escaped otherwise they will not be retrieved by
|
20
|
+
# the default I18n backend:
|
21
|
+
#
|
22
|
+
# true, false, on, off, yes, no
|
23
|
+
#
|
24
|
+
# Instead, surround them with single quotes.
|
25
|
+
#
|
26
|
+
# en:
|
27
|
+
# 'true': 'foo'
|
28
|
+
#
|
29
|
+
# To learn more, please read the Rails Internationalization guide
|
30
|
+
# available at http://guides.rubyonrails.org/i18n.html.
|
31
|
+
|
32
|
+
en:
|
33
|
+
hello: "Hello world"
|
data/demo/config/puma.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# Puma can serve each request in a thread from an internal thread pool.
|
2
|
+
# The `threads` method setting takes two numbers: a minimum and maximum.
|
3
|
+
# Any libraries that use thread pools should be configured to match
|
4
|
+
# the maximum value specified for Puma. Default is set to 5 threads for minimum
|
5
|
+
# and maximum; this matches the default thread size of Active Record.
|
6
|
+
#
|
7
|
+
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
|
8
|
+
threads threads_count, threads_count
|
9
|
+
|
10
|
+
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
|
11
|
+
#
|
12
|
+
port ENV.fetch("PORT") { 3000 }
|
13
|
+
|
14
|
+
# Specifies the `environment` that Puma will run in.
|
15
|
+
#
|
16
|
+
environment ENV.fetch("RAILS_ENV") { "development" }
|
17
|
+
|
18
|
+
# Specifies the number of `workers` to boot in clustered mode.
|
19
|
+
# Workers are forked webserver processes. If using threads and workers together
|
20
|
+
# the concurrency of the application would be max `threads` * `workers`.
|
21
|
+
# Workers do not work on JRuby or Windows (both of which do not support
|
22
|
+
# processes).
|
23
|
+
#
|
24
|
+
# workers ENV.fetch("WEB_CONCURRENCY") { 2 }
|
25
|
+
|
26
|
+
# Use the `preload_app!` method when specifying a `workers` number.
|
27
|
+
# This directive tells Puma to first boot the application and load code
|
28
|
+
# before forking the application. This takes advantage of Copy On Write
|
29
|
+
# process behavior so workers use less memory. If you use this option
|
30
|
+
# you need to make sure to reconnect any threads in the `on_worker_boot`
|
31
|
+
# block.
|
32
|
+
#
|
33
|
+
# preload_app!
|
34
|
+
|
35
|
+
# If you are preloading your application and using Active Record, it's
|
36
|
+
# recommended that you close any connections to the database before workers
|
37
|
+
# are forked to prevent connection leakage.
|
38
|
+
#
|
39
|
+
# before_fork do
|
40
|
+
# ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord)
|
41
|
+
# end
|
42
|
+
|
43
|
+
# The code in the `on_worker_boot` will be called if you are using
|
44
|
+
# clustered mode by specifying a number of `workers`. After each worker
|
45
|
+
# process is booted, this block will be run. If you are using the `preload_app!`
|
46
|
+
# option, you will want to use this block to reconnect to any threads
|
47
|
+
# or connections that may have been created at application boot, as Ruby
|
48
|
+
# cannot share connections between processes.
|
49
|
+
#
|
50
|
+
# on_worker_boot do
|
51
|
+
# ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
|
55
|
+
# Allow puma to be restarted by `rails restart` command.
|
56
|
+
plugin :tmp_restart
|
@@ -0,0 +1,35 @@
|
|
1
|
+
test:
|
2
|
+
service: Disk
|
3
|
+
root: <%= Rails.root.join("tmp/storage") %>
|
4
|
+
|
5
|
+
local:
|
6
|
+
service: Disk
|
7
|
+
root: <%= Rails.root.join("storage") %>
|
8
|
+
|
9
|
+
# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
|
10
|
+
# amazon:
|
11
|
+
# service: S3
|
12
|
+
# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
|
13
|
+
# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
|
14
|
+
# region: us-east-1
|
15
|
+
# bucket: your_own_bucket
|
16
|
+
|
17
|
+
# Remember not to checkin your GCS keyfile to a repository
|
18
|
+
# google:
|
19
|
+
# service: GCS
|
20
|
+
# project: your_project
|
21
|
+
# keyfile: <%= Rails.root.join("path/to/gcs.keyfile") %>
|
22
|
+
# bucket: your_own_bucket
|
23
|
+
|
24
|
+
# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
|
25
|
+
# microsoft:
|
26
|
+
# service: AzureStorage
|
27
|
+
# path: your_azure_storage_path
|
28
|
+
# storage_account_name: your_account_name
|
29
|
+
# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
|
30
|
+
# container: your_container_name
|
31
|
+
|
32
|
+
# mirror:
|
33
|
+
# service: Mirror
|
34
|
+
# primary: local
|
35
|
+
# mirrors: [ amazon, google, microsoft ]
|
data/demo/db/schema.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
ActiveRecord::Schema.define(version: 1) do
|
2
|
+
|
3
|
+
create_table :addresses, force: :cascade do |t|
|
4
|
+
t.integer :user_id
|
5
|
+
t.string :street
|
6
|
+
t.string :city
|
7
|
+
t.string :state
|
8
|
+
t.string :zip_code
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
|
12
|
+
create_table :users, force: :cascade do |t|
|
13
|
+
t.string :email
|
14
|
+
t.string :password
|
15
|
+
t.text :comments
|
16
|
+
t.string :status
|
17
|
+
t.string :misc
|
18
|
+
t.text :preferences
|
19
|
+
t.boolean :terms, default: false
|
20
|
+
t.string :type
|
21
|
+
t.timestamps
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
data/demo/log/.keep
ADDED
File without changes
|
data/demo/package.json
ADDED
File without changes
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module BootstrapForm
|
2
|
+
# This module implements the old ActiveSupport alias_method_chain feature
|
3
|
+
# with a new name, and without the deprecation warnings. In ActiveSupport 5+,
|
4
|
+
# this style of patching was deprecated in favor of Module.prepend. But
|
5
|
+
# Module.prepend is not present in Ruby 1.9, which we would still like to
|
6
|
+
# support. So we continue to use of alias_method_chain, albeit with a
|
7
|
+
# different name to avoid collisions.
|
8
|
+
module Aliasing
|
9
|
+
# This code is copied and pasted from ActiveSupport, but with :bootstrap
|
10
|
+
# hardcoded as the feature name, and with the deprecation warning removed.
|
11
|
+
def bootstrap_method_alias(target)
|
12
|
+
feature = :bootstrap
|
13
|
+
|
14
|
+
# Strip out punctuation on predicates, bang or writer methods since
|
15
|
+
# e.g. target?_without_feature is not a valid method name.
|
16
|
+
aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
|
17
|
+
yield(aliased_target, punctuation) if block_given?
|
18
|
+
|
19
|
+
with_method = "#{aliased_target}_with_#{feature}#{punctuation}"
|
20
|
+
without_method = "#{aliased_target}_without_#{feature}#{punctuation}"
|
21
|
+
|
22
|
+
alias_method without_method, target
|
23
|
+
alias_method target, with_method
|
24
|
+
|
25
|
+
case
|
26
|
+
when public_method_defined?(without_method)
|
27
|
+
public target
|
28
|
+
when protected_method_defined?(without_method)
|
29
|
+
protected target
|
30
|
+
when private_method_defined?(without_method)
|
31
|
+
private target
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,564 @@
|
|
1
|
+
require_relative 'aliasing'
|
2
|
+
require_relative 'helpers/bootstrap'
|
3
|
+
|
4
|
+
module BootstrapForm
|
5
|
+
class FormBuilder < ActionView::Helpers::FormBuilder
|
6
|
+
extend BootstrapForm::Aliasing
|
7
|
+
include BootstrapForm::Helpers::Bootstrap
|
8
|
+
|
9
|
+
attr_reader :layout, :label_col, :control_col, :has_error, :inline_errors, :label_errors
|
10
|
+
|
11
|
+
FIELD_HELPERS = %w{color_field date_field datetime_field datetime_local_field
|
12
|
+
email_field month_field number_field password_field phone_field
|
13
|
+
range_field search_field telephone_field text_area text_field time_field
|
14
|
+
url_field week_field}
|
15
|
+
|
16
|
+
DATE_SELECT_HELPERS = %w{date_select time_select datetime_select}
|
17
|
+
|
18
|
+
delegate :content_tag, :capture, :concat, to: :@template
|
19
|
+
|
20
|
+
def initialize(object_name, object, template, options)
|
21
|
+
@layout = options[:layout]
|
22
|
+
@label_col = options[:label_col] || default_label_col
|
23
|
+
@control_col = options[:control_col] || default_control_col
|
24
|
+
@label_errors = options[:label_errors] || false
|
25
|
+
@inline_errors = if options[:inline_errors].nil?
|
26
|
+
@label_errors != true
|
27
|
+
else
|
28
|
+
options[:inline_errors] != false
|
29
|
+
end
|
30
|
+
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
FIELD_HELPERS.each do |method_name|
|
35
|
+
with_method_name = "#{method_name}_with_bootstrap"
|
36
|
+
without_method_name = "#{method_name}_without_bootstrap"
|
37
|
+
|
38
|
+
define_method(with_method_name) do |name, options = {}|
|
39
|
+
form_group_builder(name, options) do
|
40
|
+
send(without_method_name, name, options)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
bootstrap_method_alias method_name
|
45
|
+
end
|
46
|
+
|
47
|
+
DATE_SELECT_HELPERS.each do |method_name|
|
48
|
+
with_method_name = "#{method_name}_with_bootstrap"
|
49
|
+
without_method_name = "#{method_name}_without_bootstrap"
|
50
|
+
|
51
|
+
define_method(with_method_name) do |name, options = {}, html_options = {}|
|
52
|
+
prevent_prepend_and_append!(options)
|
53
|
+
form_group_builder(name, options, html_options) do
|
54
|
+
content_tag(:div, send(without_method_name, name, options, html_options), class: control_specific_class(method_name))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
bootstrap_method_alias method_name
|
59
|
+
end
|
60
|
+
|
61
|
+
def file_field_with_bootstrap(name, options = {})
|
62
|
+
prevent_prepend_and_append!(options)
|
63
|
+
options = options.reverse_merge(control_class: "custom-file-input")
|
64
|
+
form_group_builder(name, options) do
|
65
|
+
content_tag(:div, class: "custom-file") do
|
66
|
+
placeholder = options.delete(:placeholder) || "Choose file"
|
67
|
+
placeholder_opts = { class: "custom-file-label" }
|
68
|
+
placeholder_opts[:for] = options[:id] if options[:id].present?
|
69
|
+
|
70
|
+
input = file_field_without_bootstrap(name, options)
|
71
|
+
placeholder_label = label(name, placeholder, placeholder_opts)
|
72
|
+
concat(input)
|
73
|
+
concat(placeholder_label)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
bootstrap_method_alias :file_field
|
79
|
+
|
80
|
+
def select_with_bootstrap(method, choices = nil, options = {}, html_options = {}, &block)
|
81
|
+
form_group_builder(method, options, html_options) do
|
82
|
+
select_without_bootstrap(method, choices, options, html_options, &block)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
bootstrap_method_alias :select
|
87
|
+
|
88
|
+
def collection_select_with_bootstrap(method, collection, value_method, text_method, options = {}, html_options = {})
|
89
|
+
prevent_prepend_and_append!(options)
|
90
|
+
form_group_builder(method, options, html_options) do
|
91
|
+
collection_select_without_bootstrap(method, collection, value_method, text_method, options, html_options)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
bootstrap_method_alias :collection_select
|
96
|
+
|
97
|
+
def grouped_collection_select_with_bootstrap(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
|
98
|
+
prevent_prepend_and_append!(options)
|
99
|
+
form_group_builder(method, options, html_options) do
|
100
|
+
grouped_collection_select_without_bootstrap(method, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
bootstrap_method_alias :grouped_collection_select
|
105
|
+
|
106
|
+
def time_zone_select_with_bootstrap(method, priority_zones = nil, options = {}, html_options = {})
|
107
|
+
prevent_prepend_and_append!(options)
|
108
|
+
form_group_builder(method, options, html_options) do
|
109
|
+
time_zone_select_without_bootstrap(method, priority_zones, options, html_options)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
bootstrap_method_alias :time_zone_select
|
114
|
+
|
115
|
+
def check_box_with_bootstrap(name, options = {}, checked_value = "1", unchecked_value = "0", &block)
|
116
|
+
prevent_prepend_and_append!(options)
|
117
|
+
options = options.symbolize_keys!
|
118
|
+
check_box_options = options.except(:label, :label_class, :help, :inline, :custom, :hide_label, :skip_label)
|
119
|
+
check_box_classes = [check_box_options[:class]]
|
120
|
+
check_box_classes << "position-static" if options[:skip_label] || options[:hide_label]
|
121
|
+
if options[:custom]
|
122
|
+
validation = nil
|
123
|
+
validation = "is-invalid" if has_error?(name)
|
124
|
+
check_box_options[:class] = (["custom-control-input", validation] + check_box_classes).compact.join(' ')
|
125
|
+
else
|
126
|
+
check_box_options[:class] = (["form-check-input"] + check_box_classes).compact.join(' ')
|
127
|
+
end
|
128
|
+
|
129
|
+
checkbox_html = check_box_without_bootstrap(name, check_box_options, checked_value, unchecked_value)
|
130
|
+
label_content = block_given? ? capture(&block) : options[:label]
|
131
|
+
label_description = label_content || (object && object.class.human_attribute_name(name)) || name.to_s.humanize
|
132
|
+
|
133
|
+
label_name = name
|
134
|
+
# label's `for` attribute needs to match checkbox tag's id,
|
135
|
+
# IE sanitized value, IE
|
136
|
+
# https://github.com/rails/rails/blob/5-0-stable/actionview/lib/action_view/helpers/tags/base.rb#L123-L125
|
137
|
+
if options[:multiple]
|
138
|
+
label_name =
|
139
|
+
"#{name}_#{checked_value.to_s.gsub(/\s/, "_").gsub(/[^-[[:word:]]]/, "").mb_chars.downcase.to_s}"
|
140
|
+
end
|
141
|
+
|
142
|
+
label_classes = [options[:label_class]]
|
143
|
+
label_classes << hide_class if options[:hide_label]
|
144
|
+
|
145
|
+
if options[:custom]
|
146
|
+
div_class = ["custom-control", "custom-checkbox"]
|
147
|
+
div_class.append("custom-control-inline") if layout_inline?(options[:inline])
|
148
|
+
label_class = label_classes.prepend("custom-control-label").compact.join(" ")
|
149
|
+
content_tag(:div, class: div_class.compact.join(" ")) do
|
150
|
+
if options[:skip_label]
|
151
|
+
checkbox_html
|
152
|
+
else
|
153
|
+
# TODO: Notice we don't seem to pass the ID into the custom control.
|
154
|
+
checkbox_html.concat(label(label_name, label_description, class: label_class))
|
155
|
+
end
|
156
|
+
end
|
157
|
+
else
|
158
|
+
wrapper_class = "form-check"
|
159
|
+
wrapper_class += " form-check-inline" if layout_inline?(options[:inline])
|
160
|
+
label_class = label_classes.prepend("form-check-label").compact.join(" ")
|
161
|
+
content_tag(:div, class: wrapper_class) do
|
162
|
+
if options[:skip_label]
|
163
|
+
checkbox_html
|
164
|
+
else
|
165
|
+
checkbox_html
|
166
|
+
.concat(label(label_name,
|
167
|
+
label_description,
|
168
|
+
{ class: label_class }.merge(options[:id].present? ? { for: options[:id] } : {})))
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
bootstrap_method_alias :check_box
|
175
|
+
|
176
|
+
def radio_button_with_bootstrap(name, value, *args)
|
177
|
+
prevent_prepend_and_append!(options)
|
178
|
+
options = args.extract_options!.symbolize_keys!
|
179
|
+
radio_options = options.except(:label, :label_class, :help, :inline, :custom, :hide_label, :skip_label)
|
180
|
+
radio_classes = [options[:class]]
|
181
|
+
radio_classes << "position-static" if options[:skip_label] || options[:hide_label]
|
182
|
+
if options[:custom]
|
183
|
+
radio_options[:class] = radio_classes.prepend("custom-control-input").compact.join(' ')
|
184
|
+
else
|
185
|
+
radio_options[:class] = radio_classes.prepend("form-check-input").compact.join(' ')
|
186
|
+
end
|
187
|
+
args << radio_options
|
188
|
+
radio_html = radio_button_without_bootstrap(name, value, *args)
|
189
|
+
|
190
|
+
disabled_class = " disabled" if options[:disabled]
|
191
|
+
label_classes = [options[:label_class]]
|
192
|
+
label_classes << hide_class if options[:hide_label]
|
193
|
+
|
194
|
+
if options[:custom]
|
195
|
+
div_class = ["custom-control", "custom-radio"]
|
196
|
+
div_class.append("custom-control-inline") if layout_inline?(options[:inline])
|
197
|
+
label_class = label_classes.prepend("custom-control-label").compact.join(" ")
|
198
|
+
content_tag(:div, class: div_class.compact.join(" ")) do
|
199
|
+
if options[:skip_label]
|
200
|
+
radio_html
|
201
|
+
else
|
202
|
+
# TODO: Notice we don't seem to pass the ID into the custom control.
|
203
|
+
radio_html.concat(label(name, options[:label], value: value, class: label_class))
|
204
|
+
end
|
205
|
+
end
|
206
|
+
else
|
207
|
+
wrapper_class = "form-check"
|
208
|
+
wrapper_class += " form-check-inline" if layout_inline?(options[:inline])
|
209
|
+
label_class = label_classes.prepend("form-check-label").compact.join(" ")
|
210
|
+
content_tag(:div, class: "#{wrapper_class}#{disabled_class}") do
|
211
|
+
if options[:skip_label]
|
212
|
+
radio_html
|
213
|
+
else
|
214
|
+
radio_html
|
215
|
+
.concat(label(name, options[:label], { value: value, class: label_class }.merge(options[:id].present? ? { for: options[:id] } : {})))
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
bootstrap_method_alias :radio_button
|
222
|
+
|
223
|
+
def collection_check_boxes_with_bootstrap(*args)
|
224
|
+
prevent_prepend_and_append!(options)
|
225
|
+
html = inputs_collection(*args) do |name, value, options|
|
226
|
+
options[:multiple] = true
|
227
|
+
check_box(name, options, value, nil)
|
228
|
+
end
|
229
|
+
hidden_field(args.first,{value: "", multiple: true}).concat(html)
|
230
|
+
end
|
231
|
+
|
232
|
+
bootstrap_method_alias :collection_check_boxes
|
233
|
+
|
234
|
+
def collection_radio_buttons_with_bootstrap(*args)
|
235
|
+
prevent_prepend_and_append!(options)
|
236
|
+
inputs_collection(*args) do |name, value, options|
|
237
|
+
radio_button(name, value, options)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
bootstrap_method_alias :collection_radio_buttons
|
242
|
+
|
243
|
+
def form_group(*args, &block)
|
244
|
+
options = args.extract_options!
|
245
|
+
name = args.first
|
246
|
+
|
247
|
+
options[:class] = ["form-group", options[:class]].compact.join(' ')
|
248
|
+
options[:class] << " row" if get_group_layout(options[:layout]) == :horizontal
|
249
|
+
options[:class] << " form-inline" if field_inline_override?(options[:layout])
|
250
|
+
options[:class] << " #{feedback_class}" if options[:icon]
|
251
|
+
|
252
|
+
content_tag(:div, options.except(:append, :id, :label, :help, :icon, :input_group_class, :label_col, :control_col, :layout, :prepend)) do
|
253
|
+
label = generate_label(options[:id], name, options[:label], options[:label_col], options[:layout]) if options[:label]
|
254
|
+
control = prepend_and_append_input(name, options, &block).to_s
|
255
|
+
|
256
|
+
help = options[:help]
|
257
|
+
help_text = generate_help(name, help).to_s
|
258
|
+
|
259
|
+
if get_group_layout(options[:layout]) == :horizontal
|
260
|
+
control_class = options[:control_col] || control_col
|
261
|
+
unless options[:label]
|
262
|
+
control_offset = offset_col(options[:label_col] || @label_col)
|
263
|
+
control_class = "#{control_class} #{control_offset}"
|
264
|
+
end
|
265
|
+
control = content_tag(:div, control + help_text, class: control_class)
|
266
|
+
concat(label).concat(control)
|
267
|
+
else
|
268
|
+
concat(label).concat(control).concat(help_text)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def fields_for_with_bootstrap(record_name, record_object = nil, fields_options = {}, &block)
|
274
|
+
if record_object.is_a?(Hash) && record_object.extractable_options?
|
275
|
+
fields_options = record_object
|
276
|
+
record_object = nil
|
277
|
+
end
|
278
|
+
fields_options[:layout] ||= options[:layout]
|
279
|
+
fields_options[:label_col] = fields_options[:label_col].present? ? "#{fields_options[:label_col]}" : options[:label_col]
|
280
|
+
fields_options[:control_col] ||= options[:control_col]
|
281
|
+
fields_options[:inline_errors] ||= options[:inline_errors]
|
282
|
+
fields_options[:label_errors] ||= options[:label_errors]
|
283
|
+
fields_for_without_bootstrap(record_name, record_object, fields_options, &block)
|
284
|
+
end
|
285
|
+
|
286
|
+
bootstrap_method_alias :fields_for
|
287
|
+
|
288
|
+
# the Rails `fields` method passes its options
|
289
|
+
# to the builder, so there is no need to write a `bootstrap_form` helper
|
290
|
+
# for the `fields` method.
|
291
|
+
|
292
|
+
private
|
293
|
+
|
294
|
+
def layout_default?(field_layout = nil)
|
295
|
+
[:default, nil].include? layout_in_effect(field_layout)
|
296
|
+
end
|
297
|
+
|
298
|
+
def layout_horizontal?(field_layout = nil)
|
299
|
+
layout_in_effect(field_layout) == :horizontal
|
300
|
+
end
|
301
|
+
|
302
|
+
def layout_inline?(field_layout = nil)
|
303
|
+
layout_in_effect(field_layout) == :inline
|
304
|
+
end
|
305
|
+
|
306
|
+
def field_inline_override?(field_layout = nil)
|
307
|
+
field_layout == :inline && layout != :inline
|
308
|
+
end
|
309
|
+
|
310
|
+
# true and false should only come from check_box and radio_button,
|
311
|
+
# and those don't have a :horizontal layout
|
312
|
+
def layout_in_effect(field_layout)
|
313
|
+
field_layout = :inline if field_layout == true
|
314
|
+
field_layout = :default if field_layout == false
|
315
|
+
field_layout || layout
|
316
|
+
end
|
317
|
+
|
318
|
+
def get_group_layout(group_layout)
|
319
|
+
group_layout || layout
|
320
|
+
end
|
321
|
+
|
322
|
+
def default_label_col
|
323
|
+
"col-sm-2"
|
324
|
+
end
|
325
|
+
|
326
|
+
def offset_col(label_col)
|
327
|
+
label_col.sub(/^col-(\w+)-(\d)$/, 'offset-\1-\2')
|
328
|
+
end
|
329
|
+
|
330
|
+
def default_control_col
|
331
|
+
"col-sm-10"
|
332
|
+
end
|
333
|
+
|
334
|
+
def hide_class
|
335
|
+
"sr-only" # still accessible for screen readers
|
336
|
+
end
|
337
|
+
|
338
|
+
def control_class
|
339
|
+
"form-control"
|
340
|
+
end
|
341
|
+
|
342
|
+
def feedback_class
|
343
|
+
"has-feedback"
|
344
|
+
end
|
345
|
+
|
346
|
+
def control_specific_class(method)
|
347
|
+
"rails-bootstrap-forms-#{method.gsub(/_/, "-")}"
|
348
|
+
end
|
349
|
+
|
350
|
+
def has_error?(name)
|
351
|
+
object.respond_to?(:errors) && !(name.nil? || object.errors[name].empty?)
|
352
|
+
end
|
353
|
+
|
354
|
+
def required_attribute?(obj, attribute)
|
355
|
+
|
356
|
+
return false unless obj and attribute
|
357
|
+
|
358
|
+
target = (obj.class == Class) ? obj : obj.class
|
359
|
+
|
360
|
+
target_validators = if target.respond_to? :validators_on
|
361
|
+
target.validators_on(attribute).map(&:class)
|
362
|
+
else
|
363
|
+
[]
|
364
|
+
end
|
365
|
+
|
366
|
+
has_presence_validator = target_validators.include?(
|
367
|
+
ActiveModel::Validations::PresenceValidator)
|
368
|
+
|
369
|
+
if defined? ActiveRecord::Validations::PresenceValidator
|
370
|
+
has_presence_validator |= target_validators.include?(
|
371
|
+
ActiveRecord::Validations::PresenceValidator)
|
372
|
+
end
|
373
|
+
|
374
|
+
has_presence_validator
|
375
|
+
end
|
376
|
+
|
377
|
+
def form_group_builder(method, options, html_options = nil)
|
378
|
+
options.symbolize_keys!
|
379
|
+
html_options.symbolize_keys! if html_options
|
380
|
+
|
381
|
+
# Add control_class; allow it to be overridden by :control_class option
|
382
|
+
css_options = html_options || options
|
383
|
+
control_classes = css_options.delete(:control_class) { control_class }
|
384
|
+
css_options[:class] = [control_classes, css_options[:class]].compact.join(" ")
|
385
|
+
css_options[:class] << " is-invalid" if has_error?(method)
|
386
|
+
|
387
|
+
wrapper_class = css_options.delete(:wrapper_class)
|
388
|
+
wrapper_options = css_options.delete(:wrapper)
|
389
|
+
help = options.delete(:help)
|
390
|
+
icon = options.delete(:icon)
|
391
|
+
label_col = options.delete(:label_col)
|
392
|
+
control_col = options.delete(:control_col)
|
393
|
+
layout = get_group_layout(options.delete(:layout))
|
394
|
+
form_group_options = {
|
395
|
+
id: options[:id],
|
396
|
+
help: help,
|
397
|
+
icon: icon,
|
398
|
+
label_col: label_col,
|
399
|
+
control_col: control_col,
|
400
|
+
layout: layout,
|
401
|
+
class: wrapper_class
|
402
|
+
}
|
403
|
+
|
404
|
+
form_group_options[:append] = options.delete(:append) if options[:append]
|
405
|
+
form_group_options[:prepend] = options.delete(:prepend) if options[:prepend]
|
406
|
+
form_group_options[:input_group_class] = options.delete(:input_group_class) if options[:input_group_class]
|
407
|
+
|
408
|
+
if wrapper_options.is_a?(Hash)
|
409
|
+
form_group_options.merge!(wrapper_options)
|
410
|
+
end
|
411
|
+
|
412
|
+
unless options.delete(:skip_label)
|
413
|
+
if options[:label].is_a?(Hash)
|
414
|
+
label_text = options[:label].delete(:text)
|
415
|
+
label_class = options[:label].delete(:class)
|
416
|
+
options.delete(:label)
|
417
|
+
end
|
418
|
+
label_class ||= options.delete(:label_class)
|
419
|
+
label_class = hide_class if options.delete(:hide_label)
|
420
|
+
|
421
|
+
if options[:label].is_a?(String)
|
422
|
+
label_text ||= options.delete(:label)
|
423
|
+
end
|
424
|
+
|
425
|
+
form_group_options[:label] = {
|
426
|
+
text: label_text,
|
427
|
+
class: label_class,
|
428
|
+
required: options[:required],
|
429
|
+
skip_required: options.delete(:skip_required)
|
430
|
+
}.merge(css_options[:id].present? ? { for: css_options[:id] } : {})
|
431
|
+
end
|
432
|
+
|
433
|
+
form_group(method, form_group_options) do
|
434
|
+
yield
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
def convert_form_tag_options(method, options = {})
|
439
|
+
unless @options[:skip_default_ids]
|
440
|
+
options[:name] ||= method
|
441
|
+
options[:id] ||= method
|
442
|
+
end
|
443
|
+
options
|
444
|
+
end
|
445
|
+
|
446
|
+
def generate_label(id, name, options, custom_label_col, group_layout)
|
447
|
+
classes = [options[:class]]
|
448
|
+
|
449
|
+
if layout_horizontal?(group_layout)
|
450
|
+
classes << "col-form-label"
|
451
|
+
classes << (custom_label_col || label_col)
|
452
|
+
elsif layout_inline?(group_layout)
|
453
|
+
classes << "mr-sm-2"
|
454
|
+
end
|
455
|
+
|
456
|
+
unless options.delete(:skip_required)
|
457
|
+
classes << "required" if options.delete(:required) || required_attribute?(object, name)
|
458
|
+
end
|
459
|
+
|
460
|
+
options[:class] = classes.compact.join(" ").strip
|
461
|
+
options.delete(:class) if options[:class].empty?
|
462
|
+
|
463
|
+
if label_errors && has_error?(name)
|
464
|
+
error_messages = get_error_messages(name)
|
465
|
+
label_text = (options[:text] || object.class.human_attribute_name(name)).to_s.concat(" #{error_messages}")
|
466
|
+
options[:class] = [options[:class], "text-danger"].compact.join(" ")
|
467
|
+
label(name, label_text, options.except(:text))
|
468
|
+
else
|
469
|
+
label(name, options[:text], options.except(:text))
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
def has_inline_error?(name)
|
474
|
+
has_error?(name) && inline_errors
|
475
|
+
end
|
476
|
+
|
477
|
+
def generate_error(name)
|
478
|
+
if has_inline_error?(name)
|
479
|
+
help_text = get_error_messages(name)
|
480
|
+
help_klass = 'invalid-feedback'
|
481
|
+
help_tag = :div
|
482
|
+
|
483
|
+
content_tag(help_tag, help_text, class: help_klass)
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
def generate_help(name, help_text)
|
488
|
+
return if help_text == false || has_inline_error?(name)
|
489
|
+
|
490
|
+
help_klass ||= 'form-text text-muted'
|
491
|
+
help_text ||= get_help_text_by_i18n_key(name)
|
492
|
+
help_tag ||= :small
|
493
|
+
|
494
|
+
content_tag(help_tag, help_text, class: help_klass) if help_text.present?
|
495
|
+
end
|
496
|
+
|
497
|
+
def get_error_messages(name)
|
498
|
+
object.errors[name].join(", ")
|
499
|
+
end
|
500
|
+
|
501
|
+
def inputs_collection(name, collection, value, text, options = {}, &block)
|
502
|
+
options[:inline] ||= layout_inline?(options[:layout])
|
503
|
+
form_group_builder(name, options) do
|
504
|
+
inputs = ""
|
505
|
+
|
506
|
+
collection.each do |obj|
|
507
|
+
input_options = options.merge(label: text.respond_to?(:call) ? text.call(obj) : obj.send(text))
|
508
|
+
|
509
|
+
input_value = value.respond_to?(:call) ? value.call(obj) : obj.send(value)
|
510
|
+
if checked = input_options[:checked]
|
511
|
+
input_options[:checked] = checked == input_value ||
|
512
|
+
Array(checked).try(:include?, input_value) ||
|
513
|
+
checked == obj ||
|
514
|
+
Array(checked).try(:include?, obj)
|
515
|
+
end
|
516
|
+
|
517
|
+
if disabled = input_options[:disabled]
|
518
|
+
input_options[:disabled] = if disabled.is_a?(Array)
|
519
|
+
disabled.include?(input_value)
|
520
|
+
else
|
521
|
+
disabled
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
input_options.delete(:class)
|
526
|
+
inputs << block.call(name, input_value, input_options)
|
527
|
+
end
|
528
|
+
|
529
|
+
inputs.html_safe
|
530
|
+
end
|
531
|
+
end
|
532
|
+
|
533
|
+
def get_help_text_by_i18n_key(name)
|
534
|
+
if object
|
535
|
+
|
536
|
+
if object.class.respond_to?(:model_name)
|
537
|
+
partial_scope = object.class.model_name.name
|
538
|
+
else
|
539
|
+
partial_scope = object.class.name
|
540
|
+
end
|
541
|
+
|
542
|
+
underscored_scope = "activerecord.help.#{partial_scope.underscore}"
|
543
|
+
downcased_scope = "activerecord.help.#{partial_scope.downcase}"
|
544
|
+
# First check for a subkey :html, as it is also accepted by i18n, and the simple check for name would return an hash instead of a string (both with .presence returning true!)
|
545
|
+
help_text = I18n.t("#{name}.html", scope: underscored_scope, default: '').html_safe.presence
|
546
|
+
help_text ||= if text = I18n.t("#{name}.html", scope: downcased_scope, default: '').html_safe.presence
|
547
|
+
warn "I18n key '#{downcased_scope}.#{name}' is deprecated, use '#{underscored_scope}.#{name}' instead"
|
548
|
+
text
|
549
|
+
end
|
550
|
+
help_text ||= I18n.t(name, scope: underscored_scope, default: '').presence
|
551
|
+
help_text ||= if text = I18n.t(name, scope: downcased_scope, default: '').presence
|
552
|
+
warn "I18n key '#{downcased_scope}.#{name}' is deprecated, use '#{underscored_scope}.#{name}' instead"
|
553
|
+
text
|
554
|
+
end
|
555
|
+
help_text ||= I18n.t("#{name}_html", scope: underscored_scope, default: '').html_safe.presence
|
556
|
+
help_text ||= if text = I18n.t("#{name}_html", scope: downcased_scope, default: '').html_safe.presence
|
557
|
+
warn "I18n key '#{downcased_scope}.#{name}' is deprecated, use '#{underscored_scope}.#{name}' instead"
|
558
|
+
text
|
559
|
+
end
|
560
|
+
help_text
|
561
|
+
end
|
562
|
+
end
|
563
|
+
end
|
564
|
+
end
|