aaf-lipstick 1.1.0 → 2.0.0

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.
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