aaf-lipstick 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|