bivouac 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. data/README +9 -0
  2. data/bin/bivouac +15 -19
  3. data/doc/rdoc/classes/BivouacHelpers.html +117 -0
  4. data/doc/rdoc/classes/BivouacHelpers/BaseView.html +140 -0
  5. data/doc/rdoc/classes/BivouacHelpers/FormView.html +398 -0
  6. data/doc/rdoc/classes/BivouacHelpers/HtmlView.html +274 -0
  7. data/doc/rdoc/classes/BivouacHelpers/JavaScriptView.html +573 -0
  8. data/doc/rdoc/classes/BivouacHelpers/ScriptAculoUsView.html +258 -0
  9. data/doc/rdoc/classes/BivouacHelpers/TooltipView.html +158 -0
  10. data/doc/rdoc/classes/JavaScriptGenerator.html +564 -0
  11. data/doc/rdoc/created.rid +1 -1
  12. data/doc/rdoc/files/AUTHORS.html +14 -0
  13. data/doc/rdoc/files/COPYING.html +14 -0
  14. data/doc/rdoc/files/README.html +40 -1
  15. data/doc/rdoc/files/lib/bivouac/helpers/view/goh/base_rb.html +109 -0
  16. data/doc/rdoc/files/lib/bivouac/helpers/view/goh/form_rb.html +109 -0
  17. data/doc/rdoc/files/lib/bivouac/helpers/view/goh/html_rb.html +109 -0
  18. data/doc/rdoc/files/lib/bivouac/helpers/view/goh/javascript_rb.html +113 -0
  19. data/doc/rdoc/files/lib/bivouac/helpers/view/goh/scriptaculous_rb.html +113 -0
  20. data/doc/rdoc/files/lib/bivouac/helpers/view/goh/tooltip_rb.html +109 -0
  21. data/examples/bivouac_sample/app/bivouac_sample.rb +61 -0
  22. data/examples/bivouac_sample/app/controllers/autocomplete.rb +24 -0
  23. data/examples/bivouac_sample/app/controllers/drag_and_drop.rb +11 -0
  24. data/examples/bivouac_sample/app/controllers/index.rb +7 -0
  25. data/examples/bivouac_sample/app/controllers/observe.rb +14 -0
  26. data/examples/bivouac_sample/app/controllers/periodically_call.rb +12 -0
  27. data/examples/bivouac_sample/app/controllers/remote_form.rb +10 -0
  28. data/examples/bivouac_sample/app/controllers/remote_link.rb +11 -0
  29. data/examples/bivouac_sample/app/controllers/submit_remote.rb +17 -0
  30. data/examples/bivouac_sample/app/controllers/toggle.rb +10 -0
  31. data/examples/bivouac_sample/app/controllers/toggle_sortable.rb +11 -0
  32. data/examples/bivouac_sample/app/helpers/_helpers.rb +25 -0
  33. data/examples/bivouac_sample/app/helpers/source.rb +25 -0
  34. data/examples/bivouac_sample/app/views/_autocomplete_result.rb +9 -0
  35. data/examples/bivouac_sample/app/views/_drag_and_drop_result.rb +5 -0
  36. data/examples/bivouac_sample/app/views/_observe_result.rb +17 -0
  37. data/examples/bivouac_sample/app/views/_periodically_call_result.rb +5 -0
  38. data/examples/bivouac_sample/app/views/_remote_link_result.rb +5 -0
  39. data/examples/bivouac_sample/app/views/_sortable_result.rb +5 -0
  40. data/examples/bivouac_sample/app/views/_submit_remote_result.rb +5 -0
  41. data/examples/bivouac_sample/app/views/autocomplete.rb +28 -0
  42. data/examples/bivouac_sample/app/views/drag_and_drop.rb +35 -0
  43. data/examples/bivouac_sample/app/views/index.rb +19 -0
  44. data/examples/bivouac_sample/app/views/observe.rb +19 -0
  45. data/examples/bivouac_sample/app/views/periodically_call.rb +17 -0
  46. data/examples/bivouac_sample/app/views/remote_form.rb +19 -0
  47. data/examples/bivouac_sample/app/views/remote_link.rb +20 -0
  48. data/examples/bivouac_sample/app/views/submit_remote.rb +21 -0
  49. data/examples/bivouac_sample/app/views/toggle.rb +44 -0
  50. data/examples/bivouac_sample/app/views/toggle_sortable.rb +52 -0
  51. data/examples/bivouac_sample/config/environment.rb +37 -0
  52. data/examples/bivouac_sample/config/postamble.rb +62 -0
  53. data/examples/bivouac_sample/log/BivouacSample.log +77 -0
  54. data/examples/bivouac_sample/public/images/camping.png +0 -0
  55. data/examples/bivouac_sample/public/index.html +242 -0
  56. data/examples/bivouac_sample/public/javascripts/builder.js +131 -0
  57. data/examples/bivouac_sample/public/javascripts/controls.js +835 -0
  58. data/examples/bivouac_sample/public/javascripts/dragdrop.js +944 -0
  59. data/examples/bivouac_sample/public/javascripts/effects.js +1090 -0
  60. data/examples/bivouac_sample/public/javascripts/prototype.js +2515 -0
  61. data/examples/bivouac_sample/public/javascripts/scriptaculous.js +51 -0
  62. data/examples/bivouac_sample/public/javascripts/slider.js +278 -0
  63. data/examples/bivouac_sample/public/javascripts/tooltip.js +208 -0
  64. data/examples/bivouac_sample/public/javascripts/unittest.js +564 -0
  65. data/examples/bivouac_sample/public/stylesheets/autocomplete.css +22 -0
  66. data/examples/bivouac_sample/public/stylesheets/coderay.css +104 -0
  67. data/examples/bivouac_sample/script/generate +3 -0
  68. data/examples/bivouac_sample/script/server +5 -0
  69. data/lib/bivouac/helpers/view/goh/base.rb +16 -0
  70. data/lib/bivouac/helpers/view/goh/form.rb +170 -0
  71. data/lib/bivouac/helpers/view/goh/html.rb +138 -0
  72. data/lib/bivouac/helpers/view/goh/javascript.rb +532 -0
  73. data/lib/bivouac/helpers/view/goh/scriptaculous.rb +133 -0
  74. data/lib/bivouac/helpers/view/goh/tooltip.rb +33 -0
  75. data/lib/bivouac/template.rb +3 -3
  76. data/lib/bivouac/template/application/helpers_erb.rb +11 -0
  77. data/lib/bivouac/template/application/helpers_goh.rb +25 -0
  78. data/lib/bivouac/template/application/postamble.rb +62 -0
  79. data/lib/bivouac/template/application_erb.rb +4 -0
  80. data/lib/bivouac/template/application_goh.rb +4 -0
  81. data/lib/bivouac/template/environment.rb +19 -5
  82. data/lib/bivouac/template/server.rb +1 -1
  83. data/lib/bivouac/template/static/autocomplete.css +22 -0
  84. data/lib/bivouac/template/static/builder.js +131 -0
  85. data/lib/bivouac/template/static/controls.js +835 -0
  86. data/lib/bivouac/template/static/dragdrop.js +944 -0
  87. data/lib/bivouac/template/static/effects.js +1090 -0
  88. data/lib/bivouac/template/static/prototype.js +2515 -0
  89. data/lib/bivouac/template/static/scriptaculous.js +51 -0
  90. data/lib/bivouac/template/static/slider.js +278 -0
  91. data/lib/bivouac/template/static/tooltip.js +208 -0
  92. data/lib/bivouac/template/static/unittest.js +564 -0
  93. metadata +124 -7
  94. data/lib/bivouac/template/application/postamble_cgi.rb +0 -8
  95. data/lib/bivouac/template/application/postamble_fastcgi.rb +0 -8
  96. data/lib/bivouac/template/application/postamble_mongrel.rb +0 -19
  97. data/lib/bivouac/template/application/postamble_none.rb +0 -1
  98. data/lib/bivouac/template/application/postamble_webrick.rb +0 -19
