kiss 1.7.4 → 1.8

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.
@@ -2,134 +2,101 @@ require 'kiss/form/field';
2
2
 
3
3
  class Kiss
4
4
  class Form
5
+ ### Abstract Field Classes ###
6
+
7
+ # Base class for single-value fields.
5
8
  class Field
6
- dsl_accessor :name, :type, :form, :currency, :label, :no_label, :prompt, :value, :read_only,
7
- :ignore, :save, :options, :options_value_key, :options_display_key, :options_display_transform,
8
- :required, :unique, :cancel, :columns, :style, :hidden_join, :html, :other, :other_field, :object,
9
- :format, :display_format, :key, :match, :tip, :statement, :attach_errors, :factor, :digest,
10
- :min_value_size, :max_value_size, :choose_here
11
- alias_method :option_value_key, :options_value_key
12
- alias_method :option_display_key, :options_display_key
13
- alias_method :option_display_transform, :options_display_transform
9
+ CURRENCY_SYMBOLS = {
10
+ :dollars => '$'
11
+ }
12
+
13
+ _attr_accessor :errors
14
+
15
+ dsl_accessor(
16
+ :form, # (Kiss::Form) form to which this field is attached
17
+
18
+ :object, # (Kiss::Model) object which holds data for this field
19
+ :save, # (Boolean) true iff field value should be saved to object (default: true)
20
+
21
+ :name, # (String) name attribute of field element and input param
22
+ :key, # (Symbol) key in object for the data accessed by this field
23
+ :value, # (data type depends on format) value of field
24
+
25
+ :required, # (Boolean) true iff user must supply a enter/choose for this field (default: false)
26
+ :format, # (Symbol) format for validation of field value input (see Kiss::Format)
27
+ :match, # (String) name of field whose input value must match this field's value
28
+ :unique, # (Boolean) true iff this field's value must be unique for key in the object model
29
+
30
+ :label, # (String) label to present before this field in HTML
31
+ :prompt, # (String) prompt to present above this field in HTML
32
+ :tip, # (String) tip to present following this field in HTML
33
+ # TODO: Some currencies should be prefixed, while others should be suffixed.
34
+ :currency, # (String) currency to present directly before this field in HTML
35
+ :style, # (String) style attribute for field element in HTML
36
+ :html, # (Hash or String) extra HTML attributes to add to field element
37
+ :attach_errors, # (Boolean) if true, display field errors with this field; else at top of form
38
+
39
+ :factor, # (Numeric) factor to be multiplied by value
40
+ :digest, # (Symbol) digest to be applied to value before saving to object
41
+
42
+ :min_value_size, # (Numeric) minimum size of field value
43
+ :max_value_size # (Numeric) maximum size of field value
44
+ )
14
45
  alias_method :min_value_length, :min_value_size
15
46
  alias_method :max_value_length, :max_value_size
16
47
 
17
48
  def method_missing(method, *args, &block)
18
49
  @_form.action.send method, *args, &block
19
50
  end
20
-
21
- def options_keys(value, display)
22
- @_options_value_key = value
23
- @_options_display_key = display
24
- end
25
- alias_method :option_keys, :options_keys
26
51
 
27
52
  def debug(*args)
28
53
  @_form.delegate.request.debug(args.first, Kernel.caller[0])
29
54
  end
55
+
56
+ class << self
57
+ _attr_accessor :type
58
+ end
59
+
60
+ def type
61
+ self.class.type
62
+ end
30
63
 
31
64
  def initialize(form, *args, &block)
65
+ # call as method to allow subclasses to override
66
+ self.form = form
67
+
32
68
  # defaults
33
- @_form = form
34
69
  @_save = true
35
70
  @_currency = nil
36
- @_attrs = args.to_attrs
37
- @_type = :text
38
71
  @_object = @_form.object
39
- @_save = true
40
72
  @_attach_errors = true
41
73
 
42
- _instance_variables_set_from_attrs(@_attrs)
74
+ _instance_variables_set_from_attrs(args.to_attrs)
43
75
  instance_eval(&block) if block_given?
