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.
data/lib/aaf-lipstick.rb CHANGED
@@ -1 +1,2 @@
1
+ # frozen_string_literal: true
1
2
  require 'lipstick'
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/auto_validation'
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/action_view_tilt_template' if defined?(Tilt)
14
+ require 'lipstick/version'
@@ -1,12 +1,11 @@
1
- require 'active_model'
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
- fail('Lipstick::AutoValidation requires a class which responds' \
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 = semantic_ui_validator(attr, validator)
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 => :semantic_presence_validator,
31
- v::LengthValidator => :semantic_length_validator,
32
- v::NumericalityValidator => :semantic_numericality_validator
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 semantic_ui_validator(attr, validator)
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 semantic_presence_validator(_attr, _validator, humanized)
46
- { empty: "Please enter a value for #{humanized}" }
49
+ def lipstick_presence_validator(_attr, _validator, humanized)
50
+ { required: { message: "Please enter a value for #{humanized}" } }
47
51
  end
48
52
 
49
- def semantic_length_validator(_attr, validator, humanized)
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
- " (minimum #{min} characters)"
55
- max_message = "Please enter a shorter value for #{humanized}" \
56
- " (maximum #{max} characters)"
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["length[#{min}]"] = min_message if min
60
- out["maxLength[#{max}]"] = max_message if max
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 semantic_numericality_validator(_attr, _validator, humanized)
65
- { integer: "Please enter a numeric value for #{humanized}" }
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
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'kramdown'
2
3
  require 'erubis'
3
4
 
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
1
2
  module Lipstick
2
3
  class Engine < ::Rails::Engine
4
+ require 'active_support'
5
+ require 'active_support/core_ext/string/filters'
3
6
  isolate_namespace Lipstick
4
7
 
5
8
  initializer :assets do |app|
@@ -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
@@ -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/semantic_form_builder'
9
- require 'lipstick/helpers/form_helper'
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 = {}, &block)
7
- if flag_enabled?(:inside_inline_form_tag)
8
- html_opts[:class] = "#{html_opts[:class]} inline".strip
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
- html_opts[:class] = "#{html_opts[:class]} field".strip
12
- content_tag('div', html_opts, &block)
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
- alias_method :orig_form_tag, :form_tag
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 form_tag(form_opts, html_opts = {}, &block)
18
- html_opts[:class] = "#{html_opts[:class]} ui form".strip
19
- orig_form_tag(form_opts, html_opts, &block)
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 inline_form_tag(form_opts, html_opts = {}, &block)
23
- with_flag_enabled(:inside_inline_form_tag) do
24
- html_opts[:class] = "#{html_opts[:class]} ui form inline-form".strip
25
- orig_form_tag(form_opts, html_opts, &block)
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
- def text_field_tag(*args)
34
- content_tag('div', class: 'ui input') { super }
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 field_help_text(text)
38
- icon_tag('field-help-text blue help', 'data-content' => text)
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 button_tag(content_or_options = nil, options = nil, &block)
42
- add_class = ->(m) { m.dup.merge(class: "#{m[:class]} ui button".strip) }
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
- super(add_class.call(content_or_options), &block)
98
+ content_or_options[:class] ||= 'btn-default'
99
+ add_css_class(content_or_options, 'btn')
100
+ super
46
101
  else
47
- super(content_or_options, add_class.call(options || {}), &block)
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
- css_class = 'ui tiny red icon delete button floating dropdown'
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
- def radio_button_tag(name, value, checked = false, options = {}, &block)
69
- field_block do
70
- content_tag('div', class: 'ui radio checkbox') do
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] = SemanticFormBuilder
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
- # <%= validate_form('#new-test-object', :test_object) do -%>
95
- # <%= validate_field(:name, ...) %> Validate the test_object[name] field
96
- # <%- end -%>
97
- def validate_form(selector, sym = nil, &block)
98
- content_tag('script', type: 'text/javascript') do
99
- jquery_callback(validation_body(selector, sym, &block)).html_safe
100
- end
101
- end
102
-
103
- # Generates a validator for a field. `opts` is a Hash containing the
104
- # `type` and `prompt` for each desired validation per Semantic UI:
105
- # http://semantic-ui.com/modules/form.html
106
- #
107
- # e.g.
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 validation_body(selector, sym, &block)
147
- 'var validations = {};' +
148
- validation_name_mapping_function(sym) +
149
- capture(&block) +
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
- # When we're using form_for, we need to map ObjectType#name to a field
154
- # named like: 'object_type[name]'
155
- # Otherwise, we just use the name directly.
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
- def validation_install(selector)
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
- def validation_for_field(name, opts)
176
- rules = opts.map { |t, m| { type: t, prompt: m } }
177
- "v.rules = v.rules.concat(#{rules.to_json});" \
178
- "v.identifier = map_name(#{name.to_json});" \
179
- 'return v;'
160
+ button_tag(opts) do
161
+ concat(icon_tag('trash'))
162
+ concat(' ')
163
+ concat(label)
164
+ end
180
165
  end
181
166
 
182
- def with_flag_enabled(flag)
183
- old = Thread.current[flag]
184
- begin
185
- Thread.current[flag] = true
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 flag_enabled?(flag)
193
- Thread.current[flag]
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