maiha-mjs 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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