44
76
 
45
- @_errors = []
77
+ raise 'field must have a name' unless @_name
78
+ @_name = @_name
79
+ @_key ||= @_name.to_sym
80
+ @_label ||= @_name.titleize unless self.is_a?(SubmitField)
46
81
 
82
+ @_errors = []
47
83
  @_format = Kiss::Format.lookup(@_format)
48
- @_display_format = Kiss::Format.lookup(@_display_format)
49
-
50
- raise 'field must have a name' unless @_name
51
- @_key ||= @_name
52
- @_label ||= @_name.titleize unless @_type == :submit
84
+ @_tip = ((legend = @_format.legend) ? "(#{legend})" : nil) unless defined? @_tip
53
85
 
54
- # object's value overrides any form field value
55
- # form field value is intended as default in case object value is missing
56
- if @_object && (value = @_object[@_key.to_sym])
86
+ # object's value (if present) overrides default form field value
87
+ if @_object && (value = @_object[@_key])
57
88
  @_value = value
58
89
  end
59
90
 
60
91
  if @_currency.is_a?(Symbol)
61
- @_currency = case @_currency
62
- when :dollars
63
- '$'
64
- else
65
- ''
66
- end
92
+ @_currency = CURRENCY_SYMBOLS[@_currency] || ''
67
93
  end
68
94
 
69
- if @_options
70
- if @_options[0].is_a?(Array)
71
- @_options_value_key ||= 0
72
- (@_options_display_key ||= (@_options[0].size == 1) ? 0 : 1)
73
- elsif defined?(Kiss::Model) && @_options[0].is_a?(Kiss::Model)
74
- model_klass = @_options[0].class
75
- @_options_value_key ||= model_klass.value_column
76
- @_options_display_key ||= model_klass.display_column
77
- elsif @_options[0].is_a?(Hash)
78
- @_options_value_key ||= :id
79
- @_options_display_key ||= :name
80
- end
81
- end
82
-
83
- @_tip = ((legend = @_format.legend) ? "(#{legend})" : nil) unless defined? @_tip
84
-
85
95
  @_form.has_required_fields ||= @_required
86
96
  end
87
97
 
88
- def other_field_html
89
- return '' unless @_other
90
-
91
- other_checked = @_value && !option_pairs.any? {|v, d| v == @_value }
92
-
93
- (@_columns ? '<br/>' : '&nbsp; ') + [
94
- input_tag_html(
95
- { :value => 'other', :html => { :id => @_name+'.other' } },
96
- other_checked ? 'checked' : ''
97
- ),
98
- @_other[:label] || 'Other',
99
- ': ',
100
- @_currency.to_s,
101
- input_tag_html({
102
- :type => :text,
103
- :name => @_name+'.other',
104
- :value => other_checked ? value_to_s(@_value) : nil,
105
- :html => {
106
- :onfocus => "document.getElementById('#{@_name}.other').checked = true"
107
- }
108
- }.merge(@_other))
109
- ].join
110
- end
111
-
112
- def column_layout(elements_html)
113
- if elements_html.empty?
114
- ''
115
- elsif @_columns
116
- layout_columns = [@_columns, elements_html.size].min
117
- num_elements_per_column = ((elements_html.size + layout_columns - 1) / layout_columns).to_i
118
- layout_columns = ((elements_html.size + num_elements_per_column - 1) / num_elements_per_column).to_i
119
-
120
- style = "style=\"width: #{(100 / layout_columns).to_i - 1}%\""
121
-
122
- '<table class="kiss_field_columns"><tr>' +
123
- (0...layout_columns).map do |i|
124
- "<td #{style}>" + elements_html[i * num_elements_per_column, num_elements_per_column].join('<br/>') + "</td>"
125
- end.join + '</table>'
126
- else
127
- elements_html.map {|h| "<nobr>#{h}</nobr>"}.join('&nbsp; ')
128
- end
129
- end
130
-
131
98
  def param
132
- @_param ||= @_form.params[@_name.to_s]
99
+ @_param ||= @_form.params[@_name]
133
100
  end
