apotomo 1.0.5 → 1.1.0.rc1
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/.gitignore +5 -0
- data/CHANGES.textile +35 -7
- data/Gemfile +0 -2
- data/README.rdoc +9 -9
- data/apotomo.gemspec +3 -3
- data/lib/apotomo.rb +4 -47
- data/lib/apotomo/event.rb +2 -0
- data/lib/apotomo/event_handler.rb +0 -3
- data/lib/apotomo/event_methods.rb +6 -2
- data/lib/apotomo/invoke_event_handler.rb +5 -3
- data/lib/apotomo/javascript_generator.rb +19 -16
- data/lib/apotomo/rails/controller_methods.rb +84 -131
- data/lib/apotomo/rails/view_helper.rb +15 -31
- data/lib/apotomo/railtie.rb +24 -0
- data/lib/apotomo/request_processor.rb +17 -48
- data/lib/apotomo/test_case.rb +5 -5
- data/lib/apotomo/tree_node.rb +52 -61
- data/lib/apotomo/version.rb +1 -1
- data/lib/apotomo/widget.rb +70 -146
- data/lib/apotomo/widget/javascript_methods.rb +39 -0
- data/lib/apotomo/widget_shortcuts.rb +14 -40
- data/lib/generators/apotomo/widget_generator.rb +8 -9
- data/lib/generators/erb/widget_generator.rb +17 -0
- data/lib/generators/haml/widget_generator.rb +20 -0
- data/lib/generators/{apotomo/templates → templates}/view.erb +1 -1
- data/lib/generators/templates/view.haml +4 -0
- data/lib/generators/{apotomo/templates → templates}/widget.rb +1 -1
- data/lib/generators/{apotomo/templates → templates}/widget_test.rb +1 -1
- data/lib/generators/test_unit/widget_generator.rb +14 -0
- data/test/rails/caching_test.rb +10 -17
- data/test/rails/controller_methods_test.rb +9 -81
- data/test/rails/rails_integration_test.rb +76 -60
- data/test/rails/view_helper_test.rb +17 -28
- data/test/rails/widget_generator_test.rb +19 -31
- data/test/support/test_case_methods.rb +6 -20
- data/test/test_helper.rb +15 -25
- data/test/unit/event_handler_test.rb +1 -0
- data/test/unit/event_methods_test.rb +20 -8
- data/test/unit/event_test.rb +5 -0
- data/test/unit/javascript_generator_test.rb +19 -19
- data/test/unit/render_test.rb +17 -112
- data/test/unit/request_processor_test.rb +73 -111
- data/test/unit/test_case_test.rb +13 -7
- data/test/unit/widget_shortcuts_test.rb +24 -53
- data/test/unit/widget_test.rb +76 -36
- data/test/widgets/mouse/eat.erb +1 -0
- data/test/{fixtures → widgets}/mouse/eating.html.erb +0 -0
- data/test/{fixtures → widgets}/mouse/educate.html.erb +0 -0
- data/test/{fixtures → widgets}/mouse/feed.html.erb +0 -0
- data/test/{fixtures → widgets}/mouse/make_me_squeak.html.erb +0 -0
- data/test/{fixtures → widgets}/mouse/posing.html.erb +0 -0
- data/test/widgets/mouse/snuggle.html.erb +1 -0
- metadata +32 -50
- data/lib/apotomo/container_widget.rb +0 -10
- data/lib/apotomo/persistence.rb +0 -112
- data/lib/apotomo/rails/view_methods.rb +0 -7
- data/lib/apotomo/stateful_widget.rb +0 -29
- data/lib/apotomo/transition.rb +0 -46
- data/lib/generators/apotomo/templates/view.haml +0 -4
- data/test/dummy/log/production.log +0 -0
- data/test/dummy/log/server.log +0 -0
- data/test/dummy/public/javascripts/application.js +0 -2
- data/test/dummy/public/javascripts/controls.js +0 -965
- data/test/dummy/public/javascripts/dragdrop.js +0 -974
- data/test/dummy/public/javascripts/effects.js +0 -1123
- data/test/dummy/public/javascripts/prototype.js +0 -6001
- data/test/dummy/public/javascripts/rails.js +0 -175
- data/test/dummy/script/rails +0 -6
- data/test/dummy/tmp/app/cells/mouse_widget.rb +0 -11
- data/test/dummy/tmp/app/cells/mouse_widget/snuggle.html.erb +0 -7
- data/test/dummy/tmp/app/cells/mouse_widget/squeak.html.erb +0 -7
- data/test/dummy/tmp/test/widgets/mouse_widget_test.rb +0 -12
- data/test/fixtures/application_widget_tree.rb +0 -2
- data/test/fixtures/mouse/snuggle.html.erb +0 -1
- data/test/rails/view_methods_test.rb +0 -38
- data/test/unit/container_test.rb +0 -21
- data/test/unit/invoke_test.rb +0 -126
- data/test/unit/persistence_test.rb +0 -201
- data/test/unit/stateful_widget_test.rb +0 -58
- data/test/unit/test_addressing.rb +0 -110
- data/test/unit/test_jump_to_state.rb +0 -89
- data/test/unit/test_tab_panel.rb +0 -71
- data/test/unit/transition_test.rb +0 -34
@@ -0,0 +1 @@
|
|
1
|
+
<div id="<%= widget_id %>">burp!</div>
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
<div id="<%= widget_id %>"><snuggle><%= render_widget 'kid' %></snuggle></div>
|
metadata
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: apotomo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
prerelease:
|
4
|
+
prerelease: true
|
5
5
|
segments:
|
6
6
|
- 1
|
7
|
+
- 1
|
7
8
|
- 0
|
8
|
-
-
|
9
|
-
version: 1.0.
|
9
|
+
- rc1
|
10
|
+
version: 1.1.0.rc1
|
10
11
|
platform: ruby
|
11
12
|
authors:
|
12
13
|
- Nick Sutterer
|
@@ -14,7 +15,7 @@ autorequire:
|
|
14
15
|
bindir: bin
|
15
16
|
cert_chain: []
|
16
17
|
|
17
|
-
date: 2011-02-
|
18
|
+
date: 2011-02-24 00:00:00 +01:00
|
18
19
|
default_executable:
|
19
20
|
dependencies:
|
20
21
|
- !ruby/object:Gem::Dependency
|
@@ -41,9 +42,9 @@ dependencies:
|
|
41
42
|
- !ruby/object:Gem::Version
|
42
43
|
segments:
|
43
44
|
- 3
|
45
|
+
- 5
|
44
46
|
- 4
|
45
|
-
|
46
|
-
version: 3.4.0
|
47
|
+
version: 3.5.4
|
47
48
|
type: :runtime
|
48
49
|
version_requirements: *id002
|
49
50
|
- !ruby/object:Gem::Dependency
|
@@ -57,8 +58,8 @@ dependencies:
|
|
57
58
|
segments:
|
58
59
|
- 0
|
59
60
|
- 1
|
60
|
-
-
|
61
|
-
version: 0.1.
|
61
|
+
- 2
|
62
|
+
version: 0.1.2
|
62
63
|
type: :runtime
|
63
64
|
version_requirements: *id003
|
64
65
|
- !ruby/object:Gem::Dependency
|
@@ -89,7 +90,7 @@ dependencies:
|
|
89
90
|
version: "0"
|
90
91
|
type: :development
|
91
92
|
version_requirements: *id005
|
92
|
-
description: Web component framework for Rails providing
|
93
|
+
description: Web component framework for Rails providing widgets that trigger events and know when and how to update themselves with AJAX.
|
93
94
|
email:
|
94
95
|
- apotonick@gmail.com
|
95
96
|
executables: []
|
@@ -99,6 +100,7 @@ extensions: []
|
|
99
100
|
extra_rdoc_files: []
|
100
101
|
|
101
102
|
files:
|
103
|
+
- .gitignore
|
102
104
|
- CHANGES.textile
|
103
105
|
- Gemfile
|
104
106
|
- README.rdoc
|
@@ -108,31 +110,31 @@ files:
|
|
108
110
|
- config/routes.rb
|
109
111
|
- lib/apotomo.rb
|
110
112
|
- lib/apotomo/apotomo.rake
|
111
|
-
- lib/apotomo/container_widget.rb
|
112
113
|
- lib/apotomo/event.rb
|
113
114
|
- lib/apotomo/event_handler.rb
|
114
115
|
- lib/apotomo/event_methods.rb
|
115
116
|
- lib/apotomo/invoke_event_handler.rb
|
116
117
|
- lib/apotomo/javascript_generator.rb
|
117
|
-
- lib/apotomo/persistence.rb
|
118
118
|
- lib/apotomo/proc_event_handler.rb
|
119
119
|
- lib/apotomo/rails/controller_methods.rb
|
120
120
|
- lib/apotomo/rails/view_helper.rb
|
121
|
-
- lib/apotomo/
|
121
|
+
- lib/apotomo/railtie.rb
|
122
122
|
- lib/apotomo/request_processor.rb
|
123
|
-
- lib/apotomo/stateful_widget.rb
|
124
123
|
- lib/apotomo/test_case.rb
|
125
|
-
- lib/apotomo/transition.rb
|
126
124
|
- lib/apotomo/tree_node.rb
|
127
125
|
- lib/apotomo/version.rb
|
128
126
|
- lib/apotomo/widget.rb
|
127
|
+
- lib/apotomo/widget/javascript_methods.rb
|
129
128
|
- lib/apotomo/widget_shortcuts.rb
|
130
129
|
- lib/generators/apotomo/USAGE
|
131
|
-
- lib/generators/apotomo/templates/view.erb
|
132
|
-
- lib/generators/apotomo/templates/view.haml
|
133
|
-
- lib/generators/apotomo/templates/widget.rb
|
134
|
-
- lib/generators/apotomo/templates/widget_test.rb
|
135
130
|
- lib/generators/apotomo/widget_generator.rb
|
131
|
+
- lib/generators/erb/widget_generator.rb
|
132
|
+
- lib/generators/haml/widget_generator.rb
|
133
|
+
- lib/generators/templates/view.erb
|
134
|
+
- lib/generators/templates/view.haml
|
135
|
+
- lib/generators/templates/widget.rb
|
136
|
+
- lib/generators/templates/widget_test.rb
|
137
|
+
- lib/generators/test_unit/widget_generator.rb
|
136
138
|
- test/dummy/Rakefile
|
137
139
|
- test/dummy/app/controllers/application_controller.rb
|
138
140
|
- test/dummy/app/helpers/application_helper.rb
|
@@ -153,58 +155,36 @@ files:
|
|
153
155
|
- test/dummy/config/locales/en.yml
|
154
156
|
- test/dummy/config/routes.rb
|
155
157
|
- test/dummy/db/test.sqlite3
|
156
|
-
- test/dummy/log/production.log
|
157
|
-
- test/dummy/log/server.log
|
158
158
|
- test/dummy/public/404.html
|
159
159
|
- test/dummy/public/422.html
|
160
160
|
- test/dummy/public/500.html
|
161
161
|
- test/dummy/public/favicon.ico
|
162
|
-
- test/dummy/public/javascripts/application.js
|
163
|
-
- test/dummy/public/javascripts/controls.js
|
164
|
-
- test/dummy/public/javascripts/dragdrop.js
|
165
|
-
- test/dummy/public/javascripts/effects.js
|
166
|
-
- test/dummy/public/javascripts/prototype.js
|
167
|
-
- test/dummy/public/javascripts/rails.js
|
168
162
|
- test/dummy/public/stylesheets/.gitkeep
|
169
|
-
- test/dummy/script/rails
|
170
|
-
- test/dummy/tmp/app/cells/mouse_widget.rb
|
171
|
-
- test/dummy/tmp/app/cells/mouse_widget/snuggle.html.erb
|
172
|
-
- test/dummy/tmp/app/cells/mouse_widget/squeak.html.erb
|
173
|
-
- test/dummy/tmp/test/widgets/mouse_widget_test.rb
|
174
|
-
- test/fixtures/application_widget_tree.rb
|
175
|
-
- test/fixtures/mouse/eating.html.erb
|
176
|
-
- test/fixtures/mouse/educate.html.erb
|
177
|
-
- test/fixtures/mouse/feed.html.erb
|
178
|
-
- test/fixtures/mouse/make_me_squeak.html.erb
|
179
|
-
- test/fixtures/mouse/posing.html.erb
|
180
|
-
- test/fixtures/mouse/snuggle.html.erb
|
181
163
|
- test/rails/caching_test.rb
|
182
164
|
- test/rails/controller_methods_test.rb
|
183
165
|
- test/rails/rails_integration_test.rb
|
184
166
|
- test/rails/view_helper_test.rb
|
185
|
-
- test/rails/view_methods_test.rb
|
186
167
|
- test/rails/widget_generator_test.rb
|
187
168
|
- test/support/test_case_methods.rb
|
188
169
|
- test/test_helper.rb
|
189
170
|
- test/unit/apotomo_test.rb
|
190
|
-
- test/unit/container_test.rb
|
191
171
|
- test/unit/event_handler_test.rb
|
192
172
|
- test/unit/event_methods_test.rb
|
193
173
|
- test/unit/event_test.rb
|
194
|
-
- test/unit/invoke_test.rb
|
195
174
|
- test/unit/javascript_generator_test.rb
|
196
175
|
- test/unit/onfire_integration_test.rb
|
197
|
-
- test/unit/persistence_test.rb
|
198
176
|
- test/unit/render_test.rb
|
199
177
|
- test/unit/request_processor_test.rb
|
200
|
-
- test/unit/stateful_widget_test.rb
|
201
|
-
- test/unit/test_addressing.rb
|
202
178
|
- test/unit/test_case_test.rb
|
203
|
-
- test/unit/test_jump_to_state.rb
|
204
|
-
- test/unit/test_tab_panel.rb
|
205
|
-
- test/unit/transition_test.rb
|
206
179
|
- test/unit/widget_shortcuts_test.rb
|
207
180
|
- test/unit/widget_test.rb
|
181
|
+
- test/widgets/mouse/eat.erb
|
182
|
+
- test/widgets/mouse/eating.html.erb
|
183
|
+
- test/widgets/mouse/educate.html.erb
|
184
|
+
- test/widgets/mouse/feed.html.erb
|
185
|
+
- test/widgets/mouse/make_me_squeak.html.erb
|
186
|
+
- test/widgets/mouse/posing.html.erb
|
187
|
+
- test/widgets/mouse/snuggle.html.erb
|
208
188
|
has_rdoc: true
|
209
189
|
homepage: http://github.com/apotonick/apotomo
|
210
190
|
licenses: []
|
@@ -225,11 +205,13 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
225
205
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
226
206
|
none: false
|
227
207
|
requirements:
|
228
|
-
- - "
|
208
|
+
- - ">"
|
229
209
|
- !ruby/object:Gem::Version
|
230
210
|
segments:
|
231
|
-
-
|
232
|
-
|
211
|
+
- 1
|
212
|
+
- 3
|
213
|
+
- 1
|
214
|
+
version: 1.3.1
|
233
215
|
requirements: []
|
234
216
|
|
235
217
|
rubyforge_project:
|
data/lib/apotomo/persistence.rb
DELETED
@@ -1,112 +0,0 @@
|
|
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
|
-
def dump_tree
|
46
|
-
collect { |n| [n.class, n.name, n.root? ? nil : n.parent.name] }
|
47
|
-
end
|
48
|
-
|
49
|
-
|
50
|
-
module ClassMethods
|
51
|
-
# Dump the shit to storage.
|
52
|
-
def freeze_for(storage, root)
|
53
|
-
storage[:apotomo_stateful_branches] = []
|
54
|
-
storage[:apotomo_widget_ivars] = {}
|
55
|
-
|
56
|
-
stateful_branches_for(root).each do |branch|
|
57
|
-
branch.freeze_data_to(storage[:apotomo_widget_ivars]) # save ivars.
|
58
|
-
storage[:apotomo_stateful_branches] << branch.dump_tree
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
# Create tree from storage and add branches to root/stateless parents.
|
63
|
-
def thaw_for(controller, storage, root)
|
64
|
-
branches = storage.delete(:apotomo_stateful_branches) || []
|
65
|
-
branches.each do |data|
|
66
|
-
branch = load_tree(controller, data)
|
67
|
-
parent = root.find_widget(data.first.last) or raise "Couldn't find parent `#{data.first.last}` for `#{branch.name}`"
|
68
|
-
|
69
|
-
parent << branch
|
70
|
-
branch.thaw_data_from(storage.delete(:apotomo_widget_ivars) || {})
|
71
|
-
end
|
72
|
-
|
73
|
-
root
|
74
|
-
end
|
75
|
-
|
76
|
-
def frozen_widget_in?(storage)
|
77
|
-
storage[:apotomo_stateful_branches].kind_of? Array
|
78
|
-
end
|
79
|
-
|
80
|
-
def flush_storage(storage)
|
81
|
-
storage[:apotomo_stateful_branches] = nil
|
82
|
-
storage[:apotomo_widget_ivars] = nil
|
83
|
-
end
|
84
|
-
|
85
|
-
# Find the first stateful widgets on each branch from +root+.
|
86
|
-
def stateful_branches_for(root)
|
87
|
-
to_traverse = [root]
|
88
|
-
stateful_roots = []
|
89
|
-
|
90
|
-
while node = to_traverse.shift
|
91
|
-
if node.kind_of?(StatefulWidget)
|
92
|
-
stateful_roots << node and next
|
93
|
-
end
|
94
|
-
to_traverse += node.children
|
95
|
-
end
|
96
|
-
|
97
|
-
stateful_roots
|
98
|
-
end
|
99
|
-
|
100
|
-
private
|
101
|
-
def load_tree(parent_controller, cold_widgets)
|
102
|
-
root = nil
|
103
|
-
cold_widgets.each do |data|
|
104
|
-
node = data[0].new(parent_controller, data[1], "")
|
105
|
-
root = node and next unless root
|
106
|
-
root.find_widget(data[2]) << node
|
107
|
-
end
|
108
|
-
root
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|
112
|
-
end
|
@@ -1,29 +0,0 @@
|
|
1
|
-
require 'apotomo/widget'
|
2
|
-
require 'apotomo/persistence'
|
3
|
-
|
4
|
-
module Apotomo
|
5
|
-
class StatefulWidget < Widget
|
6
|
-
include Persistence
|
7
|
-
|
8
|
-
attr_accessor :version
|
9
|
-
|
10
|
-
def initialize(*)
|
11
|
-
super
|
12
|
-
@version = 0
|
13
|
-
end
|
14
|
-
|
15
|
-
|
16
|
-
# Defines the instance vars that should <em>not</em> survive between requests,
|
17
|
-
# which means they're not frozen in Apotomo::StatefulWidget#freeze.
|
18
|
-
def ivars_to_forget
|
19
|
-
unfreezable_ivars
|
20
|
-
end
|
21
|
-
|
22
|
-
def unfreezable_ivars
|
23
|
-
[:@childrenHash, :@children, :@parent, :@parent_controller, :@_request, :@_config, :@cell, :@invoke_block, :@rendered_children, :@page_updates, :@opts, :@params,
|
24
|
-
:@suppress_javascript ### FIXME: implement with ActiveHelper and :locals.
|
25
|
-
|
26
|
-
]
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
data/lib/apotomo/transition.rb
DELETED
@@ -1,46 +0,0 @@
|
|
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.to_s] = 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.to_s]
|
45
|
-
end
|
46
|
-
end
|
File without changes
|
data/test/dummy/log/server.log
DELETED
File without changes
|
@@ -1,965 +0,0 @@
|
|
1
|
-
// script.aculo.us controls.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009
|
2
|
-
|
3
|
-
// Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
|
4
|
-
// (c) 2005-2009 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
|
5
|
-
// (c) 2005-2009 Jon Tirsen (http://www.tirsen.com)
|
6
|
-
// Contributors:
|
7
|
-
// Richard Livsey
|
8
|
-
// Rahul Bhargava
|
9
|
-
// Rob Wills
|
10
|
-
//
|
11
|
-
// script.aculo.us is freely distributable under the terms of an MIT-style license.
|
12
|
-
// For details, see the script.aculo.us web site: http://script.aculo.us/
|
13
|
-
|
14
|
-
// Autocompleter.Base handles all the autocompletion functionality
|
15
|
-
// that's independent of the data source for autocompletion. This
|
16
|
-
// includes drawing the autocompletion menu, observing keyboard
|
17
|
-
// and mouse events, and similar.
|
18
|
-
//
|
19
|
-
// Specific autocompleters need to provide, at the very least,
|
20
|
-
// a getUpdatedChoices function that will be invoked every time
|
21
|
-
// the text inside the monitored textbox changes. This method
|
22
|
-
// should get the text for which to provide autocompletion by
|
23
|
-
// invoking this.getToken(), NOT by directly accessing
|
24
|
-
// this.element.value. This is to allow incremental tokenized
|
25
|
-
// autocompletion. Specific auto-completion logic (AJAX, etc)
|
26
|
-
// belongs in getUpdatedChoices.
|
27
|
-
//
|
28
|
-
// Tokenized incremental autocompletion is enabled automatically
|
29
|
-
// when an autocompleter is instantiated with the 'tokens' option
|
30
|
-
// in the options parameter, e.g.:
|
31
|
-
// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
|
32
|
-
// will incrementally autocomplete with a comma as the token.
|
33
|
-
// Additionally, ',' in the above example can be replaced with
|
34
|
-
// a token array, e.g. { tokens: [',', '\n'] } which
|
35
|
-
// enables autocompletion on multiple tokens. This is most
|
36
|
-
// useful when one of the tokens is \n (a newline), as it
|
37
|
-
// allows smart autocompletion after linebreaks.
|
38
|
-
|
39
|
-
if(typeof Effect == 'undefined')
|
40
|
-
throw("controls.js requires including script.aculo.us' effects.js library");
|
41
|
-
|
42
|
-
var Autocompleter = { };
|
43
|
-
Autocompleter.Base = Class.create({
|
44
|
-
baseInitialize: function(element, update, options) {
|
45
|
-
element = $(element);
|
46
|
-
this.element = element;
|
47
|
-
this.update = $(update);
|
48
|
-
this.hasFocus = false;
|
49
|
-
this.changed = false;
|
50
|
-
this.active = false;
|
51
|
-
this.index = 0;
|
52
|
-
this.entryCount = 0;
|
53
|
-
this.oldElementValue = this.element.value;
|
54
|
-
|
55
|
-
if(this.setOptions)
|
56
|
-
this.setOptions(options);
|
57
|
-
else
|
58
|
-
this.options = options || { };
|
59
|
-
|
60
|
-
this.options.paramName = this.options.paramName || this.element.name;
|
61
|
-
this.options.tokens = this.options.tokens || [];
|
62
|
-
this.options.frequency = this.options.frequency || 0.4;
|
63
|
-
this.options.minChars = this.options.minChars || 1;
|
64
|
-
this.options.onShow = this.options.onShow ||
|
65
|
-
function(element, update){
|
66
|
-
if(!update.style.position || update.style.position=='absolute') {
|
67
|
-
update.style.position = 'absolute';
|
68
|
-
Position.clone(element, update, {
|
69
|
-
setHeight: false,
|
70
|
-
offsetTop: element.offsetHeight
|
71
|
-
});
|
72
|
-
}
|
73
|
-
Effect.Appear(update,{duration:0.15});
|
74
|
-
};
|
75
|
-
this.options.onHide = this.options.onHide ||
|
76
|
-
function(element, update){ new Effect.Fade(update,{duration:0.15}) };
|
77
|
-
|
78
|
-
if(typeof(this.options.tokens) == 'string')
|
79
|
-
this.options.tokens = new Array(this.options.tokens);
|
80
|
-
// Force carriage returns as token delimiters anyway
|
81
|
-
if (!this.options.tokens.include('\n'))
|
82
|
-
this.options.tokens.push('\n');
|
83
|
-
|
84
|
-
this.observer = null;
|
85
|
-
|
86
|
-
this.element.setAttribute('autocomplete','off');
|
87
|
-
|
88
|
-
Element.hide(this.update);
|
89
|
-
|
90
|
-
Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
|
91
|
-
Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this));
|
92
|
-
},
|
93
|
-
|
94
|
-
show: function() {
|
95
|
-
if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
|
96
|
-
if(!this.iefix &&
|
97
|
-
(Prototype.Browser.IE) &&
|
98
|
-
(Element.getStyle(this.update, 'position')=='absolute')) {
|
99
|
-
new Insertion.After(this.update,
|
100
|
-
'<iframe id="' + this.update.id + '_iefix" '+
|
101
|
-
'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
|
102
|
-
'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
|
103
|
-
this.iefix = $(this.update.id+'_iefix');
|
104
|
-
}
|
105
|
-
if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
|
106
|
-
},
|
107
|
-
|
108
|
-
fixIEOverlapping: function() {
|
109
|
-
Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
|
110
|
-
this.iefix.style.zIndex = 1;
|
111
|
-
this.update.style.zIndex = 2;
|
112
|
-
Element.show(this.iefix);
|
113
|
-
},
|
114
|
-
|
115
|
-
hide: function() {
|
116
|
-
this.stopIndicator();
|
117
|
-
if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
|
118
|
-
if(this.iefix) Element.hide(this.iefix);
|
119
|
-
},
|
120
|
-
|
121
|
-
startIndicator: function() {
|
122
|
-
if(this.options.indicator) Element.show(this.options.indicator);
|
123
|
-
},
|
124
|
-
|
125
|
-
stopIndicator: function() {
|
126
|
-
if(this.options.indicator) Element.hide(this.options.indicator);
|
127
|
-
},
|
128
|
-
|
129
|
-
onKeyPress: function(event) {
|
130
|
-
if(this.active)
|
131
|
-
switch(event.keyCode) {
|
132
|
-
case Event.KEY_TAB:
|
133
|
-
case Event.KEY_RETURN:
|
134
|
-
this.selectEntry();
|
135
|
-
Event.stop(event);
|
136
|
-
case Event.KEY_ESC:
|
137
|
-
this.hide();
|
138
|
-
this.active = false;
|
139
|
-
Event.stop(event);
|
140
|
-
return;
|
141
|
-
case Event.KEY_LEFT:
|
142
|
-
case Event.KEY_RIGHT:
|
143
|
-
return;
|
144
|
-
case Event.KEY_UP:
|
145
|
-
this.markPrevious();
|
146
|
-
this.render();
|
147
|
-
Event.stop(event);
|
148
|
-
return;
|
149
|
-
case Event.KEY_DOWN:
|
150
|
-
this.markNext();
|
151
|
-
this.render();
|
152
|
-
Event.stop(event);
|
153
|
-
return;
|
154
|
-
}
|
155
|
-
else
|
156
|
-
if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
|
157
|
-
(Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;
|
158
|
-
|
159
|
-
this.changed = true;
|
160
|
-
this.hasFocus = true;
|
161
|
-
|
162
|
-
if(this.observer) clearTimeout(this.observer);
|
163
|
-
this.observer =
|
164
|
-
setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
|
165
|
-
},
|
166
|
-
|
167
|
-
activate: function() {
|
168
|
-
this.changed = false;
|
169
|
-
this.hasFocus = true;
|
170
|
-
this.getUpdatedChoices();
|
171
|
-
},
|
172
|
-
|
173
|
-
onHover: function(event) {
|
174
|
-
var element = Event.findElement(event, 'LI');
|
175
|
-
if(this.index != element.autocompleteIndex)
|
176
|
-
{
|
177
|
-
this.index = element.autocompleteIndex;
|
178
|
-
this.render();
|
179
|
-
}
|
180
|
-
Event.stop(event);
|
181
|
-
},
|
182
|
-
|
183
|
-
onClick: function(event) {
|
184
|
-
var element = Event.findElement(event, 'LI');
|
185
|
-
this.index = element.autocompleteIndex;
|
186
|
-
this.selectEntry();
|
187
|
-
this.hide();
|
188
|
-
},
|
189
|
-
|
190
|
-
onBlur: function(event) {
|
191
|
-
// needed to make click events working
|
192
|
-
setTimeout(this.hide.bind(this), 250);
|
193
|
-
this.hasFocus = false;
|
194
|
-
this.active = false;
|
195
|
-
},
|
196
|
-
|
197
|
-
render: function() {
|
198
|
-
if(this.entryCount > 0) {
|
199
|
-
for (var i = 0; i < this.entryCount; i++)
|
200
|
-
this.index==i ?
|
201
|
-
Element.addClassName(this.getEntry(i),"selected") :
|
202
|
-
Element.removeClassName(this.getEntry(i),"selected");
|
203
|
-
if(this.hasFocus) {
|
204
|
-
this.show();
|
205
|
-
this.active = true;
|
206
|
-
}
|
207
|
-
} else {
|
208
|
-
this.active = false;
|
209
|
-
this.hide();
|
210
|
-
}
|
211
|
-
},
|
212
|
-
|
213
|
-
markPrevious: function() {
|
214
|
-
if(this.index > 0) this.index--;
|
215
|
-
else this.index = this.entryCount-1;
|
216
|
-
this.getEntry(this.index).scrollIntoView(true);
|
217
|
-
},
|
218
|
-
|
219
|
-
markNext: function() {
|
220
|
-
if(this.index < this.entryCount-1) this.index++;
|
221
|
-
else this.index = 0;
|
222
|
-
this.getEntry(this.index).scrollIntoView(false);
|
223
|
-
},
|
224
|
-
|
225
|
-
getEntry: function(index) {
|
226
|
-
return this.update.firstChild.childNodes[index];
|
227
|
-
},
|
228
|
-
|
229
|
-
getCurrentEntry: function() {
|
230
|
-
return this.getEntry(this.index);
|
231
|
-
},
|
232
|
-
|
233
|
-
selectEntry: function() {
|
234
|
-
this.active = false;
|
235
|
-
this.updateElement(this.getCurrentEntry());
|
236
|
-
},
|
237
|
-
|
238
|
-
updateElement: function(selectedElement) {
|
239
|
-
if (this.options.updateElement) {
|
240
|
-
this.options.updateElement(selectedElement);
|
241
|
-
return;
|
242
|
-
}
|
243
|
-
var value = '';
|
244
|
-
if (this.options.select) {
|
245
|
-
var nodes = $(selectedElement).select('.' + this.options.select) || [];
|
246
|
-
if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
|
247
|
-
} else
|
248
|
-
value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
|
249
|
-
|
250
|
-
var bounds = this.getTokenBounds();
|
251
|
-
if (bounds[0] != -1) {
|
252
|
-
var newValue = this.element.value.substr(0, bounds[0]);
|
253
|
-
var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
|
254
|
-
if (whitespace)
|
255
|
-
newValue += whitespace[0];
|
256
|
-
this.element.value = newValue + value + this.element.value.substr(bounds[1]);
|
257
|
-
} else {
|
258
|
-
this.element.value = value;
|
259
|
-
}
|
260
|
-
this.oldElementValue = this.element.value;
|
261
|
-
this.element.focus();
|
262
|
-
|
263
|
-
if (this.options.afterUpdateElement)
|
264
|
-
this.options.afterUpdateElement(this.element, selectedElement);
|
265
|
-
},
|
266
|
-
|
267
|
-
updateChoices: function(choices) {
|
268
|
-
if(!this.changed && this.hasFocus) {
|
269
|
-
this.update.innerHTML = choices;
|
270
|
-
Element.cleanWhitespace(this.update);
|
271
|
-
Element.cleanWhitespace(this.update.down());
|
272
|
-
|
273
|
-
if(this.update.firstChild && this.update.down().childNodes) {
|
274
|
-
this.entryCount =
|
275
|
-
this.update.down().childNodes.length;
|
276
|
-
for (var i = 0; i < this.entryCount; i++) {
|
277
|
-
var entry = this.getEntry(i);
|
278
|
-
entry.autocompleteIndex = i;
|
279
|
-
this.addObservers(entry);
|
280
|
-
}
|
281
|
-
} else {
|
282
|
-
this.entryCount = 0;
|
283
|
-
}
|
284
|
-
|
285
|
-
this.stopIndicator();
|
286
|
-
this.index = 0;
|
287
|
-
|
288
|
-
if(this.entryCount==1 && this.options.autoSelect) {
|
289
|
-
this.selectEntry();
|
290
|
-
this.hide();
|
291
|
-
} else {
|
292
|
-
this.render();
|
293
|
-
}
|
294
|
-
}
|
295
|
-
},
|
296
|
-
|
297
|
-
addObservers: function(element) {
|
298
|
-
Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
|
299
|
-
Event.observe(element, "click", this.onClick.bindAsEventListener(this));
|
300
|
-
},
|
301
|
-
|
302
|
-
onObserverEvent: function() {
|
303
|
-
this.changed = false;
|
304
|
-
this.tokenBounds = null;
|
305
|
-
if(this.getToken().length>=this.options.minChars) {
|
306
|
-
this.getUpdatedChoices();
|
307
|
-
} else {
|
308
|
-
this.active = false;
|
309
|
-
this.hide();
|
310
|
-
}
|
311
|
-
this.oldElementValue = this.element.value;
|
312
|
-
},
|
313
|
-
|
314
|
-
getToken: function() {
|
315
|
-
var bounds = this.getTokenBounds();
|
316
|
-
return this.element.value.substring(bounds[0], bounds[1]).strip();
|
317
|
-
},
|
318
|
-
|
319
|
-
getTokenBounds: function() {
|
320
|
-
if (null != this.tokenBounds) return this.tokenBounds;
|
321
|
-
var value = this.element.value;
|
322
|
-
if (value.strip().empty()) return [-1, 0];
|
323
|
-
var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
|
324
|
-
var offset = (diff == this.oldElementValue.length ? 1 : 0);
|
325
|
-
var prevTokenPos = -1, nextTokenPos = value.length;
|
326
|
-
var tp;
|
327
|
-
for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
|
328
|
-
tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
|
329
|
-
if (tp > prevTokenPos) prevTokenPos = tp;
|
330
|
-
tp = value.indexOf(this.options.tokens[index], diff + offset);
|
331
|
-
if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
|
332
|
-
}
|
333
|
-
return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
|
334
|
-
}
|
335
|
-
});
|
336
|
-
|
337
|
-
Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
|
338
|
-
var boundary = Math.min(newS.length, oldS.length);
|
339
|
-
for (var index = 0; index < boundary; ++index)
|
340
|
-
if (newS[index] != oldS[index])
|
341
|
-
return index;
|
342
|
-
return boundary;
|
343
|
-
};
|
344
|
-
|
345
|
-
Ajax.Autocompleter = Class.create(Autocompleter.Base, {
|
346
|
-
initialize: function(element, update, url, options) {
|
347
|
-
this.baseInitialize(element, update, options);
|
348
|
-
this.options.asynchronous = true;
|
349
|
-
this.options.onComplete = this.onComplete.bind(this);
|
350
|
-
this.options.defaultParams = this.options.parameters || null;
|
351
|
-
this.url = url;
|
352
|
-
},
|
353
|
-
|
354
|
-
getUpdatedChoices: function() {
|
355
|
-
this.startIndicator();
|
356
|
-
|
357
|
-
var entry = encodeURIComponent(this.options.paramName) + '=' +
|
358
|
-
encodeURIComponent(this.getToken());
|
359
|
-
|
360
|
-
this.options.parameters = this.options.callback ?
|
361
|
-
this.options.callback(this.element, entry) : entry;
|
362
|
-
|
363
|
-
if(this.options.defaultParams)
|
364
|
-
this.options.parameters += '&' + this.options.defaultParams;
|
365
|
-
|
366
|
-
new Ajax.Request(this.url, this.options);
|
367
|
-
},
|
368
|
-
|
369
|
-
onComplete: function(request) {
|
370
|
-
this.updateChoices(request.responseText);
|
371
|
-
}
|
372
|
-
});
|
373
|
-
|
374
|
-
// The local array autocompleter. Used when you'd prefer to
|
375
|
-
// inject an array of autocompletion options into the page, rather
|
376
|
-
// than sending out Ajax queries, which can be quite slow sometimes.
|
377
|
-
//
|
378
|
-
// The constructor takes four parameters. The first two are, as usual,
|
379
|
-
// the id of the monitored textbox, and id of the autocompletion menu.
|
380
|
-
// The third is the array you want to autocomplete from, and the fourth
|
381
|
-
// is the options block.
|
382
|
-
//
|
383
|
-
// Extra local autocompletion options:
|
384
|
-
// - choices - How many autocompletion choices to offer
|
385
|
-
//
|
386
|
-
// - partialSearch - If false, the autocompleter will match entered
|
387
|
-
// text only at the beginning of strings in the
|
388
|
-
// autocomplete array. Defaults to true, which will
|
389
|
-
// match text at the beginning of any *word* in the
|
390
|
-
// strings in the autocomplete array. If you want to
|
391
|
-
// search anywhere in the string, additionally set
|
392
|
-
// the option fullSearch to true (default: off).
|
393
|
-
//
|
394
|
-
// - fullSsearch - Search anywhere in autocomplete array strings.
|
395
|
-
//
|
396
|
-
// - partialChars - How many characters to enter before triggering
|
397
|
-
// a partial match (unlike minChars, which defines
|
398
|
-
// how many characters are required to do any match
|
399
|
-
// at all). Defaults to 2.
|
400
|
-
//
|
401
|
-
// - ignoreCase - Whether to ignore case when autocompleting.
|
402
|
-
// Defaults to true.
|
403
|
-
//
|
404
|
-
// It's possible to pass in a custom function as the 'selector'
|
405
|
-
// option, if you prefer to write your own autocompletion logic.
|
406
|
-
// In that case, the other options above will not apply unless
|
407
|
-
// you support them.
|
408
|
-
|
409
|
-
Autocompleter.Local = Class.create(Autocompleter.Base, {
|
410
|
-
initialize: function(element, update, array, options) {
|
411
|
-
this.baseInitialize(element, update, options);
|
412
|
-
this.options.array = array;
|
413
|
-
},
|
414
|
-
|
415
|
-
getUpdatedChoices: function() {
|
416
|
-
this.updateChoices(this.options.selector(this));
|
417
|
-
},
|
418
|
-
|
419
|
-
setOptions: function(options) {
|
420
|
-
this.options = Object.extend({
|
421
|
-
choices: 10,
|
422
|
-
partialSearch: true,
|
423
|
-
partialChars: 2,
|
424
|
-
ignoreCase: true,
|
425
|
-
fullSearch: false,
|
426
|
-
selector: function(instance) {
|
427
|
-
var ret = []; // Beginning matches
|
428
|
-
var partial = []; // Inside matches
|
429
|
-
var entry = instance.getToken();
|
430
|
-
var count = 0;
|
431
|
-
|
432
|
-
for (var i = 0; i < instance.options.array.length &&
|
433
|
-
ret.length < instance.options.choices ; i++) {
|
434
|
-
|
435
|
-
var elem = instance.options.array[i];
|
436
|
-
var foundPos = instance.options.ignoreCase ?
|
437
|
-
elem.toLowerCase().indexOf(entry.toLowerCase()) :
|
438
|
-
elem.indexOf(entry);
|
439
|
-
|
440
|
-
while (foundPos != -1) {
|
441
|
-
if (foundPos == 0 && elem.length != entry.length) {
|
442
|
-
ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
|
443
|
-
elem.substr(entry.length) + "</li>");
|
444
|
-
break;
|
445
|
-
} else if (entry.length >= instance.options.partialChars &&
|
446
|
-
instance.options.partialSearch && foundPos != -1) {
|
447
|
-
if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
|
448
|
-
partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
|
449
|
-
elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
|
450
|
-
foundPos + entry.length) + "</li>");
|
451
|
-
break;
|
452
|
-
}
|
453
|
-
}
|
454
|
-
|
455
|
-
foundPos = instance.options.ignoreCase ?
|
456
|
-
elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
|
457
|
-
elem.indexOf(entry, foundPos + 1);
|
458
|
-
|
459
|
-
}
|
460
|
-
}
|
461
|
-
if (partial.length)
|
462
|
-
ret = ret.concat(partial.slice(0, instance.options.choices - ret.length));
|
463
|
-
return "<ul>" + ret.join('') + "</ul>";
|
464
|
-
}
|
465
|
-
}, options || { });
|
466
|
-
}
|
467
|
-
});
|
468
|
-
|
469
|
-
// AJAX in-place editor and collection editor
|
470
|
-
// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
|
471
|
-
|
472
|
-
// Use this if you notice weird scrolling problems on some browsers,
|
473
|
-
// the DOM might be a bit confused when this gets called so do this
|
474
|
-
// waits 1 ms (with setTimeout) until it does the activation
|
475
|
-
Field.scrollFreeActivate = function(field) {
|
476
|
-
setTimeout(function() {
|
477
|
-
Field.activate(field);
|
478
|
-
}, 1);
|
479
|
-
};
|
480
|
-
|
481
|
-
Ajax.InPlaceEditor = Class.create({
|
482
|
-
initialize: function(element, url, options) {
|
483
|
-
this.url = url;
|
484
|
-
this.element = element = $(element);
|
485
|
-
this.prepareOptions();
|
486
|
-
this._controls = { };
|
487
|
-
arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
|
488
|
-
Object.extend(this.options, options || { });
|
489
|
-
if (!this.options.formId && this.element.id) {
|
490
|
-
this.options.formId = this.element.id + '-inplaceeditor';
|
491
|
-
if ($(this.options.formId))
|
492
|
-
this.options.formId = '';
|
493
|
-
}
|
494
|
-
if (this.options.externalControl)
|
495
|
-
this.options.externalControl = $(this.options.externalControl);
|
496
|
-
if (!this.options.externalControl)
|
497
|
-
this.options.externalControlOnly = false;
|
498
|
-
this._originalBackground = this.element.getStyle('background-color') || 'transparent';
|
499
|
-
this.element.title = this.options.clickToEditText;
|
500
|
-
this._boundCancelHandler = this.handleFormCancellation.bind(this);
|
501
|
-
this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
|
502
|
-
this._boundFailureHandler = this.handleAJAXFailure.bind(this);
|
503
|
-
this._boundSubmitHandler = this.handleFormSubmission.bind(this);
|
504
|
-
this._boundWrapperHandler = this.wrapUp.bind(this);
|
505
|
-
this.registerListeners();
|
506
|
-
},
|
507
|
-
checkForEscapeOrReturn: function(e) {
|
508
|
-
if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
|
509
|
-
if (Event.KEY_ESC == e.keyCode)
|
510
|
-
this.handleFormCancellation(e);
|
511
|
-
else if (Event.KEY_RETURN == e.keyCode)
|
512
|
-
this.handleFormSubmission(e);
|
513
|
-
},
|
514
|
-
createControl: function(mode, handler, extraClasses) {
|
515
|
-
var control = this.options[mode + 'Control'];
|
516
|
-
var text = this.options[mode + 'Text'];
|
517
|
-
if ('button' == control) {
|
518
|
-
var btn = document.createElement('input');
|
519
|
-
btn.type = 'submit';
|
520
|
-
btn.value = text;
|
521
|
-
btn.className = 'editor_' + mode + '_button';
|
522
|
-
if ('cancel' == mode)
|
523
|
-
btn.onclick = this._boundCancelHandler;
|
524
|
-
this._form.appendChild(btn);
|
525
|
-
this._controls[mode] = btn;
|
526
|
-
} else if ('link' == control) {
|
527
|
-
var link = document.createElement('a');
|
528
|
-
link.href = '#';
|
529
|
-
link.appendChild(document.createTextNode(text));
|
530
|
-
link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
|
531
|
-
link.className = 'editor_' + mode + '_link';
|
532
|
-
if (extraClasses)
|
533
|
-
link.className += ' ' + extraClasses;
|
534
|
-
this._form.appendChild(link);
|
535
|
-
this._controls[mode] = link;
|
536
|
-
}
|
537
|
-
},
|
538
|
-
createEditField: function() {
|
539
|
-
var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
|
540
|
-
var fld;
|
541
|
-
if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
|
542
|
-
fld = document.createElement('input');
|
543
|
-
fld.type = 'text';
|
544
|
-
var size = this.options.size || this.options.cols || 0;
|
545
|
-
if (0 < size) fld.size = size;
|
546
|
-
} else {
|
547
|
-
fld = document.createElement('textarea');
|
548
|
-
fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
|
549
|
-
fld.cols = this.options.cols || 40;
|
550
|
-
}
|
551
|
-
fld.name = this.options.paramName;
|
552
|
-
fld.value = text; // No HTML breaks conversion anymore
|
553
|
-
fld.className = 'editor_field';
|
554
|
-
if (this.options.submitOnBlur)
|
555
|
-
fld.onblur = this._boundSubmitHandler;
|
556
|
-
this._controls.editor = fld;
|
557
|
-
if (this.options.loadTextURL)
|
558
|
-
this.loadExternalText();
|
559
|
-
this._form.appendChild(this._controls.editor);
|
560
|
-
},
|
561
|
-
createForm: function() {
|
562
|
-
var ipe = this;
|
563
|
-
function addText(mode, condition) {
|
564
|
-
var text = ipe.options['text' + mode + 'Controls'];
|
565
|
-
if (!text || condition === false) return;
|
566
|
-
ipe._form.appendChild(document.createTextNode(text));
|
567
|
-
};
|
568
|
-
this._form = $(document.createElement('form'));
|
569
|
-
this._form.id = this.options.formId;
|
570
|
-
this._form.addClassName(this.options.formClassName);
|
571
|
-
this._form.onsubmit = this._boundSubmitHandler;
|
572
|
-
this.createEditField();
|
573
|
-
if ('textarea' == this._controls.editor.tagName.toLowerCase())
|
574
|
-
this._form.appendChild(document.createElement('br'));
|
575
|
-
if (this.options.onFormCustomization)
|
576
|
-
this.options.onFormCustomization(this, this._form);
|
577
|
-
addText('Before', this.options.okControl || this.options.cancelControl);
|
578
|
-
this.createControl('ok', this._boundSubmitHandler);
|
579
|
-
addText('Between', this.options.okControl && this.options.cancelControl);
|
580
|
-
this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
|
581
|
-
addText('After', this.options.okControl || this.options.cancelControl);
|
582
|
-
},
|
583
|
-
destroy: function() {
|
584
|
-
if (this._oldInnerHTML)
|
585
|
-
this.element.innerHTML = this._oldInnerHTML;
|
586
|
-
this.leaveEditMode();
|
587
|
-
this.unregisterListeners();
|
588
|
-
},
|
589
|
-
enterEditMode: function(e) {
|
590
|
-
if (this._saving || this._editing) return;
|
591
|
-
this._editing = true;
|
592
|
-
this.triggerCallback('onEnterEditMode');
|
593
|
-
if (this.options.externalControl)
|
594
|
-
this.options.externalControl.hide();
|
595
|
-
this.element.hide();
|
596
|
-
this.createForm();
|
597
|
-
this.element.parentNode.insertBefore(this._form, this.element);
|
598
|
-
if (!this.options.loadTextURL)
|
599
|
-
this.postProcessEditField();
|
600
|
-
if (e) Event.stop(e);
|
601
|
-
},
|
602
|
-
enterHover: function(e) {
|
603
|
-
if (this.options.hoverClassName)
|
604
|
-
this.element.addClassName(this.options.hoverClassName);
|
605
|
-
if (this._saving) return;
|
606
|
-
this.triggerCallback('onEnterHover');
|
607
|
-
},
|
608
|
-
getText: function() {
|
609
|
-
return this.element.innerHTML.unescapeHTML();
|
610
|
-
},
|
611
|
-
handleAJAXFailure: function(transport) {
|
612
|
-
this.triggerCallback('onFailure', transport);
|
613
|
-
if (this._oldInnerHTML) {
|
614
|
-
this.element.innerHTML = this._oldInnerHTML;
|
615
|
-
this._oldInnerHTML = null;
|
616
|
-
}
|
617
|
-
},
|
618
|
-
handleFormCancellation: function(e) {
|
619
|
-
this.wrapUp();
|
620
|
-
if (e) Event.stop(e);
|
621
|
-
},
|
622
|
-
handleFormSubmission: function(e) {
|
623
|
-
var form = this._form;
|
624
|
-
var value = $F(this._controls.editor);
|
625
|
-
this.prepareSubmission();
|
626
|
-
var params = this.options.callback(form, value) || '';
|
627
|
-
if (Object.isString(params))
|
628
|
-
params = params.toQueryParams();
|
629
|
-
params.editorId = this.element.id;
|
630
|
-
if (this.options.htmlResponse) {
|
631
|
-
var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
|
632
|
-
Object.extend(options, {
|
633
|
-
parameters: params,
|
634
|
-
onComplete: this._boundWrapperHandler,
|
635
|
-
onFailure: this._boundFailureHandler
|
636
|
-
});
|
637
|
-
new Ajax.Updater({ success: this.element }, this.url, options);
|
638
|
-
} else {
|
639
|
-
var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
|
640
|
-
Object.extend(options, {
|
641
|
-
parameters: params,
|
642
|
-
onComplete: this._boundWrapperHandler,
|
643
|
-
onFailure: this._boundFailureHandler
|
644
|
-
});
|
645
|
-
new Ajax.Request(this.url, options);
|
646
|
-
}
|
647
|
-
if (e) Event.stop(e);
|
648
|
-
},
|
649
|
-
leaveEditMode: function() {
|
650
|
-
this.element.removeClassName(this.options.savingClassName);
|
651
|
-
this.removeForm();
|
652
|
-
this.leaveHover();
|
653
|
-
this.element.style.backgroundColor = this._originalBackground;
|
654
|
-
this.element.show();
|
655
|
-
if (this.options.externalControl)
|
656
|
-
this.options.externalControl.show();
|
657
|
-
this._saving = false;
|
658
|
-
this._editing = false;
|
659
|
-
this._oldInnerHTML = null;
|
660
|
-
this.triggerCallback('onLeaveEditMode');
|
661
|
-
},
|
662
|
-
leaveHover: function(e) {
|
663
|
-
if (this.options.hoverClassName)
|
664
|
-
this.element.removeClassName(this.options.hoverClassName);
|
665
|
-
if (this._saving) return;
|
666
|
-
this.triggerCallback('onLeaveHover');
|
667
|
-
},
|
668
|
-
loadExternalText: function() {
|
669
|
-
this._form.addClassName(this.options.loadingClassName);
|
670
|
-
this._controls.editor.disabled = true;
|
671
|
-
var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
|
672
|
-
Object.extend(options, {
|
673
|
-
parameters: 'editorId=' + encodeURIComponent(this.element.id),
|
674
|
-
onComplete: Prototype.emptyFunction,
|
675
|
-
onSuccess: function(transport) {
|
676
|
-
this._form.removeClassName(this.options.loadingClassName);
|
677
|
-
var text = transport.responseText;
|
678
|
-
if (this.options.stripLoadedTextTags)
|
679
|
-
text = text.stripTags();
|
680
|
-
this._controls.editor.value = text;
|
681
|
-
this._controls.editor.disabled = false;
|
682
|
-
this.postProcessEditField();
|
683
|
-
}.bind(this),
|
684
|
-
onFailure: this._boundFailureHandler
|
685
|
-
});
|
686
|
-
new Ajax.Request(this.options.loadTextURL, options);
|
687
|
-
},
|
688
|
-
postProcessEditField: function() {
|
689
|
-
var fpc = this.options.fieldPostCreation;
|
690
|
-
if (fpc)
|
691
|
-
$(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
|
692
|
-
},
|
693
|
-
prepareOptions: function() {
|
694
|
-
this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
|
695
|
-
Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
|
696
|
-
[this._extraDefaultOptions].flatten().compact().each(function(defs) {
|
697
|
-
Object.extend(this.options, defs);
|
698
|
-
}.bind(this));
|
699
|
-
},
|
700
|
-
prepareSubmission: function() {
|
701
|
-
this._saving = true;
|
702
|
-
this.removeForm();
|
703
|
-
this.leaveHover();
|
704
|
-
this.showSaving();
|
705
|
-
},
|
706
|
-
registerListeners: function() {
|
707
|
-
this._listeners = { };
|
708
|
-
var listener;
|
709
|
-
$H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
|
710
|
-
listener = this[pair.value].bind(this);
|
711
|
-
this._listeners[pair.key] = listener;
|
712
|
-
if (!this.options.externalControlOnly)
|
713
|
-
this.element.observe(pair.key, listener);
|
714
|
-
if (this.options.externalControl)
|
715
|
-
this.options.externalControl.observe(pair.key, listener);
|
716
|
-
}.bind(this));
|
717
|
-
},
|
718
|
-
removeForm: function() {
|
719
|
-
if (!this._form) return;
|
720
|
-
this._form.remove();
|
721
|
-
this._form = null;
|
722
|
-
this._controls = { };
|
723
|
-
},
|
724
|
-
showSaving: function() {
|
725
|
-
this._oldInnerHTML = this.element.innerHTML;
|
726
|
-
this.element.innerHTML = this.options.savingText;
|
727
|
-
this.element.addClassName(this.options.savingClassName);
|
728
|
-
this.element.style.backgroundColor = this._originalBackground;
|
729
|
-
this.element.show();
|
730
|
-
},
|
731
|
-
triggerCallback: function(cbName, arg) {
|
732
|
-
if ('function' == typeof this.options[cbName]) {
|
733
|
-
this.options[cbName](this, arg);
|
734
|
-
}
|
735
|
-
},
|
736
|
-
unregisterListeners: function() {
|
737
|
-
$H(this._listeners).each(function(pair) {
|
738
|
-
if (!this.options.externalControlOnly)
|
739
|
-
this.element.stopObserving(pair.key, pair.value);
|
740
|
-
if (this.options.externalControl)
|
741
|
-
this.options.externalControl.stopObserving(pair.key, pair.value);
|
742
|
-
}.bind(this));
|
743
|
-
},
|
744
|
-
wrapUp: function(transport) {
|
745
|
-
this.leaveEditMode();
|
746
|
-
// Can't use triggerCallback due to backward compatibility: requires
|
747
|
-
// binding + direct element
|
748
|
-
this._boundComplete(transport, this.element);
|
749
|
-
}
|
750
|
-
});
|
751
|
-
|
752
|
-
Object.extend(Ajax.InPlaceEditor.prototype, {
|
753
|
-
dispose: Ajax.InPlaceEditor.prototype.destroy
|
754
|
-
});
|
755
|
-
|
756
|
-
Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
|
757
|
-
initialize: function($super, element, url, options) {
|
758
|
-
this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
|
759
|
-
$super(element, url, options);
|
760
|
-
},
|
761
|
-
|
762
|
-
createEditField: function() {
|
763
|
-
var list = document.createElement('select');
|
764
|
-
list.name = this.options.paramName;
|
765
|
-
list.size = 1;
|
766
|
-
this._controls.editor = list;
|
767
|
-
this._collection = this.options.collection || [];
|
768
|
-
if (this.options.loadCollectionURL)
|
769
|
-
this.loadCollection();
|
770
|
-
else
|
771
|
-
this.checkForExternalText();
|
772
|
-
this._form.appendChild(this._controls.editor);
|
773
|
-
},
|
774
|
-
|
775
|
-
loadCollection: function() {
|
776
|
-
this._form.addClassName(this.options.loadingClassName);
|
777
|
-
this.showLoadingText(this.options.loadingCollectionText);
|
778
|
-
var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
|
779
|
-
Object.extend(options, {
|
780
|
-
parameters: 'editorId=' + encodeURIComponent(this.element.id),
|
781
|
-
onComplete: Prototype.emptyFunction,
|
782
|
-
onSuccess: function(transport) {
|
783
|
-
var js = transport.responseText.strip();
|
784
|
-
if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
|
785
|
-
throw('Server returned an invalid collection representation.');
|
786
|
-
this._collection = eval(js);
|
787
|
-
this.checkForExternalText();
|
788
|
-
}.bind(this),
|
789
|
-
onFailure: this.onFailure
|
790
|
-
});
|
791
|
-
new Ajax.Request(this.options.loadCollectionURL, options);
|
792
|
-
},
|
793
|
-
|
794
|
-
showLoadingText: function(text) {
|
795
|
-
this._controls.editor.disabled = true;
|
796
|
-
var tempOption = this._controls.editor.firstChild;
|
797
|
-
if (!tempOption) {
|
798
|
-
tempOption = document.createElement('option');
|
799
|
-
tempOption.value = '';
|
800
|
-
this._controls.editor.appendChild(tempOption);
|
801
|
-
tempOption.selected = true;
|
802
|
-
}
|
803
|
-
tempOption.update((text || '').stripScripts().stripTags());
|
804
|
-
},
|
805
|
-
|
806
|
-
checkForExternalText: function() {
|
807
|
-
this._text = this.getText();
|
808
|
-
if (this.options.loadTextURL)
|
809
|
-
this.loadExternalText();
|
810
|
-
else
|
811
|
-
this.buildOptionList();
|
812
|
-
},
|
813
|
-
|
814
|
-
loadExternalText: function() {
|
815
|
-
this.showLoadingText(this.options.loadingText);
|
816
|
-
var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
|
817
|
-
Object.extend(options, {
|
818
|
-
parameters: 'editorId=' + encodeURIComponent(this.element.id),
|
819
|
-
onComplete: Prototype.emptyFunction,
|
820
|
-
onSuccess: function(transport) {
|
821
|
-
this._text = transport.responseText.strip();
|
822
|
-
this.buildOptionList();
|
823
|
-
}.bind(this),
|
824
|
-
onFailure: this.onFailure
|
825
|
-
});
|
826
|
-
new Ajax.Request(this.options.loadTextURL, options);
|
827
|
-
},
|
828
|
-
|
829
|
-
buildOptionList: function() {
|
830
|
-
this._form.removeClassName(this.options.loadingClassName);
|
831
|
-
this._collection = this._collection.map(function(entry) {
|
832
|
-
return 2 === entry.length ? entry : [entry, entry].flatten();
|
833
|
-
});
|
834
|
-
var marker = ('value' in this.options) ? this.options.value : this._text;
|
835
|
-
var textFound = this._collection.any(function(entry) {
|
836
|
-
return entry[0] == marker;
|
837
|
-
}.bind(this));
|
838
|
-
this._controls.editor.update('');
|
839
|
-
var option;
|
840
|
-
this._collection.each(function(entry, index) {
|
841
|
-
option = document.createElement('option');
|
842
|
-
option.value = entry[0];
|
843
|
-
option.selected = textFound ? entry[0] == marker : 0 == index;
|
844
|
-
option.appendChild(document.createTextNode(entry[1]));
|
845
|
-
this._controls.editor.appendChild(option);
|
846
|
-
}.bind(this));
|
847
|
-
this._controls.editor.disabled = false;
|
848
|
-
Field.scrollFreeActivate(this._controls.editor);
|
849
|
-
}
|
850
|
-
});
|
851
|
-
|
852
|
-
//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
|
853
|
-
//**** This only exists for a while, in order to let ****
|
854
|
-
//**** users adapt to the new API. Read up on the new ****
|
855
|
-
//**** API and convert your code to it ASAP! ****
|
856
|
-
|
857
|
-
Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
|
858
|
-
if (!options) return;
|
859
|
-
function fallback(name, expr) {
|
860
|
-
if (name in options || expr === undefined) return;
|
861
|
-
options[name] = expr;
|
862
|
-
};
|
863
|
-
fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
|
864
|
-
options.cancelLink == options.cancelButton == false ? false : undefined)));
|
865
|
-
fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
|
866
|
-
options.okLink == options.okButton == false ? false : undefined)));
|
867
|
-
fallback('highlightColor', options.highlightcolor);
|
868
|
-
fallback('highlightEndColor', options.highlightendcolor);
|
869
|
-
};
|
870
|
-
|
871
|
-
Object.extend(Ajax.InPlaceEditor, {
|
872
|
-
DefaultOptions: {
|
873
|
-
ajaxOptions: { },
|
874
|
-
autoRows: 3, // Use when multi-line w/ rows == 1
|
875
|
-
cancelControl: 'link', // 'link'|'button'|false
|
876
|
-
cancelText: 'cancel',
|
877
|
-
clickToEditText: 'Click to edit',
|
878
|
-
externalControl: null, // id|elt
|
879
|
-
externalControlOnly: false,
|
880
|
-
fieldPostCreation: 'activate', // 'activate'|'focus'|false
|
881
|
-
formClassName: 'inplaceeditor-form',
|
882
|
-
formId: null, // id|elt
|
883
|
-
highlightColor: '#ffff99',
|
884
|
-
highlightEndColor: '#ffffff',
|
885
|
-
hoverClassName: '',
|
886
|
-
htmlResponse: true,
|
887
|
-
loadingClassName: 'inplaceeditor-loading',
|
888
|
-
loadingText: 'Loading...',
|
889
|
-
okControl: 'button', // 'link'|'button'|false
|
890
|
-
okText: 'ok',
|
891
|
-
paramName: 'value',
|
892
|
-
rows: 1, // If 1 and multi-line, uses autoRows
|
893
|
-
savingClassName: 'inplaceeditor-saving',
|
894
|
-
savingText: 'Saving...',
|
895
|
-
size: 0,
|
896
|
-
stripLoadedTextTags: false,
|
897
|
-
submitOnBlur: false,
|
898
|
-
textAfterControls: '',
|
899
|
-
textBeforeControls: '',
|
900
|
-
textBetweenControls: ''
|
901
|
-
},
|
902
|
-
DefaultCallbacks: {
|
903
|
-
callback: function(form) {
|
904
|
-
return Form.serialize(form);
|
905
|
-
},
|
906
|
-
onComplete: function(transport, element) {
|
907
|
-
// For backward compatibility, this one is bound to the IPE, and passes
|
908
|
-
// the element directly. It was too often customized, so we don't break it.
|
909
|
-
new Effect.Highlight(element, {
|
910
|
-
startcolor: this.options.highlightColor, keepBackgroundImage: true });
|
911
|
-
},
|
912
|
-
onEnterEditMode: null,
|
913
|
-
onEnterHover: function(ipe) {
|
914
|
-
ipe.element.style.backgroundColor = ipe.options.highlightColor;
|
915
|
-
if (ipe._effect)
|
916
|
-
ipe._effect.cancel();
|
917
|
-
},
|
918
|
-
onFailure: function(transport, ipe) {
|
919
|
-
alert('Error communication with the server: ' + transport.responseText.stripTags());
|
920
|
-
},
|
921
|
-
onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
|
922
|
-
onLeaveEditMode: null,
|
923
|
-
onLeaveHover: function(ipe) {
|
924
|
-
ipe._effect = new Effect.Highlight(ipe.element, {
|
925
|
-
startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
|
926
|
-
restorecolor: ipe._originalBackground, keepBackgroundImage: true
|
927
|
-
});
|
928
|
-
}
|
929
|
-
},
|
930
|
-
Listeners: {
|
931
|
-
click: 'enterEditMode',
|
932
|
-
keydown: 'checkForEscapeOrReturn',
|
933
|
-
mouseover: 'enterHover',
|
934
|
-
mouseout: 'leaveHover'
|
935
|
-
}
|
936
|
-
});
|
937
|
-
|
938
|
-
Ajax.InPlaceCollectionEditor.DefaultOptions = {
|
939
|
-
loadingCollectionText: 'Loading options...'
|
940
|
-
};
|
941
|
-
|
942
|
-
// Delayed observer, like Form.Element.Observer,
|
943
|
-
// but waits for delay after last key input
|
944
|
-
// Ideal for live-search fields
|
945
|
-
|
946
|
-
Form.Element.DelayedObserver = Class.create({
|
947
|
-
initialize: function(element, delay, callback) {
|
948
|
-
this.delay = delay || 0.5;
|
949
|
-
this.element = $(element);
|
950
|
-
this.callback = callback;
|
951
|
-
this.timer = null;
|
952
|
-
this.lastValue = $F(this.element);
|
953
|
-
Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
|
954
|
-
},
|
955
|
-
delayedListener: function(event) {
|
956
|
-
if(this.lastValue == $F(this.element)) return;
|
957
|
-
if(this.timer) clearTimeout(this.timer);
|
958
|
-
this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
|
959
|
-
this.lastValue = $F(this.element);
|
960
|
-
},
|
961
|
-
onTimerEvent: function() {
|
962
|
-
this.timer = null;
|
963
|
-
this.callback(this.element, $F(this.element));
|
964
|
-
}
|
965
|
-
});
|