storefront 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +76 -0
- data/init.rb +1 -0
- data/lib/storefront.rb +21 -0
- data/lib/storefront/dashboard.rb +184 -0
- data/lib/storefront/form.rb +65 -0
- data/lib/storefront/form/elements.rb +101 -0
- data/lib/storefront/form/errors.rb +57 -0
- data/lib/storefront/form/fields.rb +420 -0
- data/lib/storefront/form/hints.rb +18 -0
- data/lib/storefront/form/inputs.rb +254 -0
- data/lib/storefront/form/labels.rb +81 -0
- data/lib/storefront/form/model.rb +84 -0
- data/lib/storefront/helpers/attribute_helper.rb +60 -0
- data/lib/storefront/helpers/body_helper.rb +18 -0
- data/lib/storefront/helpers/browser_helper.rb +54 -0
- data/lib/storefront/helpers/cache_helper.rb +25 -0
- data/lib/storefront/helpers/collection_helper.rb +14 -0
- data/lib/storefront/helpers/component_helper.rb +35 -0
- data/lib/storefront/helpers/dashboard_helper.rb +37 -0
- data/lib/storefront/helpers/debug_helper.rb +11 -0
- data/lib/storefront/helpers/definition_list_helper.rb +29 -0
- data/lib/storefront/helpers/error_helper.rb +11 -0
- data/lib/storefront/helpers/flash_helper.rb +14 -0
- data/lib/storefront/helpers/form_helper.rb +8 -0
- data/lib/storefront/helpers/format_helper.rb +59 -0
- data/lib/storefront/helpers/head_helper.rb +229 -0
- data/lib/storefront/helpers/image_helper.rb +54 -0
- data/lib/storefront/helpers/list_helper.rb +47 -0
- data/lib/storefront/helpers/locale_helper.rb +175 -0
- data/lib/storefront/helpers/open_graph_helper.rb +5 -0
- data/lib/storefront/helpers/page_helper.rb +73 -0
- data/lib/storefront/helpers/presenter_helper.rb +5 -0
- data/lib/storefront/helpers/request_helper.rb +66 -0
- data/lib/storefront/helpers/semantic/location_helper.rb +18 -0
- data/lib/storefront/helpers/semantic/time_helper.rb +14 -0
- data/lib/storefront/helpers/sidebar_helper.rb +27 -0
- data/lib/storefront/helpers/system_helper.rb +18 -0
- data/lib/storefront/helpers/table_helper.rb +38 -0
- data/lib/storefront/helpers/time_helper.rb +20 -0
- data/lib/storefront/helpers/url_helper.rb +32 -0
- data/lib/storefront/helpers/user_helper.rb +15 -0
- data/lib/storefront/helpers/widget_helper.rb +10 -0
- data/lib/storefront/helpers/workflow_helper.rb +50 -0
- data/lib/storefront/microdata/address.rb +7 -0
- data/lib/storefront/microdata/event.rb +13 -0
- data/lib/storefront/microdata/geo.rb +7 -0
- data/lib/storefront/microdata/organization.rb +7 -0
- data/lib/storefront/microdata/person.rb +7 -0
- data/lib/storefront/microformat/event.rb +7 -0
- data/lib/storefront/microformat/person.rb +7 -0
- data/lib/storefront/railtie.rb +28 -0
- data/lib/storefront/table.rb +191 -0
- data/rails/init.rb +1 -0
- data/test/form_helper_test.rb +5 -0
- data/test/support/mock_controller.rb +15 -0
- data/test/support/mock_response.rb +14 -0
- data/test/support/models.rb +0 -0
- data/test/test_helper.rb +34 -0
- metadata +111 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
module Storefront
|
2
|
+
class Form
|
3
|
+
module Hints
|
4
|
+
def hints_for(key, options = {})
|
5
|
+
value = options[:hint_attributes].delete(:value)
|
6
|
+
template.capture_haml do
|
7
|
+
if value.present?
|
8
|
+
template.haml_tag :figure, options[:hint_attributes] do
|
9
|
+
template.haml_concat value.html_safe
|
10
|
+
end
|
11
|
+
else
|
12
|
+
template.haml_tag :figure, options[:hint_attributes]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,254 @@
|
|
1
|
+
module Storefront
|
2
|
+
class Form
|
3
|
+
module Inputs
|
4
|
+
def input_for(key, options)
|
5
|
+
send(:"#{options.delete(:as)}_input", key, options)
|
6
|
+
end
|
7
|
+
|
8
|
+
def default_input_type(method, options = {}) #:nodoc:
|
9
|
+
if column = column_for(method)
|
10
|
+
# Special cases where the column type doesn't map to an input method.
|
11
|
+
case column.type
|
12
|
+
when :string
|
13
|
+
return :password if method.to_s =~ /password/
|
14
|
+
return :country if method.to_s =~ /country$/
|
15
|
+
return :time_zone if method.to_s =~ /time_zone/
|
16
|
+
return :email if method.to_s =~ /email/
|
17
|
+
return :url if method.to_s =~ /^url$|^website$|_url$/
|
18
|
+
return :phone if method.to_s =~ /(phone|fax)/
|
19
|
+
return :search if method.to_s =~ /^search$/
|
20
|
+
when :integer
|
21
|
+
return :select if reflection_for(method)
|
22
|
+
return :numeric
|
23
|
+
when :float, :decimal
|
24
|
+
return :numeric
|
25
|
+
when :timestamp
|
26
|
+
return :datetime
|
27
|
+
end
|
28
|
+
|
29
|
+
# Try look for hints in options hash. Quite common senario: Enum keys stored as string in the database.
|
30
|
+
return :select if column.type == :string && options.key?(:collection)
|
31
|
+
# Try 3: Assume the input name will be the same as the column type (e.g. string_input).
|
32
|
+
return column.type
|
33
|
+
else
|
34
|
+
if @object
|
35
|
+
return :select if reflection_for(method)
|
36
|
+
|
37
|
+
return :file if is_file?(method, options)
|
38
|
+
end
|
39
|
+
|
40
|
+
return :select if options.key?(:collection)
|
41
|
+
return :password if method.to_s =~ /password/
|
42
|
+
return :string
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def is_file?(method, options = {})
|
47
|
+
@files ||= {}
|
48
|
+
@files[method] ||= (options[:as].present? && options[:as] == :file) || begin
|
49
|
+
file = @object.send(method) if @object && @object.respond_to?(method)
|
50
|
+
file && file_methods.any?{|m| file.respond_to?(m)}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def file_methods
|
55
|
+
[:file?, :public_filename, :filename]
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
def base_input(input_type, key, options = {}, &block)
|
60
|
+
if [:numeric, :string, :password, :text, :phone, :url, :email].include?(input_type)
|
61
|
+
attributes = default_string_options(key, input_type).merge(options[:input_attributes])
|
62
|
+
else
|
63
|
+
attributes = options[:input_attributes]
|
64
|
+
end
|
65
|
+
merge_class! attributes, key.to_s, options[:class]
|
66
|
+
attributes[:required] = true if options[:required] == true
|
67
|
+
template.capture_haml do
|
68
|
+
template.haml_tag :input, attributes.merge(:type => input_type), &block
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def core_input(tag, *args, &block)
|
73
|
+
options = args.extract_options!
|
74
|
+
attributes = options[:input_attributes]
|
75
|
+
attributes[:required] = true if options[:required] == true
|
76
|
+
template.capture_haml do
|
77
|
+
if block_given?
|
78
|
+
template.haml_tag tag, attributes, &block
|
79
|
+
else
|
80
|
+
template.haml_tag tag, args.shift, attributes
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# autofocus, pattern, placeholder, title, size, data-validate, data-validate-match, data-validate-unique
|
86
|
+
def string_input(key, attributes = {})
|
87
|
+
base_input :string, key, attributes
|
88
|
+
end
|
89
|
+
|
90
|
+
# maxlength, placeholder, required, wrap, readonly
|
91
|
+
def textarea_input(key, options = {})
|
92
|
+
attributes = options[:input_attributes]
|
93
|
+
value = attributes.delete(:value)
|
94
|
+
core_input :textarea, value, attributes
|
95
|
+
end
|
96
|
+
alias_method :text_input, :textarea_input
|
97
|
+
|
98
|
+
def checkbox_input(key, attributes = {})
|
99
|
+
hidden_input key, attributes.merge(:value => 0)
|
100
|
+
base_input :checkbox, key, attributes.merge(:value => 1)
|
101
|
+
end
|
102
|
+
|
103
|
+
# prompt, blank, multiple
|
104
|
+
def select_input(key, options = {})
|
105
|
+
collection = Array(options.delete(:collection) || [])
|
106
|
+
if options[:include_blank] != false
|
107
|
+
prompt = options[:prompt] || ""
|
108
|
+
collection = [[prompt, ""]] + collection
|
109
|
+
end
|
110
|
+
attributes = options[:input_attributes]
|
111
|
+
selected = attributes.delete(:value)
|
112
|
+
core_input :select, options do
|
113
|
+
collection.map do |item|
|
114
|
+
name, options = item, {}
|
115
|
+
case item
|
116
|
+
when Array
|
117
|
+
name = item[0]
|
118
|
+
options[:value] = item[1]
|
119
|
+
when Hash
|
120
|
+
name = item[:name]
|
121
|
+
options[:value] = item[:value]
|
122
|
+
else
|
123
|
+
options[:value] = item
|
124
|
+
end
|
125
|
+
options[:selected] = "true" if selected.present? && options[:value] == selected
|
126
|
+
template.haml_tag :option, name, options
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def hidden_input(key, attributes = {})
|
132
|
+
base_input :hidden, key, attributes
|
133
|
+
end
|
134
|
+
|
135
|
+
def number_input(key, attributes = {})
|
136
|
+
base_input :string, key, attributes.merge(:class => "number", "data-type" => "number")
|
137
|
+
end
|
138
|
+
|
139
|
+
def search_input(key, attributes = {})
|
140
|
+
base_input :search, key, merge_class(attributes, "search").merge("data-type" => "search")
|
141
|
+
end
|
142
|
+
|
143
|
+
def boolean_input(key, attributes = {})
|
144
|
+
select_input(key, attributes.merge(:collection => [["Yes", "1"], ["No", "0"]]))
|
145
|
+
#checkbox_input(key, attributes = {})
|
146
|
+
end
|
147
|
+
|
148
|
+
# accept, maxlength="2"
|
149
|
+
def file_input(key, attributes = {})
|
150
|
+
base_input :file, key, attributes
|
151
|
+
end
|
152
|
+
|
153
|
+
def password_input(key, attributes = {})
|
154
|
+
base_input :password, key, attributes
|
155
|
+
end
|
156
|
+
|
157
|
+
def email_input(key, attributes = {})
|
158
|
+
base_input :email, key, attributes
|
159
|
+
end
|
160
|
+
|
161
|
+
def url_input(key, attributes = {})
|
162
|
+
base_input :url, key, attributes
|
163
|
+
end
|
164
|
+
|
165
|
+
def phone_input(key, attributes = {})
|
166
|
+
base_input :tel, key, attributes.merge(:class => "phone")
|
167
|
+
end
|
168
|
+
|
169
|
+
def fax_input(key, attributes = {})
|
170
|
+
base_input :tel, key, attributes.merge(:class => "phone fax")
|
171
|
+
end
|
172
|
+
|
173
|
+
def date_input(key, attributes = {})
|
174
|
+
base_input :string, key, attributes.merge(:class => "date", "data-type" => "date")
|
175
|
+
end
|
176
|
+
|
177
|
+
def money_input(key, attributes = {})
|
178
|
+
base_input :string, key, merge_class(attributes, "money").merge("data-type" => "money")
|
179
|
+
end
|
180
|
+
|
181
|
+
def percent_input(key, attributes = {})
|
182
|
+
base_input :string, key, merge_class(attributes, "percent").merge("data-type" => "percent")
|
183
|
+
end
|
184
|
+
|
185
|
+
def watched_input(key, attributes = {})
|
186
|
+
text_input key, merge_class(attributes, "watch-characters").merge(:"data-character-count" => (attributes.delete(:count) || 100))
|
187
|
+
# haml_tag :figure, :class => "watched"
|
188
|
+
end
|
189
|
+
|
190
|
+
def color_input(key, attributes = {})
|
191
|
+
|
192
|
+
end
|
193
|
+
|
194
|
+
def range_input(key, attributes = {})
|
195
|
+
|
196
|
+
end
|
197
|
+
|
198
|
+
def autocomplete_input(key, attributes = {})
|
199
|
+
|
200
|
+
end
|
201
|
+
|
202
|
+
def date_range_input(key, attributes = {})
|
203
|
+
|
204
|
+
end
|
205
|
+
|
206
|
+
def slider_input(key, attributes = {})
|
207
|
+
|
208
|
+
end
|
209
|
+
|
210
|
+
def state_input(key, attributes = {})
|
211
|
+
|
212
|
+
end
|
213
|
+
|
214
|
+
def partial(key, attributes = {})
|
215
|
+
|
216
|
+
end
|
217
|
+
|
218
|
+
def input_id(attribute, name = "input")
|
219
|
+
([keys] + [@index ? @index.to_s : nil, attribute]).compact.join("-").gsub("_", "-") + "-#{name}"
|
220
|
+
end
|
221
|
+
|
222
|
+
def input_name(attribute, options = {})
|
223
|
+
param_for([keys] + [@index ? @index.to_s : nil, attribute])
|
224
|
+
end
|
225
|
+
|
226
|
+
def input_value(attribute, default = nil)
|
227
|
+
return default if default.present?
|
228
|
+
|
229
|
+
if object.respond_to?(attribute)
|
230
|
+
object.send(attribute)
|
231
|
+
else
|
232
|
+
nil
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
## FORMTASTIC STUFF
|
237
|
+
def create_boolean_collection(options) #:nodoc:
|
238
|
+
options[:true] ||= ::Formtastic::I18n.t(:yes)
|
239
|
+
options[:false] ||= ::Formtastic::I18n.t(:no)
|
240
|
+
options[:value_as_class] = true unless options.key?(:value_as_class)
|
241
|
+
|
242
|
+
[ [ options.delete(:true), true], [ options.delete(:false), false ] ]
|
243
|
+
end
|
244
|
+
|
245
|
+
def validation_max_limit
|
246
|
+
255
|
247
|
+
end
|
248
|
+
|
249
|
+
def default_text_field_size
|
250
|
+
nil
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Storefront
|
2
|
+
class Form
|
3
|
+
module Labels
|
4
|
+
def label_for(key, options = {})
|
5
|
+
attributes = options[:label_attributes]
|
6
|
+
text = attributes.delete(:value)
|
7
|
+
result = template.capture_haml do
|
8
|
+
template.haml_tag :label, options[:label_attributes] do
|
9
|
+
template.haml_tag :span, text
|
10
|
+
if options[:required] == true
|
11
|
+
template.haml_tag :abbr, "*", :title => "required"
|
12
|
+
else
|
13
|
+
template.haml_tag :abbr, "", :title => "optional"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
result
|
18
|
+
end
|
19
|
+
|
20
|
+
def label_method
|
21
|
+
:titleize
|
22
|
+
end
|
23
|
+
|
24
|
+
def humanized_attribute_name(method) #:nodoc:
|
25
|
+
if @object && @object.class.respond_to?(:human_attribute_name)
|
26
|
+
humanized_name = @object.class.human_attribute_name(method.to_s)
|
27
|
+
if humanized_name == method.to_s.send(:humanize)
|
28
|
+
method.to_s.send(label_method)
|
29
|
+
else
|
30
|
+
humanized_name
|
31
|
+
end
|
32
|
+
else
|
33
|
+
method.to_s.send(label_method)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def localized_string(key, value, type, options = {}) #:nodoc:
|
38
|
+
key = value if value.is_a?(::Symbol)
|
39
|
+
|
40
|
+
if value.is_a?(::String)
|
41
|
+
value.html_safe
|
42
|
+
else
|
43
|
+
use_i18n = true
|
44
|
+
|
45
|
+
if use_i18n
|
46
|
+
model_name, nested_model_name = normalize_model_name(self.model_name.underscore)
|
47
|
+
action_name = template.params[:action].to_s rescue ''
|
48
|
+
attribute_name = key.to_s
|
49
|
+
|
50
|
+
defaults = ::Formtastic::I18n::SCOPES.reject do |i18n_scope|
|
51
|
+
nested_model_name.nil? && i18n_scope.match(/nested_model/)
|
52
|
+
end.collect do |i18n_scope|
|
53
|
+
i18n_path = i18n_scope.dup
|
54
|
+
i18n_path.gsub!('%{action}', action_name)
|
55
|
+
i18n_path.gsub!('%{model}', model_name)
|
56
|
+
i18n_path.gsub!('%{nested_model}', nested_model_name) unless nested_model_name.nil?
|
57
|
+
i18n_path.gsub!('%{attribute}', attribute_name)
|
58
|
+
i18n_path.gsub!('..', '.')
|
59
|
+
i18n_path.to_sym
|
60
|
+
end
|
61
|
+
defaults << ''
|
62
|
+
|
63
|
+
defaults.uniq!
|
64
|
+
|
65
|
+
default_key = defaults.shift
|
66
|
+
i18n_value = ::Formtastic::I18n.t(default_key,
|
67
|
+
options.merge(:default => defaults, :scope => type.to_s.pluralize.to_sym))
|
68
|
+
if i18n_value.blank? && type == :label
|
69
|
+
# This is effectively what Rails label helper does for i18n lookup
|
70
|
+
options[:scope] = [:helpers, type]
|
71
|
+
options[:default] = defaults
|
72
|
+
i18n_value = ::I18n.t(default_key, options)
|
73
|
+
end
|
74
|
+
|
75
|
+
i18n_value.blank? ? nil : i18n_value
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Storefront
|
2
|
+
class Form
|
3
|
+
module Model
|
4
|
+
def model_name
|
5
|
+
@object.present? ? @object.class.name : @object_name.to_s.classify
|
6
|
+
end
|
7
|
+
|
8
|
+
def normalize_model_name(name)
|
9
|
+
if name =~ /(.+)\[(.+)\]/
|
10
|
+
[$1, $2]
|
11
|
+
else
|
12
|
+
[name]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# If an association method is passed in (f.input :author) try to find the
|
17
|
+
# reflection object.
|
18
|
+
#
|
19
|
+
def reflection_for(method) #:nodoc:
|
20
|
+
@object.class.reflect_on_association(method) if @object.class.respond_to?(:reflect_on_association)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Get a column object for a specified attribute method - if possible.
|
24
|
+
#
|
25
|
+
def column_for(method) #:nodoc:
|
26
|
+
@object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
|
27
|
+
end
|
28
|
+
|
29
|
+
def validations_for(method, mode = :active)
|
30
|
+
# ActiveModel?
|
31
|
+
validations = if @object && @object.class.respond_to?(:validators_on)
|
32
|
+
@object.class.validators_on(method)
|
33
|
+
else
|
34
|
+
# ValidationReflection plugin?
|
35
|
+
if @object && @object.class.respond_to?(:reflect_on_validations_for)
|
36
|
+
@object.class.reflect_on_validations_for(method)
|
37
|
+
else
|
38
|
+
[]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
validations = validations.select do |validation|
|
43
|
+
(validation.options.present? ? options_require_validation?(validation.options) : true)
|
44
|
+
end unless mode == :all
|
45
|
+
|
46
|
+
return validations
|
47
|
+
end
|
48
|
+
|
49
|
+
def sanitized_object_name #:nodoc:
|
50
|
+
@sanitized_object_name ||= @object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_maxlength_for(method)
|
54
|
+
validation = validations_for(method).find do |validation|
|
55
|
+
(validation.respond_to?(:macro) && validation.macro == :validates_length_of) || # Rails 2 validation
|
56
|
+
(validation.respond_to?(:kind) && validation.kind == :length) # Rails 3 validator
|
57
|
+
end
|
58
|
+
|
59
|
+
if validation
|
60
|
+
validation.options[:maximum] || (validation.options[:within].present? ? validation.options[:within].max : nil)
|
61
|
+
else
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def default_string_options(method, type) #:nodoc:
|
67
|
+
validation_max_limit = get_maxlength_for(method)
|
68
|
+
column = column_for(method)
|
69
|
+
|
70
|
+
if type == :text
|
71
|
+
{ :rows => default_text_area_height,
|
72
|
+
:cols => default_text_area_width }
|
73
|
+
elsif type == :numeric || column.nil? || !column.respond_to?(:limit) || column.limit.nil?
|
74
|
+
{ :maxlength => validation_max_limit,
|
75
|
+
:size => default_text_field_size }
|
76
|
+
else
|
77
|
+
{ :maxlength => validation_max_limit || column.limit,
|
78
|
+
:size => default_text_field_size }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Storefront
|
2
|
+
module AttributeHelper
|
3
|
+
def underscore_class_name(record)
|
4
|
+
record.class.base_class.snake_case
|
5
|
+
end
|
6
|
+
|
7
|
+
def datetime_for(time)
|
8
|
+
with_time_at time
|
9
|
+
end
|
10
|
+
|
11
|
+
def record_method(method, *args, &block)
|
12
|
+
record = args.shift
|
13
|
+
sent_method = "#{method}_#{underscore_class_name(record)}"
|
14
|
+
if args.length > 0 && options = args.pop
|
15
|
+
send(sent_method, record, options, &block)
|
16
|
+
else
|
17
|
+
send(sent_method, record, &block)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def merge_class!(attributes, *values)
|
22
|
+
return {} if attributes.nil?
|
23
|
+
hash = merge_class(attributes, *values)
|
24
|
+
attributes[:class] = hash[:class] if hash
|
25
|
+
attributes
|
26
|
+
end
|
27
|
+
|
28
|
+
def merge_class(attributes, *values)
|
29
|
+
if values.present?
|
30
|
+
if attributes[:class].present?
|
31
|
+
classes = attributes[:class].split(/\s+/)
|
32
|
+
else
|
33
|
+
classes = []
|
34
|
+
end
|
35
|
+
classes += values.compact.map(&:to_s)
|
36
|
+
classes.uniq!
|
37
|
+
else
|
38
|
+
classes = nil
|
39
|
+
end
|
40
|
+
|
41
|
+
if classes.present?
|
42
|
+
attributes.merge(:class => classes.join(" "))
|
43
|
+
else
|
44
|
+
attributes
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def index_class(index, length)
|
49
|
+
return nil if length.blank? || (length <= 1)
|
50
|
+
|
51
|
+
if index == 0
|
52
|
+
"first"
|
53
|
+
elsif index == length - 1
|
54
|
+
"last"
|
55
|
+
else
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|