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,9 @@
|
|
1
|
+
module Apotomo
|
2
|
+
# Events are created by Apotomo in #fire. They bubble up from their source to root and trigger
|
3
|
+
# event handlers.
|
4
|
+
class Event < Onfire::Event
|
5
|
+
def _dump(depth)
|
6
|
+
raise "You're trying to serialize an instance of Apotomo::Event. Don't do that."
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Apotomo
|
2
|
+
# EventHandlers are "callbacks", not knowing why they exist, but what to do.
|
3
|
+
class EventHandler
|
4
|
+
|
5
|
+
def process_event(event)
|
6
|
+
# do something, and return content.
|
7
|
+
nil
|
8
|
+
end
|
9
|
+
|
10
|
+
def ==(other)
|
11
|
+
self.to_s == other.to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
# Invoked by Onfire.
|
15
|
+
def call(event)
|
16
|
+
event.source.root.page_updates << process_event(event)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'apotomo/invoke_event_handler'
|
23
|
+
require 'apotomo/proc_event_handler'
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'apotomo/event_handler'
|
2
|
+
|
3
|
+
module Apotomo
|
4
|
+
# Introduces event-processing functions into the StatefulWidget.
|
5
|
+
|
6
|
+
module EventMethods
|
7
|
+
attr_writer :page_updates
|
8
|
+
# Replacement for the EventProcessor singleton queue.
|
9
|
+
def page_updates
|
10
|
+
@page_updates ||= []
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.included(base)
|
14
|
+
base.extend(ClassMethods)
|
15
|
+
base.initialize_hooks << :add_class_event_handlers
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_class_event_handlers(*)
|
19
|
+
self.class.responds_to_event_options.each { |options| respond_to_event(*options) }
|
20
|
+
end
|
21
|
+
|
22
|
+
module ClassMethods
|
23
|
+
def responds_to_event(*options)
|
24
|
+
responds_to_event_options << options
|
25
|
+
end
|
26
|
+
alias_method :respond_to_event, :responds_to_event
|
27
|
+
|
28
|
+
def responds_to_event_options
|
29
|
+
@responds_to_event_options ||= []
|
30
|
+
end
|
31
|
+
end
|
32
|
+
# Instructs the widget to look out for <tt>type</tt> Events that are passing by while bubbling.
|
33
|
+
# If an appropriate event is encountered the widget will send the targeted widget (or itself) to another
|
34
|
+
# state, which implies an update of the invoked widget.
|
35
|
+
#
|
36
|
+
# You may configure the event handler with the following <tt>options</tt>:
|
37
|
+
# :with => (required) the state to invoke on the target widget
|
38
|
+
# :on => (optional) the targeted widget's id, defaults to <tt>self.name</tt>
|
39
|
+
# :from => (optional) the source id of the widget that triggered the event, defaults to any widget
|
40
|
+
#
|
41
|
+
# Example:
|
42
|
+
#
|
43
|
+
# trap = cell(:input_field, :smell_like_cheese, 'mouse_trap')
|
44
|
+
# trap.respond_to_event :mouseOver, :with => :catch_mouse
|
45
|
+
#
|
46
|
+
# This would instruct <tt>trap</tt> to catch a <tt>:mouseOver</tt> event from any widget (including itself) and
|
47
|
+
# to invoke the state <tt>:catch_mouse</tt> on itself as trigger.
|
48
|
+
#
|
49
|
+
#
|
50
|
+
# hunter = cell(:form, :hunt_for_mice, 'my_form')
|
51
|
+
# hunter << cell(:input_field, :smell_like_cheese, 'mouse_trap')
|
52
|
+
# hunter << cell(:text_area, :stick_like_honey, 'bear_trap')
|
53
|
+
# hunter.respond_to_event :captured, :from => 'mouse_trap', :with => :refill_cheese, :on => 'mouse_trap'
|
54
|
+
#
|
55
|
+
# As both the bear- and the mouse trap can trigger a <tt>:captured</tt> event the later <tt>respond_to_event</tt>
|
56
|
+
# would invoke <tt>:refill_cheese</tt> on the <tt>mouse_trap</tt> widget as soon as this and only this widget fired.
|
57
|
+
# It is important to understand the <tt>:from</tt> parameter as it filters the event source - it wouldn't make
|
58
|
+
# sense to refill the mouse trap if the bear trap snapped, would it?
|
59
|
+
|
60
|
+
def respond_to_event(type, options)
|
61
|
+
options[:once] = true if options[:once].nil?
|
62
|
+
|
63
|
+
handler_opts = {}
|
64
|
+
handler_opts[:widget_id] = options[:on] || self.name
|
65
|
+
handler_opts[:state] = options[:with]
|
66
|
+
|
67
|
+
handler = InvokeEventHandler.new(handler_opts)
|
68
|
+
|
69
|
+
return if options[:once] and event_table.all_handlers_for(type, options[:from]).include?(handler)
|
70
|
+
|
71
|
+
on(type, :do => handler, :from => options[:from])
|
72
|
+
end
|
73
|
+
|
74
|
+
def trigger(*args)
|
75
|
+
fire(*args)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Invokes <tt>state</tt> on the widget <em>and</end> updates itself on the page. This should
|
79
|
+
# never be called from outside but in setters when some internal value changed and must be
|
80
|
+
# displayed instantly.
|
81
|
+
#
|
82
|
+
# Implements the following pattern (TODO: remove example as soon as invoke! proofed):
|
83
|
+
#
|
84
|
+
# def title=(str)
|
85
|
+
# @title = str
|
86
|
+
# peek(:update, self.name, :display, self.name)
|
87
|
+
# trigger(:update)
|
88
|
+
# end
|
89
|
+
def invoke!(state)
|
90
|
+
### TODO: encapsulate in PageUpdateQueue:
|
91
|
+
Apotomo::EventProcessor.instance.processed_handlers << [name, invoke(:state)]
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
|
96
|
+
protected
|
97
|
+
# Get all handlers from self for the passed event (overriding Onfire#local_event_handlers).
|
98
|
+
def local_event_handlers(event)
|
99
|
+
event_table.all_handlers_for(event.type, event.source.name) # we key with widget_id.
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Apotomo
|
2
|
+
class InvokeEventHandler < EventHandler
|
3
|
+
attr_accessor :widget_id, :state
|
4
|
+
|
5
|
+
def initialize(opts={})
|
6
|
+
@widget_id = opts.delete(:widget_id)
|
7
|
+
@state = opts.delete(:state)
|
8
|
+
end
|
9
|
+
|
10
|
+
def process_event(event)
|
11
|
+
target = event.source.root.find_by_path(widget_id) ### DISCUSS: widget_id or widget_selector?
|
12
|
+
|
13
|
+
#::Rails.logger.debug "EventHandler: invoking #{target.name}##{state}"
|
14
|
+
### DISCUSS: let target access event?
|
15
|
+
### pass additional opts to #invoke?
|
16
|
+
### DISCUSS: pass block here?
|
17
|
+
target.opts[:event] = event
|
18
|
+
|
19
|
+
target.invoke(state)
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s; "InvokeEventHandler:#{widget_id}##{state}"; end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Apotomo
|
2
|
+
class JavascriptGenerator
|
3
|
+
def initialize(framework)
|
4
|
+
raise "No JS framework specified" if framework.blank?
|
5
|
+
extend "apotomo/javascript_generator/#{framework}".camelize.constantize
|
6
|
+
end
|
7
|
+
|
8
|
+
def <<(javascript)
|
9
|
+
"#{javascript}"
|
10
|
+
end
|
11
|
+
|
12
|
+
# Copied from ActionView::Helpers::JavascriptHelper.
|
13
|
+
JS_ESCAPE_MAP = {
|
14
|
+
'\\' => '\\\\',
|
15
|
+
'</' => '<\/',
|
16
|
+
"\r\n" => '\n',
|
17
|
+
"\n" => '\n',
|
18
|
+
"\r" => '\n',
|
19
|
+
'"' => '\\"',
|
20
|
+
"'" => "\\'" }
|
21
|
+
|
22
|
+
# Escape carrier returns and single and double quotes for JavaScript segments.
|
23
|
+
def self.escape(javascript)
|
24
|
+
return javascript.gsub(/(\\|<\/|\r\n|[\n\r"'])/) { JS_ESCAPE_MAP[$1] } if javascript
|
25
|
+
|
26
|
+
''
|
27
|
+
end
|
28
|
+
|
29
|
+
def escape(javascript)
|
30
|
+
self.class.escape(javascript)
|
31
|
+
end
|
32
|
+
|
33
|
+
module Prototype
|
34
|
+
def prototype; end
|
35
|
+
def element(id); "$(\"#{id}\")"; end
|
36
|
+
def xhr(url); "new Ajax.Request(\"#{url}\")"; end
|
37
|
+
def update(id, markup); element(id) + '.update("'+escape(markup)+'")'; end
|
38
|
+
def replace(id, markup); element(id) + '.replace("'+escape(markup)+'")'; end
|
39
|
+
end
|
40
|
+
|
41
|
+
module Right
|
42
|
+
def right; end
|
43
|
+
def element(id); "$(\"#{id}\")"; end
|
44
|
+
def xhr(url); "new Xhr(\"#{url}\", {evalScripts:true}).send()"; end
|
45
|
+
def update(id, markup); element(id) + '.update("'+escape(markup)+'")'; end
|
46
|
+
def replace(id, markup); element(id) + '.replace("'+escape(markup)+'")'; end
|
47
|
+
end
|
48
|
+
|
49
|
+
module Jquery
|
50
|
+
def jquery; end
|
51
|
+
def element(id); "$(\"##{id}\")"; end
|
52
|
+
def xhr(url); "$.ajax({url: \"#{url}\"})"; end
|
53
|
+
def update(id, markup); element(id) + '.html("'+escape(markup)+'")'; end
|
54
|
+
def replace(id, markup); element(id) + '.replaceWith("'+escape(markup)+'")'; end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
module Apotomo
|
2
|
+
# Methods needed to serialize the widget tree and back.
|
3
|
+
module Persistence
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
# For Ruby 1.8/1.9 compatibility.
|
9
|
+
def symbolized_instance_variables
|
10
|
+
instance_variables.map { |ivar| ivar.to_sym }
|
11
|
+
end
|
12
|
+
|
13
|
+
def freeze_ivars_to(storage)
|
14
|
+
frozen = {}
|
15
|
+
(symbolized_instance_variables - unfreezable_ivars).each do |ivar|
|
16
|
+
frozen[ivar] = instance_variable_get(ivar)
|
17
|
+
end
|
18
|
+
storage[path] = frozen
|
19
|
+
end
|
20
|
+
|
21
|
+
### FIXME: rewrite so that root might be stateless as well.
|
22
|
+
def freeze_data_to(storage)
|
23
|
+
freeze_ivars_to(storage)# if self.kind_of?(StatefulWidget)
|
24
|
+
children.each { |child| child.freeze_data_to(storage) if child.kind_of?(StatefulWidget) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def freeze_to(storage)
|
28
|
+
storage[:apotomo_root] = self # save structure.
|
29
|
+
storage[:apotomo_widget_ivars] = {}
|
30
|
+
freeze_data_to(storage[:apotomo_widget_ivars]) # save ivars.
|
31
|
+
end
|
32
|
+
|
33
|
+
def thaw_ivars_from(storage)
|
34
|
+
storage.fetch(path, {}).each do |k, v|
|
35
|
+
instance_variable_set(k, v)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def thaw_data_from(storage)
|
40
|
+
thaw_ivars_from(storage)
|
41
|
+
children.each { |child| child.thaw_data_from(storage) }
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
# Serializes the widget node structure (not children, not content).
|
46
|
+
def dump_node
|
47
|
+
field_sep = self.class.field_sep
|
48
|
+
"#{@name}#{field_sep}#{self.class}#{field_sep}#{root? ? @name : parent.name}"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Serializes the tree structure.
|
52
|
+
def _dump(depth)
|
53
|
+
inject("") { |str, node| str << node.dump_node << self.class.node_sep }
|
54
|
+
end
|
55
|
+
|
56
|
+
module ClassMethods
|
57
|
+
def field_sep; '|'; end
|
58
|
+
def node_sep; "\n"; end
|
59
|
+
|
60
|
+
# Creates an empty widget instance from <tt>line</tt>.
|
61
|
+
def load_node(line)
|
62
|
+
name, klass, parent = line.split(field_sep)
|
63
|
+
[klass.constantize.new(name, nil), parent]
|
64
|
+
end
|
65
|
+
|
66
|
+
def _load(str)
|
67
|
+
nodes = {}
|
68
|
+
root = nil
|
69
|
+
str.split(node_sep).each do |line|
|
70
|
+
node, parent = load_node(line)
|
71
|
+
nodes[node.name] = node
|
72
|
+
|
73
|
+
if node.name == parent # we're at the root node.
|
74
|
+
root = node and next
|
75
|
+
end
|
76
|
+
|
77
|
+
nodes[parent].add(node)
|
78
|
+
end
|
79
|
+
root
|
80
|
+
end
|
81
|
+
|
82
|
+
def freeze_for(storage, root)
|
83
|
+
storage[:apotomo_stateful_branches] = []
|
84
|
+
storage[:apotomo_widget_ivars] = {}
|
85
|
+
|
86
|
+
stateful_branches_for(root).each do |branch|
|
87
|
+
branch.freeze_data_to(storage[:apotomo_widget_ivars]) # save ivars.
|
88
|
+
storage[:apotomo_stateful_branches] << [branch, branch.parent.name]
|
89
|
+
branch.root! # disconnect from tree.
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def thaw_for(storage, root)
|
94
|
+
branches = storage.delete(:apotomo_stateful_branches) || []
|
95
|
+
branches.each do |config|
|
96
|
+
branch = config.first
|
97
|
+
parent = root.find_widget(config.last) or raise "Couldn't find parent `#{config.last}` for `#{branch.name}`"
|
98
|
+
|
99
|
+
parent << branch
|
100
|
+
branch.thaw_data_from(storage.delete(:apotomo_widget_ivars) || {})
|
101
|
+
end
|
102
|
+
|
103
|
+
root
|
104
|
+
end
|
105
|
+
|
106
|
+
def thaw_from(storage)
|
107
|
+
root = storage[:apotomo_root]
|
108
|
+
root.thaw_data_from(storage.fetch(:apotomo_widget_ivars, {}))
|
109
|
+
root
|
110
|
+
end
|
111
|
+
|
112
|
+
def frozen_widget_in?(storage)
|
113
|
+
branches = storage[:apotomo_stateful_branches]
|
114
|
+
branches.present? and branches.first.first.kind_of? Apotomo::StatefulWidget
|
115
|
+
end
|
116
|
+
|
117
|
+
def flush_storage(storage)
|
118
|
+
storage[:apotomo_stateful_branches] = nil
|
119
|
+
storage[:apotomo_widget_ivars] = nil
|
120
|
+
end
|
121
|
+
|
122
|
+
# Find the first stateful widgets on each branch from +root+.
|
123
|
+
def stateful_branches_for(root)
|
124
|
+
to_traverse = [root]
|
125
|
+
stateful_roots = []
|
126
|
+
|
127
|
+
while node = to_traverse.shift
|
128
|
+
if node.kind_of?(StatefulWidget)
|
129
|
+
stateful_roots << node and next
|
130
|
+
end
|
131
|
+
to_traverse += node.children
|
132
|
+
end
|
133
|
+
|
134
|
+
stateful_roots
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Apotomo
|
2
|
+
class ProcEventHandler < EventHandler
|
3
|
+
attr_accessor :proc
|
4
|
+
|
5
|
+
def initialize(opts={})
|
6
|
+
@proc = opts.delete(:proc)
|
7
|
+
end
|
8
|
+
|
9
|
+
def process_event(event)
|
10
|
+
Rails.logger.debug "ProcEventHandler: calling #{@proc}"
|
11
|
+
#@proc.call(event)
|
12
|
+
event.source.controller.send(@proc, event)
|
13
|
+
nil ### DISCUSS: needed so that controller doesn't evaluate the "content".
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s; "ProcEventHandler:#{proc}"; end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'apotomo/request_processor'
|
2
|
+
require 'apotomo/rails/view_methods'
|
3
|
+
|
4
|
+
module Apotomo
|
5
|
+
module Rails
|
6
|
+
module ControllerMethods
|
7
|
+
include WidgetShortcuts
|
8
|
+
|
9
|
+
def self.included(base) #:nodoc:
|
10
|
+
base.class_eval do
|
11
|
+
extend WidgetShortcuts
|
12
|
+
extend ClassMethods
|
13
|
+
|
14
|
+
class_inheritable_array :uses_widgets_blocks
|
15
|
+
self.uses_widgets_blocks = []
|
16
|
+
|
17
|
+
helper ::Apotomo::Rails::ViewMethods
|
18
|
+
|
19
|
+
after_filter :apotomo_freeze
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
def uses_widgets(&block)
|
25
|
+
uses_widgets_blocks << block
|
26
|
+
end
|
27
|
+
|
28
|
+
alias_method :has_widgets, :uses_widgets
|
29
|
+
end
|
30
|
+
|
31
|
+
def bound_use_widgets_blocks
|
32
|
+
session[:bound_use_widgets_blocks] ||= ProcHash.new
|
33
|
+
end
|
34
|
+
|
35
|
+
def flush_bound_use_widgets_blocks
|
36
|
+
session[:bound_use_widgets_blocks] = nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def apotomo_request_processor
|
40
|
+
return @apotomo_request_processor if @apotomo_request_processor
|
41
|
+
|
42
|
+
# happens once per request:
|
43
|
+
### DISCUSS: policy in production?
|
44
|
+
options = { :flush_widgets => params[:flush_widgets],
|
45
|
+
:js_framework => Apotomo.js_framework || :prototype,
|
46
|
+
} ### TODO: process rails options (flush_tree, version)
|
47
|
+
|
48
|
+
@apotomo_request_processor = Apotomo::RequestProcessor.new(session, options, self.class.uses_widgets_blocks)
|
49
|
+
|
50
|
+
flush_bound_use_widgets_blocks if @apotomo_request_processor.widgets_flushed?
|
51
|
+
|
52
|
+
|
53
|
+
@apotomo_request_processor
|
54
|
+
end
|
55
|
+
|
56
|
+
def apotomo_root
|
57
|
+
apotomo_request_processor.root
|
58
|
+
end
|
59
|
+
|
60
|
+
# Yields the root widget for manipulating the widget tree in a controller action.
|
61
|
+
# Note that the passed block is executed once per session and not in every request.
|
62
|
+
#
|
63
|
+
# Example:
|
64
|
+
# def login
|
65
|
+
# use_widgets do |root|
|
66
|
+
# root << cell(:login, :form, 'login_box')
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# @box = render_widget 'login_box'
|
70
|
+
# end
|
71
|
+
def use_widgets(&block)
|
72
|
+
root = apotomo_root ### DISCUSS: let RequestProcessor initialize so we get flushed, eventually. maybe add a :before filter for that? or move #use_widgets to RequestProcessor?
|
73
|
+
|
74
|
+
return if bound_use_widgets_blocks.include?(block)
|
75
|
+
|
76
|
+
yield root
|
77
|
+
|
78
|
+
bound_use_widgets_blocks << block # remember the proc.
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
def render_widget(widget, options={}, &block)
|
83
|
+
apotomo_request_processor.render_widget_for(widget, options, self, &block)
|
84
|
+
end
|
85
|
+
|
86
|
+
def apotomo_freeze
|
87
|
+
apotomo_request_processor.freeze!
|
88
|
+
end
|
89
|
+
|
90
|
+
def render_event_response
|
91
|
+
page_updates = apotomo_request_processor.process_for({:type => params[:type], :source => params[:source]}, self)
|
92
|
+
|
93
|
+
return render_iframe_updates(page_updates) if params[:apotomo_iframe]
|
94
|
+
|
95
|
+
render :text => apotomo_request_processor.render_page_updates(page_updates), :content_type => Mime::JS
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns the url to trigger a +type+ event from +:source+, which is a non-optional parameter.
|
99
|
+
# Additional +options+ will be appended to the query string.
|
100
|
+
#
|
101
|
+
# Note that this method will use the framework's internal routing if available (e.g. #url_for in Rails).
|
102
|
+
#
|
103
|
+
# Example:
|
104
|
+
# url_for_event(:paginate, :source => 'mouse', :page => 2)
|
105
|
+
# #=> http://apotomo.de/mouse/process_event_request?type=paginate&source=mouse&page=2
|
106
|
+
def url_for_event(type, options)
|
107
|
+
options.reverse_merge!(:type => type)
|
108
|
+
|
109
|
+
apotomo_event_path(apotomo_request_processor.address_for(options))
|
110
|
+
end
|
111
|
+
|
112
|
+
protected
|
113
|
+
|
114
|
+
|
115
|
+
# Renders the page updates through an iframe. Copied from responds_to_parent,
|
116
|
+
# see http://github.com/markcatley/responds_to_parent .
|
117
|
+
def render_iframe_updates(page_updates)
|
118
|
+
script = apotomo_request_processor.render_page_updates(page_updates)
|
119
|
+
escaped_script = Apotomo::JavascriptGenerator.escape(script)
|
120
|
+
|
121
|
+
render :text => "<html><body><script type='text/javascript' charset='utf-8'>
|
122
|
+
var loc = document.location;
|
123
|
+
with(window.parent) { setTimeout(function() { window.eval('#{escaped_script}'); window.loc && loc.replace('about:blank'); }, 1) }
|
124
|
+
</script></body></html>", :content_type => 'text/html'
|
125
|
+
end
|
126
|
+
|
127
|
+
def respond_to_event(type, options)
|
128
|
+
handler = ProcEventHandler.new
|
129
|
+
handler.proc = options[:with]
|
130
|
+
### TODO: pass :from => (event source).
|
131
|
+
|
132
|
+
# attach once, not every request:
|
133
|
+
apotomo_root.evt_table.add_handler_once(handler, :event_type => type)
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
|
138
|
+
### DISCUSS: rename? should say "this controller action wants apotomo's deep linking!"
|
139
|
+
### DISCUSS: move to deep_link_methods?
|
140
|
+
def respond_to_url_change
|
141
|
+
return if apotomo_root.find_widget('deep_link') # add only once.
|
142
|
+
apotomo_root << widget("apotomo/deep_link_widget", :setup, 'deep_link')
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
class ProcHash < Array
|
147
|
+
def id_for_proc(proc)
|
148
|
+
proc.to_s.split('@').last
|
149
|
+
end
|
150
|
+
|
151
|
+
def <<(proc)
|
152
|
+
super(id_for_proc(proc))
|
153
|
+
end
|
154
|
+
|
155
|
+
def include?(proc)
|
156
|
+
super(id_for_proc(proc))
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|