134
101
 
135
102
  def value
@@ -137,9 +104,10 @@ class Kiss
137
104
  end
138
105
 
139
106
  def value_string
140
- v = value
107
+ return param if param
108
+ v = self.value
141
109
  v *= @_factor if v && @_factor
142
- param || value_to_s(v)
110
+ value_to_s(v)
143
111
  end
144
112
 
145
113
  def add_error(message)
@@ -152,14 +120,10 @@ class Kiss
152
120
  value ? @_format.value_to_s(value, @_form.context) : ''
153
121
  end
154
122
 
155
- def display_to_s(value)
156
- value ? @_display_format.value_to_s(value).send(@_options_display_transform) : ''
157
- end
158
-
159
123
  def require_value(enter_verb = 'enter')
160
124
  if (param !~ /\S/)
161
125
  # value required
162
- add_error("Please #{enter_verb} #{@_label}")
126
+ add_error("Please #{enter_verb} #{@_label.downcase}.")
163
127
  return
164
128
  end
165
129
  end
@@ -179,7 +143,14 @@ class Kiss
179
143
  p *= @_factor if @_factor
180
144
  value = p
181
145
  rescue Kiss::Format::ValidateError => e
182
- return add_error("#{e.message.capitalize}")
146
+ return add_error("#{e.message.capitalize}.")
147
+ end
148
+
149
+ if @_min_value_size && value.size < @_min_value_size
150
+ return add_error("#{name.capitalize} must be at least #{@_min_value_size} characters long.")
151
+ end
152
+ if @_max_value_size && value.size < @_max_value_size
153
+ return add_error("#{name.capitalize} must be no more than #{@_max_value_size} characters long.")
183
154
  end
184
155
 
185
156
  if @_match
@@ -190,7 +161,7 @@ class Kiss
190
161
  end
191
162
 
192
163
  if @_save && @_unique
193
- dataset = @_object.model.filter(@_name.to_sym => value)
164
+ dataset = @_object.model.filter(@_key => value)
194
165
  unless (@_object.new? ? dataset : dataset.exclude(@_object.pk_hash)).empty?
195
166
  return add_error("There is already another #{@_object.model.name.singularize.gsub('_', ' ')} with the same #{@_label.downcase.gsub('_', ' ')}.")
196
167
  end
@@ -201,7 +172,7 @@ class Kiss
201
172
 
202
173
  def errors_html
203
174
  return nil unless @_errors.size > 0
204
-
175
+
205
176
  if @_errors.size == 1
206
177
  content = @_errors[0]
207
178
  else
@@ -220,7 +191,7 @@ class Kiss
220
191
  attrs[:style] ||= "width: #{width}px"
221
192
  end
222
193
 
223
- @_currency.to_s + input_tag_html( attrs ) + tip_html(attrs)
194
+ @_currency.to_s + input_tag_html(attrs) + tip_html(attrs)
224
195
  end
225
196
 
226
197
  def table_row_html
@@ -233,7 +204,7 @@ class Kiss
233
204
 
234
205
  def html(*args)
235
206
  errors = errors_html
