de_rjs 0.2.1

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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d660448d4fe1823a9f71d6a2f4f4ba5d1e38ac95
4
+ data.tar.gz: bd42c4e907d97a136d75939823d7371f5aac7b47
5
+ SHA512:
6
+ metadata.gz: d86d3dfc21d72253e42118f5034a533d70fd1d7c52e20164bb6a9afe69c70763d0acb2ccc4a3e6814eab8e3e45334a70481845b6221bbb6b2c13b9ddfca9da89
7
+ data.tar.gz: ce15d413bdc07281b20cc5a4ef8110c3927b6b3ee4cd42470496b3bdff159c72bd03ab2fd3708097360fe130678a9ffb7cbf0a2ee383648a89aee7e46abe78a2
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/README ADDED
@@ -0,0 +1,3 @@
1
+ Use this to de-RJS your application.
2
+
3
+ Converts your .rjs code into js.erb compliant code
@@ -0,0 +1,10 @@
1
+ require 'rake/testtask'
2
+
3
+ task :default => :test
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << 'test'
7
+ t.test_files = Dir.glob('test/*_test.rb') + Dir.glob('test/{controller,template}/**/*_test.rb')
8
+ t.warning = true
9
+ t.verbose = true
10
+ end
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
4
+
5
+ require 'de_rjs/runner'
6
+ DeRjs::Runner.new(ARGV).execute
@@ -0,0 +1,12 @@
1
+ require 'rails'
2
+ require 'active_support'
3
+
4
+ unless defined? JQUERY_VAR
5
+ JQUERY_VAR = 'jQuery'
6
+ end
7
+
8
+ module DeRjs
9
+ end
10
+
11
+ require 'de_rjs/jquery_generator'
12
+ require 'de_rjs/rewriter'
@@ -0,0 +1,789 @@
1
+ unless defined? JQUERY_VAR
2
+ JQUERY_VAR = 'jQuery'
3
+ end
4
+
5
+ module DeRjs
6
+ # A string that returns itself as its JSON-encoded form.
7
+ class JsonLiteral < String
8
+ def as_json(options = nil) self end #:nodoc:
9
+ def encode_json(encoder) self end #:nodoc:
10
+ end
11
+
12
+ class JqueryGenerator #:nodoc:
13
+ class OutputBuffer < Array
14
+ def encoding
15
+ Encoding::UTF_8
16
+ end
17
+ end
18
+ def initialize(context, &block) #:nodoc:
19
+ @context, @lines = context, OutputBuffer.new
20
+ include_helpers_from_context
21
+ #@context.with_output_buffer(@lines) do
22
+ #@context.instance_exec(self, &block)
23
+ #end
24
+ self.instance_exec(&block)
25
+ end
26
+
27
+ private
28
+ def include_helpers_from_context
29
+ #extend @context.helpers if @context.respond_to?(:helpers) && @context.helpers
30
+ extend GeneratorMethods
31
+ end
32
+
33
+ # JavaScriptGenerator generates blocks of JavaScript code that allow you
34
+ # to change the content and presentation of multiple DOM elements. Use
35
+ # this in your Ajax response bodies, either in a <tt>\<script></tt> tag
36
+ # or as plain JavaScript sent with a Content-type of "text/javascript".
37
+ #
38
+ # Create new instances with PrototypeHelper#update_page or with
39
+ # ActionController::Base#render, then call +insert_html+, +replace_html+,
40
+ # +remove+, +show+, +hide+, +visual_effect+, or any other of the built-in
41
+ # methods on the yielded generator in any order you like to modify the
42
+ # content and appearance of the current page.
43
+ #
44
+ # Example:
45
+ #
46
+ # # Generates:
47
+ # # new Element.insert("list", { bottom: "<li>Some item</li>" });
48
+ # # new Effect.Highlight("list");
49
+ # # ["status-indicator", "cancel-link"].each(Element.hide);
50
+ # update_page do |page|
51
+ # page.insert_html :bottom, 'list', "<li>#{@item.name}</li>"
52
+ # page.visual_effect :highlight, 'list'
53
+ # page.hide 'status-indicator', 'cancel-link'
54
+ # end
55
+ #
56
+ #
57
+ # Helper methods can be used in conjunction with JavaScriptGenerator.
58
+ # When a helper method is called inside an update block on the +page+
59
+ # object, that method will also have access to a +page+ object.
60
+ #
61
+ # Example:
62
+ #
63
+ # module ApplicationHelper
64
+ # def update_time
65
+ # page.replace_html 'time', Time.now.to_s(:db)
66
+ # page.visual_effect :highlight, 'time'
67
+ # end
68
+ # end
69
+ #
70
+ # # Controller action
71
+ # def poll
72
+ # render(:update) { |page| page.update_time }
73
+ # end
74
+ #
75
+ # Calls to JavaScriptGenerator not matching a helper method below
76
+ # generate a proxy to the JavaScript Class named by the method called.
77
+ #
78
+ # Examples:
79
+ #
80
+ # # Generates:
81
+ # # Foo.init();
82
+ # update_page do |page|
83
+ # page.foo.init
84
+ # end
85
+ #
86
+ # # Generates:
87
+ # # Event.observe('one', 'click', function () {
88
+ # # $('two').show();
89
+ # # });
90
+ # update_page do |page|
91
+ # page.event.observe('one', 'click') do |p|
92
+ # p[:two].show
93
+ # end
94
+ # end
95
+ #
96
+ # You can also use PrototypeHelper#update_page_tag instead of
97
+ # PrototypeHelper#update_page to wrap the generated JavaScript in a
98
+ # <tt>\<script></tt> tag.
99
+ module GeneratorMethods
100
+ def to_s #:nodoc:
101
+ #(@lines * $/).tap do |javascript|
102
+ #if ActionView::Base.debug_rjs
103
+ #source = javascript.dup
104
+ #javascript.replace "try {\n#{source}\n} catch (e) "
105
+ #javascript << "{ alert('RJS error:\\n\\n' + e.toString()); alert('#{source.gsub('\\','\0\0').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" }}'); throw e }"
106
+ #end
107
+ #end
108
+
109
+ @lines * $/
110
+ end
111
+
112
+ def jquery_id(id) #:nodoc:
113
+ id.sub(/<%=.*%>/,'').to_s.count('#.*,>+~:[/ ') == 0 ? "##{id}" : id
114
+ end
115
+
116
+ def jquery_ids(ids) #:nodoc:
117
+ Array(ids).map{|id| jquery_id(id)}.join(',')
118
+ end
119
+
120
+ # Returns a element reference by finding it through +id+ in the DOM. This element can then be
121
+ # used for further method calls. Examples:
122
+ #
123
+ # page['blank_slate'] # => $('blank_slate');
124
+ # page['blank_slate'].show # => $('blank_slate').show();
125
+ # page['blank_slate'].show('first').up # => $('blank_slate').show('first').up();
126
+ #
127
+ # You can also pass in a record, which will use ActionController::RecordIdentifier.dom_id to lookup
128
+ # the correct id:
129
+ #
130
+ # page[@post] # => $('post_45')
131
+ # page[Post.new] # => $('new_post')
132
+ def [](id)
133
+ case id
134
+ when String, Symbol, NilClass
135
+ JavaScriptElementProxy.new(self, id)
136
+ else
137
+ JavaScriptElementProxy.new(self, ActionController::RecordIdentifier.dom_id(id))
138
+ end
139
+ end
140
+
141
+ # Returns an object whose <tt>to_json</tt> evaluates to +code+. Use this to pass a literal JavaScript
142
+ # expression as an argument to another JavaScriptGenerator method.
143
+ def literal(code)
144
+ JsonLiteral.new(code.to_s)
145
+ end
146
+
147
+ # Returns a collection reference by finding it through a CSS +pattern+ in the DOM. This collection can then be
148
+ # used for further method calls. Examples:
149
+ #
150
+ # page.select('p') # => $$('p');
151
+ # page.select('p.welcome b').first # => $$('p.welcome b').first();
152
+ # page.select('p.welcome b').first.hide # => $$('p.welcome b').first().hide();
153
+ #
154
+ # You can also use prototype enumerations with the collection. Observe:
155
+ #
156
+ # # Generates: $$('#items li').each(function(value) { value.hide(); });
157
+ # page.select('#items li').each do |value|
158
+ # value.hide
159
+ # end
160
+ #
161
+ # Though you can call the block param anything you want, they are always rendered in the
162
+ # javascript as 'value, index.' Other enumerations, like collect() return the last statement:
163
+ #
164
+ # # Generates: var hidden = $$('#items li').collect(function(value, index) { return value.hide(); });
165
+ # page.select('#items li').collect('hidden') do |item|
166
+ # item.hide
167
+ # end
168
+ #
169
+ def select(pattern)
170
+ JavaScriptElementCollectionProxy.new(self, pattern)
171
+ end
172
+
173
+ # Inserts HTML at the specified +position+ relative to the DOM element
174
+ # identified by the given +id+.
175
+ #
176
+ # +position+ may be one of:
177
+ #
178
+ # <tt>:top</tt>:: HTML is inserted inside the element, before the
179
+ # element's existing content.
180
+ # <tt>:bottom</tt>:: HTML is inserted inside the element, after the
181
+ # element's existing content.
182
+ # <tt>:before</tt>:: HTML is inserted immediately preceding the element.
183
+ # <tt>:after</tt>:: HTML is inserted immediately following the element.
184
+ #
185
+ # +options_for_render+ may be either a string of HTML to insert, or a hash
186
+ # of options to be passed to ActionView::Base#render. For example:
187
+ #
188
+ # # Insert the rendered 'navigation' partial just before the DOM
189
+ # # element with ID 'content'.
190
+ # # Generates: Element.insert("content", { before: "-- Contents of 'navigation' partial --" });
191
+ # page.insert_html :before, 'content', :partial => 'navigation'
192
+ #
193
+ # # Add a list item to the bottom of the <ul> with ID 'list'.
194
+ # # Generates: Element.insert("list", { bottom: "<li>Last item</li>" });
195
+ # page.insert_html :bottom, 'list', '<li>Last item</li>'
196
+ #
197
+ def insert_html(position, id, *options_for_render)
198
+ insertion = position.to_s.downcase
199
+ insertion = 'append' if insertion == 'bottom'
200
+ insertion = 'prepend' if insertion == 'top'
201
+ call "#{JQUERY_VAR}(\"#{jquery_id(id)}\").#{insertion}", render(*options_for_render)
202
+ # content = javascript_object_for(render(*options_for_render))
203
+ # record "Element.insert(\"#{id}\", { #{position.to_s.downcase}: #{content} });"
204
+ end
205
+
206
+ # Replaces the inner HTML of the DOM element with the given +id+.
207
+ #
208
+ # +options_for_render+ may be either a string of HTML to insert, or a hash
209
+ # of options to be passed to ActionView::Base#render. For example:
210
+ #
211
+ # # Replace the HTML of the DOM element having ID 'person-45' with the
212
+ # # 'person' partial for the appropriate object.
213
+ # # Generates: Element.update("person-45", "-- Contents of 'person' partial --");
214
+ # page.replace_html 'person-45', :partial => 'person', :object => @person
215
+ #
216
+ def replace_html(id, *options_for_render)
217
+ call "#{JQUERY_VAR}(\"#{jquery_id(id)}\").html", render(*options_for_render)
218
+ # call 'Element.update', id, render(*options_for_render)
219
+ end
220
+
221
+ # Replaces the "outer HTML" (i.e., the entire element, not just its
222
+ # contents) of the DOM element with the given +id+.
223
+ #
224
+ # +options_for_render+ may be either a string of HTML to insert, or a hash
225
+ # of options to be passed to ActionView::Base#render. For example:
226
+ #
227
+ # # Replace the DOM element having ID 'person-45' with the
228
+ # # 'person' partial for the appropriate object.
229
+ # page.replace 'person-45', :partial => 'person', :object => @person
230
+ #
231
+ # This allows the same partial that is used for the +insert_html+ to
232
+ # be also used for the input to +replace+ without resorting to
233
+ # the use of wrapper elements.
234
+ #
235
+ # Examples:
236
+ #
237
+ # <div id="people">
238
+ # <%= render :partial => 'person', :collection => @people %>
239
+ # </div>
240
+ #
241
+ # # Insert a new person
242
+ # #
243
+ # # Generates: new Insertion.Bottom({object: "Matz", partial: "person"}, "");
244
+ # page.insert_html :bottom, :partial => 'person', :object => @person
245
+ #
246
+ # # Replace an existing person
247
+ #
248
+ # # Generates: Element.replace("person_45", "-- Contents of partial --");
249
+ # page.replace 'person_45', :partial => 'person', :object => @person
250
+ #
251
+ def replace(id, *options_for_render)
252
+ call "#{JQUERY_VAR}(\"#{jquery_id(id)}\").replaceWith", render(*options_for_render)
253
+ #call 'Element.replace', id, render(*options_for_render)
254
+ end
255
+
256
+ # Removes the DOM elements with the given +ids+ from the page.
257
+ #
258
+ # Example:
259
+ #
260
+ # # Remove a few people
261
+ # # Generates: ["person_23", "person_9", "person_2"].each(Element.remove);
262
+ # page.remove 'person_23', 'person_9', 'person_2'
263
+ #
264
+ def remove(*ids)
265
+ call "#{JQUERY_VAR}(\"#{jquery_ids(ids)}\").remove"
266
+ #loop_on_multiple_args 'Element.remove', ids
267
+ end
268
+
269
+ # Shows hidden DOM elements with the given +ids+.
270
+ #
271
+ # Example:
272
+ #
273
+ # # Show a few people
274
+ # # Generates: ["person_6", "person_13", "person_223"].each(Element.show);
275
+ # page.show 'person_6', 'person_13', 'person_223'
276
+ #
277
+ def show(*ids)
278
+ call "#{JQUERY_VAR}(\"#{jquery_ids(ids)}\").show"
279
+ #loop_on_multiple_args 'Element.show', ids
280
+ end
281
+
282
+ # Hides the visible DOM elements with the given +ids+.
283
+ #
284
+ # Example:
285
+ #
286
+ # # Hide a few people
287
+ # # Generates: ["person_29", "person_9", "person_0"].each(Element.hide);
288
+ # page.hide 'person_29', 'person_9', 'person_0'
289
+ #
290
+ def hide(*ids)
291
+ call "#{JQUERY_VAR}(\"#{jquery_ids(ids)}\").hide"
292
+ #loop_on_multiple_args 'Element.hide', ids
293
+ end
294
+
295
+ # Toggles the visibility of the DOM elements with the given +ids+.
296
+ # Example:
297
+ #
298
+ # # Show a few people
299
+ # # Generates: ["person_14", "person_12", "person_23"].each(Element.toggle);
300
+ # page.toggle 'person_14', 'person_12', 'person_23' # Hides the elements
301
+ # page.toggle 'person_14', 'person_12', 'person_23' # Shows the previously hidden elements
302
+ #
303
+ def toggle(*ids)
304
+ call "#{JQUERY_VAR}(\"#{jquery_ids(ids)}\").toggle"
305
+ #loop_on_multiple_args 'Element.toggle', ids
306
+ end
307
+
308
+ # Displays an alert dialog with the given +message+.
309
+ #
310
+ # Example:
311
+ #
312
+ # # Generates: alert('This message is from Rails!')
313
+ # page.alert('This message is from Rails!')
314
+ def alert(message)
315
+ call 'alert', message
316
+ end
317
+
318
+ # Redirects the browser to the given +location+ using JavaScript, in the same form as +url_for+.
319
+ #
320
+ # Examples:
321
+ #
322
+ # # Generates: window.location.href = "/mycontroller";
323
+ # page.redirect_to(:action => 'index')
324
+ #
325
+ # # Generates: window.location.href = "/account/signup";
326
+ # page.redirect_to(:controller => 'account', :action => 'signup')
327
+ def redirect_to(location)
328
+ #url = location.is_a?(String) ? location : @context.url_for(location)
329
+ url = location.to_s
330
+ record "window.location.href = #{url.inspect}"
331
+ end
332
+
333
+ # Reloads the browser's current +location+ using JavaScript
334
+ #
335
+ # Examples:
336
+ #
337
+ # # Generates: window.location.reload();
338
+ # page.reload
339
+ def reload
340
+ record 'window.location.reload()'
341
+ end
342
+
343
+ # Calls the JavaScript +function+, optionally with the given +arguments+.
344
+ #
345
+ # If a block is given, the block will be passed to a new JavaScriptGenerator;
346
+ # the resulting JavaScript code will then be wrapped inside <tt>function() { ... }</tt>
347
+ # and passed as the called function's final argument.
348
+ #
349
+ # Examples:
350
+ #
351
+ # # Generates: Element.replace(my_element, "My content to replace with.")
352
+ # page.call 'Element.replace', 'my_element', "My content to replace with."
353
+ #
354
+ # # Generates: alert('My message!')
355
+ # page.call 'alert', 'My message!'
356
+ #
357
+ # # Generates:
358
+ # # my_method(function() {
359
+ # # $("one").show();
360
+ # # $("two").hide();
361
+ # # });
362
+ # page.call(:my_method) do |p|
363
+ # p[:one].show
364
+ # p[:two].hide
365
+ # end
366
+ def call(function, *arguments, &block)
367
+ record "#{function}(#{arguments_for_call(arguments, block)})"
368
+ end
369
+
370
+ # Assigns the JavaScript +variable+ the given +value+.
371
+ #
372
+ # Examples:
373
+ #
374
+ # # Generates: my_string = "This is mine!";
375
+ # page.assign 'my_string', 'This is mine!'
376
+ #
377
+ # # Generates: record_count = 33;
378
+ # page.assign 'record_count', 33
379
+ #
380
+ # # Generates: tabulated_total = 47
381
+ # page.assign 'tabulated_total', @total_from_cart
382
+ #
383
+ def assign(variable, value)
384
+ record "#{variable} = #{javascript_object_for(value)}"
385
+ end
386
+
387
+ # Writes raw JavaScript to the page.
388
+ #
389
+ # Example:
390
+ #
391
+ # page << "alert('JavaScript with Prototype.');"
392
+ def <<(javascript)
393
+ @lines << javascript
394
+ end
395
+
396
+ # Executes the content of the block after a delay of +seconds+. Example:
397
+ #
398
+ # # Generates:
399
+ # # setTimeout(function() {
400
+ # # ;
401
+ # # new Effect.Fade("notice",{});
402
+ # # }, 20000);
403
+ # page.delay(20) do
404
+ # page.visual_effect :fade, 'notice'
405
+ # end
406
+ def delay(seconds = 1)
407
+ record "setTimeout(function() {\n\n"
408
+ yield
409
+ record "}, #{(seconds * 1000).to_i})"
410
+ end
411
+
412
+ # Starts a script.aculo.us visual effect. See
413
+ # ActionView::Helpers::ScriptaculousHelper for more information.
414
+ def visual_effect(name, id = nil, options = {})
415
+ record jquery_ui_visual_effect(name, id, options)
416
+ end
417
+
418
+ SCRIPTACULOUS_EFFECTS = {
419
+ :appear => {:method => 'fade', :mode => 'show'},
420
+ :blind_down => {:method => 'blind', :mode => 'show', :options => {:direction => 'vertical'}},
421
+ :blind_up => {:method => 'blind', :mode => 'hide', :options => {:direction => 'vertical'}},
422
+ :blind_right => {:method => 'blind', :mode => 'show', :options => {:direction => 'horizontal'}},
423
+ :blind_left => {:method => 'blind', :mode => 'hide', :options => {:direction => 'horizontal'}},
424
+ :bounce_in => {:method => 'bounce', :mode => 'show', :options => {:direction => 'up'}},
425
+ :bounce_out => {:method => 'bounce', :mode => 'hide', :options => {:direction => 'up'}},
426
+ :drop_in => {:method => 'drop', :mode => 'show', :options => {:direction => 'up'}},
427
+ :drop_out => {:method => 'drop', :mode => 'hide', :options => {:direction => 'down'}},
428
+ :fade => {:method => 'fade', :mode => 'hide'},
429
+ :fold_in => {:method => 'fold', :mode => 'hide'},
430
+ :fold_out => {:method => 'fold', :mode => 'show'},
431
+ :grow => {:method => 'scale', :mode => 'show'},
432
+ :shrink => {:method => 'scale', :mode => 'hide'},
433
+ :slide_down => {:method => 'slide', :mode => 'show', :options => {:direction => 'up'}},
434
+ :slide_up => {:method => 'slide', :mode => 'hide', :options => {:direction => 'up'}},
435
+ :slide_right => {:method => 'slide', :mode => 'show', :options => {:direction => 'left'}},
436
+ :slide_left => {:method => 'slide', :mode => 'hide', :options => {:direction => 'left'}},
437
+ :squish => {:method => 'scale', :mode => 'hide', :options => {:origin => "['top','left']"}},
438
+ :switch_on => {:method => 'clip', :mode => 'show', :options => {:direction => 'vertical'}},
439
+ :switch_off => {:method => 'clip', :mode => 'hide', :options => {:direction => 'vertical'}},
440
+ :toggle_appear => {:method => 'fade', :mode => 'toggle'},
441
+ :toggle_slide => {:method => 'slide', :mode => 'toggle', :options => {:direction => 'up'}},
442
+ :toggle_blind => {:method => 'blind', :mode => 'toggle', :options => {:direction => 'vertical'}},
443
+ }
444
+
445
+ # Returns a JavaScript snippet to be used on the Ajax callbacks for
446
+ # starting visual effects.
447
+ #
448
+ # If no +element_id+ is given, it assumes "element" which should be a local
449
+ # variable in the generated JavaScript execution context. This can be
450
+ # used for example with +drop_receiving_element+:
451
+ #
452
+ # <%= drop_receiving_element (...), :loading => visual_effect(:fade) %>
453
+ #
454
+ # This would fade the element that was dropped on the drop receiving
455
+ # element.
456
+ #
457
+ # For toggling visual effects, you can use <tt>:toggle_appear</tt>, <tt>:toggle_slide</tt>, and
458
+ # <tt>:toggle_blind</tt> which will alternate between appear/fade, slidedown/slideup, and
459
+ # blinddown/blindup respectively.
460
+ #
461
+ # You can change the behaviour with various options, see
462
+ # http://script.aculo.us for more documentation.
463
+ def jquery_ui_visual_effect(name, element_id = false, js_options = {})
464
+ #element = element_id ? ActiveSupport::JSON.encode(jquery_id((JavaScriptVariableProxy === element_id) ? element_id.as_json : element_id)) : "this"
465
+ if element_id
466
+ element = if element_id =~ /\A<%=.*%>\z/ # if completely using erb
467
+ "\"##{element_id}\"" # USER BEWARE !
468
+ else
469
+ ActiveSupport::JSON.encode(jquery_id((JavaScriptVariableProxy === element_id) ? element_id.as_json : element_id))
470
+ end
471
+ else
472
+ element = "this"
473
+ end
474
+
475
+ if SCRIPTACULOUS_EFFECTS.has_key? name.to_sym
476
+ effect = SCRIPTACULOUS_EFFECTS[name.to_sym]
477
+ name = effect[:method]
478
+ mode = effect[:mode]
479
+ js_options = js_options.merge(effect[:options]) if effect[:options]
480
+ end
481
+
482
+ js_options[:queue] = if js_options[:queue].is_a?(Hash)
483
+ '{' + js_options[:queue].map {|k, v| k == :limit ? "#{k}:#{v}" : "#{k}:'#{v}'" }.join(',') + '}'
484
+ elsif js_options[:queue]
485
+ "'#{js_options[:queue]}'"
486
+ end if js_options[:queue]
487
+
488
+ [:color, :direction, :startcolor, :endcolor].each do |option|
489
+ js_options[option] = "'#{js_options[option]}'" if js_options[option]
490
+ end
491
+
492
+ js_options[:duration] = (js_options[:duration] * 1000).to_i if js_options.has_key? :duration
493
+
494
+ #if ['fadeIn','fadeOut','fadeToggle'].include?(name)
495
+ # "$(\"#{jquery_id(element_id)}\").#{name}();"
496
+ #else
497
+ "#{JQUERY_VAR}(#{element}).#{mode || "effect"}(\"#{name}\",#{options_for_javascript(js_options)});"
498
+ #end
499
+
500
+ end
501
+
502
+ def arguments_for_call(arguments, block = nil)
503
+ arguments << block_to_function(block) if block
504
+ arguments.map { |argument| javascript_object_for(argument) }.join ', '
505
+ end
506
+
507
+ private
508
+ def options_for_javascript(options)
509
+ if options.empty?
510
+ '{}'
511
+ else
512
+ "{#{options.keys.map { |k| "#{k}:#{options[k]}" }.sort.join(', ')}}"
513
+ end
514
+ end
515
+
516
+ def loop_on_multiple_args(method, ids)
517
+ record(ids.size>1 ?
518
+ "#{javascript_object_for(ids)}.each(#{method})" :
519
+ "#{method}(#{javascript_object_for(ids.first)})")
520
+ end
521
+
522
+ def page
523
+ self
524
+ end
525
+
526
+ def record(line)
527
+ line = "#{line.to_s.chomp.gsub(/\;\z/, '')};"
528
+ self << line
529
+ line
530
+ end
531
+
532
+ def render(*options)
533
+ with_formats(:html) do
534
+ case option = options.first
535
+ when Hash
536
+ @context.render(*options)
537
+ else
538
+ option.to_s
539
+ end
540
+ end
541
+ end
542
+
543
+ def with_formats(*args)
544
+ return yield unless @context
545
+
546
+ lookup = @context.lookup_context
547
+ begin
548
+ old_formats, lookup.formats = lookup.formats, args
549
+ yield
550
+ ensure
551
+ lookup.formats = old_formats
552
+ end
553
+ end
554
+
555
+ def javascript_object_for(object)
556
+ if object.is_a?(String) && object =~ /\A<%=.*%>\z/ # if completely using e
557
+ "\"#{object}\""
558
+ else
559
+ ::ActiveSupport::JSON.encode(object)
560
+ end
561
+ end
562
+
563
+ def block_to_function(block)
564
+ generator = self.class.new(@context, &block)
565
+ literal("function() { #{generator.to_s} }")
566
+ end
567
+
568
+ def method_missing(method, *arguments)
569
+ JavaScriptProxy.new(self, method.to_s.camelize)
570
+ end
571
+ end
572
+ end
573
+
574
+ class JavaScriptProxy < (Rails::VERSION::MAJOR >= 4) ? ::ActiveSupport::ProxyObject : ::ActiveSupport::BasicObject #:nodoc:
575
+
576
+ def initialize(generator, root = nil)
577
+ @generator = generator
578
+ @generator << root if root
579
+ end
580
+
581
+ def is_a?(klass)
582
+ klass == JavaScriptProxy
583
+ end
584
+
585
+ private
586
+ def method_missing(method, *arguments, &block)
587
+ if method.to_s =~ /(.*)=$/
588
+ assign($1, arguments.first)
589
+ else
590
+ call("#{method.to_s.camelize(:lower)}", *arguments, &block)
591
+ end
592
+ end
593
+
594
+ def call(function, *arguments, &block)
595
+ append_to_function_chain!("#{function}(#{@generator.send(:arguments_for_call, arguments, block)})")
596
+ self
597
+ end
598
+
599
+ def assign(variable, value)
600
+ append_to_function_chain!("#{variable} = #{@generator.send(:javascript_object_for, value)}")
601
+ end
602
+
603
+ def function_chain
604
+ @function_chain ||= @generator.instance_variable_get(:@lines)
605
+ end
606
+
607
+ def append_to_function_chain!(call)
608
+ function_chain[-1].chomp!(';')
609
+ function_chain[-1] += ".#{call};"
610
+ end
611
+ end
612
+
613
+ class JavaScriptElementProxy < JavaScriptProxy #:nodoc:
614
+ def initialize(generator, id)
615
+ id = id.sub(/<%=.*%>/,'').to_s.count('#.*,>+~:[/ ') == 0 ? "##{id}" : id
616
+ #id = id.to_s.count('#.*,>+~:[/ ') == 0 ? "##{id}" : id
617
+ @id = id
618
+ if id =~ /\A#?<%=.*%>\z/ # if completely using erb
619
+ super(generator, "#{::JQUERY_VAR}(\"#{id}\");") # USER BEWARE !
620
+ else
621
+ super(generator, "#{::JQUERY_VAR}(#{::ActiveSupport::JSON.encode(id)});")
622
+ end
623
+ end
624
+
625
+ # Allows access of element attributes through +attribute+. Examples:
626
+ #
627
+ # page['foo']['style'] # => $('foo').style;
628
+ # page['foo']['style']['color'] # => $('blank_slate').style.color;
629
+ # page['foo']['style']['color'] = 'red' # => $('blank_slate').style.color = 'red';
630
+ # page['foo']['style'].color = 'red' # => $('blank_slate').style.color = 'red';
631
+ def [](attribute)
632
+ append_to_function_chain!(attribute)
633
+ self
634
+ end
635
+
636
+ def []=(variable, value)
637
+ assign(variable, value)
638
+ end
639
+
640
+ def replace_html(*options_for_render)
641
+ call 'html', @generator.send(:render, *options_for_render)
642
+ end
643
+
644
+ def replace(*options_for_render)
645
+ call 'replaceWith', @generator.send(:render, *options_for_render)
646
+ end
647
+
648
+ def reload(options_for_replace = {})
649
+ replace(options_for_replace.merge({ :partial => @id.to_s.sub(/^#/,'') }))
650
+ end
651
+
652
+ def value()
653
+ call 'val()'
654
+ end
655
+
656
+ def value=(value)
657
+ call 'val', value
658
+ end
659
+ end
660
+
661
+ class JavaScriptVariableProxy < JavaScriptProxy #:nodoc:
662
+ def initialize(generator, variable)
663
+ @variable = JsonLiteral.new(variable)
664
+ @empty = true # only record lines if we have to. gets rid of unnecessary linebreaks
665
+ super(generator)
666
+ end
667
+
668
+ # The JSON Encoder calls this to check for the +to_json+ method
669
+ # Since it's a blank slate object, I suppose it responds to anything.
670
+ def respond_to?(*)
671
+ true
672
+ end
673
+
674
+ def as_json(options = nil)
675
+ @variable
676
+ end
677
+
678
+ private
679
+ def append_to_function_chain!(call)
680
+ @generator << @variable if @empty
681
+ @empty = false
682
+ super
683
+ end
684
+ end
685
+
686
+ class JavaScriptCollectionProxy < JavaScriptProxy #:nodoc:
687
+ ENUMERABLE_METHODS_WITH_RETURN = [:all, :any, :collect, :map, :detect, :find, :find_all, :select, :max, :min, :partition, :reject, :sort_by, :in_groups_of, :each_slice] unless defined? ENUMERABLE_METHODS_WITH_RETURN
688
+ ENUMERABLE_METHODS = ENUMERABLE_METHODS_WITH_RETURN + [:each] unless defined? ENUMERABLE_METHODS
689
+ attr_reader :generator
690
+ delegate :arguments_for_call, :to => :generator
691
+
692
+ def initialize(generator, pattern)
693
+ super(generator, @pattern = pattern)
694
+ end
695
+
696
+ def each_slice(variable, number, &block)
697
+ if block
698
+ enumerate :eachSlice, :variable => variable, :method_args => [number], :yield_args => %w(value index), :return => true, &block
699
+ else
700
+ add_variable_assignment!(variable)
701
+ append_enumerable_function!("eachSlice(#{::ActiveSupport::JSON.encode(number)});")
702
+ end
703
+ end
704
+
705
+ def grep(variable, pattern, &block)
706
+ enumerate :grep, :variable => variable, :return => true, :method_args => [JsonLiteral.new(pattern.inspect)], :yield_args => %w(value index), &block
707
+ end
708
+
709
+ def in_groups_of(variable, number, fill_with = nil)
710
+ arguments = [number]
711
+ arguments << fill_with unless fill_with.nil?
712
+ add_variable_assignment!(variable)
713
+ append_enumerable_function!("inGroupsOf(#{arguments_for_call arguments});")
714
+ end
715
+
716
+ def inject(variable, memo, &block)
717
+ enumerate :inject, :variable => variable, :method_args => [memo], :yield_args => %w(memo value index), :return => true, &block
718
+ end
719
+
720
+ def pluck(variable, property)
721
+ add_variable_assignment!(variable)
722
+ append_enumerable_function!("pluck(#{::ActiveSupport::JSON.encode(property)});")
723
+ end
724
+
725
+ def zip(variable, *arguments, &block)
726
+ add_variable_assignment!(variable)
727
+ append_enumerable_function!("zip(#{arguments_for_call arguments}")
728
+ if block
729
+ function_chain[-1] += ", function(array) {"
730
+ yield JsonLiteral.new('array')
731
+ add_return_statement!
732
+ @generator << '});'
733
+ else
734
+ function_chain[-1] += ');'
735
+ end
736
+ end
737
+
738
+ private
739
+ def method_missing(method, *arguments, &block)
740
+ if ENUMERABLE_METHODS.include?(method)
741
+ returnable = ENUMERABLE_METHODS_WITH_RETURN.include?(method)
742
+ variable = arguments.first if returnable
743
+ enumerate(method, {:variable => (arguments.first if returnable), :return => returnable, :yield_args => %w(value index)}, &block)
744
+ else
745
+ super
746
+ end
747
+ end
748
+
749
+ # Options
750
+ # * variable - name of the variable to set the result of the enumeration to
751
+ # * method_args - array of the javascript enumeration method args that occur before the function
752
+ # * yield_args - array of the javascript yield args
753
+ # * return - true if the enumeration should return the last statement
754
+ def enumerate(enumerable, options = {}, &block)
755
+ options[:method_args] ||= []
756
+ options[:yield_args] ||= []
757
+ yield_args = options[:yield_args] * ', '
758
+ method_args = arguments_for_call options[:method_args] # foo, bar, function
759
+ method_args << ', ' unless method_args.blank?
760
+ add_variable_assignment!(options[:variable]) if options[:variable]
761
+ append_enumerable_function!("#{enumerable.to_s.camelize(:lower)}(#{method_args}function(#{yield_args}) {")
762
+ # only yield as many params as were passed in the block
763
+ yield(*options[:yield_args].collect { |p| JavaScriptVariableProxy.new(@generator, p) }[0..block.arity-1])
764
+ add_return_statement! if options[:return]
765
+ @generator << '});'
766
+ end
767
+
768
+ def add_variable_assignment!(variable)
769
+ function_chain.push("var #{variable} = #{function_chain.pop}")
770
+ end
771
+
772
+ def add_return_statement!
773
+ unless function_chain.last =~ /return/
774
+ function_chain.push("return #{function_chain.pop.chomp(';')};")
775
+ end
776
+ end
777
+
778
+ def append_enumerable_function!(call)
779
+ function_chain[-1].chomp!(';')
780
+ function_chain[-1] += ".#{call}"
781
+ end
782
+ end
783
+
784
+ class JavaScriptElementCollectionProxy < JavaScriptCollectionProxy #:nodoc:\
785
+ def initialize(generator, pattern)
786
+ super(generator, "#{::JQUERY_VAR}(#{::ActiveSupport::JSON.encode(pattern)})")
787
+ end
788
+ end
789
+ end