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 +20 -0
- data/README +89 -0
- data/Rakefile +55 -0
- data/TODO +15 -0
- data/app/controllers/application.rb +5 -0
- data/app/controllers/main.rb +7 -0
- data/app/helpers/application_helper.rb +63 -0
- data/app/views/layout/mjs.html.erb +16 -0
- data/app/views/main/index.html.erb +1 -0
- data/lib/mjs.rb +86 -0
- data/lib/mjs/helper.rb +40 -0
- data/lib/mjs/java_script_context.rb +478 -0
- data/lib/mjs/merbtasks.rb +103 -0
- data/lib/mjs/page_object.rb +16 -0
- data/lib/mjs/slicetasks.rb +20 -0
- data/lib/mjs/spectasks.rb +53 -0
- data/lib/mjs/utils.rb +13 -0
- data/public/javascripts/master.js +0 -0
- data/public/stylesheets/master.css +2 -0
- data/spec/mjs_spec.rb +20 -0
- data/spec/requests/main_spec.rb +30 -0
- data/spec/spec_helper.rb +58 -0
- data/stubs/app/controllers/application.rb +2 -0
- data/stubs/app/controllers/main.rb +2 -0
- metadata +97 -0
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,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
|