apotomo 0.1.1
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/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
|