maiha-mjs 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Your Name
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,89 @@
1
+ Mjs
2
+ ===
3
+
4
+ A slice for the Merb framework that offers Ajax actions like RJS with jQuery
5
+ This privides new 'link_to' method for clients and 'page' object for servers.
6
+
7
+
8
+ Methods
9
+ =======
10
+
11
+ * Merb::Controller#link_to(name, url='', opts={})
12
+ link_to now recognize :remote, :submit option
13
+
14
+ * Merb::Controller#page
15
+ page method is reserved for RJS object.
16
+ You can access dom elements by it as many times and in anywhere.
17
+ No more ugly render(:update) block, just type 'page' as you want!
18
+
19
+
20
+ Setup
21
+ =====
22
+
23
+ Include jQuery as usual
24
+
25
+ app/views/layouts/application.html.erb:
26
+ <%= js_include_tag "jquery.js" -%>
27
+
28
+
29
+ Example1
30
+ ========
31
+ [ajax get request]
32
+
33
+ 1. Add :remote option to create ajax link
34
+
35
+ app/views/top/index.html.erb:
36
+ <div id="message"></div>
37
+ <%= link_to "hello", url(:action=>"hello"), :remote=>true %>
38
+ ^^^^^^^^^^^^^
39
+ generates:
40
+ <a onclick="; $.getScript('/top/hello'); return false;" href="#">hello</a>
41
+
42
+ 2. Use 'page' object to respond as RJS.
43
+
44
+ app/controllers/top.rb:
45
+ def hello
46
+ page[:message].text "Good morning!"
47
+ return page
48
+ end
49
+
50
+ generates:
51
+ $("#message").text("Good morning!");
52
+
53
+
54
+ Example2
55
+ ========
56
+ [ajax post request]
57
+
58
+ 1. Add :submit option to specify a DOM element that should be serialized
59
+
60
+ app/views/top/index.html.erb:
61
+ <div id="edit">
62
+ <%= text_field ... %>
63
+ <%= text_area ... %>
64
+ <%= link_to "update", url(:action=>"update"), :submit=>:edit %>
65
+ ^^^^^^^^^^^^^
66
+ generates:
67
+ <a onclick="; $.post('/top/update', $('#edit input').serialize(), null, 'script');; return false;" href="#">update</a>
68
+
69
+ 2. enjoy 'page' as you like
70
+
71
+ app/controllers/top.rb:
72
+ def update(id)
73
+ @item = Item.find(id)
74
+ ... # update logic is here
75
+ page[:message].text "successfully saved"
76
+ update_canvas_for(@item)
77
+ return page
78
+ end
79
+
80
+ private
81
+ def update_canvas_for(item)
82
+ page[:workspace].html partial("record", :item=>item)
83
+ rescue => error
84
+ page[:message].text error.to_s
85
+ page << "alert('something wrong!')"
86
+ end
87
+
88
+
89
+ Copyright (c) 2008 maiha@wota.jp, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+
4
+ require 'merb-core'
5
+ require 'merb-core/tasks/merb'
6
+
7
+ GEM_NAME = "mjs"
8
+ AUTHOR = "maiha"
9
+ EMAIL = "maiha@wota.jp"
10
+ HOMEPAGE = "http://github.com/maiha/mjs"
11
+ SUMMARY = "A slice for the Merb framework that offers Ajax actions like RJS with jQuery"
12
+ GEM_VERSION = "0.0.1"
13
+
14
+ spec = Gem::Specification.new do |s|
15
+ s.rubyforge_project = 'merb'
16
+ s.name = GEM_NAME
17
+ s.version = GEM_VERSION
18
+ s.platform = Gem::Platform::RUBY
19
+ s.has_rdoc = true
20
+ s.extra_rdoc_files = ["README", "LICENSE", 'TODO']
21
+ s.summary = SUMMARY
22
+ s.description = s.summary
23
+ s.author = AUTHOR
24
+ s.email = EMAIL
25
+ s.homepage = HOMEPAGE
26
+ s.add_dependency('merb-slices', '>= 1.0.7.1')
27
+ s.require_path = 'lib'
28
+ s.files = %w(LICENSE README Rakefile TODO) + Dir.glob("{lib,spec,app,public,stubs}/**/*")
29
+ end
30
+
31
+ Rake::GemPackageTask.new(spec) do |pkg|
32
+ pkg.gem_spec = spec
33
+ end
34
+
35
+ desc "Install the gem"
36
+ task :install do
37
+ Merb::RakeHelper.install(GEM_NAME, :version => GEM_VERSION)
38
+ end
39
+
40
+ desc "Uninstall the gem"
41
+ task :uninstall do
42
+ Merb::RakeHelper.uninstall(GEM_NAME, :version => GEM_VERSION)
43
+ end
44
+
45
+ desc "Create a gemspec file"
46
+ task :gemspec do
47
+ File.open("#{GEM_NAME}.gemspec", "w") do |file|
48
+ file.puts spec.to_ruby
49
+ end
50
+ end
51
+
52
+ require 'spec/rake/spectask'
53
+ require 'merb-core/test/tasks/spectasks'
54
+ desc 'Default: run spec examples'
55
+ task :default => 'spec'
data/TODO ADDED
@@ -0,0 +1,15 @@
1
+ TODO:
2
+
3
+ - Fix Mjs.description and Mjs.version
4
+ - Fix LICENSE with your name
5
+ - Fix Rakefile with your name and contact info
6
+ - Add your code to lib/mjs.rb
7
+ - Add your Merb rake tasks to lib/mjs/merbtasks.rb
8
+
9
+ Remove anything that you don't need:
10
+
11
+ - app/controllers/main.rb Mjs::Main controller
12
+ - app/views/layout/mjs.html.erb
13
+ - spec/controllers/main_spec.rb controller specs
14
+ - public/* any public files
15
+ - stubs/* any stub files
@@ -0,0 +1,5 @@
1
+ class Mjs::Application < Merb::Controller
2
+
3
+ controller_for_slice
4
+
5
+ end
@@ -0,0 +1,7 @@
1
+ class Mjs::Main < Mjs::Application
2
+
3
+ def index
4
+ render
5
+ end
6
+
7
+ end
@@ -0,0 +1,63 @@
1
+ module Merb
2
+ module Mjs
3
+ module ApplicationHelper
4
+ # @param *segments<Array[#to_s]> Path segments to append.
5
+ #
6
+ # @return <String>
7
+ # A path relative to the public directory, with added segments.
8
+ def image_path(*segments)
9
+ public_path_for(:image, *segments)
10
+ end
11
+
12
+ # @param *segments<Array[#to_s]> Path segments to append.
13
+ #
14
+ # @return <String>
15
+ # A path relative to the public directory, with added segments.
16
+ def javascript_path(*segments)
17
+ public_path_for(:javascript, *segments)
18
+ end
19
+
20
+ # @param *segments<Array[#to_s]> Path segments to append.
21
+ #
22
+ # @return <String>
23
+ # A path relative to the public directory, with added segments.
24
+ def stylesheet_path(*segments)
25
+ public_path_for(:stylesheet, *segments)
26
+ end
27
+
28
+ # Construct a path relative to the public directory
29
+ #
30
+ # @param <Symbol> The type of component.
31
+ # @param *segments<Array[#to_s]> Path segments to append.
32
+ #
33
+ # @return <String>
34
+ # A path relative to the public directory, with added segments.
35
+ def public_path_for(type, *segments)
36
+ ::Mjs.public_path_for(type, *segments)
37
+ end
38
+
39
+ # Construct an app-level path.
40
+ #
41
+ # @param <Symbol> The type of component.
42
+ # @param *segments<Array[#to_s]> Path segments to append.
43
+ #
44
+ # @return <String>
45
+ # A path within the host application, with added segments.
46
+ def app_path_for(type, *segments)
47
+ ::Mjs.app_path_for(type, *segments)
48
+ end
49
+
50
+ # Construct a slice-level path.
51
+ #
52
+ # @param <Symbol> The type of component.
53
+ # @param *segments<Array[#to_s]> Path segments to append.
54
+ #
55
+ # @return <String>
56
+ # A path within the slice source (Gem), with added segments.
57
+ def slice_path_for(type, *segments)
58
+ ::Mjs.slice_path_for(type, *segments)
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-us" lang="en-us">
3
+ <head>
4
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
5
+ <title>Fresh Mjs Slice</title>
6
+ <link href="<%= public_path_for :stylesheet, 'master.css' %>" type="text/css" charset="utf-8" rel="stylesheet" media="all" />
7
+ <script src="<%= public_path_for :javascript, 'master.js' %>" type="text/javascript" charset="utf-8"></script>
8
+ </head>
9
+ <!-- you can override this layout at slices/mjs/app/views/layout/mjs.html.erb -->
10
+ <body class="mjs-slice">
11
+ <div id="container">
12
+ <h1>Mjs Slice</h1>
13
+ <div id="main"><%= catch_content :for_layout %></div>
14
+ </div>
15
+ </body>
16
+ </html>
@@ -0,0 +1 @@
1
+ <strong><%= slice.description %></strong> (v. <%= slice.version %>)
data/lib/mjs.rb ADDED
@@ -0,0 +1,86 @@
1
+ if defined?(Merb::Plugins)
2
+
3
+ $:.unshift File.dirname(__FILE__)
4
+
5
+ dependency 'merb-slices', :immediate => true
6
+ Merb::Plugins.add_rakefiles "mjs/merbtasks", "mjs/slicetasks", "mjs/spectasks"
7
+
8
+ # Register the Slice for the current host application
9
+ Merb::Slices::register(__FILE__)
10
+
11
+ # Slice configuration - set this in a before_app_loads callback.
12
+ # By default a Slice uses its own layout, so you can swicht to
13
+ # the main application layout or no layout at all if needed.
14
+ #
15
+ # Configuration options:
16
+ # :layout - the layout to use; defaults to :mjs
17
+ # :mirror - which path component types to use on copy operations; defaults to all
18
+ Merb::Slices::config[:mjs][:layout] ||= :mjs
19
+
20
+ # All Slice code is expected to be namespaced inside a module
21
+ module Mjs
22
+
23
+ # Slice metadata
24
+ self.description = "A slice for the Merb framework that offers Ajax actions like RJS with jQuery"
25
+ self.version = "0.0.1"
26
+ self.author = "maiha"
27
+
28
+ # Stub classes loaded hook - runs before LoadClasses BootLoader
29
+ # right after a slice's classes have been loaded internally.
30
+ def self.loaded
31
+ require 'mjs/helper'
32
+ end
33
+
34
+ # Initialization hook - runs before AfterAppLoads BootLoader
35
+ def self.init
36
+ Merb::Controller.send(:include, ::Mjs::Helper)
37
+ end
38
+
39
+ # Activation hook - runs after AfterAppLoads BootLoader
40
+ def self.activate
41
+ end
42
+
43
+ # Deactivation hook - triggered by Merb::Slices.deactivate(Mjs)
44
+ def self.deactivate
45
+ end
46
+
47
+ # Setup routes inside the host application
48
+ #
49
+ # @param scope<Merb::Router::Behaviour>
50
+ # Routes will be added within this scope (namespace). In fact, any
51
+ # router behaviour is a valid namespace, so you can attach
52
+ # routes at any level of your router setup.
53
+ #
54
+ # @note prefix your named routes with :mjs_
55
+ # to avoid potential conflicts with global named routes.
56
+ def self.setup_router(scope)
57
+ # example of a named route
58
+ scope.match('/index(.:format)').to(:controller => 'main', :action => 'index').name(:index)
59
+ # the slice is mounted at /mjs - note that it comes before default_routes
60
+ scope.match('/').to(:controller => 'main', :action => 'index').name(:home)
61
+ # enable slice-level default routes by default
62
+ scope.default_routes
63
+ end
64
+
65
+ end
66
+
67
+ # Setup the slice layout for Mjs
68
+ #
69
+ # Use Mjs.push_path and Mjs.push_app_path
70
+ # to set paths to mjs-level and app-level paths. Example:
71
+ #
72
+ # Mjs.push_path(:application, Mjs.root)
73
+ # Mjs.push_app_path(:application, Merb.root / 'slices' / 'mjs')
74
+ # ...
75
+ #
76
+ # Any component path that hasn't been set will default to Mjs.root
77
+ #
78
+ # Or just call setup_default_structure! to setup a basic Merb MVC structure.
79
+ Mjs.setup_default_structure!
80
+
81
+ # Add dependencies for other Mjs classes below. Example:
82
+ # dependency "mjs/other"
83
+
84
+
85
+
86
+ end
data/lib/mjs/helper.rb ADDED
@@ -0,0 +1,40 @@
1
+ require 'mjs/java_script_context'
2
+
3
+ module Mjs
4
+ module Helper
5
+ def page
6
+ @page ||= Mjs::JavaScriptContext.new
7
+ end
8
+
9
+ # override! :link_to # for Ajax
10
+ def link_to(name, url='', opts={})
11
+ opts[:href] ||= url
12
+ if url.is_a?(DataMapper::Resource)
13
+ record = opts[:href]
14
+ if record.new_record?
15
+ opts[:href] = resource(record.class.name.downcase.pluralize.intern, :new)
16
+ else
17
+ opts[:href] = resource(record)
18
+ end
19
+ end
20
+
21
+ opts[:remote] ||= true if opts[:submit]
22
+ return super unless opts.delete(:remote)
23
+
24
+ submit = opts.delete(:submit)
25
+ target =
26
+ case submit
27
+ when Symbol then "$('##{submit} input')"
28
+ when String then "$('#{submit}')"
29
+ when NilClass # GET requst
30
+ else
31
+ raise ArgumentError, "link_to :submit expects Symbol or String, but got #{submit.class.name}"
32
+ end
33
+
34
+ ajax = submit ? "$.post('#{opts[:href]}', #{target}.serialize(), null, 'script');" : "$.getScript('#{opts[:href]}')"
35
+ opts[:onclick] = "#{opts.delete(:onclick)}; #{ajax}; return false;"
36
+ opts[:href] = '#'
37
+ %{<a #{ opts.to_xml_attributes }>#{name}</a>}
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,478 @@
1
+ require 'mjs/utils'
2
+
3
+ #
4
+ # This file is almost derived from prototype_helper.rb of RoR
5
+ #
6
+ module Mjs
7
+ class JavaScriptContext #:nodoc:
8
+
9
+ ######################################################################
10
+ ### define :each method for Rack::Response
11
+ ### because Merb::Rack::StreamWrapper can't create response body correctly
12
+
13
+ def each(&callback)
14
+ callback.call(to_s)
15
+ end
16
+
17
+ def initialize
18
+ @lines = []
19
+ end
20
+
21
+ def to_s
22
+ javascript = @lines * $/
23
+ end
24
+
25
+ # Returns a element reference by finding it through +id+ in the DOM. This element can then be
26
+ # used for further method calls. Examples:
27
+ #
28
+ # page['blank_slate'] # => $('blank_slate');
29
+ # page['blank_slate'].show # => $('blank_slate').show();
30
+ # page['blank_slate'].show('first').up # => $('blank_slate').show('first').up();
31
+ #
32
+ # You can also pass in a record, which will use ActionController::RecordIdentifier.dom_id to lookup
33
+ # the correct id:
34
+ #
35
+ # page[@post] # => $('post_45')
36
+ # page[Post.new] # => $('new_post')
37
+ def [](id)
38
+ case id
39
+ when Symbol
40
+ JavaScriptElementProxy.new(self, "##{id}")
41
+ when String, NilClass
42
+ JavaScriptElementProxy.new(self, id)
43
+ else
44
+ raise NotImplementedError, "[MJS] RecordIdentifier.dom_id(id)"
45
+ JavaScriptElementProxy.new(self, ActionController::RecordIdentifier.dom_id(id))
46
+ end
47
+ end
48
+
49
+ # Returns an object whose <tt>to_json</tt> evaluates to +code+. Use this to pass a literal JavaScript
50
+ # expression as an argument to another JavaScriptGenerator method.
51
+ def literal(code)
52
+ raise NotImplementedError, "[MJS] ActiveSupport::JSON::Variable.new(code.to_s)"
53
+ ActiveSupport::JSON::Variable.new(code.to_s)
54
+ end
55
+
56
+ # Returns a collection reference by finding it through a CSS +pattern+ in the DOM. This collection can then be
57
+ # used for further method calls. Examples:
58
+ #
59
+ # page.select('p') # => $$('p');
60
+ # page.select('p.welcome b').first # => $$('p.welcome b').first();
61
+ # page.select('p.welcome b').first.hide # => $$('p.welcome b').first().hide();
62
+ #
63
+ # You can also use prototype enumerations with the collection. Observe:
64
+ #
65
+ # # Generates: $$('#items li').each(function(value) { value.hide(); });
66
+ # page.select('#items li').each do |value|
67
+ # value.hide
68
+ # end
69
+ #
70
+ # Though you can call the block param anything you want, they are always rendered in the
71
+ # javascript as 'value, index.' Other enumerations, like collect() return the last statement:
72
+ #
73
+ # # Generates: var hidden = $$('#items li').collect(function(value, index) { return value.hide(); });
74
+ # page.select('#items li').collect('hidden') do |item|
75
+ # item.hide
76
+ # end
77
+ #
78
+ def select(pattern)
79
+ JavaScriptElementCollectionProxy.new(self, pattern)
80
+ end
81
+
82
+ # Inserts HTML at the specified +position+ relative to the DOM element
83
+ # identified by the given +id+.
84
+ #
85
+ # +position+ may be one of:
86
+ #
87
+ # <tt>:top</tt>:: HTML is inserted inside the element, before the
88
+ # element's existing content.
89
+ # <tt>:bottom</tt>:: HTML is inserted inside the element, after the
90
+ # element's existing content.
91
+ # <tt>:before</tt>:: HTML is inserted immediately preceding the element.
92
+ # <tt>:after</tt>:: HTML is inserted immediately following the element.
93
+ #
94
+ # +options_for_render+ may be either a string of HTML to insert, or a hash
95
+ # of options to be passed to ActionView::Base#render. For example:
96
+ #
97
+ # # Insert the rendered 'navigation' partial just before the DOM
98
+ # # element with ID 'content'.
99
+ # # Generates: Element.insert("content", { before: "-- Contents of 'navigation' partial --" });
100
+ # page.insert_html :before, 'content', :partial => 'navigation'
101
+ #
102
+ # # Add a list item to the bottom of the <ul> with ID 'list'.
103
+ # # Generates: Element.insert("list", { bottom: "<li>Last item</li>" });
104
+ # page.insert_html :bottom, 'list', '<li>Last item</li>'
105
+ #
106
+ def insert_html(position, id, *options_for_render)
107
+ content = javascript_object_for(render(*options_for_render))
108
+ record "Element.insert(\"#{id}\", { #{position.to_s.downcase}: #{content} });"
109
+ end
110
+
111
+ # Replaces the inner HTML of the DOM element with the given +id+.
112
+ #
113
+ # +options_for_render+ may be either a string of HTML to insert, or a hash
114
+ # of options to be passed to ActionView::Base#render. For example:
115
+ #
116
+ # # Replace the HTML of the DOM element having ID 'person-45' with the
117
+ # # 'person' partial for the appropriate object.
118
+ # # Generates: Element.update("person-45", "-- Contents of 'person' partial --");
119
+ # page.replace_html 'person-45', :partial => 'person', :object => @person
120
+ #
121
+ def replace_html(id, *options_for_render)
122
+ call 'Element.update', id, render(*options_for_render)
123
+ end
124
+
125
+ # Replaces the "outer HTML" (i.e., the entire element, not just its
126
+ # contents) of the DOM element with the given +id+.
127
+ #
128
+ # +options_for_render+ may be either a string of HTML to insert, or a hash
129
+ # of options to be passed to ActionView::Base#render. For example:
130
+ #
131
+ # # Replace the DOM element having ID 'person-45' with the
132
+ # # 'person' partial for the appropriate object.
133
+ # page.replace 'person-45', :partial => 'person', :object => @person
134
+ #
135
+ # This allows the same partial that is used for the +insert_html+ to
136
+ # be also used for the input to +replace+ without resorting to
137
+ # the use of wrapper elements.
138
+ #
139
+ # Examples:
140
+ #
141
+ # <div id="people">
142
+ # <%= render :partial => 'person', :collection => @people %>
143
+ # </div>
144
+ #
145
+ # # Insert a new person
146
+ # #
147
+ # # Generates: new Insertion.Bottom({object: "Matz", partial: "person"}, "");
148
+ # page.insert_html :bottom, :partial => 'person', :object => @person
149
+ #
150
+ # # Replace an existing person
151
+ #
152
+ # # Generates: Element.replace("person_45", "-- Contents of partial --");
153
+ # page.replace 'person_45', :partial => 'person', :object => @person
154
+ #
155
+ def replace(id, *options_for_render)
156
+ call 'Element.replace', id, render(*options_for_render)
157
+ end
158
+
159
+ # Removes the DOM elements with the given +ids+ from the page.
160
+ #
161
+ # Example:
162
+ #
163
+ # # Remove a few people
164
+ # # Generates: ["person_23", "person_9", "person_2"].each(Element.remove);
165
+ # page.remove 'person_23', 'person_9', 'person_2'
166
+ #
167
+ def remove(*ids)
168
+ loop_on_multiple_args 'Element.remove', ids
169
+ end
170
+
171
+ # Shows hidden DOM elements with the given +ids+.
172
+ #
173
+ # Example:
174
+ #
175
+ # # Show a few people
176
+ # # Generates: ["person_6", "person_13", "person_223"].each(Element.show);
177
+ # page.show 'person_6', 'person_13', 'person_223'
178
+ #
179
+ def show(*ids)
180
+ loop_on_multiple_args 'Element.show', ids
181
+ end
182
+
183
+ # Hides the visible DOM elements with the given +ids+.
184
+ #
185
+ # Example:
186
+ #
187
+ # # Hide a few people
188
+ # # Generates: ["person_29", "person_9", "person_0"].each(Element.hide);
189
+ # page.hide 'person_29', 'person_9', 'person_0'
190
+ #
191
+ def hide(*ids)
192
+ loop_on_multiple_args 'Element.hide', ids
193
+ end
194
+
195
+ # Toggles the visibility of the DOM elements with the given +ids+.
196
+ # Example:
197
+ #
198
+ # # Show a few people
199
+ # # Generates: ["person_14", "person_12", "person_23"].each(Element.toggle);
200
+ # page.toggle 'person_14', 'person_12', 'person_23' # Hides the elements
201
+ # page.toggle 'person_14', 'person_12', 'person_23' # Shows the previously hidden elements
202
+ #
203
+ def toggle(*ids)
204
+ loop_on_multiple_args 'Element.toggle', ids
205
+ end
206
+
207
+ # Displays an alert dialog with the given +message+.
208
+ #
209
+ # Example:
210
+ #
211
+ # # Generates: alert('This message is from Rails!')
212
+ # page.alert('This message is from Rails!')
213
+ def alert(message)
214
+ call 'alert', message
215
+ end
216
+
217
+ # Redirects the browser to the given +location+ using JavaScript, in the same form as +url_for+.
218
+ #
219
+ # Examples:
220
+ #
221
+ # # Generates: window.location.href = "/mycontroller";
222
+ # page.redirect_to(:action => 'index')
223
+ #
224
+ # # Generates: window.location.href = "/account/signup";
225
+ # page.redirect_to(:controller => 'account', :action => 'signup')
226
+ def redirect_to(location)
227
+ url = location.is_a?(String) ? location : @context.url_for(location)
228
+ record "window.location.href = #{url.inspect}"
229
+ end
230
+
231
+ # Reloads the browser's current +location+ using JavaScript
232
+ #
233
+ # Examples:
234
+ #
235
+ # # Generates: window.location.reload();
236
+ # page.reload
237
+ def reload
238
+ record 'window.location.reload()'
239
+ end
240
+
241
+ # Calls the JavaScript +function+, optionally with the given +arguments+.
242
+ #
243
+ # If a block is given, the block will be passed to a new JavaScriptGenerator;
244
+ # the resulting JavaScript code will then be wrapped inside <tt>function() { ... }</tt>
245
+ # and passed as the called function's final argument.
246
+ #
247
+ # Examples:
248
+ #
249
+ # # Generates: Element.replace(my_element, "My content to replace with.")
250
+ # page.call 'Element.replace', 'my_element', "My content to replace with."
251
+ #
252
+ # # Generates: alert('My message!')
253
+ # page.call 'alert', 'My message!'
254
+ #
255
+ # # Generates:
256
+ # # my_method(function() {
257
+ # # $("one").show();
258
+ # # $("two").hide();
259
+ # # });
260
+ # page.call(:my_method) do |p|
261
+ # p[:one].show
262
+ # p[:two].hide
263
+ # end
264
+ def call(function, *arguments, &block)
265
+ record "#{function}(#{arguments_for_call(arguments, block)})"
266
+ end
267
+
268
+ # Assigns the JavaScript +variable+ the given +value+.
269
+ #
270
+ # Examples:
271
+ #
272
+ # # Generates: my_string = "This is mine!";
273
+ # page.assign 'my_string', 'This is mine!'
274
+ #
275
+ # # Generates: record_count = 33;
276
+ # page.assign 'record_count', 33
277
+ #
278
+ # # Generates: tabulated_total = 47
279
+ # page.assign 'tabulated_total', @total_from_cart
280
+ #
281
+ def assign(variable, value)
282
+ record "#{variable} = #{javascript_object_for(value)}"
283
+ end
284
+
285
+ # Writes raw JavaScript to the page.
286
+ #
287
+ # Example:
288
+ #
289
+ # page << "alert('JavaScript with Prototype.');"
290
+ def <<(javascript)
291
+ @lines << javascript
292
+ end
293
+
294
+ # Executes the content of the block after a delay of +seconds+. Example:
295
+ #
296
+ # # Generates:
297
+ # # setTimeout(function() {
298
+ # # ;
299
+ # # new Effect.Fade("notice",{});
300
+ # # }, 20000);
301
+ # page.delay(20) do
302
+ # page.visual_effect :fade, 'notice'
303
+ # end
304
+ def delay(seconds = 1)
305
+ record "setTimeout(function() {\n\n"
306
+ yield
307
+ record "}, #{(seconds * 1000).to_i})"
308
+ end
309
+
310
+ # Starts a script.aculo.us visual effect. See
311
+ # ActionView::Helpers::ScriptaculousHelper for more information.
312
+ def visual_effect(name, id = nil, options = {})
313
+ record @context.send(:visual_effect, name, id, options)
314
+ end
315
+
316
+ # Creates a script.aculo.us sortable element. Useful
317
+ # to recreate sortable elements after items get added
318
+ # or deleted.
319
+ # See ActionView::Helpers::ScriptaculousHelper for more information.
320
+ def sortable(id, options = {})
321
+ record @context.send(:sortable_element_js, id, options)
322
+ end
323
+
324
+ # Creates a script.aculo.us draggable element.
325
+ # See ActionView::Helpers::ScriptaculousHelper for more information.
326
+ def draggable(id, options = {})
327
+ record @context.send(:draggable_element_js, id, options)
328
+ end
329
+
330
+ # Creates a script.aculo.us drop receiving element.
331
+ # See ActionView::Helpers::ScriptaculousHelper for more information.
332
+ def drop_receiving(id, options = {})
333
+ record @context.send(:drop_receiving_element_js, id, options)
334
+ end
335
+
336
+ private
337
+ def loop_on_multiple_args(method, ids)
338
+ record(ids.size>1 ?
339
+ "#{javascript_object_for(ids)}.each(#{method})" :
340
+ "#{method}(#{ids.first.to_json})")
341
+ end
342
+
343
+ def page
344
+ self
345
+ end
346
+
347
+ def record(line)
348
+ returning line = "#{line.to_s.chomp.gsub(/\;\z/, '')};" do
349
+ self << line
350
+ end
351
+ end
352
+
353
+ def render(*options_for_render)
354
+ old_format = @context && @context.template_format
355
+ @context.template_format = :html if @context
356
+ Hash === options_for_render.first ?
357
+ @context.render(*options_for_render) :
358
+ options_for_render.first.to_s
359
+ ensure
360
+ @context.template_format = old_format if @context
361
+ end
362
+
363
+ def javascript_object_for(object)
364
+ object.respond_to?(:to_json) ? object.to_json : object.inspect
365
+ end
366
+
367
+ def arguments_for_call(arguments, block = nil)
368
+ arguments << block_to_function(block) if block
369
+ arguments.map { |argument| javascript_object_for(argument) }.join ', '
370
+ end
371
+
372
+ def block_to_function(block)
373
+ generator = self.class.new(@context, &block)
374
+ literal("function() { #{generator.to_s} }")
375
+ end
376
+
377
+ def method_missing(method, *arguments)
378
+ JavaScriptProxy.new(self, Mjs::Utils.camelize(method))
379
+ end
380
+ end # class JavaScriptGenerator
381
+
382
+ # class JavaScriptProxy < ActiveSupport::BasicObject #:nodoc:
383
+ # [TODO] BlackSlate is not supported yet
384
+ class JavaScriptProxy
385
+
386
+ def initialize(generator, root = nil)
387
+ @generator = generator
388
+ @generator << root if root
389
+ end
390
+
391
+ private
392
+ def method_missing(method, *arguments, &block)
393
+ if method.to_s =~ /(.*)=$/
394
+ assign($1, arguments.first)
395
+ else
396
+ call("#{Mjs::Utils.camelize(method, :lower)}", *arguments, &block)
397
+ end
398
+ end
399
+
400
+ def call(function, *arguments, &block)
401
+ append_to_function_chain!("#{function}(#{@generator.send(:arguments_for_call, arguments, block)})")
402
+ self
403
+ end
404
+
405
+ def assign(variable, value)
406
+ append_to_function_chain!("#{variable} = #{@generator.send(:javascript_object_for, value)}")
407
+ end
408
+
409
+ def function_chain
410
+ @function_chain ||= @generator.instance_variable_get(:@lines)
411
+ end
412
+
413
+ def append_to_function_chain!(call)
414
+ function_chain[-1].chomp!(';')
415
+ function_chain[-1] += ".#{call};"
416
+ end
417
+ end
418
+
419
+ class JavaScriptElementProxy < JavaScriptProxy #:nodoc:
420
+ def initialize(generator, id)
421
+ @id = id
422
+ super(generator, "$(#{id.to_json})")
423
+ end
424
+
425
+ # Allows access of element attributes through +attribute+. Examples:
426
+ #
427
+ # page['foo']['style'] # => $('foo').style;
428
+ # page['foo']['style']['color'] # => $('blank_slate').style.color;
429
+ # page['foo']['style']['color'] = 'red' # => $('blank_slate').style.color = 'red';
430
+ # page['foo']['style'].color = 'red' # => $('blank_slate').style.color = 'red';
431
+ def [](attribute)
432
+ append_to_function_chain!(attribute)
433
+ self
434
+ end
435
+
436
+ def []=(variable, value)
437
+ assign(variable, value)
438
+ end
439
+
440
+ def replace_html(*options_for_render)
441
+ call 'update', @generator.send(:render, *options_for_render)
442
+ end
443
+
444
+ def replace(*options_for_render)
445
+ call 'replace', @generator.send(:render, *options_for_render)
446
+ end
447
+
448
+ def reload(options_for_replace = {})
449
+ replace(options_for_replace.merge({ :partial => @id.to_s }))
450
+ end
451
+
452
+ end
453
+
454
+ class JavaScriptVariableProxy < JavaScriptProxy #:nodoc:
455
+ def initialize(generator, variable)
456
+ @variable = variable
457
+ @empty = true # only record lines if we have to. gets rid of unnecessary linebreaks
458
+ super(generator)
459
+ end
460
+
461
+ # The JSON Encoder calls this to check for the +to_json+ method
462
+ # Since it's a blank slate object, I suppose it responds to anything.
463
+ def respond_to?(method)
464
+ true
465
+ end
466
+
467
+ def to_json(options = nil)
468
+ @variable
469
+ end
470
+
471
+ private
472
+ def append_to_function_chain!(call)
473
+ @generator << @variable if @empty
474
+ @empty = false
475
+ super
476
+ end
477
+ end
478
+ end