aaf-lipstick 1.1.0

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