storefront 0.2.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.
- 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
|