apotomo 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +10 -0
- data/Gemfile.lock +47 -0
- data/README +141 -0
- data/README.rdoc +141 -0
- data/Rakefile +78 -0
- data/TODO +36 -0
- data/app/cells/apotomo/child_switch_widget/switch.html.erb +1 -0
- data/app/cells/apotomo/child_switch_widget/switch.rhtml +1 -0
- data/app/cells/apotomo/deep_link_widget.rb +27 -0
- data/app/cells/apotomo/deep_link_widget/setup.html.erb +20 -0
- data/app/cells/apotomo/java_script_widget.rb +12 -0
- data/app/cells/apotomo/tab_panel_widget.rb +87 -0
- data/app/cells/apotomo/tab_panel_widget/display.html.erb +57 -0
- data/app/cells/apotomo/tab_widget.rb +18 -0
- data/app/cells/apotomo/tab_widget/display.html.erb +1 -0
- data/config/routes.rb +3 -0
- data/generators/widget/USAGE +15 -0
- data/generators/widget/templates/functional_test.rb +8 -0
- data/generators/widget/templates/view.html.erb +2 -0
- data/generators/widget/templates/view.html.haml +3 -0
- data/generators/widget/templates/widget.rb +8 -0
- data/generators/widget/widget_generator.rb +34 -0
- data/lib/apotomo.rb +59 -0
- data/lib/apotomo/caching.rb +37 -0
- data/lib/apotomo/container_widget.rb +10 -0
- data/lib/apotomo/deep_link_methods.rb +90 -0
- data/lib/apotomo/event.rb +9 -0
- data/lib/apotomo/event_handler.rb +23 -0
- data/lib/apotomo/event_methods.rb +102 -0
- data/lib/apotomo/invoke_event_handler.rb +24 -0
- data/lib/apotomo/javascript_generator.rb +57 -0
- data/lib/apotomo/persistence.rb +139 -0
- data/lib/apotomo/proc_event_handler.rb +18 -0
- data/lib/apotomo/rails/controller_methods.rb +161 -0
- data/lib/apotomo/rails/view_helper.rb +95 -0
- data/lib/apotomo/rails/view_methods.rb +7 -0
- data/lib/apotomo/request_processor.rb +92 -0
- data/lib/apotomo/stateful_widget.rb +8 -0
- data/lib/apotomo/transition.rb +46 -0
- data/lib/apotomo/tree_node.rb +186 -0
- data/lib/apotomo/version.rb +5 -0
- data/lib/apotomo/widget.rb +289 -0
- data/lib/apotomo/widget_shortcuts.rb +36 -0
- data/rails/init.rb +0 -0
- data/test/fixtures/application_widget_tree.rb +2 -0
- data/test/rails/controller_methods_test.rb +206 -0
- data/test/rails/rails_integration_test.rb +99 -0
- data/test/rails/view_helper_test.rb +77 -0
- data/test/rails/view_methods_test.rb +40 -0
- data/test/rails/widget_generator_test.rb +47 -0
- data/test/support/assertions_helper.rb +13 -0
- data/test/support/test_case_methods.rb +68 -0
- data/test/test_helper.rb +77 -0
- data/test/unit/apotomo_test.rb +20 -0
- data/test/unit/container_test.rb +20 -0
- data/test/unit/event_handler_test.rb +67 -0
- data/test/unit/event_methods_test.rb +83 -0
- data/test/unit/event_test.rb +30 -0
- data/test/unit/invoke_test.rb +123 -0
- data/test/unit/javascript_generator_test.rb +90 -0
- data/test/unit/onfire_integration_test.rb +19 -0
- data/test/unit/persistence_test.rb +240 -0
- data/test/unit/render_test.rb +203 -0
- data/test/unit/request_processor_test.rb +178 -0
- data/test/unit/stateful_widget_test.rb +135 -0
- data/test/unit/test_addressing.rb +111 -0
- data/test/unit/test_caching.rb +54 -0
- data/test/unit/test_jump_to_state.rb +89 -0
- data/test/unit/test_tab_panel.rb +72 -0
- data/test/unit/test_widget_shortcuts.rb +45 -0
- data/test/unit/transition_test.rb +33 -0
- data/test/unit/widget_shortcuts_test.rb +68 -0
- data/test/unit/widget_test.rb +24 -0
- metadata +215 -0
@@ -0,0 +1 @@
|
|
1
|
+
<%= @content.to_s %>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= @content.to_s %>
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Apotomo::DeepLinkWidget < Apotomo::StatefulWidget
|
2
|
+
|
3
|
+
transition :from => :setup, :to => :process
|
4
|
+
transition :in => :process
|
5
|
+
|
6
|
+
def setup
|
7
|
+
root.respond_to_event :externalChange, :on => 'deep_link', :with => :process
|
8
|
+
root.respond_to_event :internalChange, :on => 'deep_link', :with => :process
|
9
|
+
|
10
|
+
render
|
11
|
+
end
|
12
|
+
|
13
|
+
def process
|
14
|
+
# find out what changed in the deep link
|
15
|
+
# find the update root (### DISCUSS: this might be more than one root, as in A--B)
|
16
|
+
#path = param(:deep_link) # path is #tab=users/icon=3
|
17
|
+
|
18
|
+
update_root = root.find {|w| w.responds_to_url_change? and w.responds_to_url_change_for?(url_fragment)} ### DISCUSS: we just look for one root here.
|
19
|
+
|
20
|
+
if update_root
|
21
|
+
controller.logger.debug "deep_link#process: `#{update_root.name}` responds to :urlChange"
|
22
|
+
update_root.trigger(:urlChange)
|
23
|
+
end
|
24
|
+
|
25
|
+
render :nothing => true
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<%= javascript_tag "
|
2
|
+
SWFAddress.onInternalChange = function() {
|
3
|
+
//alert('internalChange');
|
4
|
+
#{remote_function :url => address_to_event(:type=>:internalChange), :with => '\'deep_link=\'+SWFAddress.getPath()'}
|
5
|
+
}
|
6
|
+
|
7
|
+
SWFAddress.onExternalChange = function() {
|
8
|
+
//alert(SWFAddress.getPath()+SWFAddress.getQueryString());
|
9
|
+
#{remote_function :url => address_to_event(:type=>:internalChange), :with => '\'deep_link=\'+SWFAddress.getPath()'}
|
10
|
+
}
|
11
|
+
|
12
|
+
|
13
|
+
SWFAddress.onChange = function() {
|
14
|
+
}
|
15
|
+
|
16
|
+
SWFAddress.onInit = function() {
|
17
|
+
#{remote_function :url => address_to_event(:type=>:internalChange), :with => '\'deep_link=\'+SWFAddress.getPath()'}
|
18
|
+
//alert('init');
|
19
|
+
}
|
20
|
+
" %>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Apotomo
|
2
|
+
|
3
|
+
### TODO: if a state doesn't return anything, the view-finding is invoked, which
|
4
|
+
### is nonsense in a JS widget. current work-around: return render :js => ""
|
5
|
+
|
6
|
+
class JavaScriptWidget < StatefulWidget
|
7
|
+
def frame_content(content)
|
8
|
+
content
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Apotomo
|
2
|
+
class TabPanelWidget < StatefulWidget
|
3
|
+
transition :from => :display, :to => :switch
|
4
|
+
transition :in => :switch
|
5
|
+
|
6
|
+
attr_accessor :current_child_id
|
7
|
+
|
8
|
+
|
9
|
+
# Called in StatefulWidget's constructor.
|
10
|
+
def initialize_deep_link_for(id, start_states, opts)
|
11
|
+
return unless opts[:is_url_listener]
|
12
|
+
|
13
|
+
respond_to_event :urlChange, :from => self.name, :with => :switch
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
def display
|
19
|
+
respond_to_event(:switchChild, :with => :switch)
|
20
|
+
|
21
|
+
|
22
|
+
@current_child_id = find_current_child.name
|
23
|
+
|
24
|
+
render :locals => {:tabs => children}
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
def switch
|
29
|
+
@current_child_id = find_current_child.name
|
30
|
+
|
31
|
+
render :view => :display, :locals => {:tabs => children}
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def children_to_render
|
36
|
+
[children.find{ |c| c.name == @current_child_id } ]
|
37
|
+
end
|
38
|
+
|
39
|
+
### DISCUSS: use #find_param instead of #param to provide a cleaner parameter retrieval?
|
40
|
+
def find_current_child
|
41
|
+
if responds_to_url_change?
|
42
|
+
child_id = url_fragment[param_name]
|
43
|
+
else
|
44
|
+
child_id = param(param_name)
|
45
|
+
end
|
46
|
+
|
47
|
+
find_child(child_id) || find_child(@current_child_id) || default_child
|
48
|
+
end
|
49
|
+
|
50
|
+
def default_child; children.first; end
|
51
|
+
|
52
|
+
|
53
|
+
def find_child(id)
|
54
|
+
children.find { |c| c.name.to_s == id }
|
55
|
+
end
|
56
|
+
|
57
|
+
def param_name; name; end
|
58
|
+
|
59
|
+
|
60
|
+
# Called by deep_link_widget#process to query if we're involved in an URL change.
|
61
|
+
def responds_to_url_change_for?(fragment)
|
62
|
+
# don't respond to an empty/invalid/ fragment as we don't get any information from it:
|
63
|
+
return if fragment[param_name].blank?
|
64
|
+
|
65
|
+
fragment[param_name] != @current_child_id
|
66
|
+
end
|
67
|
+
|
68
|
+
def local_fragment
|
69
|
+
"#{param_name}=#{current_child_id}"
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
# Used in view to create the tab link in deep-linking mode.
|
74
|
+
def url_fragment_for_tab(tab)
|
75
|
+
url_fragment_for("#{param_name}=#{tab.name}")
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def address(way={}, target=self, state=nil)
|
80
|
+
way.merge!( local_address(target, way, state) )
|
81
|
+
|
82
|
+
return way if isRoot?
|
83
|
+
|
84
|
+
return parent.address(way, target)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
<style type="text/css">
|
2
|
+
.TabPanel ul {
|
3
|
+
display: block;
|
4
|
+
list-style: none;
|
5
|
+
padding: 0;
|
6
|
+
margin: 0 0 -1px 0;
|
7
|
+
height: 100%;
|
8
|
+
font-size: 14px;
|
9
|
+
border-left: 1px solid #888888;
|
10
|
+
|
11
|
+
}
|
12
|
+
|
13
|
+
.TabPanel ul li {
|
14
|
+
float: left;
|
15
|
+
margin: 0;
|
16
|
+
padding: 5px;
|
17
|
+
background-color: #dbdbdb;
|
18
|
+
color: #888888;
|
19
|
+
border-top: 1px solid #888888;
|
20
|
+
border-right: 1px solid #888888;
|
21
|
+
border-bottom: 1px solid #888888;
|
22
|
+
}
|
23
|
+
|
24
|
+
.TabPanel ul li.active {
|
25
|
+
border-bottom: 1px solid #ffffff;
|
26
|
+
background-color: #ffffff;
|
27
|
+
color: #000000;
|
28
|
+
|
29
|
+
}
|
30
|
+
|
31
|
+
.TabPanel ul li a, .TabPanel ul li a:visited {
|
32
|
+
color: #888888
|
33
|
+
}
|
34
|
+
</style>
|
35
|
+
|
36
|
+
|
37
|
+
<div class="TabPanel">
|
38
|
+
<ul>
|
39
|
+
<% tabs.each do |tab|
|
40
|
+
tab_class = ""
|
41
|
+
tab_class = "active" if tab.name == @current_child_id
|
42
|
+
%>
|
43
|
+
<li class="<%= tab_class %>">
|
44
|
+
<%- if @cell.responds_to_url_change? %>
|
45
|
+
<%= link_to_function tab.title, update_url(@cell.url_fragment_for_tab(tab)) %>
|
46
|
+
<%- else %>
|
47
|
+
<%= link_to_event tab.title, {:type => :switchChild, @cell.param_name => tab.name} %>
|
48
|
+
<% end %>
|
49
|
+
</li>
|
50
|
+
<% end -%>
|
51
|
+
|
52
|
+
<div style="clear: both;" />
|
53
|
+
</ul>
|
54
|
+
</div>
|
55
|
+
<div style="clear: both;"></div>
|
56
|
+
|
57
|
+
<%= rendered_children.first %>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= rendered_children.collect{|e| e.last}.join("") %>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
Description:
|
2
|
+
Stubs out a new cell widget, its state views and a functional test.
|
3
|
+
Pass the cell name, either CamelCased or under_scored, and a list
|
4
|
+
of states as arguments.
|
5
|
+
|
6
|
+
This generates a cell class in app/cells and view templates in
|
7
|
+
app/cells/cell_name.
|
8
|
+
|
9
|
+
Example:
|
10
|
+
'./script/generate widget Posting new'
|
11
|
+
|
12
|
+
This will create an Apotomo Posting cell:
|
13
|
+
Cell: app/cells/posting_cell.rb
|
14
|
+
Views: app/cells/posting/new.html.erb
|
15
|
+
Test: test/functional/test_posting_cell.rb
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'rails_generator/generators/components/controller/controller_generator'
|
2
|
+
|
3
|
+
class WidgetGenerator < ControllerGenerator
|
4
|
+
def add_options!(opt)
|
5
|
+
opt.on('--haml') { |value| options[:view_format] = 'haml' }
|
6
|
+
end
|
7
|
+
|
8
|
+
def manifest
|
9
|
+
options.reverse_merge! :view_format => 'erb'
|
10
|
+
|
11
|
+
record do |m|
|
12
|
+
# Check for class naming collisions.
|
13
|
+
m.class_collisions class_path, "#{class_name}"
|
14
|
+
|
15
|
+
# Directories
|
16
|
+
m.directory File.join('app/cells', class_path)
|
17
|
+
m.directory File.join('app/cells', class_path, file_name)
|
18
|
+
m.directory File.join('test/widgets')
|
19
|
+
|
20
|
+
# Widget
|
21
|
+
m.template 'widget.rb', File.join('app/cells', class_path, "#{file_name}.rb")
|
22
|
+
|
23
|
+
# View template for each state.
|
24
|
+
format = options[:view_format]
|
25
|
+
actions.each do |state|
|
26
|
+
path = File.join('app/cells', class_path, file_name, "#{state}.html.#{format}")
|
27
|
+
m.template "view.html.#{format}", path, :assigns => { :action => state, :path => path }
|
28
|
+
end
|
29
|
+
|
30
|
+
# Functional test for the widget.
|
31
|
+
m.template 'functional_test.rb', File.join('test/widgets/', "#{file_name}_test.rb"), :assigns => {:states => actions}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/apotomo.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# Copyright (c) 2007-2010 Nick Sutterer <apotonick@gmail.com>
|
2
|
+
#
|
3
|
+
# The MIT License
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
13
|
+
# all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
# THE SOFTWARE.
|
22
|
+
#
|
23
|
+
|
24
|
+
|
25
|
+
require 'cells'
|
26
|
+
require 'onfire'
|
27
|
+
|
28
|
+
module Apotomo
|
29
|
+
class << self
|
30
|
+
def js_framework=(js_framework)
|
31
|
+
@js_framework = js_framework
|
32
|
+
@js_generator = ::Apotomo::JavascriptGenerator.new(js_framework)
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_reader :js_generator, :js_framework
|
36
|
+
|
37
|
+
# Apotomo setup/configuration helper for initializer.
|
38
|
+
#
|
39
|
+
# == Usage/Examples:
|
40
|
+
#
|
41
|
+
# Apotomo.setup do |config|
|
42
|
+
# config.js_framework = :jquery
|
43
|
+
# end
|
44
|
+
def setup
|
45
|
+
yield self
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
### FIXME: move to rails.rb
|
51
|
+
|
52
|
+
require 'apotomo/javascript_generator'
|
53
|
+
Apotomo.js_framework = :prototype ### DISCUSS: move to rails.rb
|
54
|
+
|
55
|
+
### DISCUSS: move to 'apotomo/widgets'?
|
56
|
+
require 'apotomo/widget'
|
57
|
+
require 'apotomo/stateful_widget'
|
58
|
+
require 'apotomo/container_widget'
|
59
|
+
require 'apotomo/widget_shortcuts'
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# Introduces caching of rendered state views into the StatefulWidget.
|
2
|
+
module Apotomo::Caching
|
3
|
+
|
4
|
+
def self.included(base) #:nodoc:
|
5
|
+
base.class_eval do
|
6
|
+
extend ClassMethods
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
# If <tt>version_proc</tt> is omitted, Apotomo provides some basic caching
|
13
|
+
# mechanism: the state view rendered for <tt>state</tt> will be cached as long
|
14
|
+
# as you (or e.g. an EventHandler) calls #dirty!. It will then be re-rendered
|
15
|
+
# and cached again.
|
16
|
+
# You may override that to provide fine-grained caching, with multiple cache versions
|
17
|
+
# for the same state.
|
18
|
+
def cache(state, version_proc=:cache_version)
|
19
|
+
super(state, version_proc)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def cache_version
|
24
|
+
@version ||= 0
|
25
|
+
{:v => @version}
|
26
|
+
end
|
27
|
+
|
28
|
+
def increment_version
|
29
|
+
@version += 1
|
30
|
+
end
|
31
|
+
|
32
|
+
# Instruct caching to re-render all cached state views.
|
33
|
+
def dirty!
|
34
|
+
increment_version
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Apotomo
|
2
|
+
module DeepLinkMethods
|
3
|
+
def self.included(base)
|
4
|
+
base.initialize_hooks << :initialize_deep_link_for
|
5
|
+
end
|
6
|
+
|
7
|
+
|
8
|
+
# Called in StatefulWidget's constructor.
|
9
|
+
def initialize_deep_link_for(id, start_states, opts)
|
10
|
+
#add_deep_link if opts[:is_url_listener] ### DISCUSS: remove #add_de
|
11
|
+
end
|
12
|
+
|
13
|
+
def responds_to_url_change?
|
14
|
+
evt_table.all_handlers_for(:urlChange, name).size > 0
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
### DISCUSS: private? rename to compute_url_fragment_for ?
|
19
|
+
# Computes the fragment part of the widget's url by querying all widgets up to root.
|
20
|
+
# Widgets managing a certain state will usually insert state recovery information
|
21
|
+
# via local_fragment.
|
22
|
+
def url_fragment_for(local_portion=nil, portions=[])
|
23
|
+
local_portion = local_fragment if responds_to_url_change? and local_portion.nil?
|
24
|
+
|
25
|
+
portions.unshift(local_portion) # prepend portions as we move up.
|
26
|
+
|
27
|
+
return portions.compact.join("/") if root?
|
28
|
+
|
29
|
+
parent.url_fragment_for(nil, portions)
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# Called when widget :is_url_listener. Adds the local url fragment portion to the url.
|
34
|
+
def local_fragment
|
35
|
+
#"#{local_fragment_key}=#{state_name}"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Key found in the url fragment, pointing to the local fragment.
|
39
|
+
#def local_fragment_key
|
40
|
+
# name
|
41
|
+
#end
|
42
|
+
|
43
|
+
|
44
|
+
# Called by DeepLinkWidget#process to query if we're involved in an URL change.
|
45
|
+
# Do return false if you're not interested in the change.
|
46
|
+
#
|
47
|
+
# This especially means:
|
48
|
+
# * the fragment doesn't include you or is empty
|
49
|
+
# fragment[name].blank?
|
50
|
+
# * your portion in the fragment didn't change
|
51
|
+
# tab=first/content=html vs. tab=first/content=markdown
|
52
|
+
# fragment[:tab] != @active_tab
|
53
|
+
def responds_to_url_change_for?(fragment)
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
|
58
|
+
class UrlFragment
|
59
|
+
attr_reader :fragment
|
60
|
+
|
61
|
+
def initialize(fragment)
|
62
|
+
@fragment = fragment || ""
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_s
|
66
|
+
fragment.to_s
|
67
|
+
end
|
68
|
+
|
69
|
+
def blank?
|
70
|
+
fragment.blank?
|
71
|
+
end
|
72
|
+
|
73
|
+
### TODO: make path separator configurable.
|
74
|
+
def [](key)
|
75
|
+
if path_portion = fragment.split("/").find {|i| i.include?(key.to_s)}
|
76
|
+
return path_portion.sub("#{key}=", "")
|
77
|
+
end
|
78
|
+
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Query object for the url fragment. Use this to retrieve state information from the
|
84
|
+
# deep link.
|
85
|
+
def url_fragment
|
86
|
+
UrlFragment.new(param(:deep_link))
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|