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,95 @@
|
|
1
|
+
module Apotomo
|
2
|
+
module Rails
|
3
|
+
module ViewHelper
|
4
|
+
# needs :@controller
|
5
|
+
|
6
|
+
# Returns the app JavaScript generator.
|
7
|
+
def js_generator
|
8
|
+
Apotomo.js_generator
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
# Generates the JavaScript code to report an event of <tt>type</tt> to Apotomo with AJAX.
|
13
|
+
# As always per default the event source is the currently rendered widget.
|
14
|
+
# Internally this method just uses <tt>remote_function</tt> for JS output.
|
15
|
+
#
|
16
|
+
# Example:
|
17
|
+
#
|
18
|
+
# <%= image_tag "cheese.png", :onMouseover => trigger_event(:mouseAlarm) %>
|
19
|
+
#
|
20
|
+
# will trigger the event <tt>:mouseAlarm</tt> when moving the mouse over the cheese image.
|
21
|
+
def trigger_event(type, options={})
|
22
|
+
js_generator.xhr(url_for_event(type, options))
|
23
|
+
end
|
24
|
+
|
25
|
+
# Creates a link that triggers an event via AJAX.
|
26
|
+
# This link will <em>only</em> work in JavaScript-able browsers.
|
27
|
+
#
|
28
|
+
# Note that the link is created using #link_to_remote.
|
29
|
+
def link_to_event(title, type, options={}, html_options={})
|
30
|
+
link_to_remote(title, {:url => url_for_event(type, options)}, html_options)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Creates a form tag that triggers an event via AJAX when submitted.
|
34
|
+
# See StatefulWidget::address_for_event for options.
|
35
|
+
#
|
36
|
+
# The values of form elements are available via StatefulWidget#param.
|
37
|
+
def form_to_event(type, options={}, html_options={}, &block)
|
38
|
+
return multipart_form_to_event(type, options, html_options, &block) if options.delete(:multipart)
|
39
|
+
|
40
|
+
form_remote_tag({:url => url_for_event(type, options), :html => html_options}, &block)
|
41
|
+
### FIXME: couldn't get obstrusive js working, i don't understand rails helpers.
|
42
|
+
#html_options[:onSubmit] = js_generator.escape(js_generator.xhr(url_for_event(type, options)))
|
43
|
+
#puts html_options.inspect
|
44
|
+
#form_tag(url_for_event(type, options), html_options, &block)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Creates a form that submits itself via an iFrame and executes the response
|
48
|
+
# in the parent window. This is needed to upload files via AJAX.
|
49
|
+
#
|
50
|
+
# Better call <tt>#form_to_event :multipart => true</tt> and stay forward-compatible.
|
51
|
+
def multipart_form_to_event(type, options={}, html_options={}, &block)
|
52
|
+
options.reverse_merge! :apotomo_iframe => true
|
53
|
+
html_options.reverse_merge! :target => :apotomo_iframe, :multipart => true
|
54
|
+
|
55
|
+
# i hate rails:
|
56
|
+
concat('<iframe id="apotomo_iframe" name="apotomo_iframe" style="display: none;"></iframe>') << form_tag(url_for_event(type, options), html_options, &block)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns the url to trigger a +type+ event from the currently rendered widget.
|
60
|
+
# The source can be changed with +:source+. Additional +options+ will be appended to the query string.
|
61
|
+
#
|
62
|
+
# Note that this method will use the framework's internal routing if available (e.g. #url_for in Rails).
|
63
|
+
#
|
64
|
+
# Example:
|
65
|
+
# url_for_event(:paginate, :page => 2)
|
66
|
+
# #=> http://apotomo.de/mouse/process_event_request?type=paginate&source=mouse&page=2
|
67
|
+
def url_for_event(type, options={})
|
68
|
+
options.reverse_merge! :source => @cell.name
|
69
|
+
@controller.url_for_event(type, options)
|
70
|
+
end
|
71
|
+
|
72
|
+
### TODO: test me.
|
73
|
+
def update_url(fragment)
|
74
|
+
'SWFAddress.setValue("' + fragment + '");'
|
75
|
+
end
|
76
|
+
|
77
|
+
### TODO: test me.
|
78
|
+
### DISCUSS: rename to rendered_children ?
|
79
|
+
def content
|
80
|
+
@rendered_children.collect{|e| e.last}.join("\n")
|
81
|
+
end
|
82
|
+
|
83
|
+
# needs: suppress_javascript
|
84
|
+
def widget_javascript(*args, &block)
|
85
|
+
return if @suppress_js ### FIXME: implement with ActiveHelper and :locals.
|
86
|
+
|
87
|
+
javascript_tag(*args, &block)
|
88
|
+
end
|
89
|
+
|
90
|
+
def widget_div(*args, &block)
|
91
|
+
content_tag(:div, :id => @cell.name, :class => :widget, &block)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Apotomo
|
2
|
+
class RequestProcessor
|
3
|
+
include WidgetShortcuts
|
4
|
+
|
5
|
+
attr_reader :session, :root
|
6
|
+
|
7
|
+
def initialize(session, options={}, uses_widgets_blocks=[])
|
8
|
+
@session = session
|
9
|
+
@widgets_flushed = false
|
10
|
+
|
11
|
+
@root = widget('apotomo/widget', 'root')
|
12
|
+
uses_widgets_blocks.each { |blk| blk.call(@root) } # add stateless widgets.
|
13
|
+
|
14
|
+
if options[:flush_widgets].blank? and ::Apotomo::StatefulWidget.frozen_widget_in?(session)
|
15
|
+
@root = ::Apotomo::StatefulWidget.thaw_for(session, @root)
|
16
|
+
else
|
17
|
+
#@root = flushed_root
|
18
|
+
|
19
|
+
flushed_root ### FIXME: set internal mode to flushed
|
20
|
+
end
|
21
|
+
|
22
|
+
#handle_version!(options[:version])
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def flushed_root
|
27
|
+
StatefulWidget.flush_storage(session)
|
28
|
+
@widgets_flushed = true
|
29
|
+
#widget('apotomo/widget', 'root')
|
30
|
+
end
|
31
|
+
|
32
|
+
### DISCUSS: do we need the version feature, or should we push that into user code?
|
33
|
+
def handle_version!(version)
|
34
|
+
return if version.blank?
|
35
|
+
return if root.version == version
|
36
|
+
|
37
|
+
@root = flushed_root
|
38
|
+
@root.version = version
|
39
|
+
end
|
40
|
+
|
41
|
+
def widgets_flushed?; @widgets_flushed; end
|
42
|
+
|
43
|
+
# Fires the request event in the widget tree and collects the rendered page updates.
|
44
|
+
def process_for(request_params, controller)
|
45
|
+
### TODO: move controller dependency to rails/merb/sinatra layer only!
|
46
|
+
self.root.controller = controller
|
47
|
+
|
48
|
+
source = self.root.find_widget(request_params[:source]) or raise "Source #{request_params[:source].inspect} non-existent."
|
49
|
+
|
50
|
+
source.fire(request_params[:type].to_sym)
|
51
|
+
source.root.page_updates ### DISCUSS: that's another dependency.
|
52
|
+
end
|
53
|
+
|
54
|
+
### FIXME: remove me!
|
55
|
+
def render_page_updates(page_updates)
|
56
|
+
page_updates.join("\n")
|
57
|
+
end
|
58
|
+
|
59
|
+
# Serializes the current widget tree to the storage that was passed in the constructor.
|
60
|
+
# Call this at the end of a request.
|
61
|
+
def freeze!
|
62
|
+
Apotomo::StatefulWidget.freeze_for(@session, root)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Renders the widget named <tt>widget_id</tt>, passing optional <tt>opts</tt> and a block to it.
|
66
|
+
# Use this in your #render_widget wrapper.
|
67
|
+
def render_widget_for(widget_id, opts, controller, &block)
|
68
|
+
if widget_id.kind_of?(::Apotomo::Widget)
|
69
|
+
widget = widget_id
|
70
|
+
else
|
71
|
+
widget = root.find_widget(widget_id)
|
72
|
+
raise "Couldn't render non-existent widget `#{widget_id}`" unless widget
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
### TODO: pass options in invoke.
|
77
|
+
widget.opts = opts unless opts.empty?
|
78
|
+
|
79
|
+
### TODO: move controller dependency to rails/merb/sinatra layer only!
|
80
|
+
widget.root.controller = controller
|
81
|
+
|
82
|
+
widget.invoke(&block)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Computes the address hash for a +:source+ widget and an event +:type+.
|
86
|
+
# Additional parameters will be merged.
|
87
|
+
def address_for(options)
|
88
|
+
raise "You forgot to provide :source or :type" unless options.has_key?(:source) and options.has_key?(:type)
|
89
|
+
options
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Apotomo::Transition
|
2
|
+
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
# Defines a transition for an implicit invoke.
|
9
|
+
#
|
10
|
+
# Usually when a container widget renders its kids there is no <tt>state</tt> passed to the
|
11
|
+
# kid's #invoke ("implicit invoke") and thus the kid will enter its start state again.
|
12
|
+
# You can customize that behaviour by setting a transition to let the widget jump to the
|
13
|
+
# defined <tt>:to</tt> state in place of the start state.
|
14
|
+
#
|
15
|
+
# Example:
|
16
|
+
# class Kid < MouseWidget
|
17
|
+
# transition :from => :sleep, :to => :snore
|
18
|
+
#
|
19
|
+
# Next time when mum renders and kid is in <tt>:sleep</tt> state kid will not return to its
|
20
|
+
# start state but invoke <tt>:snore</tt>.
|
21
|
+
#
|
22
|
+
# class Kid < MouseWidget
|
23
|
+
# transition :in => :snore
|
24
|
+
#
|
25
|
+
# In subsequent render cycles from mum kid will keep snoring whereas other kids would go back
|
26
|
+
# to the start state.
|
27
|
+
def transition(options)
|
28
|
+
if from = options[:from]
|
29
|
+
class_transitions[from] = options[:to]
|
30
|
+
elsif loop = options[:in]
|
31
|
+
transition :from => loop, :to => loop
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def class_transitions
|
36
|
+
@class_transitions ||= {}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
# Returns the next state for <tt>state</tt> or nil. A next state must have been defined
|
42
|
+
# with #transition.
|
43
|
+
def next_state_for(state)
|
44
|
+
self.class.class_transitions[state]
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
# stolen from ... ? couldn't find the original lib on the net.
|
2
|
+
### TODO: insert copyright notice!
|
3
|
+
|
4
|
+
module TreeNode
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
attr_reader :content, :name, :parent
|
8
|
+
attr_writer :content, :parent
|
9
|
+
|
10
|
+
def self.included(base)
|
11
|
+
base.initialize_hooks << :initialize_tree_node_for
|
12
|
+
end
|
13
|
+
|
14
|
+
# Constructor which expects the name of the node
|
15
|
+
#
|
16
|
+
# name of the node is expected to be unique across the
|
17
|
+
# tree.
|
18
|
+
def initialize_tree_node_for(name, *args)
|
19
|
+
self.setAsRoot!
|
20
|
+
|
21
|
+
@childrenHash = Hash.new
|
22
|
+
@children = []
|
23
|
+
end
|
24
|
+
|
25
|
+
# Print the string representation of this node.
|
26
|
+
def to_s
|
27
|
+
s = size()
|
28
|
+
"Node ID: #{@name} Content: #{@content} Parent: " +
|
29
|
+
(root?() ? "ROOT" : "#{@parent.name}") +
|
30
|
+
" Children: #{@children.length}" +
|
31
|
+
" Total Nodes: #{s}"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Convenience synonym for Tree#add method.
|
35
|
+
# This method allows a convenient method to add
|
36
|
+
# children hierarchies in the tree.
|
37
|
+
# E.g. root << child << grand_child
|
38
|
+
def <<(child)
|
39
|
+
add(child)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Adds the specified child node to the receiver node.
|
43
|
+
# The child node's parent is set to be the receiver.
|
44
|
+
# The child is added as the last child in the current
|
45
|
+
# list of children for the receiver node.
|
46
|
+
def add(child)
|
47
|
+
raise "Child already added" if @childrenHash.has_key?(child.name)
|
48
|
+
|
49
|
+
@childrenHash[child.name] = child
|
50
|
+
@children << child
|
51
|
+
child.parent = self
|
52
|
+
return child
|
53
|
+
end
|
54
|
+
|
55
|
+
# Removes the specified child node from the receiver node.
|
56
|
+
# The removed children nodes are orphaned but available
|
57
|
+
# if an alternate reference exists.
|
58
|
+
# Returns the child node.
|
59
|
+
def remove!(child)
|
60
|
+
@childrenHash.delete(child.name)
|
61
|
+
@children.delete(child)
|
62
|
+
child.setAsRoot! unless child == nil
|
63
|
+
return child
|
64
|
+
end
|
65
|
+
|
66
|
+
# Removes this node from its parent. If this is the root node,
|
67
|
+
# then does nothing.
|
68
|
+
def removeFromParent!
|
69
|
+
@parent.remove!(self) unless root?
|
70
|
+
end
|
71
|
+
|
72
|
+
# Removes all children from the receiver node.
|
73
|
+
def remove_all!
|
74
|
+
for child in @children
|
75
|
+
child.setAsRoot!
|
76
|
+
end
|
77
|
+
@childrenHash.clear
|
78
|
+
@children.clear
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
# Private method which sets this node as a root node.
|
84
|
+
def setAsRoot!
|
85
|
+
@parent = nil
|
86
|
+
end
|
87
|
+
|
88
|
+
def root!
|
89
|
+
setAsRoot!
|
90
|
+
end
|
91
|
+
|
92
|
+
# Indicates whether this node is a root node. Note that
|
93
|
+
# orphaned children will also be reported as root nodes.
|
94
|
+
def root?
|
95
|
+
@parent == nil
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns an array of all the immediate children.
|
99
|
+
# If a block is given, yields each child node to the block.
|
100
|
+
def children
|
101
|
+
if block_given?
|
102
|
+
@children.each {|child| yield child}
|
103
|
+
else
|
104
|
+
@children
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns every node (including the receiver node) from the
|
109
|
+
# tree to the specified block.
|
110
|
+
def each &block
|
111
|
+
yield self
|
112
|
+
children { |child| child.each(&block) }
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns the requested node from the set of immediate
|
116
|
+
# children.
|
117
|
+
#
|
118
|
+
# If the key is _numeric_, then the in-sequence array of
|
119
|
+
# children is accessed (see Tree#children).
|
120
|
+
# If the key is not _numeric_, then it is assumed to be
|
121
|
+
# the *name* of the child node to be returned.
|
122
|
+
def [](key)
|
123
|
+
raise "Key needs to be provided" if key == nil
|
124
|
+
|
125
|
+
if key.kind_of?(Integer)
|
126
|
+
@children[key]
|
127
|
+
else
|
128
|
+
@childrenHash[key]
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Returns the total number of nodes in this tree, rooted
|
133
|
+
# at the receiver node.
|
134
|
+
def size
|
135
|
+
@children.inject(1) {|sum, node| sum + node.size}
|
136
|
+
end
|
137
|
+
|
138
|
+
# Pretty prints the tree starting with the receiver node.
|
139
|
+
def printTree(tab = 0)
|
140
|
+
puts((' ' * tab) + self.to_s)
|
141
|
+
children {|child| child.printTree(tab + 4)}
|
142
|
+
end
|
143
|
+
|
144
|
+
# Returns the root for this node.
|
145
|
+
def root
|
146
|
+
root = self
|
147
|
+
root = root.parent while !root.root?
|
148
|
+
root
|
149
|
+
end
|
150
|
+
|
151
|
+
# Provides a comparision operation for the nodes. Comparision
|
152
|
+
# is based on the natural character-set ordering for the
|
153
|
+
# node names.
|
154
|
+
def <=>(other)
|
155
|
+
return +1 if other == nil
|
156
|
+
self.name <=> other.name
|
157
|
+
end
|
158
|
+
|
159
|
+
protected :parent=, :setAsRoot!
|
160
|
+
|
161
|
+
def find_by_path(selector)
|
162
|
+
next_node = self
|
163
|
+
last = nil # prevents self-finding loop.
|
164
|
+
selector.to_s.split(/ /).each do |node_id|
|
165
|
+
last = next_node = next_node.find {|n|
|
166
|
+
n.name.to_s == node_id.to_s and not n==last
|
167
|
+
}
|
168
|
+
end
|
169
|
+
|
170
|
+
return next_node
|
171
|
+
end
|
172
|
+
|
173
|
+
|
174
|
+
# Returns the path from the widget to root, encoded as
|
175
|
+
# a string of slash-seperated names.
|
176
|
+
def path
|
177
|
+
path = [name]
|
178
|
+
ancestor = parent
|
179
|
+
while ancestor
|
180
|
+
path << ancestor.name
|
181
|
+
ancestor = ancestor.parent
|
182
|
+
end
|
183
|
+
|
184
|
+
path.reverse.join("/")
|
185
|
+
end
|
186
|
+
end
|