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