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,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
|