kiss 0.9

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.
@@ -0,0 +1,359 @@
1
+ class Kiss
2
+ # Generates HTML exception reports for Rack::ShowExceptions and
3
+ # Rack::LogExceptions.
4
+ class ExceptionReport
5
+ @@context = 7
6
+
7
+ class << self
8
+ def generate(env, exception)
9
+ req = Rack::Request.new(env)
10
+ path = (req.script_name + req.path_info).squeeze("/")
11
+ url = req.scheme + '://' + req.host + path
12
+
13
+ backtrace = exception.backtrace
14
+ #backtrace.shift while lines[0] =~ /\/lib\/kiss(\/|\.rb)/
15
+
16
+ frames = backtrace.map { |line|
17
+ frame = {}
18
+ if line =~ /(.*?):(\d+)(:in `(.*)')?/
19
+ frame.filename = $1
20
+ frame.lineno = $2.to_i
21
+ frame.function = $4
22
+ lines = nil
23
+
24
+ begin
25
+ lineno = frame.lineno-1
26
+ lines ||= ::File.readlines(frame.filename)
27
+ frame.pre_context_lineno = [lineno-@@context, 0].max
28
+ frame.pre_context = lines[frame.pre_context_lineno...lineno]
29
+ frame.context_line = lines[lineno].chomp
30
+ frame.post_context_lineno = [lineno+@@context, lines.size].min
31
+ frame.post_context = lines[lineno+1..frame.post_context_lineno]
32
+ rescue
33
+ end
34
+
35
+ frame
36
+ else
37
+ nil
38
+ end
39
+ }.compact
40
+
41
+ env["rack.errors"].puts "#{exception.class}: #{exception.message}"
42
+ env["rack.errors"].puts exception.backtrace.map { |l| "\t" + l }
43
+ env["rack.errors"].flush
44
+
45
+ db_query = begin (Sequel::MySQL::Database.last_query) rescue nil end
46
+
47
+ @@erubis ||= Erubis::Eruby.new(template)
48
+ @@erubis.result(binding)
49
+ end
50
+
51
+ def absolute_path(filename)
52
+ filename = ( filename =~ /\A\// ? '' : (Dir.pwd + '/') ) + filename
53
+ end
54
+
55
+ def textmate_href(frame)
56
+ "txmt://open?url=file://"+(h(absolute_path(frame.filename)))+"&amp;line="+(h frame.lineno)
57
+ end
58
+
59
+ def h(obj) # :nodoc:
60
+ case obj
61
+ when String
62
+ Rack::Utils.escape_html(obj).gsub(/^(\s+)/) {'&nbsp;' * $1.length}
63
+ else
64
+ Rack::Utils.escape_html(obj.inspect)
65
+ end
66
+ end
67
+
68
+ # :stopdoc:
69
+
70
+ # adapted from Django <djangoproject.com>
71
+ # Copyright (c) 2005, the Lawrence Journal-World
72
+ # Used under the modified BSD license:
73
+ # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
74
+ def template
75
+ <<-EOT
76
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
77
+ <html lang="en">
78
+ <head>
79
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
80
+ <meta name="robots" content="NONE,NOARCHIVE" />
81
+ <title>Exception: <%=h exception.class %> - <%=h url %></title>
82
+ <style type="text/css">
83
+ html * { padding:0; margin:0; }
84
+ body *, .body * { padding:10px 20px; }
85
+ body * *, .body * * { padding:0; }
86
+ body { font:small sans-serif; }
87
+ body>div { border-bottom:1px solid #ddd; }
88
+ h1 { font-weight:normal; font-size: 18px; font-style: italic; color: #d96 }
89
+ h2 { margin-bottom:.8em; }
90
+ h2 span { font-size:80%; color:#000; font-weight:normal; }
91
+ h3 { margin:1em 0 .5em 0; }
92
+ h4 { margin:0 0 .5em 0; font-weight: normal; }
93
+ table {
94
+ border:1px solid #ccc; border-collapse: collapse; background:white; }
95
+ tbody td, tbody th { vertical-align:top; padding:2px 3px; }
96
+ thead th {
97
+ padding:1px 6px 1px 3px; background:#fefefe; text-align:left;
98
+ font-weight:normal; font-size:11px; border:1px solid #ddd; }
99
+ tbody th { text-align:right; color:#666; padding-right:.5em; }
100
+ table.vars { margin:5px 0 2px 40px; }
101
+ table.vars td, table.req td { font-family:monospace; }
102
+ table td.code { width:100%;}
103
+ table td.code div { overflow:hidden; }
104
+ table.source th { color:#666; }
105
+ table.source td {
106
+ font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
107
+ ul.traceback { list-style-type:none; }
108
+ ul.traceback li.frame { margin-bottom:1em; }
109
+ div.context { margin: 10px 0 10px 40px; background-color: #fff; }
110
+ div.context ol {
111
+ padding: 1px; margin-bottom: -1px; }
112
+ div.context ol li {
113
+ font-family:monospace; color:#666; cursor:pointer; padding: 0 2px; }
114
+ div.context ol.context-line li { color:black; background-color:#eca; }
115
+ div.context ol.context-line li span { float: right; }
116
+ div.commands { margin-left: 40px; }
117
+ div.commands a { color:black; text-decoration:none; }
118
+ #summary { background: #ffd; }
119
+ #summary h2 { font-weight: normal; color: #a10; font-size: 14px; font-weight: bold }
120
+ #summary ul#quicklinks { list-style-type: none; margin-bottom: 2em; }
121
+ #summary ul#quicklinks li { float: left; padding: 0 1em; }
122
+ #summary ul#quicklinks>li+li { border-left: 1px #666 solid; }
123
+ #explanation { background:#eee; }
124
+ #template, #template-not-exist { background:#f6f6f6; }
125
+ #template-not-exist ul { margin: 0 0 0 20px; }
126
+ #traceback { background:#eee; }
127
+ #requestinfo { background:#f6f6f6; padding-left:120px; }
128
+ #summary table { border:none; background:transparent; }
129
+ #requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; }
130
+ #requestinfo h3 { margin-bottom:-1em; }
131
+ .error { background: #ffc; }
132
+ .specific { color:#cc3300; font-weight:bold; }
133
+ </style>
134
+ <script type="text/javascript">
135
+ //<!--
136
+ function getElementsByClassName(oElm, strTagName, strClassName){
137
+ // Written by Jonathan Snook, http://www.snook.ca/jon;
138
+ // Add-ons by Robert Nyman, http://www.robertnyman.com
139
+ var arrElements = (strTagName == "*" && document.all)? document.all :
140
+ oElm.getElementsByTagName(strTagName);
141
+ var arrReturnElements = new Array();
142
+ strClassName = strClassName.replace(/\-/g, "\\-");
143
+ var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$$)");
144
+ var oElement;
145
+ for(var i=0; i<arrElements.length; i++){
146
+ oElement = arrElements[i];
147
+ if(oRegExp.test(oElement.className)){
148
+ arrReturnElements.push(oElement);
149
+ }
150
+ }
151
+ return (arrReturnElements)
152
+ }
153
+ function hideAll(elems) {
154
+ for (var e = 0; e < elems.length; e++) {
155
+ elems[e].style.display = 'none';
156
+ }
157
+ }
158
+ window.onload = function() {
159
+ hideAll(getElementsByClassName(document, 'table', 'vars'));
160
+ hideAll(getElementsByClassName(document, 'ol', 'pre-context'));
161
+ hideAll(getElementsByClassName(document, 'ol', 'post-context'));
162
+ toggle('pre<%=h frames.first.object_id %>', 'post<%=h frames.first.object_id %>')
163
+ }
164
+ function toggle() {
165
+ for (var i = 0; i < arguments.length; i++) {
166
+ var e = document.getElementById(arguments[i]);
167
+ if (e) {
168
+ e.style.display = e.style.display == 'none' ? 'block' : 'none';
169
+ }
170
+ }
171
+ return false;
172
+ }
173
+
174
+ function varToggle(link, id) {
175
+ toggle('v' + id);
176
+ var s = link.getElementsByTagName('span')[0];
177
+ var uarr = String.fromCharCode(0x25b6);
178
+ var darr = String.fromCharCode(0x25bc);
179
+ s.innerHTML = s.innerHTML == uarr ? darr : uarr;
180
+ return false;
181
+ }
182
+ //-->
183
+ </script>
184
+ </head>
185
+ <body>
186
+
187
+ <div id="summary">
188
+ <a name="summary"></a>
189
+ <h1><%=h exception.class %></h1>
190
+ <h2><code><%=(h exception.message).gsub(/\n/,'<br>') %></code></h2>
191
+ <table><tr>
192
+ <th>Ruby</th>
193
+ <td><code><%=h frames.first.filename %></code>: in <code><%=h frames.first.function %></code>, <a href="<%= textmate_href(frames.first) %>">line <%=h frames.first.lineno %></a></td>
194
+ </tr><tr>
195
+ <th>Web</th>
196
+ <td><code><%=h req.request_method %> <%=h(url)%></code></td>
197
+ </tr></table>
198
+
199
+ <h3>Jump to:</h3>
200
+ <ul id="quicklinks">
201
+ <li><a href="#cache">Cache</a></li>
202
+ <li><a href="#db-query">DB Query</a></li>
203
+ <li><a href="#get-info">GET</a></li>
204
+ <li><a href="#post-info">POST</a></li>
205
+ <li><a href="#cookie-info">Cookies</a></li>
206
+ <li><a href="#env-info">ENV</a></li>
207
+ </ul>
208
+ </div>
209
+
210
+ <div id="traceback">
211
+ <h2>Traceback <span>(innermost first)</span></h2>
212
+ <ul class="traceback">
213
+ <% frames.each { |frame| %>
214
+ <li class="frame">
215
+ <a href="<%= textmate_href(frame) %>">line <%= h frame.lineno %></a> of <code><%=h frame.filename %></code> (in <code><b><%=h frame.function %></b></code>)
216
+
217
+ <% if frame.context_line %>
218
+ <div class="context" id="c<%=h frame.object_id %>">
219
+ <% if frame.pre_context %>
220
+ <ol start="<%=h frame.pre_context_lineno+1 %>" class="pre-context" id="pre<%=h frame.object_id %>">
221
+ <% frame.pre_context.each { |line| %>
222
+ <li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h line %></li>
223
+ <% } %>
224
+ </ol>
225
+ <% end %>
226
+
227
+ <ol start="<%=h frame.lineno %>" class="context-line">
228
+ <li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h frame.context_line %></li></ol>
229
+
230
+ <% if frame.post_context %>
231
+ <ol start='<%=h frame.lineno+1 %>' class="post-context" id="post<%=h frame.object_id %>">
232
+ <% frame.post_context.each { |line| %>
233
+ <li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h line %></li>
234
+ <% } %>
235
+ </ol>
236
+ <% end %>
237
+ </div>
238
+ <% end %>
239
+ </li>
240
+ <% } %>
241
+ </ul>
242
+ </div>
243
+
244
+ <div id="requestinfo">
245
+ <h2>Additional information</h2>
246
+
247
+ <a name="cache"></a>
248
+ <h3 id="cache">Cache</h3>
249
+ <p><code><%=h Kiss.exception_cache.inspect %></code></p>
250
+
251
+ <a name="db-query"></a>
252
+ <h3 id="db-query">DB Query</h3>
253
+ <p><% if db_query %><code><%=(h db_query).gsub(/\n/,'<br/>') %></code><% else %>No database queries executed.<% end %></p>
254
+
255
+ <a name="get-info"></a>
256
+ <h3 id="get-info">GET</h3>
257
+ <% unless req.GET.empty? %>
258
+ <table class="req">
259
+ <thead>
260
+ <tr>
261
+ <th>Variable</th>
262
+ <th>Value</th>
263
+ </tr>
264
+ </thead>
265
+ <tbody>
266
+ <% req.GET.sort_by { |k, v| k.to_s }.each { |key, val| %>
267
+ <tr>
268
+ <td><%=h key %></td>
269
+ <td class="code"><div><%=h val.inspect %></div></td>
270
+ </tr>
271
+ <% } %>
272
+ </tbody>
273
+ </table>
274
+ <% else %>
275
+ <p>No GET data.</p>
276
+ <% end %>
277
+
278
+ <a name="post-info"></a>
279
+ <h3 id="post-info">POST</h3>
280
+ <% unless req.POST.empty? %>
281
+ <table class="req">
282
+ <thead>
283
+ <tr>
284
+ <th>Variable</th>
285
+ <th>Value</th>
286
+ </tr>
287
+ </thead>
288
+ <tbody>
289
+ <% req.POST.sort_by { |k, v| k.to_s }.each { |key, val| %>
290
+ <tr>
291
+ <td><%=h key %></td>
292
+ <td class="code"><div><%=h val.inspect %></div></td>
293
+ </tr>
294
+ <% } %>
295
+ </tbody>
296
+ </table>
297
+ <% else %>
298
+ <p>No POST data.</p>
299
+ <% end %>
300
+
301
+ <a name="cookie-info"></a>
302
+ <h3 id="cookie-info">Cookies</h3>
303
+ <% unless req.cookies.empty? %>
304
+ <table class="req">
305
+ <thead>
306
+ <tr>
307
+ <th>Variable</th>
308
+ <th>Value</th>
309
+ </tr>
310
+ </thead>
311
+ <tbody>
312
+ <% req.cookies.each { |key, val| %>
313
+ <tr>
314
+ <td><%=h key %></td>
315
+ <td class="code"><div><%=h val.inspect %></div></td>
316
+ </tr>
317
+ <% } %>
318
+ </tbody>
319
+ </table>
320
+ <% else %>
321
+ <p>No cookie data.</p>
322
+ <% end %>
323
+
324
+ <a name="env-info"></a>
325
+ <h3 id="env-info">Rack ENV</h3>
326
+ <table class="req">
327
+ <thead>
328
+ <tr>
329
+ <th>Variable</th>
330
+ <th>Value</th>
331
+ </tr>
332
+ </thead>
333
+ <tbody>
334
+ <% env.sort_by { |k, v| k.to_s }.each { |key, val| %>
335
+ <tr>
336
+ <td><%=h key %></td>
337
+ <td class="code"><div><%=h val %></div></td>
338
+ </tr>
339
+ <% } %>
340
+ </tbody>
341
+ </table>
342
+
343
+ </div>
344
+
345
+ <div id="explanation">
346
+ <p>
347
+ Generated by Kiss::ExceptionReport.
348
+ </p>
349
+ </div>
350
+
351
+ </body>
352
+ </html>
353
+ EOT
354
+ end
355
+
356
+ # :startdoc:
357
+ end
358
+ end
359
+ end
@@ -0,0 +1,296 @@
1
+ class Kiss
2
+ class Form
3
+ class Field
4
+ attr_accessor :name,:type,:form,:format,:currency,:label,:no_label,:prompt,:value,:ignore,:save,
5
+ :options,:options_value_key,:options_display_key,:required,:cancel,:columns,:style,:hidden_join
6
+
7
+ include Kiss::Form::AttributesSetter
8
+
9
+ def initialize(attrs)
10
+ @save = true
11
+ @value = ''
12
+
13
+ set_attributes(attrs,[:name,:type])
14
+
15
+ @errors = []
16
+ end
17
+
18
+ def column_layout(elements_html)
19
+ if @columns
20
+ # format elements into cells
21
+ elements_html.map! { |el| "<td>" + el + "</td>" }
22
+
23
+ # compute height = ceiling(number of elements / columns)
24
+ height = ((elements_html.size + columns - 1)/columns).to_i
25
+
26
+ # group cells into columns
27
+ element_ranges = (1..height).to_a.map {|i| ((i-1)*@columns..(i*@columns-1))}
28
+ return ['<table class="kiss_field_columns">',element_ranges.map do |range|
29
+ ["<tr>", elements_html[range], "</tr>"]
30
+ end,'</table>'].flatten.join
31
+ join_with = ''
32
+ else
33
+ join_with = '&nbsp; '
34
+ end
35
+
36
+ # else columns <= 1
37
+ elements_html.join(join_with)
38
+ end
39
+
40
+ def sequel_value
41
+ case @format
42
+ when :date:
43
+ return Kiss.mdy_to_ymd(value)
44
+ when :datetime:
45
+ datetime = value.sub(/([ap]m)\s*\Z/i,' \1')
46
+ date, time, ampm = datetime.split(/\s+/)
47
+
48
+ hours, minutes = time.split(/:/)
49
+ hours = 0 if hours.to_i == 12 && ampm
50
+ if (ampm.downcase == 'pm')
51
+ hours = hours.to_i + 12
52
+ end
53
+
54
+ return Kiss.mdy_to_ymd(date) + " #{hours}:#{minutes}"
55
+ else
56
+ return value.is_a?(Array) ? value.map {|v| v.gsub(/,/,'\,')}.join(',') : value
57
+ end
58
+ end
59
+
60
+ def value
61
+ @value ||= @form.params[@name]
62
+ end
63
+
64
+ def add_error(message)
65
+ @errors << message
66
+ @form.has_field_errors = true
67
+ end
68
+
69
+ def validate
70
+ if (error = Kiss.validate_value(value,@format,@required))
71
+ add_error("#{@label} #{error}")
72
+ end
73
+
74
+ value
75
+ end
76
+
77
+ def errors_html
78
+ return nil unless @errors.size > 0
79
+
80
+ if @errors.size == 1
81
+ content = @errors[0]
82
+ else
83
+ content = "<ul>" + @errors.map {|e| "<li>#{e}</li>"}.join + "</ul>"
84
+ end
85
+
86
+ %Q(<span class="#{@form.field_error_class}">#{content}</span><br clear="all" />)
87
+ end
88
+
89
+ def element_html
90
+ if @currency == :dollars
91
+ return '$' + input_tag_html( :value => @value, :style => 'width: 80px' )
92
+ end
93
+ input_tag_html( :value => @value )
94
+ end
95
+
96
+ def html
97
+ errors = errors_html
98
+ element_html + (errors ? %Q(<br/>#{errors}) : '')
99
+ end
100
+
101
+ def input_tag_html(attrs = {}, extra_html = '')
102
+ attrs = attrs.clone
103
+ attrs[:name] ||= @name
104
+ attrs[:type] ||= @type
105
+ attrs[:style] ||= @style
106
+
107
+ attrs_html = []
108
+ attrs.each_pair {|k,v| attrs_html.push %Q(#{k}="#{v}") }
109
+ [ '<input', attrs_html, extra_html + '/>' ].flatten.join(' ')
110
+ end
111
+ end
112
+
113
+ class HiddenField < Field
114
+ end
115
+ class TextField < Field
116
+ end
117
+
118
+ class TextAreaField < Field
119
+ attr_accessor :rows,:cols
120
+
121
+ def initialize(*args)
122
+ @rows = 5
123
+ @cols = 20
124
+ super(*args)
125
+ end
126
+ def element_html
127
+ %Q(<textarea name="#{@name}" rows="#{@rows ||= 1}" cols="#{@cols ||= 1}" style="#{@style ||= ''}">#{@value}</textarea>)
128
+ end
129
+ end
130
+
131
+ class PasswordField < Field
132
+ def element_html
133
+ input_tag_html
134
+ end
135
+ end
136
+
137
+ class BooleanField < Field
138
+ def element_html
139
+ input_tag_html({ :value => 1 }, @value ? 'CHECKED ' : '')
140
+ end
141
+ end
142
+
143
+ class FileField < Field
144
+ def element_html
145
+ input_tag_html
146
+ end
147
+
148
+ def get_file_name
149
+
150
+ end
151
+
152
+ def get_file_data
153
+
154
+ end
155
+ end
156
+
157
+ class SubmitField < Field
158
+ def initialize(*args)
159
+ @save = false
160
+ super(*args)
161
+ end
162
+
163
+ def element_html
164
+ elements_html.join(' ')
165
+ end
166
+
167
+ def validate
168
+ # do nothing for now
169
+ value
170
+ end
171
+
172
+ def elements_html
173
+ @options.map do |option|
174
+ input_tag_html({ :value => option })
175
+ end
176
+ end
177
+ end
178
+
179
+ # ------ MultiChoiceField
180
+
181
+ class MultiChoiceField < Field
182
+ def option_pairs
183
+ if (defined? @options_value_key)
184
+ # options_display_map not supported
185
+ # default to options_display_key
186
+ return @options.map {|option| [ option[@options_value_key], option[@options_display_key] ]}
187
+ end
188
+
189
+ @options.map {|option| [ option, option ] }
190
+ end
191
+
192
+ def matched_options(value)
193
+ value = [value].flatten
194
+ @options_value_key ?
195
+ @options.select {|o| value.select {|v| o[@options_value_key] == v } }.flatten :
196
+ @options.select {|o| value.select {|v| o == v } }.flatten
197
+ end
198
+
199
+ def validate
200
+ @value = @form.params[@name]
201
+
202
+ if (@value =~ /\S/)
203
+ add_error "#{@label} invalid" unless matched_options(@value).size > 0
204
+ elsif @required
205
+ add_error "#{@label} required"
206
+ end
207
+ end
208
+ end
209
+
210
+ class SelectField < MultiChoiceField
211
+ def element_html
212
+ return 'No options' unless @options.size > 0
213
+
214
+ placeholder_html = %Q(<option value="">Choose Here</option>)
215
+
216
+ options_html = option_pairs.map do |value,display|
217
+ selected = (@value.to_s == value.to_s) ? ' selected' : ''
218
+ %Q(<option value="#{value}"#{selected}>#{display}</option>)
219
+ end
220
+
221
+ [%Q(<select name="#{@name}" style="#{@style}">), placeholder_html, options_html, '</select>'].flatten.join
222
+ end
223
+ end
224
+
225
+ class RadioField < MultiChoiceField
226
+ def element_html
227
+ column_layout(elements_html)
228
+ end
229
+
230
+ def elements_html
231
+ return option_pairs.map do |value,display|
232
+ input_tag_html({ :value => value }, (@value.to_s == value.to_s) ? ' checked' : '' ) + display
233
+ end
234
+ end
235
+ end
236
+
237
+
238
+ # ------ MultiValueField
239
+
240
+ class MultiValueField < MultiChoiceField
241
+ def validate
242
+ @value = @form.params[@name.to_s+'[]'] || []
243
+
244
+ if @value.size > 0
245
+ @value.each do |value|
246
+ unless (matched_options(value).size > 0)
247
+ add_error "Invalid selection for #{@label}"
248
+ return
249
+ end
250
+ end
251
+ elsif @required
252
+ add_error "#{@label} required"
253
+ end
254
+ end
255
+
256
+ def selected_option_values
257
+ @selected_option_values ||= @value ? Hash[ *(@value.map {|v| [v.to_s,true]}.flatten) ] : {}
258
+ end
259
+ end
260
+
261
+ class CheckboxField < MultiValueField
262
+ def element_html
263
+ if @hidden_join
264
+ hidden_options = input_tag_html(
265
+ :type => 'hidden',
266
+ :name => "#{@name}_options",
267
+ :value => option_pairs.map {|v,d| v}.join(@hidden_join)
268
+ )
269
+ else
270
+ hidden_options = ''
271
+ end
272
+ column_layout(elements_html) + hidden_options
273
+ end
274
+
275
+ def elements_html
276
+ return option_pairs.map do |value,display|
277
+ input_tag_html({ :name => @name.to_s+'[]', :value => value }, selected_option_values[value.to_s] ? ' checked' : '' ) + display.to_s
278
+ end
279
+ end
280
+ end
281
+
282
+ class MultiSelectField < MultiValueField
283
+ def element_html
284
+ options_html = option_pairs.map do |value,display|
285
+ selected = selected_option_values[value] ? ' selected' : ''
286
+ %Q(<option value="#{value}"#{selected}>#{display}</option>)
287
+ end
288
+
289
+ [%Q(<select multiple name="#{@name}[]" size="#{@size}" style="#{@style}">), options_html, '</select>'].flatten.join
290
+ end
291
+ end
292
+ end
293
+ end
294
+
295
+ # not implemented yet:
296
+ # :multitext => MultiTextField,