aaf-lipstick 1.1.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.
@@ -0,0 +1 @@
1
+ require 'lipstick'
data/lib/lipstick.rb ADDED
@@ -0,0 +1,12 @@
1
+ module Lipstick
2
+ def self.asset_path
3
+ File.expand_path(File.join('..', 'app', 'assets'), File.dirname(__FILE__))
4
+ end
5
+ end
6
+
7
+ require 'lipstick/helpers'
8
+ require 'lipstick/email_message'
9
+ require 'lipstick/images'
10
+ require 'lipstick/auto_validation'
11
+ require 'lipstick/engine' if defined?(Rails::Engine)
12
+ require 'lipstick/action_view_tilt_template' if defined?(Tilt)
@@ -0,0 +1,17 @@
1
+ require 'action_view'
2
+
3
+ module Lipstick
4
+ class ActionViewTiltTemplate < Tilt::ERBTemplate
5
+ def prepare
6
+ @engine = ActionView::Template::Handlers::Erubis.new(data, options)
7
+ end
8
+
9
+ def precompiled_preamble(_locals)
10
+ ''
11
+ end
12
+
13
+ def precompiled_postamble(_locals)
14
+ ''
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,73 @@
1
+ require 'active_model'
2
+
3
+ module Lipstick
4
+ module AutoValidation
5
+ module ClassMethods
6
+ def self.extended(base)
7
+ return if base.respond_to?(:validators)
8
+
9
+ fail('Lipstick::AutoValidation requires a class which responds' \
10
+ ' to the `validators` method. For example, as provided by' \
11
+ ' ActiveModel::Validations')
12
+ end
13
+
14
+ def lipstick_auto_validators
15
+ validators.each_with_object({}) do |validator, map|
16
+ validator.attributes.each do |attr|
17
+ out = semantic_ui_validator(attr, validator)
18
+ next if out.nil?
19
+
20
+ map[attr.to_sym] ||= {}
21
+ map[attr.to_sym].merge!(out)
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ v = ActiveModel::Validations
29
+ VALIDATOR_TRANSLATORS = {
30
+ v::PresenceValidator => :semantic_presence_validator,
31
+ v::LengthValidator => :semantic_length_validator,
32
+ v::NumericalityValidator => :semantic_numericality_validator
33
+ }
34
+ private_constant :VALIDATOR_TRANSLATORS
35
+
36
+ def semantic_ui_validator(attr, validator)
37
+ VALIDATOR_TRANSLATORS.each do |klass, sym|
38
+ next unless validator.is_a?(klass)
39
+ return send(sym, attr, validator,
40
+ attr.to_s.humanize(capitalize: false))
41
+ end
42
+ nil
43
+ end
44
+
45
+ def semantic_presence_validator(_attr, _validator, humanized)
46
+ { empty: "Please enter a value for #{humanized}" }
47
+ end
48
+
49
+ def semantic_length_validator(_attr, validator, humanized)
50
+ min = validator.options[:minimum]
51
+ max = validator.options[:maximum]
52
+
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
+
58
+ {}.tap do |out|
59
+ out["length[#{min}]"] = min_message if min
60
+ out["maxLength[#{max}]"] = max_message if max
61
+ end
62
+ end
63
+
64
+ def semantic_numericality_validator(_attr, _validator, humanized)
65
+ { integer: "Please enter a numeric value for #{humanized}" }
66
+ end
67
+ end
68
+
69
+ def self.included(base)
70
+ base.send(:extend, ClassMethods)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,29 @@
1
+ require 'kramdown'
2
+ require 'erubis'
3
+
4
+ module Lipstick
5
+ class EmailMessage
6
+ attr_reader :title, :image_url, :content, :template
7
+
8
+ def initialize(title:, content:, image_url:, template: default_template)
9
+ @title = title
10
+ @content = Kramdown::Document.new(content).to_html
11
+ @image_url = image_url
12
+ @template = template
13
+ end
14
+
15
+ def render
16
+ Erubis::Eruby.new(template).result(binding)
17
+ end
18
+
19
+ private
20
+
21
+ TEMPLATE_PATH = '../../app/views/layouts/email_branding.html.erb'
22
+ private_constant :TEMPLATE_PATH
23
+
24
+ def default_template
25
+ file = File.expand_path(TEMPLATE_PATH, File.dirname(__FILE__))
26
+ File.read(file)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,11 @@
1
+ module Lipstick
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Lipstick
4
+
5
+ initializer :assets do |app|
6
+ app.config.assets.tap do |assets|
7
+ assets.paths << root.join('app', 'assets', 'images')
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ module Lipstick
2
+ module Helpers
3
+ end
4
+ end
5
+
6
+ require 'lipstick/helpers/layout_helper'
7
+ require 'lipstick/helpers/nav_helper'
8
+ require 'lipstick/helpers/semantic_form_builder'
9
+ require 'lipstick/helpers/form_helper'
@@ -0,0 +1,197 @@
1
+ module Lipstick
2
+ module Helpers
3
+ module FormHelper
4
+ include ActionView::Helpers::FormTagHelper
5
+
6
+ def field_block(html_opts = {}, &block)
7
+ if flag_enabled?(:inside_inline_form_tag)
8
+ html_opts[:class] = "#{html_opts[:class]} inline".strip
9
+ end
10
+
11
+ html_opts[:class] = "#{html_opts[:class]} field".strip
12
+ content_tag('div', html_opts, &block)
13
+ end
14
+
15
+ alias_method :orig_form_tag, :form_tag
16
+
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)
20
+ end
21
+
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)
26
+ end
27
+ end
28
+
29
+ def hidden_fields(&block)
30
+ content_tag('div', style: 'display: none;', &block)
31
+ end
32
+
33
+ def text_field_tag(*args)
34
+ content_tag('div', class: 'ui input') { super }
35
+ end
36
+
37
+ def field_help_text(text)
38
+ icon_tag('field-help-text blue help', 'data-content' => text)
39
+ end
40
+
41
+ def button_tag(content_or_options = nil, options = nil, &block)
42
+ add_class = ->(m) { m.dup.merge(class: "#{m[:class]} ui button".strip) }
43
+
44
+ if content_or_options.is_a?(Hash)
45
+ super(add_class.call(content_or_options), &block)
46
+ else
47
+ super(content_or_options, add_class.call(options || {}), &block)
48
+ end
49
+ end
50
+
51
+ 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
67
+
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
74
+ end
75
+ end
76
+
77
+ def form_for(obj, opts = {}, &block)
78
+ opts[:builder] = SemanticFormBuilder
79
+ opts[:html] ||= {}
80
+ opts[:html][:class] = "#{opts[:html][:class]} ui form".strip
81
+ super(obj, opts, &block)
82
+ end
83
+
84
+ def radio_button_block(&block)
85
+ content_tag('div', class: 'grouped fields', &block)
86
+ end
87
+
88
+ # Generates the wrapping code for validating a form. The selector is
89
+ # passed to jQuery, and must uniquely select the form being validated.
90
+ # `sym` is the object name when using a `form_for` helper to generate the
91
+ # form.
92
+ #
93
+ # 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
141
+ end
142
+ end
143
+
144
+ private
145
+
146
+ def validation_body(selector, sym, &block)
147
+ 'var validations = {};' +
148
+ validation_name_mapping_function(sym) +
149
+ capture(&block) +
150
+ validation_install(selector)
151
+ end
152
+
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
165
+
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
174
+
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;'
180
+ end
181
+
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
190
+ end
191
+
192
+ def flag_enabled?(flag)
193
+ Thread.current[flag]
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,143 @@
1
+ require 'action_view'
2
+
3
+ module Lipstick
4
+ module Helpers
5
+ module LayoutHelper
6
+ include ActionView::Helpers::TagHelper
7
+ include ActionView::Helpers::TextHelper
8
+ include ActionView::Helpers::CaptureHelper
9
+
10
+ def aaf_header(title:, environment: nil, &bl)
11
+ content_tag('div', class: 'aaf-header') do
12
+ concat(aaf_banner(title, environment))
13
+ concat(capture(&bl))
14
+ end
15
+ end
16
+
17
+ def aaf_footer(&bl)
18
+ content_tag('footer') do
19
+ concat(content_tag('div', '', class: 'ui divider'))
20
+ concat(capture(&bl))
21
+ end
22
+ end
23
+
24
+ def logged_in_user(user)
25
+ return if user.nil?
26
+ content_tag('p') do
27
+ concat('Logged in as: ')
28
+ concat(content_tag('strong', user.name))
29
+ concat(" (#{user.targeted_id})")
30
+ end
31
+ end
32
+
33
+ def page_header(header, subheader = nil)
34
+ content_tag('h2', class: 'ui header') do
35
+ concat(header)
36
+ if subheader
37
+ concat(content_tag('div', subheader, class: 'sub header'))
38
+ end
39
+ end
40
+ end
41
+
42
+ def divider_tag
43
+ content_tag('div', '', class: 'ui divider')
44
+ end
45
+
46
+ def yes_no_string(boolean)
47
+ boolean ? 'Yes' : 'No'
48
+ end
49
+
50
+ def icon_tag(icon_class, html_opts = {})
51
+ html_opts[:class] = "#{html_opts[:class]} icon #{icon_class}".strip
52
+ content_tag('i', '', html_opts)
53
+ end
54
+
55
+ # button_link_to(url_opts) { 'Link Text' }
56
+ # button_link_to(url_opts, html_opts) { 'Link Text' }
57
+ # button_link_to('Link Text', url_opts)
58
+ # button_link_to('Link Text', url_opts, html_opts)
59
+ def button_link_to(*args, &block)
60
+ args.unshift(capture(&block)) if block_given?
61
+ text, url_opts, html_opts = args
62
+ html_opts ||= {}
63
+ html_opts[:class] = "#{html_opts[:class]} ui button".strip
64
+ link_to(text, url_opts, html_opts)
65
+ end
66
+
67
+ def info_message(title, &block)
68
+ message_block(title, 'info', 'info', &block)
69
+ end
70
+
71
+ def error_message(title, &block)
72
+ message_block(title, 'error', 'warning', &block)
73
+ end
74
+
75
+ def success_message(title, &block)
76
+ message_block(title, 'success', 'smile', &block)
77
+ end
78
+
79
+ def warning_message(title, &block)
80
+ message_block(title, 'warning', 'warning', &block)
81
+ end
82
+
83
+ def breadcrumbs(*links)
84
+ content_tag('div', class: 'ui breadcrumb') do
85
+ last = links.pop
86
+
87
+ links.each do |link|
88
+ concat(content_tag('div', breadcrumb_link(link), class: 'section'))
89
+ concat(icon_tag('angle double right divider'))
90
+ end
91
+
92
+ concat(content_tag('div', breadcrumb_link(last),
93
+ class: 'active section'))
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ def aaf_banner(title, environment)
100
+ content_tag('header', class: 'banner clearfix') do
101
+ concat(aaf_environment_string(environment))
102
+ header = content_tag('h2', class: 'ui inverted header') do
103
+ # The logo width is also forced in CSS. The logo is scaled to 50% in
104
+ # both width and height to improve quality on high-density displays.
105
+ concat(tag('img', src: image_path('logo.png'), class: 'logo',
106
+ width: 138, height: 80))
107
+ concat(content_tag('div', title, class: 'content'))
108
+ end
109
+ concat(header)
110
+ end
111
+ end
112
+
113
+ def aaf_environment_string(environment)
114
+ return if environment.nil?
115
+ content_tag('div', environment, class: 'environment')
116
+ end
117
+
118
+ def image_path(*)
119
+ unless defined?(super)
120
+ fail('No image_path method was found. This is typically provided by' \
121
+ ' Rails or Sprockets::Helpers')
122
+ end
123
+ super
124
+ end
125
+
126
+ def message_block(title, color_class, icon_class, &block)
127
+ content_tag('div', class: "ui icon message #{color_class}") do
128
+ concat(icon_tag(icon_class))
129
+ inner = content_tag('div', class: 'content') do
130
+ concat(content_tag('div', title, class: 'header'))
131
+ concat(capture(&block))
132
+ end
133
+ concat(inner)
134
+ end
135
+ end
136
+
137
+ def breadcrumb_link(item)
138
+ return item if item.is_a?(String)
139
+ item.reduce(nil) { |_, (k, v)| content_tag('a', k, href: v) }
140
+ end
141
+ end
142
+ end
143
+ end