aaf-lipstick 1.1.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Rakefile +7 -15
- data/app/assets/images/aaf-icon.png +0 -0
- data/app/assets/images/logo.png +0 -0
- data/app/assets/javascripts/aaf-lipstick.js +90 -0
- data/app/assets/stylesheets/aaf-lipstick.css.scss +304 -0
- data/app/views/layouts/email_branding.html.erb +35 -36
- data/lib/aaf-lipstick.rb +1 -0
- data/lib/lipstick.rb +4 -2
- data/lib/lipstick/auto_validation.rb +26 -22
- data/lib/lipstick/email_message.rb +1 -0
- data/lib/lipstick/engine.rb +3 -0
- data/lib/lipstick/filterable.rb +57 -0
- data/lib/lipstick/helpers.rb +6 -2
- data/lib/lipstick/helpers/bootstrap_form_builder.rb +61 -0
- data/lib/lipstick/helpers/compatibility_hacks.rb +39 -0
- data/lib/lipstick/helpers/form_helper.rb +117 -136
- data/lib/lipstick/helpers/form_validation_builder.rb +70 -0
- data/lib/lipstick/helpers/layout_helper.rb +98 -57
- data/lib/lipstick/helpers/nav_helper.rb +24 -29
- data/lib/lipstick/helpers/pagination_link_renderer.rb +131 -0
- data/lib/lipstick/images.rb +1 -0
- data/lib/lipstick/images/email_banner.rb +10 -4
- data/lib/lipstick/images/processor.rb +10 -4
- data/lib/lipstick/sprockets_asset_data_url_helper.rb +14 -0
- data/lib/lipstick/version.rb +2 -1
- metadata +75 -14
- data/app/assets/javascripts/aaf-layout.js +0 -35
- data/app/assets/stylesheets/aaf-layout.css.scss +0 -204
- data/lib/lipstick/action_view_tilt_template.rb +0 -17
- data/lib/lipstick/helpers/semantic_form_builder.rb +0 -2
data/lib/aaf-lipstick.rb
CHANGED
data/lib/lipstick.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Lipstick
|
2
3
|
def self.asset_path
|
3
4
|
File.expand_path(File.join('..', 'app', 'assets'), File.dirname(__FILE__))
|
@@ -7,6 +8,7 @@ end
|
|
7
8
|
require 'lipstick/helpers'
|
8
9
|
require 'lipstick/email_message'
|
9
10
|
require 'lipstick/images'
|
10
|
-
require 'lipstick/
|
11
|
+
require 'lipstick/filterable' if defined?(Arel)
|
12
|
+
require 'lipstick/auto_validation' if defined?(ActiveModel)
|
11
13
|
require 'lipstick/engine' if defined?(Rails::Engine)
|
12
|
-
require 'lipstick/
|
14
|
+
require 'lipstick/version'
|
@@ -1,12 +1,11 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
3
2
|
module Lipstick
|
4
3
|
module AutoValidation
|
5
4
|
module ClassMethods
|
6
5
|
def self.extended(base)
|
7
6
|
return if base.respond_to?(:validators)
|
8
7
|
|
9
|
-
|
8
|
+
raise('Lipstick::AutoValidation requires a class which responds' \
|
10
9
|
' to the `validators` method. For example, as provided by' \
|
11
10
|
' ActiveModel::Validations')
|
12
11
|
end
|
@@ -14,7 +13,7 @@ module Lipstick
|
|
14
13
|
def lipstick_auto_validators
|
15
14
|
validators.each_with_object({}) do |validator, map|
|
16
15
|
validator.attributes.each do |attr|
|
17
|
-
out =
|
16
|
+
out = lipstick_validator(attr, validator)
|
18
17
|
next if out.nil?
|
19
18
|
|
20
19
|
map[attr.to_sym] ||= {}
|
@@ -23,46 +22,51 @@ module Lipstick
|
|
23
22
|
end
|
24
23
|
end
|
25
24
|
|
25
|
+
def lipstick_field_name(attr)
|
26
|
+
map = @lipstick_field_names || {}
|
27
|
+
return map[attr] if map.key?(attr)
|
28
|
+
attr.to_s.humanize(capitalize: false)
|
29
|
+
end
|
30
|
+
|
26
31
|
private
|
27
32
|
|
28
33
|
v = ActiveModel::Validations
|
29
34
|
VALIDATOR_TRANSLATORS = {
|
30
|
-
v::PresenceValidator => :
|
31
|
-
v::LengthValidator => :
|
32
|
-
v::NumericalityValidator => :
|
33
|
-
}
|
35
|
+
v::PresenceValidator => :lipstick_presence_validator,
|
36
|
+
v::LengthValidator => :lipstick_length_validator,
|
37
|
+
v::NumericalityValidator => :lipstick_numericality_validator
|
38
|
+
}.freeze
|
34
39
|
private_constant :VALIDATOR_TRANSLATORS
|
35
40
|
|
36
|
-
def
|
41
|
+
def lipstick_validator(attr, validator)
|
37
42
|
VALIDATOR_TRANSLATORS.each do |klass, sym|
|
38
43
|
next unless validator.is_a?(klass)
|
39
|
-
return send(sym, attr, validator,
|
40
|
-
attr.to_s.humanize(capitalize: false))
|
44
|
+
return send(sym, attr, validator, lipstick_field_name(attr.to_sym))
|
41
45
|
end
|
42
46
|
nil
|
43
47
|
end
|
44
48
|
|
45
|
-
def
|
46
|
-
{
|
49
|
+
def lipstick_presence_validator(_attr, _validator, humanized)
|
50
|
+
{ required: { message: "Please enter a value for #{humanized}" } }
|
47
51
|
end
|
48
52
|
|
49
|
-
def
|
53
|
+
def lipstick_length_validator(_attr, validator, humanized)
|
50
54
|
min = validator.options[:minimum]
|
51
55
|
max = validator.options[:maximum]
|
52
56
|
|
53
|
-
min_message = "Please enter a longer value for #{humanized}" \
|
54
|
-
"
|
55
|
-
max_message = "Please enter a shorter value for #{humanized}" \
|
56
|
-
"
|
57
|
+
min_message = "Please enter a longer value for #{humanized} " \
|
58
|
+
"(minimum #{min} characters)"
|
59
|
+
max_message = "Please enter a shorter value for #{humanized} " \
|
60
|
+
"(maximum #{max} characters)"
|
57
61
|
|
58
62
|
{}.tap do |out|
|
59
|
-
out[
|
60
|
-
out[
|
63
|
+
out[:minlength] = { param: min, message: min_message } if min
|
64
|
+
out[:maxlength] = { param: max, message: max_message } if max
|
61
65
|
end
|
62
66
|
end
|
63
67
|
|
64
|
-
def
|
65
|
-
{
|
68
|
+
def lipstick_numericality_validator(_attr, _validator, humanized)
|
69
|
+
{ digits: { message: "Please enter a numeric value for #{humanized}" } }
|
66
70
|
end
|
67
71
|
end
|
68
72
|
|
data/lib/lipstick/engine.rb
CHANGED
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lipstick
|
4
|
+
module Filterable
|
5
|
+
# Provides case-insensitive matching on a case-sensitive MySQL database
|
6
|
+
# (which is mandated by Gumboot)
|
7
|
+
class CollatedArelAttribute < Arel::Nodes::Node
|
8
|
+
include Arel::Predications
|
9
|
+
|
10
|
+
attr_reader :attribute, :collation
|
11
|
+
|
12
|
+
def initialize(attribute, collation)
|
13
|
+
@attribute = attribute
|
14
|
+
@collation = collation
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module VisitCollatedArelAttribute
|
19
|
+
# rubocop:disable Style/MethodName
|
20
|
+
def visit_Lipstick_Filterable_CollatedArelAttribute(o, collector)
|
21
|
+
visit(o.attribute, collector)
|
22
|
+
collector << ' COLLATE ' << o.collation
|
23
|
+
end
|
24
|
+
# rubocop:enable Style/MethodName
|
25
|
+
end
|
26
|
+
|
27
|
+
Arel::Visitors::ToSql.include(VisitCollatedArelAttribute)
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
attr_reader :filterable_fields
|
31
|
+
|
32
|
+
def filterable_by(*fields)
|
33
|
+
@filterable_fields = fields
|
34
|
+
end
|
35
|
+
|
36
|
+
def filter(query)
|
37
|
+
filter_terms(query).reduce(all) do |scope, term|
|
38
|
+
conds = filterable_fields.map do |f|
|
39
|
+
CollatedArelAttribute.new(arel_table[f], 'utf8_unicode_ci')
|
40
|
+
.matches(term)
|
41
|
+
end
|
42
|
+
scope.where(conds.reduce { |a, e| a.or(e) })
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def filter_terms(query)
|
49
|
+
query.to_s.downcase.split(/\s+/).map { |s| "*#{s}*".gsub(/[%*]+/, '%') }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.included(base)
|
54
|
+
base.extend(ClassMethods)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/lipstick/helpers.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Lipstick
|
2
3
|
module Helpers
|
3
4
|
end
|
@@ -5,5 +6,8 @@ end
|
|
5
6
|
|
6
7
|
require 'lipstick/helpers/layout_helper'
|
7
8
|
require 'lipstick/helpers/nav_helper'
|
8
|
-
require 'lipstick/helpers/
|
9
|
-
require 'lipstick/helpers/
|
9
|
+
require 'lipstick/helpers/form_validation_builder'
|
10
|
+
require 'lipstick/helpers/bootstrap_form_builder' if defined?(ActionView)
|
11
|
+
require 'lipstick/helpers/form_helper' if defined?(ActionView)
|
12
|
+
require 'lipstick/helpers/pagination_link_renderer'
|
13
|
+
require 'lipstick/helpers/compatibility_hacks'
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# rubocop:disable Metrics/ParameterLists
|
3
|
+
|
4
|
+
class BootstrapFormBuilder < ActionView::Helpers::FormBuilder
|
5
|
+
attr_reader :template
|
6
|
+
|
7
|
+
def date_field(field, **opts)
|
8
|
+
add_css_class(opts, 'date-picker')
|
9
|
+
text_field(field, opts)
|
10
|
+
end
|
11
|
+
|
12
|
+
def text_field(field, **opts)
|
13
|
+
add_css_class(opts, 'form-control')
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def password_field(field, **opts)
|
18
|
+
add_css_class(opts, 'form-control')
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
def text_area(method, **opts)
|
23
|
+
add_css_class(opts, 'form-control')
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
def check_box(*)
|
28
|
+
template.content_tag('div', class: 'checkbox') do
|
29
|
+
template.content_tag('label') do
|
30
|
+
template.concat(super)
|
31
|
+
template.concat(template.capture { yield })
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def radio_button(*)
|
37
|
+
template.content_tag('div', class: 'radio') do
|
38
|
+
template.content_tag('label') do
|
39
|
+
template.concat(super)
|
40
|
+
template.concat(template.capture { yield })
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def select(method, choices = nil, opts = {}, html_opts = {})
|
46
|
+
add_css_class(html_opts, 'form-control')
|
47
|
+
super
|
48
|
+
end
|
49
|
+
|
50
|
+
def collection_select(method, collection, value_method, text_method,
|
51
|
+
opts = {}, html_opts = {})
|
52
|
+
add_css_class(html_opts, 'form-control')
|
53
|
+
super
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def add_css_class(opts, class_name)
|
59
|
+
opts[:class] = "#{opts[:class]} #{class_name}".strip
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Lipstick
|
3
|
+
module Helpers
|
4
|
+
module CompatibilityHacks
|
5
|
+
def concat(content)
|
6
|
+
@_out_buf << content
|
7
|
+
end
|
8
|
+
|
9
|
+
def content_tag(name, content_or_opts = nil, opts = nil, &block)
|
10
|
+
opts = content_or_opts if block_given?
|
11
|
+
content = block_given? ? capture(&block) : content_or_opts
|
12
|
+
"<#{name}#{html_attrs(opts)}>#{content}</#{name}>"
|
13
|
+
end
|
14
|
+
|
15
|
+
def tag(name, opts = nil)
|
16
|
+
"<#{name}#{html_attrs(opts)}/>"
|
17
|
+
end
|
18
|
+
|
19
|
+
def capture
|
20
|
+
old = @_out_buf
|
21
|
+
@_out_buf = io = StringIO.new
|
22
|
+
result = yield
|
23
|
+
io_length = io.length
|
24
|
+
io_length.positive? ? io.string : result
|
25
|
+
ensure
|
26
|
+
@_out_buf = old
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def html_attrs(opts)
|
32
|
+
return '' if opts.nil?
|
33
|
+
opts.reduce('') do |a, (k, v)|
|
34
|
+
%(#{a} #{k}="#{v}")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -1,28 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Lipstick
|
2
3
|
module Helpers
|
3
4
|
module FormHelper
|
4
5
|
include ActionView::Helpers::FormTagHelper
|
5
6
|
|
6
|
-
def field_block(html_opts = {}
|
7
|
-
|
8
|
-
|
7
|
+
def field_block(html_opts = {})
|
8
|
+
add_css_class(html_opts, 'form-group')
|
9
|
+
content_tag('div', html_opts) { yield }
|
10
|
+
end
|
11
|
+
|
12
|
+
def radio_button_tag(name, value, checked = false, options = {})
|
13
|
+
content_tag('div', class: 'radio') do
|
14
|
+
content_tag('label') do
|
15
|
+
concat(super)
|
16
|
+
concat(capture { yield })
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def check_box_tag(*)
|
22
|
+
content_tag('div', class: 'checkbox') do
|
23
|
+
content_tag('label') do
|
24
|
+
concat(super)
|
25
|
+
concat(capture { yield })
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
alias orig_form_tag form_tag
|
31
|
+
|
32
|
+
def inline_form_tag(url_for_options = {}, options = {})
|
33
|
+
add_css_class(options, 'form-inline')
|
34
|
+
form_tag(url_for_options, options) { yield }
|
35
|
+
end
|
36
|
+
|
37
|
+
def search_form_tag(filter, url: nil)
|
38
|
+
form_tag(url, method: :get) do
|
39
|
+
field_block { search_form_input_tag(filter) }
|
9
40
|
end
|
41
|
+
end
|
10
42
|
|
11
|
-
|
12
|
-
content_tag('div',
|
43
|
+
def search_form_input_tag(filter)
|
44
|
+
content_tag('div', class: 'row') do
|
45
|
+
content_tag('div', grouped_search_field(filter), class: 'col-lg-12')
|
46
|
+
end
|
13
47
|
end
|
14
48
|
|
15
|
-
|
49
|
+
def search_filter_text_field(filter)
|
50
|
+
orig_text_field_tag(:filter, filter,
|
51
|
+
placeholder: 'Search within these entries',
|
52
|
+
autocomplete: 'off',
|
53
|
+
class: 'form-control')
|
54
|
+
end
|
16
55
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
56
|
+
def search_button
|
57
|
+
button_tag(type: 'submit') do
|
58
|
+
concat(icon_tag('search'))
|
59
|
+
concat(' Search')
|
60
|
+
end
|
20
61
|
end
|
21
62
|
|
22
|
-
def
|
23
|
-
|
24
|
-
|
25
|
-
|
63
|
+
def grouped_search_field(filter)
|
64
|
+
content_tag('div', class: 'input-group') do
|
65
|
+
concat(search_filter_text_field(filter))
|
66
|
+
concat(content_tag('span', search_button, class: 'input-group-btn'))
|
26
67
|
end
|
27
68
|
end
|
28
69
|
|
@@ -30,167 +71,107 @@ module Lipstick
|
|
30
71
|
content_tag('div', style: 'display: none;', &block)
|
31
72
|
end
|
32
73
|
|
33
|
-
|
34
|
-
|
74
|
+
alias orig_text_field_tag text_field_tag
|
75
|
+
|
76
|
+
def text_field_tag(name, value = nil, opts = {})
|
77
|
+
add_css_class(opts, 'form-control')
|
78
|
+
super
|
35
79
|
end
|
36
80
|
|
37
|
-
def
|
38
|
-
|
81
|
+
def text_area_tag(name, content = nil, opts = {})
|
82
|
+
add_css_class(opts, 'form-control')
|
83
|
+
super
|
39
84
|
end
|
40
85
|
|
41
|
-
def
|
42
|
-
|
86
|
+
def date_field_tag(name, value = nil, **opts)
|
87
|
+
opts[:class] = "#{opts[:class]} date-picker".strip
|
88
|
+
text_field_tag(name, value, opts)
|
89
|
+
end
|
90
|
+
|
91
|
+
def select_tag(name, option_tags = nil, **opts)
|
92
|
+
add_css_class(opts, 'form-control')
|
93
|
+
super
|
94
|
+
end
|
43
95
|
|
96
|
+
def button_tag(content_or_options = nil, options = nil, &block)
|
44
97
|
if content_or_options.is_a?(Hash)
|
45
|
-
|
98
|
+
content_or_options[:class] ||= 'btn-default'
|
99
|
+
add_css_class(content_or_options, 'btn')
|
100
|
+
super
|
46
101
|
else
|
47
|
-
|
102
|
+
options ||= {}
|
103
|
+
options[:class] ||= 'btn-default'
|
104
|
+
add_css_class(options, 'btn')
|
105
|
+
super(content_or_options, options, &block)
|
48
106
|
end
|
49
107
|
end
|
50
108
|
|
51
109
|
def delete_button_tag(url, text: true, **opts)
|
52
|
-
|
53
|
-
content_tag('div', class: "#{css_class} #{opts[:class]}".strip) do
|
54
|
-
concat(icon_tag('trash'))
|
55
|
-
action = text && text.is_a?(String) ? text : 'Delete'
|
56
|
-
concat(action) if text
|
57
|
-
confirm = button_link_to(url, method: :delete, class: 'small') do
|
58
|
-
"Confirm #{action}"
|
59
|
-
end
|
60
|
-
concat(content_tag('div', confirm, class: 'menu'))
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
def error_messages_tag
|
65
|
-
content_tag('div', '', class: 'ui error message')
|
66
|
-
end
|
110
|
+
action = text && text.is_a?(String) ? text : 'Delete'
|
67
111
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
concat(super)
|
72
|
-
concat(capture(&block))
|
73
|
-
end
|
112
|
+
content_tag('div', class: 'btn-group') do
|
113
|
+
concat(delete_dropdown_opener(text && action, opts))
|
114
|
+
concat(confirm_delete_dropdown(url, action))
|
74
115
|
end
|
75
116
|
end
|
76
117
|
|
77
118
|
def form_for(obj, opts = {}, &block)
|
78
|
-
opts[:builder] =
|
79
|
-
opts[:html] ||= {}
|
80
|
-
opts[:html][:class] = "#{opts[:html][:class]} ui form".strip
|
119
|
+
opts[:builder] = BootstrapFormBuilder
|
81
120
|
super(obj, opts, &block)
|
82
121
|
end
|
83
122
|
|
84
|
-
def radio_button_block(&block)
|
85
|
-
content_tag('div', class: 'grouped fields', &block)
|
86
|
-
end
|
87
|
-
|
88
123
|
# Generates the wrapping code for validating a form. The selector is
|
89
124
|
# passed to jQuery, and must uniquely select the form being validated.
|
90
125
|
# `sym` is the object name when using a `form_for` helper to generate the
|
91
126
|
# form.
|
92
127
|
#
|
93
128
|
# e.g.
|
94
|
-
# <%=
|
95
|
-
#
|
96
|
-
#
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
# <%= validate_field(:email, email: 'Enter a valid email address') %>
|
109
|
-
# <%= validate_field(:password, :'length[6]' => '6 characters minimum') %>
|
110
|
-
# <%= validate_field(:multiple, empty: 'Not empty', url: 'Must be URL') %>
|
111
|
-
def validate_field(name, opts)
|
112
|
-
format('validations[%{name}] = ' \
|
113
|
-
'(function(v) { %{inner} })' \
|
114
|
-
'($.extend({rules: []}, validations[%{name}]));',
|
115
|
-
name: name.to_json,
|
116
|
-
inner: validation_for_field(name, opts)).html_safe
|
117
|
-
end
|
118
|
-
|
119
|
-
# Automatically generates validators for fields based on certain supported
|
120
|
-
# validators from ActiveModel::Validations. The model must include the
|
121
|
-
# Lipstick::AutoValidation module.
|
122
|
-
#
|
123
|
-
# class MyModel < ActiveRecord::Base
|
124
|
-
# include Lipstick::AutoValidation
|
125
|
-
#
|
126
|
-
# validates :name, presence: true
|
127
|
-
# validates :description, length: 1..255
|
128
|
-
# end
|
129
|
-
#
|
130
|
-
# <%= auto_validate(@object, :name, :description) %>
|
131
|
-
def auto_validate(obj, *fields)
|
132
|
-
unless obj.class.respond_to?(:lipstick_auto_validators)
|
133
|
-
fail("#{obj.class.name} does not include Lipstick::AutoValidation")
|
134
|
-
end
|
135
|
-
|
136
|
-
validators = obj.class.lipstick_auto_validators
|
137
|
-
capture do
|
138
|
-
validators.slice(*fields).each do |name, opts|
|
139
|
-
concat validate_field(name, opts)
|
140
|
-
end
|
129
|
+
# <%=
|
130
|
+
# validate_form('#new-test-object', :test_object) do |v|
|
131
|
+
# v.validate_field(:name, ...) # Validate the test_object[name] field
|
132
|
+
# end
|
133
|
+
# %>
|
134
|
+
def validate_form(selector, sym = nil)
|
135
|
+
opts = {
|
136
|
+
type: 'application/vnd.aaf.lipstick.validations+json',
|
137
|
+
'data-target': selector,
|
138
|
+
class: 'lipstick-validations'
|
139
|
+
}
|
140
|
+
|
141
|
+
content_tag('script', opts) do
|
142
|
+
validation_json(sym) { |v| yield v }.html_safe
|
141
143
|
end
|
142
144
|
end
|
143
145
|
|
144
146
|
private
|
145
147
|
|
146
|
-
def
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
validation_install(selector)
|
148
|
+
def validation_json(sym)
|
149
|
+
v = FormValidationBuilder.new(sym)
|
150
|
+
yield v
|
151
|
+
JSON.generate(v.to_h)
|
151
152
|
end
|
152
153
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
def validation_name_mapping_function(sym)
|
157
|
-
if sym.nil?
|
158
|
-
'var map_name = function(n) { return n; };'
|
159
|
-
else
|
160
|
-
'var map_name = function(n) { ' \
|
161
|
-
"return #{sym.to_json} + '[' + n + ']';" \
|
162
|
-
'};'
|
163
|
-
end
|
164
|
-
end
|
154
|
+
def delete_dropdown_opener(label, **opts)
|
155
|
+
opts = { 'aria-expanded': 'false', 'data-toggle': 'dropdown',
|
156
|
+
type: 'button', 'aria-haspopup': 'true' }.merge(opts)
|
165
157
|
|
166
|
-
|
167
|
-
format('$(%s).form(validations, { keyboardShortcuts: false });',
|
168
|
-
selector.to_json)
|
169
|
-
end
|
170
|
-
|
171
|
-
def jquery_callback(body)
|
172
|
-
format('jQuery(function($){%s});', body)
|
173
|
-
end
|
158
|
+
add_css_class(opts, 'btn-small btn-danger dropdown-toggle')
|
174
159
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
160
|
+
button_tag(opts) do
|
161
|
+
concat(icon_tag('trash'))
|
162
|
+
concat(' ')
|
163
|
+
concat(label)
|
164
|
+
end
|
180
165
|
end
|
181
166
|
|
182
|
-
def
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
yield
|
187
|
-
ensure
|
188
|
-
Thread.current[flag] = old
|
189
|
-
end
|
167
|
+
def confirm_delete_dropdown(url, action)
|
168
|
+
link = link_to("Confirm #{action}", url, class: 'confirm-delete')
|
169
|
+
item = content_tag('li', link)
|
170
|
+
content_tag('ul', item, class: 'dropdown-menu')
|
190
171
|
end
|
191
172
|
|
192
|
-
def
|
193
|
-
|
173
|
+
def add_css_class(opts, class_name)
|
174
|
+
opts[:class] = "#{opts[:class]} #{class_name}".strip
|
194
175
|
end
|
195
176
|
end
|
196
177
|
end
|