236
- element_html(*args) + (errors ? (@_columns ? '' : '<br/>') + %Q(#{errors}) : '')
207
+ element_html(*args) + (errors ? ('<br/>' + errors) : '')
237
208
  end
238
209
 
239
210
  def tag_start_html(tag_name, attrs = {}, extra_html = nil)
@@ -275,12 +246,458 @@ class Kiss
275
246
  def input_tag_html(attrs = {}, extra_html = nil)
276
247
  tag_html(
277
248
  'input',
278
- {:type => @_type}.merge(attrs),
249
+ {:type => self.class.type}.merge(attrs),
279
250
  extra_html
280
251
  )
281
252
  end
253
+
254
+ def set_value_to_hash(h)
255
+ h[self.name] = self.value
256
+ end
257
+
258
+ def set_value_to_object(obj)
259
+ k = self.key
260
+ v = (self.value != nil || obj.class.db_schema[k].allow_null) ?
261
+ self.value : (obj.class.db_schema[k][:default] ||= self.format.default)
262
+
263
+ v = Digest.const_get(self.digest.to_sym).hexdigest(value) if self.digest
264
+
265
+ obj[k] = v
266
+ end
267
+ end
268
+
269
+ # Subclass for single-value field types that present and take their value
270
+ # from a set of options.
271
+ class MultiChoiceField < Field
272
+ dsl_accessor(
273
+ # (Enumerable) set of options to be presented for user choice
274
+ :options,
275
+
276
+ # (Numeric or Symbol) This specifies which part of the option objects
277
+ # should be used as the field value to be submitted from this form.
278
+ # If each option is an enumerable, options_value_key should be a
279
+ # numeric index. If each option is a hash or model object, then this
280
+ # should be symbol key common to all of the option objects.
281
+ :options_value_key,
282
+
283
+ # (Numeric or Symbol) This specifies which part of the option objects
284
+ # should be displayed to the user in the form's HTML representation.
285
+ # If each option is an enumerable, options_value_key should be a
286
+ # numeric index. If each option is a hash or model object, then this
287
+ # should be symbol key common to all of the option objects.
288
+ :options_display_key,
289
+
290
+ # (Symbol) represents a Kiss::Format to be used in rendering each
291
+ # option display object in the form output.
292
+ :display_format,
293
+
294
+ # (Symbol) symbol of a method name to be called on each option display
295
+ # object to get its text or HTML representation for the form output
296
+ # (after applying the display_format). Defaults to :to_s.
297
+ :options_display_transform,
298
+
299
+ # (Numeric) Layout the options in a table grid with this many columns.
300
+ # If nil, layout the options on a single line with no grid.
301
+ :columns,
302
+
303
+ # (Boolean) iff true, create and add a text field for the user to
304
+ # specify a custom value for this field.
305
+ :other,
306
+
307
+ # (Kiss::Form::Field) The field to be presented as the 'Other' field
308
+ # for custom input from the user.
309
+ :other_field
310
+ )
311
+ alias_method :option_value_key, :options_value_key
312
+ alias_method :option_display_key, :options_display_key
313
+ alias_method :option_display_transform, :options_display_transform
314
+
315
+ def initialize(*args, &block)
316
+ @_options_display_transform = :to_s
317
+
318
+ super(*args, &block)
319
+
320
+ @_display_format = Kiss::Format.lookup(@_display_format)
321
+
322
+ if @_options
323
+ if @_options[0].is_a?(Array)
324
+ @_options_value_key ||= 0
325
+ (@_options_display_key ||= (@_options[0].size == 1) ? 0 : 1)
326
+ elsif defined?(Kiss::Model) && @_options[0].is_a?(Kiss::Model)
327
+ model_klass = @_options[0].class
328
+ @_options_value_key ||= model_klass.value_column
329
+ @_options_display_key ||= model_klass.display_column
330
+ elsif @_options[0].is_a?(Hash)
331
+ @_options_value_key ||= :id
332
+ @_options_display_key ||= :name
333
+ end
334
+ end
335
+
336
+ if @_other
337
+ @_other_field = @_form.create_field( { :name => @_name + '.other' }.merge(@_other) )
338
+ end
339
+ end
340
+
341
+ def form=(new_form)
342
+ super(new_form)
343
+ @_other_field.form = new_form if @_other_field
344
+ end
345
+
346
+ def display_to_s(value)
347
+ value ? @_display_format.value_to_s(value).send(@_options_display_transform) : ''
348
+ end
349
+
350
+ def options_keys(value, display)
351
+ @_options_value_key = value
352
+ @_options_display_key = display
353
+ end
354
+ alias_method :option_keys, :options_keys
355
+
356
+ def option_pairs
357
+ pairs = if @_options_value_key
358
+ if @_options_display_key.is_a?(Proc)
359
+ @_options.map {|option| [ option[@_options_value_key], @_options_display_key.call(option) ]}
360
+ else
361
+ @_options.map {|option| [
362
+ option[@_options_value_key] || option.send(@_options_value_key),
363
+ option[@_options_display_key] || option.send(@_options_display_key)
364
+ ]}
365
+ end
366
+ else
367
+ @_display_format = @_format
368
+ @_options.map {|option| [ option, option ]}
369
+ end
370
+
371
+ pairs
372
+ end
373
+
374
+ def has_option_value?(v)
375
+ !(@_options_value_key ?
376
+ @_options.select {|o| value_to_s(o[@_options_value_key]) == v } :
377
+ @_options.select {|o| value_to_s(o) == v }
378
+ ).empty?
379
+ end
380
+
381
+ def column_layout(elements_html)
382
+ if elements_html.empty?
383
+ ''
384
+ elsif @_columns
385
+ layout_columns = [@_columns, elements_html.size].min
386
+ num_elements_per_column = ((elements_html.size + layout_columns - 1) / layout_columns).to_i
387
+ layout_columns = ((elements_html.size + num_elements_per_column - 1) / num_elements_per_column).to_i
388
+
389
+ style = "style=\"width: #{(100 / layout_columns).to_i - 1}%\""
390
+
391
+ '<table class="kiss_field_columns"><tr>' +
392
+ (0...layout_columns).map do |i|
393
+ "<td #{style}>" + elements_html[i * num_elements_per_column, num_elements_per_column].join('<br/>') + "</td>"
394
+ end.join + '</table>'
395
+ else
396
+ elements_html.map {|h| "<nobr>#{h}</nobr>"}.join('&nbsp; ')
397
+ end
398
+ end
399
+
400
+ def validate
401
+ if @_other && param == 'other'
402
+ @_param = @_form.params[@_name+'.other']
403
+ end
404
+ super('select')
405
+
406
+ if @_value =~ /\S/ && !has_option_value?(@_value)
407
+ add_error "Invalid selection"
408
+ end
409
+ end
410
+
411
+ def other_field_html
412
+ return '' unless @_other
413
+
414
+ other_checked = @_value && !option_pairs.any? {|v, d| v == @_value }
415
+
416
+ (@_columns ? '<br/>' : '&nbsp; ') + [
417
+ input_tag_html(
418
+ { :value => 'other', :html => { :id => @_name+'.other' } },
419
+ other_checked ? 'checked' : ''
420
+ ),
421
+ @_other[:label] || 'Other',
422
+ ': ',
423
+ @_currency.to_s,
424
+ input_tag_html({
425
+ :type => :text,
426
+ :name => @_name+'.other',
427
+ :value => other_checked ? value_to_s(@_value) : nil,
428
+ :html => {
429
+ :onfocus => "document.getElementById('#{@_name}.other').checked = true"
430
+ }
431
+ }.merge(@_other))
432
+ ].join
433
+ end
434
+ end
435
+
436
+ # Subclass for field types in which multiple values can be chosen from a set
437
+ # of options.
438
+ class MultiValueField < MultiChoiceField
439
+ # (String) If not nil, then render a hidden form field specifying all of the
440
+ # values for this multi-value field, joined by this hidden_join delimiter.
441
+ # TODO: This may not be needed; remove if appropriate.
442
+ dsl_accessor :hidden_join
443
+
444
+ def param
445
+ @_form.params[@_name + '[]'] || []
446
+ end
447
+
448
+ def validate
449
+ begin
450
+ @_value = param.map { |p| @_format.validate(p) }
451
+ rescue Kiss::Format::ValidateError => e
452
+ return add_error("#{e.message.capitalize}.")
453
+ end
454
+
455
+ if @_value.empty? && @_required
456
+ return add_error "Please select at least one #{@_label.downcase.singularize}."
457
+ end
458
+
459
+ if @_min_value_size && @_value.size < @_min_value_size
460
+ return add_error "Please select at least #{@_min_value_size.of(@_label.downcase)}."
461
+ end
462
+
463
+ if @_max_value_size && @_value.size > @_max_value_size
464
+ return add_error "Please select no more than #{@_max_value_size.of(@_label.downcase)}."
465
+ end
466
+
467
+ @_value.each do |v|
468
+ unless has_option_value?(v)
469
+ return add_error "Invalid selection"
470
+ end
471
+ end
472
+ end
473
+
474
+ def selected_option_values
475
+ @_selected_option_values ||= @_value ? Hash[ *(@_value.map {|v| [value_to_s(v), true]}.flatten) ] : {}
476
+ end
477
+ end
478
+
479
+ ### Concrete Field Types ###
480
+
481
+ class HiddenField < Field; end
482
+ class TextField < Field; end
483
+
484
+ class TextAreaField < Field
485
+ dsl_accessor :rows, :cols
486
+
487
+ def initialize(*args)
488
+ @_rows = 5
489
+ @_cols = 20
490
+ super(*args)
491
+ end
492
+
493
+ def element_html(attrs = {})
494
+ content_tag_html(
495
+ 'textarea',
496
+ value_string,
497
+ attrs.merge(
498
+ :rows => @_rows ||= 1,
499
+ :cols => @_cols ||= 1
500
+ )
501
+ ) + tip_html(attrs)
502
+ end
282
503
  end
283
- end
284
- end
285
504
 
286
- require 'kiss/form/field_types'
505
+ class PasswordField < Field
506
+ def element_html(*args)
507
+ input_tag_html(*args)
508
+ end
509
+ end
510
+
511
+ class FileField < Field
512
+ def element_html(attrs = {})
513
+ input_tag_html(attrs) + tip_html(attrs)
514
+ end
515
+
516
+ def get_file_name; end
517
+
518
+ def get_file_data; end
519
+
520
+ def require_value(enter_verb)
521
+ p = param
522
+ return add_error("Please choose #{label.downcase}.") unless p && p[:type]
523
+ end
524
+
525
+ def validate
526
+ require_value(nil) if @_required
527
+ end
528
+ end
529
+
530
+ class RangeField < Field
531
+ dsl_accessor :min_field, :max_field
532
+
533
+ def initialize(form, *args, &block)
534
+ super(form, *args, &block)
535
+ @_type = :range
536
+
537
+ attrs = args.to_attrs
538
+ text_field_attrs = attrs.merge(
539
+ :style => 'width: 60px'
540
+ )
541
+ text_field_attrs[:html] ||= {}
542
+
543
+ [:min, :max].each do |key|
544
+ text_field_name = "#{key}_#{attrs.name}"
545
+ text_field_attrs.merge!(
546
+ :name => text_field_name,
547
+ :key => text_field_name.to_sym
548
+ )
549
+ text_field_attrs[:html] = text_field_attrs[:html].clone
550
+ text_field_attrs[:html][:placeholder] = key
551
+
552
+ instance_variable_set("@_#{key}_field".to_sym,
553
+ TextField.new(form, text_field_attrs, &block))
554
+ end
555
+ end
556
+
557
+ def validate
558
+ @_min_field.validate
559
+ @_max_field.validate
560
+
561
+ (@_min_field.errors + @_max_field.errors).each do |error|
562
+ add_error(error)
563
+ end
564
+ nil
565
+ end
566
+
567
+ def element_html
568
+ @_min_field.element_html + ' to ' + @_max_field.element_html
569
+ end
570
+
571
+ def set_value_to_hash(h)
572
+ @_min_field.set_value_to_hash(h)
573
+ @_max_field.set_value_to_hash(h)
574
+ end
575
+
576
+ def set_value_to_object(obj)
577
+ @_min_field.set_value_to_object(obj)
578
+ @_max_field.set_value_to_object(obj)
579
+ end
580
+ end
581
+
582
+ class SubmitField < MultiChoiceField
583
+ dsl_accessor :cancel
584
+
585
+ def initialize(*args)
586
+ @_save = false
587
+ super(*args)
588
+ end
589
+
590
+ def element_html(*args)
591
+ elements_html(*args).join(' ')
592
+ end
593
+
594
+ def elements_html(attrs = {})
595
+ @_options.map do |option|
596
+ input_tag_html(attrs.merge( :value => value_to_s(option) ))
597
+ end
598
+ end
599
+ end
600
+
601
+ class SelectField < MultiChoiceField
602
+ dsl_accessor :choose_here
603
+
604
+ def element_html(attrs = {})
605
+ return 'No options' unless @_options.size > 0
606
+
607
+ @_choose_here ||= 'Choose Here'
608
+ placeholder_html = %Q(<option value="">#{@_choose_here}</option>)
609
+
610
+ options_html = option_pairs.map do |option_value, option_display|
611
+ option_value_string = value_to_s(option_value)
612
+ selected = (value_string == option_value_string) ? ' selected' : ''
613
+ %Q(<option value="#{option_value_string}"#{selected}>#{display_to_s(option_display)}</option>)
614
+ end.join
615
+
616
+ content_tag_html('select', placeholder_html + options_html, attrs) + other_field_html + tip_html(attrs)
617
+ end
618
+ end
619
+
620
+ class RadioField < MultiChoiceField
621
+ def element_html(attrs = {})
622
+ column_layout(elements_html(attrs)) + other_field_html + tip_html(attrs)
623
+ end
624
+
625
+ def elements_html(attrs = {})
626
+ option_pairs.map do |option_value, option_display|
627
+ option_value_string = value_to_s(option_value)
628
+ input_tag_html(
629
+ attrs.merge( :type => 'radio', :value => option_value_string ),
630
+ (value_string == option_value_string) ? 'checked' : ''
631
+ ) + @_currency.to_s + display_to_s(option_display)
632
+ end
633
+ end
634
+ end
635
+
636
+ class BooleanField < RadioField
637
+ def initialize(*args, &block)
638
+ @_options = [[1, 'Yes'], [0, 'No']]
639
+ super(*args, &block)
640
+ end
641
+ end
642
+
643
+ class CheckboxField < MultiValueField
644
+ def element_html(attrs = {})
645
+ hidden_options = @_hidden_join ? input_tag_html(
646
+ :type => 'hidden',
647
+ :name => "#{@_name}_options",
648
+ :value => option_pairs.map {|option_value, option_display| value_to_s(option_value) }.join(@_hidden_join)
649
+ ) : ''
650
+
651
+ column_layout(elements_html(attrs)) + other_field_html + hidden_options + tip_html(attrs)
652
+ end
653
+
654
+ def elements_html(attrs = {})
655
+ name = @_name + '[]'
656
+ option_pairs.map do |option_value, option_display|
657
+ option_value_string = value_to_s(option_value)
658
+
659
+ input_tag_html(
660
+ attrs.merge( :name => name, :value => option_value_string ),
661
+ selected_option_values[option_value_string] ? 'checked' : ''
662
+ ) + @_currency.to_s + display_to_s(option_display)
663
+ end
664
+ end
665
+ end
666
+
667
+ class MultiSelectField < MultiValueField
668
+ def element_html(attrs = {})
669
+ options_html = option_pairs.map do |option_value, option_display|
670
+ option_value_string = value_to_s(option_value)
671
+ selected = selected_option_values[option_value_string] ? ' selected' : ''
672
+ %Q(<option value="#{option_value_string}"#{selected}>#{display_to_s(option_display)}</option>)
673
+ end.join
674
+
675
+ content_tag_html(
676
+ 'select',
677
+ options_html,
678
+ attrs,
679
+ 'multiple'
680
+ )
681
+ end
682
+ end
683
+
684
+ # Component type symbols used to create form definition DSL
685
+ COMPONENT_TYPES = {
686
+ :text => TextField,
687
+ :hidden => HiddenField,
688
+ :textarea => TextAreaField,
689
+ :password => PasswordField,
690
+ :boolean => BooleanField,
691
+ :range => RangeField,
692
+ :file => FileField,
693
+ :select => SelectField,
694
+ :radio => RadioField,
695
+ :checkbox => CheckboxField,
696
+ :multiselect => MultiSelectField,
697
+ :submit => SubmitField
698
+ }
699
+ COMPONENT_TYPES.each_pair do |type, klass|
700
+ klass.type = type
701
+ end
702
+ end
703
+ end