@@ -0,0 +1,532 @@
1
+ # bivouac/helpers/view/javascript
2
+ require 'active_support'
3
+
4
+ class JavaScriptGenerator
5
+ def initialize( context, &block ) #:nodoc:
6
+ @context = context
7
+ @source = ""
8
+ yield( self )
9
+ end
10
+
11
+ def to_s #:nodoc:
12
+ javascript = "try {\n#{@source}\n} catch (e) "
13
+ javascript << "{ alert('RJS error:\\n\\n' + e.toString()); alert('#{@source.gsub('\\','\0\0').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" }}'); throw e }"
14
+
15
+ return javascript
16
+ end
17
+
18
+ # Returns a element reference by finding it through +id+ in the DOM. This element can then be
19
+ # used for further method calls. Examples:
20
+ #
21
+ # page['blank_slate'] # => $('blank_slate');
22
+ # page['blank_slate'].show # => $('blank_slate').show();
23
+ # page['blank_slate'].show('first').up # => $('blank_slate').show('first').up();
24
+ def []( id )
25
+ # JavaScriptElementProxy.new(self, id)
26
+ end
27
+
28
+ # Returns a collection reference by finding it through a CSS +pattern+ in the DOM. This collection can then be
29
+ # used for further method calls. Examples:
30
+ #
31
+ # page.select('p') # => $$('p');
32
+ # page.select('p.welcome b').first # => $$('p.welcome b').first();
33
+ # page.select('p.welcome b').first.hide # => $$('p.welcome b').first().hide();
34
+ #
35
+ # You can also use prototype enumerations with the collection. Observe:
36
+ #
37
+ # page.select('#items li').each do |value|
38
+ # value.hide
39
+ # end
40
+ # # => $$('#items li').each(function(value) { value.hide(); });
41
+ #
42
+ # Though you can call the block param anything you want, they are always rendered in the
43
+ # javascript as 'value, index.' Other enumerations, like collect() return the last statement:
44
+ #
45
+ # page.select('#items li').collect('hidden') do |item|
46
+ # item.hide
47
+ # end
48
+ # # => var hidden = $$('#items li').collect(function(value, index) { return value.hide(); });
49
+ def select(pattern)
50
+ # JavaScriptElementCollectionProxy.new(self, pattern)
51
+ end
52
+
53
+ # Writes raw JavaScript to the page.
54
+ def <<(javascript)
55
+ @source << javascript
56
+ end
57
+
58
+ # Displays an alert dialog with the given +message+.
59
+ def alert(message)
60
+ call( "alert", message )
61
+ end
62
+
63
+ # Assigns the JavaScript +variable+ the given +value+.
64
+ def assign( variable, value )
65
+ record "#{variable} = #{value.inspect}"
66
+ end
67
+
68
+ # Creates a script.aculo.us draggable element.
69
+ # See ActionView::Helpers::ScriptaculousHelper for more information.
70
+ def draggable( id, options = {} )
71
+ record @context.draggable_element_js( id, options ) + ";\n"
72
+ end
73
+
74
+ # Creates a script.aculo.us drop receiving element.
75
+ # See ActionView::Helpers::ScriptaculousHelper for more information.
76
+ def drop_receiving( id, options = {} )
77
+ record @context.drop_receiving_element_js( id, options ) + ";\n"
78
+ end
79
+
80
+ # Hides the visible DOM elements with the given +ids+.
81
+ def hide( *ids )
82
+ loop_on_multiple_args 'Element.hide', ids
83
+ end
84
+
85
+ # Inserts HTML at the specified +position+ relative to the DOM element
86
+ # identified by the given +id+.
87
+ #
88
+ # +position+ may be one of:
89
+ #
90
+ # <tt>:top</tt>:: HTML is inserted inside the element, before the
91
+ # element's existing content.
92
+ # <tt>:bottom</tt>:: HTML is inserted inside the element, after the
93
+ # element's existing content.
94
+ # <tt>:before</tt>:: HTML is inserted immediately preceeding the element.
95
+ # <tt>:after</tt>:: HTML is inserted immediately following the element.
96
+ #
97
+ # +data+ may be a string of HTML to insert.
98
+ def insert_html(position, id, data)
99
+ insertion = position.to_s.camelize
100
+ call "new Insertion.#{insertion}", id, data
101
+ end
102
+
103
+ # Calls the JavaScript +function+, optionally with the given +arguments+.
104
+ def call(function, *arguments)
105
+ record "#{function}(#{arguments_for_call(arguments)});\n"
106
+ end
107
+
108
+ # Redirects the browser to the given +location+.
109
+ def redirect_to(location)
110
+ assign 'window.location.href', location
111
+ end
112
+
113
+ # Removes the DOM elements with the given ids from the page.
114
+ def remove(*ids)
115
+ loop_on_multiple_args 'Element.remove', ids
116
+ end
117
+
118
+ # Replaces the inner HTML of the DOM element with the given +id+.
119
+ #
120
+ # +data+ may be a string of HTML to insert
121
+ #
122
+ # # Replace the HTML of the DOM element having ID 'person-45' with the
123
+ # # 'person' partial for the appropriate object.
124
+ # replace_html 'person-45', render( 'person', :object => @person )
125
+ #
126
+ def replace_html(id, data)
127
+ call 'Element.update', id, data
128
+ end
129
+
130
+ # Replaces the "outer HTML" (i.e., the entire element, not just its
131
+ # contents) of the DOM element with the given +id+.
132
+ #
133
+ # +data+ may be a string of HTML to insert.
134
+ # For example:
135
+ #
136
+ # # Replace the DOM element having ID 'person-45' with the
137
+ # # 'person' partial for the appropriate object.
138
+ # replace 'person-45', render( 'person', :object => @person )
139
+ def replace(id, data)
140
+ call 'Element.replace', id, data
141
+ end
142
+
143
+ # Shows hidden DOM elements with the given +ids+.
144
+ def show(*ids)
145
+ loop_on_multiple_args 'Element.show', ids
146
+ end
147
+
148
+ # Creates a script.aculo.us sortable element. Useful to recreate
149
+ # sortable elements after items get added or deleted.
150
+ def sortable(id, options = {})
151
+ record @context.sortable_element_js( id, options ) + ";\n"
152
+ end
153
+
154
+ # Creates a script.aculo.us unsortable element. Useful to recreate
155
+ # unsortable elements after items get added or deleted.
156
+ def unsortable( id )
157
+ record @context.unsortable_element_js( id ) + ";\n"
158
+ end
159
+
160
+ # Toggles the visibility of the DOM elements with the given +ids+.
161
+ def toggle(*ids)
162
+ loop_on_multiple_args 'Element.toggle', ids
163
+ end
164
+
165
+ # Starts a http://script.aculo.us visual effect.
166
+ def visual_effect(name, element_id = false, js_options = {})
167
+ record( @context.visual_effect( name, element_id, js_options ) + ";\n" )
168
+ end
169
+
170
+ # Executes the content of the block after a delay of +seconds+. Example:
171
+ #
172
+ # page.delay(20) do
173
+ # page.visual_effect :fade, 'notice'
174
+ # end
175
+ def delay(seconds = 1)
176
+ record "setTimeout(function() {\n\n"
177
+ yield
178
+ record "}, #{(seconds * 1000).to_i})"
179
+ end
180
+
181
+ private
182
+ def record( line )
183
+ self << line
184
+ end
185
+
186
+ def arguments_for_call(arguments)
187
+ arguments.map { |argument| argument.inspect }.join ', '
188
+ end
189
+
190
+ def loop_on_multiple_args(method, ids)
191
+ record( ids.size>1 ?
192
+ "#{ids.inspect}.each(#{method});\n" :
193
+ "#{method}(#{ids.first.inspect});\n" )
194
+ end
195
+ end
196
+
197
+ module BivouacHelpers
198
+ module JavaScriptView
199
+ CALLBACKS = [ :uninitialized, :loading, :loaded, :interactive, :complete, :failure, :success ] + ('100'..'599').to_a
200
+
201
+ # Escape carrier returns and single and double quotes for JavaScript segments.
202
+ def escape_javascript(javascript)
203
+ (javascript || '').gsub('\\','\0\0').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" }
204
+ end
205
+
206
+ # Returns a JavaScript tag with the +block+ inside.
207
+ # Example:
208
+ #
209
+ # javascript_tag( "alert('Hello World!')", :defer => 'true' )
210
+ #
211
+ # Returns:
212
+ #
213
+ # <script defer="true" type="text/javascript">
214
+ # //<![CDATA[
215
+ # alert('Hello World!')
216
+ # //]]>
217
+ # </script>
218
+ def javascript_tag( content, options = {} )
219
+ options[:type] = "text/javascript"
220
+ script( options ) do
221
+ "//<![CDATA[\n" + content + "\n//]]>"
222
+ end
223
+ end
224
+
225
+ # Yields a JavaScriptGenerator and returns the generated JavaScript code.
226
+ # Use this to update multiple elements on a page in an Ajax response. See
227
+ # JavaScriptGenerator for more information.
228
+ def update_page( &block )
229
+ JavaScriptGenerator.new( self, &block ).to_s
230
+ end
231
+
232
+ # Works like update_page but wraps the generated JavaScript in a <script>
233
+ # tag. See JavaScriptGenerator for more information.
234
+ def update_page_tag( options = {}, &block )
235
+ javascript_tag update_page( &block ), options
236
+ end
237
+
238
+ # Returns a link that will trigger a JavaScript function using the onclick
239
+ # handler and return false after the fact.
240
+ #
241
+ # The function argument can be omitted in favor of an update_page block,
242
+ # which evaluates to a string when the template is rendered (instead of
243
+ # making an Ajax request first).
244
+ #
245
+ # Examples:
246
+ #
247
+ # link_to_function "Greeting", "alert('Hello world!')"
248
+ # Produces:
249
+ # <a onclick="alert('Hello world!'); return false;" href="#">Greeting</a>
250
+ #
251
+ # link_to_function(image_tag("delete"), "if (confirm('Really?')) do_delete()")
252
+ # Produces:
253
+ # <a onclick="if (confirm('Really?')) do_delete(); return false;" href="#">
254
+ # <img src="/images/delete.png?" alt="Delete"/>
255
+ # </a>
256
+ #
257
+ # link_to_function("Show me more", nil, :id => "more_link") do |page|
258
+ # page[:details].visual_effect :toggle_blind
259
+ # page[:more_link].replace_html "Show me less"
260
+ # end
261
+ # Produces:
262
+ # <a href="#" id="more_link" onclick="try {
263
+ # $(&quot;details&quot;).visualEffect(&quot;toggle_blind&quot;);
264
+ # $(&quot;more_link&quot;).update(&quot;Show me less&quot;);
265
+ # }
266
+ # catch (e) {
267
+ # alert('RJS error:\n\n' + e.toString());
268
+ # alert('$(\&quot;details\&quot;).visualEffect(\&quot;toggle_blind\&quot;);
269
+ # \n$(\&quot;more_link\&quot;).update(\&quot;Show me less\&quot;);');
270
+ # throw e
271
+ # };
272
+ # return false;">Show me more</a>
273
+ def link_to_function( name, *args, &block )
274
+ html_options = args.last.is_a?(Hash) ? args.pop : {}
275
+ function = args[0] || ''
276
+
277
+ function = update_page( &block ) if block_given?
278
+
279
+ a( html_options.merge({
280
+ :href => html_options[:href] || "#",
281
+ :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function}; return false;"
282
+ }) ) do
283
+ name
284
+ end
285
+ end
286
+
287
+ # Returns a link to a remote action defined by <tt>options[:url]</tt>
288
+ # that's called in the background using XMLHttpRequest. The result of
289
+ # that request can then be inserted into a DOM object whose id can be
290
+ # specified with <tt>options[:update]</tt>. Usually, the result would
291
+ # be a partial prepared by the controller.
292
+ #
293
+ # Examples:
294
+ # link_to_remote "Delete this post", :update => "posts",
295
+ # :url => R(Destroy, 1)
296
+ # link_to_remote(image_tag("refresh"), :update => "emails",
297
+ # :url => R(ListEmails)
298
+ #
299
+ # You can also specify a hash for <tt>options[:update]</tt> to allow for
300
+ # easy redirection of output to an other DOM element if a server-side
301
+ # error occurs:
302
+ #
303
+ # Example:
304
+ # link_to_remote "Delete this post",
305
+ # :url => R(Destroy, 1)
306
+ # :update => { :success => "posts", :failure => "error" }
307
+ #
308
+ # Optionally, you can use the <tt>options[:position]</tt> parameter to
309
+ # influence how the target DOM element is updated. It must be one of
310
+ # <tt>:before</tt>, <tt>:top</tt>, <tt>:bottom</tt>, or <tt>:after</tt>.
311
+ #
312
+ # To access the server response, use <tt>request.responseText</tt>, to
313
+ # find out the HTTP status, use <tt>request.status</tt>.
314
+ #
315
+ # Example:
316
+ # link_to_remote word,
317
+ # :url => R(Undo, word_counter)
318
+ # :complete => "undoRequestCompleted(request)"
319
+ #
320
+ # The callbacks that may be specified are (in order):
321
+ #
322
+ # <tt>:loading</tt>:: Called when the remote document is being
323
+ # loaded with data by the browser.
324
+ # <tt>:loaded</tt>:: Called when the browser has finished loading
325
+ # the remote document.
326
+ # <tt>:interactive</tt>:: Called when the user can interact with the
327
+ # remote document, even though it has not
328
+ # finished loading.
329
+ # <tt>:success</tt>:: Called when the XMLHttpRequest is completed,
330
+ # and the HTTP status code is in the 2XX range.
331
+ # <tt>:failure</tt>:: Called when the XMLHttpRequest is completed,
332
+ # and the HTTP status code is not in the 2XX
333
+ # range.
334
+ # <tt>:complete</tt>:: Called when the XMLHttpRequest is complete
335
+ # (fires after success/failure if they are
336
+ # present).
337
+ #
338
+ # You can further refine <tt>:success</tt> and <tt>:failure</tt> by
339
+ # adding additional callbacks for specific status codes.
340
+ #
341
+ # Example:
342
+ # link_to_remote word,
343
+ # :url => R(Action),
344
+ # 404 => "alert('Not found...? Wrong URL...?')",
345
+ # :failure => "alert('HTTP Error ' + request.status + '!')"
346
+ #
347
+ # A status code callback overrides the success/failure handlers if
348
+ # present.
349
+ #
350
+ # If you for some reason or another need synchronous processing (that'll
351
+ # block the browser while the request is happening), you can specify
352
+ # <tt>options[:type] = :synchronous</tt>.
353
+ #
354
+ # You can customize further browser side call logic by passing in
355
+ # JavaScript code snippets via some optional parameters. In their order
356
+ # of use these are:
357
+ #
358
+ # <tt>:confirm</tt>:: Adds confirmation dialog.
359
+ # <tt>:condition</tt>:: Perform remote request conditionally
360
+ # by this expression. Use this to
361
+ # describe browser-side conditions when
362
+ # request should not be initiated.
363
+ # <tt>:before</tt>:: Called before request is initiated.
364
+ # <tt>:after</tt>:: Called immediately after request was
365
+ # initiated and before <tt>:loading</tt>.
366
+ # <tt>:submit</tt>:: Specifies the DOM element ID that's used
367
+ # as the parent of the form elements. By
368
+ # default this is the current form, but
369
+ # it could just as well be the ID of a
370
+ # table row or any other DOM element.
371
+ def link_to_remote(name, options = {}, html_options = {})
372
+ link_to_function(name, remote_function(options), html_options)
373
+ end
374
+
375
+ # Periodically calls the specified url (<tt>options[:url]</tt>) every
376
+ # <tt>options[:frequency]</tt> seconds (default is 10). Usually used to
377
+ # update a specified div (<tt>options[:update]</tt>) with the results
378
+ # of the remote call. The options for specifying the target with :url
379
+ # and defining callbacks is the same as link_to_remote.
380
+ def periodically_call_remote(options = {})
381
+ frequency = options[:frequency] || 10 # every ten seconds by default
382
+ code = "new PeriodicalExecuter(function() {#{remote_function(options)}}, #{frequency})"
383
+ javascript_tag(code)
384
+ end
385
+
386
+ # Observes the field with the DOM ID specified by +field_id+ and makes
387
+ # an Ajax call when its contents have changed.
388
+ #
389
+ # Required +options+ are either of:
390
+ # <tt>:url</tt>:: +url_for+-style options for the action to call
391
+ # when the field has changed.
392
+ # <tt>:function</tt>:: Instead of making a remote call to a URL, you
393
+ # can specify a function to be called instead.
394
+ #
395
+ # Additional options are:
396
+ # <tt>:frequency</tt>:: The frequency (in seconds) at which changes to
397
+ # this field will be detected. Not setting this
398
+ # option at all or to a value equal to or less than
399
+ # zero will use event based observation instead of
400
+ # time based observation.
401
+ # <tt>:update</tt>:: Specifies the DOM ID of the element whose
402
+ # innerHTML should be updated with the
403
+ # XMLHttpRequest response text.
404
+ # <tt>:with</tt>:: A JavaScript expression specifying the
405
+ # parameters for the XMLHttpRequest. This defaults
406
+ # to 'value', which in the evaluated context
407
+ # refers to the new field value. If you specify a
408
+ # string without a "=", it'll be extended to mean
409
+ # the form key that the value should be assigned to.
410
+ # So :with => "term" gives "'term'=value". If a "=" is
411
+ # present, no extension will happen.
412
+ # <tt>:on</tt>:: Specifies which event handler to observe. By default,
413
+ # it's set to "changed" for text fields and areas and
414
+ # "click" for radio buttons and checkboxes. With this,
415
+ # you can specify it instead to be "blur" or "focus" or
416
+ # any other event.
417
+ #
418
+ # Additionally, you may specify any of the options documented in
419
+ # link_to_remote.
420
+ def observe_field(field_id, options = {})
421
+ if options[:frequency] && options[:frequency] > 0
422
+ build_observer('Form.Element.Observer', field_id, options)
423
+ else
424
+ build_observer('Form.Element.EventObserver', field_id, options)
425
+ end
426
+ end
427
+
428
+ # Returns the JavaScript needed for a remote function.
429
+ # Takes the same arguments as link_to_remote.
430
+ #
431
+ # Example:
432
+ # select( :id => "options",
433
+ # :onChange => remote_function(
434
+ # :update => "options",
435
+ # :url => R(Update),
436
+ # :onSuccess => visual_effect( :highlight, 'my_element' )
437
+ # )
438
+ # ) do
439
+ # option( "Hello", :value => 0 )
440
+ # option( "World", :value => 1 )
441
+ # end
442
+ def remote_function(options)
443
+ javascript_options = options_for_ajax(options)
444
+
445
+ update = ''
446
+ if options[:update] && options[:update].is_a?(Hash)
447
+ update = []
448
+ update << "success:'#{options[:update][:success]}'" if options[:update][:success]
449
+ update << "failure:'#{options[:update][:failure]}'" if options[:update][:failure]
450
+ update = '{' + update.join(',') + '}'
451
+ elsif options[:update]
452
+ update << "'#{options[:update]}'"
453
+ end
454
+
455
+ function = update.empty? ?
456
+ "new Ajax.Request(" :
457
+ "new Ajax.Updater(#{update}, "
458
+
459
+ function << "'#{options[:url]}'"
460
+ function << ", #{javascript_options})"
461
+
462
+ function = "#{options[:before]}; #{function}" if options[:before]
463
+ function = "#{function}; #{options[:after]}" if options[:after]
464
+ function = "if (#{options[:condition]}) { #{function}; }" if options[:condition]
465
+ function = "if (confirm('#{escape_javascript(options[:confirm])}')) { #{function}; }" if options[:confirm]
466
+
467
+ return function
468
+ end
469
+
470
+ private
471
+
472
+ def options_for_javascript(options) #:nodoc:
473
+ '{' + options.map {|k, v| "#{k}:#{v}"}.sort.join(', ') + '}'
474
+ end
475
+
476
+ def array_or_string_for_javascript(option)
477
+ js_option = if option.kind_of?(Array)
478
+ "['#{option.join('\',\'')}']"
479
+ elsif !option.nil?
480
+ "'#{option}'"
481
+ end
482
+ js_option
483
+ end
484
+
485
+ def options_for_ajax(options)
486
+ js_options = build_callbacks(options)
487
+
488
+ js_options['asynchronous'] = options[:type] != :synchronous
489
+ js_options['method'] = method_option_to_s(options[:method]) if options[:method]
490
+ js_options['insertion'] = "Insertion.#{options[:position].to_s.camelize}" if options[:position]
491
+ js_options['evalScripts'] = options[:script].nil? || options[:script]
492
+
493
+ if options[:form]
494
+ js_options['parameters'] = 'Form.serialize(this)'
495
+ elsif options[:submit]
496
+ js_options['parameters'] = "Form.serialize('#{options[:submit]}')"
497
+ elsif options[:with]
498
+ js_options['parameters'] = options[:with]
499
+ end
500
+
501
+ options_for_javascript(js_options)
502
+ end
503
+
504
+ def build_callbacks( options )
505
+ callbacks = {}
506
+ options.each do |callback, code|
507
+ if CALLBACKS.include?(callback)
508
+ name = 'on' + callback.to_s.capitalize
509
+ callbacks[name] = "function(request){#{code}}"
510
+ end
511
+ end
512
+ callbacks
513
+ end
514
+
515
+ def build_observer(klass, name, options = {})
516
+ if options[:with] && !options[:with].include?("=")
517
+ options[:with] = "'#{options[:with]}=' + value"
518
+ else
519
+ options[:with] ||= "'#{name}=' + value" if options[:update]
520
+ end
521
+
522
+ callback = options[:function] || remote_function(options)
523
+ javascript = "new #{klass}('#{name}', "
524
+ javascript << "#{options[:frequency]}, " if options[:frequency]
525
+ javascript << "function(element, value) {"
526
+ javascript << "#{callback}}"
527
+ javascript << ", '#{options[:on]}'" if options[:on]
528
+ javascript << ")"
529
+ javascript_tag(javascript)
530
+ end
531
+ end
532
+ end