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.
- checksums.yaml +7 -0
- data/Rakefile +17 -0
- data/app/assets/images/favicon.png +0 -0
- data/app/assets/images/logo.png +0 -0
- data/app/assets/javascripts/aaf-layout.js +35 -0
- data/app/assets/stylesheets/aaf-layout.css.scss +204 -0
- data/app/views/layouts/email_branding.html.erb +289 -0
- data/lib/aaf-lipstick.rb +1 -0
- data/lib/lipstick.rb +12 -0
- data/lib/lipstick/action_view_tilt_template.rb +17 -0
- data/lib/lipstick/auto_validation.rb +73 -0
- data/lib/lipstick/email_message.rb +29 -0
- data/lib/lipstick/engine.rb +11 -0
- data/lib/lipstick/helpers.rb +9 -0
- data/lib/lipstick/helpers/form_helper.rb +197 -0
- data/lib/lipstick/helpers/layout_helper.rb +143 -0
- data/lib/lipstick/helpers/nav_helper.rb +50 -0
- data/lib/lipstick/helpers/semantic_form_builder.rb +2 -0
- data/lib/lipstick/images.rb +7 -0
- data/lib/lipstick/images/email_banner.rb +154 -0
- data/lib/lipstick/images/processor.rb +27 -0
- data/lib/lipstick/version.rb +3 -0
- metadata +275 -0
data/lib/aaf-lipstick.rb
ADDED
@@ -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,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
|