data_fabric 1.2.0 → 1.2.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.
@@ -1 +0,0 @@
1
- DataFabric.init
@@ -1,106 +0,0 @@
1
- require 'active_record'
2
- require 'active_record/version'
3
- require 'data_fabric/version'
4
-
5
- # DataFabric adds a new level of flexibility to ActiveRecord connection handling.
6
- # You need to describe the topology for your database infrastructure in your model(s). As with ActiveRecord normally, different models can use different topologies.
7
- #
8
- # class MyHugeVolumeOfDataModel < ActiveRecord::Base
9
- # data_fabric :replicated => true, :shard_by => :city
10
- # end
11
- #
12
- # There are four supported modes of operation, depending on the options given to the data_fabric method. The plugin will look for connections in your config/database.yml with the following convention:
13
- #
14
- # No connection topology:
15
- # #{environment} - this is the default, as with ActiveRecord, e.g. "production"
16
- #
17
- # data_fabric :replicated => true
18
- # #{environment}_#{role} - no sharding, just replication, where role is "master" or "slave", e.g. "production_master"
19
- #
20
- # data_fabric :shard_by => :city
21
- # #{group}_#{shard}_#{environment} - sharding, no replication, e.g. "city_austin_production"
22
- #
23
- # data_fabric :replicated => true, :shard_by => :city
24
- # #{group}_#{shard}_#{environment}_#{role} - sharding with replication, e.g. "city_austin_production_master"
25
- #
26
- #
27
- # When marked as replicated, all write and transactional operations for the model go to the master, whereas read operations go to the slave.
28
- #
29
- # Since sharding is an application-level concern, your application must set the shard to use based on the current request or environment. The current shard for a group is set on a thread local variable. For example, you can set the shard in an ActionController around_filter based on the user as follows:
30
- #
31
- # class ApplicationController < ActionController::Base
32
- # around_filter :select_shard
33
- #
34
- # private
35
- # def select_shard(&action_block)
36
- # DataFabric.activate_shard(:city => @current_user.city, &action_block)
37
- # end
38
- # end
39
- module DataFabric
40
-
41
- # Set this logger to log DataFabric operations.
42
- # The logger should quack like a standard Ruby Logger.
43
- mattr_accessor :logger
44
-
45
- def self.init
46
- logger = ActiveRecord::Base.logger unless logger
47
- log { "Loading data_fabric #{DataFabric::Version::STRING} with ActiveRecord #{ActiveRecord::VERSION::STRING}" }
48
-
49
- if ActiveRecord::VERSION::STRING < '2.2.0'
50
- require 'data_fabric/ar20'
51
- else
52
- require 'data_fabric/ar22'
53
- end
54
- ActiveRecord::Base.send(:include, DataFabric::Extensions)
55
- end
56
-
57
- def self.activate_shard(shards, &block)
58
- ensure_setup
59
-
60
- # Save the old shard settings to handle nested activation
61
- old = Thread.current[:shards].dup
62
-
63
- shards.each_pair do |key, value|
64
- Thread.current[:shards][key.to_s] = value.to_s
65
- end
66
- if block_given?
67
- begin
68
- yield
69
- ensure
70
- Thread.current[:shards] = old
71
- end
72
- end
73
- end
74
-
75
- # For cases where you can't pass a block to activate_shards, you can
76
- # clean up the thread local settings by calling this method at the
77
- # end of processing
78
- def self.deactivate_shard(shards)
79
- ensure_setup
80
- shards.each do |key, value|
81
- Thread.current[:shards].delete(key.to_s)
82
- end
83
- end
84
-
85
- def self.active_shard(group)
86
- raise ArgumentError, 'No shard has been activated' unless Thread.current[:shards]
87
-
88
- returning(Thread.current[:shards][group.to_s]) do |shard|
89
- raise ArgumentError, "No active shard for #{group}" unless shard
90
- end
91
- end
92
-
93
- def self.shard_active_for?(group)
94
- return true unless group
95
- Thread.current[:shards] and Thread.current[:shards][group.to_s]
96
- end
97
-
98
- def self.ensure_setup
99
- Thread.current[:shards] = {} unless Thread.current[:shards]
100
- end
101
-
102
- def self.log(level=Logger::INFO, &block)
103
- logger && logger.add(level, &block)
104
- end
105
-
106
- end
@@ -1,135 +0,0 @@
1
- module DataFabric
2
- module Extensions
3
- def self.included(model)
4
- # Wire up ActiveRecord::Base
5
- model.extend ClassMethods
6
- end
7
-
8
- # Class methods injected into ActiveRecord::Base
9
- module ClassMethods
10
- def data_fabric(options)
11
- proxy = DataFabric::ConnectionProxy.new(self, options)
12
- ActiveRecord::Base.active_connections[name] = proxy
13
-
14
- raise ArgumentError, "data_fabric does not support ActiveRecord's allow_concurrency = true" if allow_concurrency
15
- DataFabric.log { "Creating data_fabric proxy for class #{name}" }
16
- end
17
- end
18
- end
19
-
20
- class ConnectionProxy
21
- def initialize(model_class, options)
22
- @model_class = model_class
23
- @replicated = options[:replicated]
24
- @shard_group = options[:shard_by]
25
- @prefix = options[:prefix]
26
- @role = 'slave' if @replicated
27
-
28
- @model_class.send :include, ActiveRecordConnectionMethods if @replicated
29
- end
30
-
31
- delegate :insert, :update, :delete, :create_table, :rename_table, :drop_table, :add_column, :remove_column,
32
- :change_column, :change_column_default, :rename_column, :add_index, :remove_index, :initialize_schema_information,
33
- :dump_schema_information, :execute, :execute_ignore_duplicate, :to => :master
34
-
35
- def cache(&block)
36
- connection.cache(&block)
37
- end
38
-
39
- def transaction(start_db_transaction = true, &block)
40
- with_master { connection.transaction(start_db_transaction, &block) }
41
- end
42
-
43
- def method_missing(method, *args, &block)
44
- DataFabric.log(Logger::DEBUG) { "Calling #{method} on #{connection}" }
45
- connection.send(method, *args, &block)
46
- end
47
-
48
- def connection_name
49
- connection_name_builder.join('_')
50
- end
51
-
52
- def disconnect!
53
- if connected?
54
- connection.disconnect!
55
- cached_connections[connection_name] = nil
56
- end
57
- end
58
-
59
- def verify!(arg)
60
- connection.verify!(arg) if connected?
61
- end
62
-
63
- def with_master
64
- # Allow nesting of with_master.
65
- old_role = @role
66
- set_role('master')
67
- yield
68
- ensure
69
- set_role(old_role)
70
- end
71
-
72
- private
73
-
74
- def cached_connections
75
- @cached_connections ||= {}
76
- end
77
-
78
- def connection_name_builder
79
- @connection_name_builder ||= begin
80
- clauses = []
81
- clauses << @prefix if @prefix
82
- clauses << @shard_group if @shard_group
83
- clauses << StringProxy.new { DataFabric.active_shard(@shard_group) } if @shard_group
84
- clauses << RAILS_ENV
85
- clauses << StringProxy.new { @role } if @replicated
86
- clauses
87
- end
88
- end
89
-
90
- def connection
91
- name = connection_name
92
- if not connected?
93
- config = ActiveRecord::Base.configurations[name]
94
- raise ArgumentError, "Unknown database config: #{name}, have #{ActiveRecord::Base.configurations.inspect}" unless config
95
- DataFabric.log { "Connecting to #{name}" }
96
- @model_class.establish_connection(config)
97
- cached_connections[name] = @model_class.connection
98
- @model_class.active_connections[@model_class.name] = self
99
- end
100
- cached_connections[name].verify!(3600)
101
- cached_connections[name]
102
- end
103
-
104
- def connected?
105
- DataFabric.shard_active_for?(@shard_group) and cached_connections[connection_name]
106
- end
107
-
108
- def set_role(role)
109
- @role = role if @replicated
110
- end
111
-
112
- def master
113
- with_master { return connection }
114
- end
115
- end
116
-
117
- module ActiveRecordConnectionMethods
118
- def self.included(base)
119
- base.alias_method_chain :reload, :master
120
- end
121
-
122
- def reload_with_master(*args, &block)
123
- connection.with_master { reload_without_master }
124
- end
125
- end
126
-
127
- class StringProxy
128
- def initialize(&block)
129
- @proc = block
130
- end
131
- def to_s
132
- @proc.call
133
- end
134
- end
135
- end
@@ -1,172 +0,0 @@
1
- module DataFabric
2
- module Extensions
3
- def self.included(model)
4
- # Wire up ActiveRecord::Base
5
- model.extend ClassMethods
6
- ConnectionProxy.shard_pools = {}
7
- end
8
-
9
- # Class methods injected into ActiveRecord::Base
10
- module ClassMethods
11
- def data_fabric(options)
12
- DataFabric.log { "Creating data_fabric proxy for class #{name}" }
13
- @proxy = DataFabric::ConnectionProxy.new(self, options)
14
-
15
- class << self
16
- def connection
17
- @proxy
18
- end
19
-
20
- def connected?
21
- @proxy.connected?
22
- end
23
-
24
- def remove_connection(klass)
25
- DataFabric.log(Logger::ERROR) { "remove_connection not implemented by data_fabric" }
26
- end
27
-
28
- def connection_pool
29
- raise "dynamic connection switching means you cannot get direct access to a pool"
30
- end
31
- end
32
- end
33
- end
34
- end
35
-
36
- class ConnectionProxy
37
- cattr_accessor :shard_pools
38
-
39
- def initialize(model_class, options)
40
- @model_class = model_class
41
- @replicated = options[:replicated]
42
- @shard_group = options[:shard_by]
43
- @prefix = options[:prefix]
44
- set_role('slave') if @replicated
45
-
46
- @model_class.send :include, ActiveRecordConnectionMethods if @replicated
47
- end
48
-
49
- delegate :insert, :update, :delete, :create_table, :rename_table, :drop_table, :add_column, :remove_column,
50
- :change_column, :change_column_default, :rename_column, :add_index, :remove_index, :initialize_schema_information,
51
- :dump_schema_information, :execute, :execute_ignore_duplicate, :to => :master
52
-
53
- delegate :insert_many, :to => :master # ar-extensions bulk insert support
54
-
55
- def transaction(start_db_transaction = true, &block)
56
- # Transaction is not re-entrant in SQLite 3 so we
57
- # need to track if we've already started an XA to avoid
58
- # calling it twice.
59
- return yield if in_transaction?
60
-
61
- with_master do
62
- connection.transaction(start_db_transaction, &block)
63
- end
64
- end
65
-
66
- def method_missing(method, *args, &block)
67
- DataFabric.log(Logger::DEBUG) { "Calling #{method} on #{connection}" }
68
- connection.send(method, *args, &block)
69
- end
70
-
71
- def connection_name
72
- connection_name_builder.join('_')
73
- end
74
-
75
- def with_master
76
- # Allow nesting of with_master.
77
- old_role = current_role
78
- set_role('master')
79
- yield
80
- ensure
81
- set_role(old_role)
82
- end
83
-
84
- private
85
-
86
- def in_transaction?
87
- current_role == 'master'
88
- end
89
-
90
- def current_pool
91
- name = connection_name
92
- self.class.shard_pools[name] ||= begin
93
- config = ActiveRecord::Base.configurations[name]
94
- raise ArgumentError, "Unknown database config: #{name}, have #{ActiveRecord::Base.configurations.inspect}" unless config
95
- ActiveRecord::ConnectionAdapters::ConnectionPool.new(spec_for(config))
96
- end
97
- end
98
-
99
- def spec_for(config)
100
- # XXX This looks pretty fragile. Will break if AR changes how it initializes connections and adapters.
101
- config = config.symbolize_keys
102
- adapter_method = "#{config[:adapter]}_connection"
103
- initialize_adapter(config[:adapter])
104
- ActiveRecord::Base::ConnectionSpecification.new(config, adapter_method)
105
- end
106
-
107
- def initialize_adapter(adapter)
108
- begin
109
- require 'rubygems'
110
- gem "activerecord-#{adapter}-adapter"
111
- require "active_record/connection_adapters/#{adapter}_adapter"
112
- rescue LoadError
113
- begin
114
- require "active_record/connection_adapters/#{adapter}_adapter"
115
- rescue LoadError
116
- raise "Please install the #{adapter} adapter: `gem install activerecord-#{adapter}-adapter` (#{$!})"
117
- end
118
- end
119
- end
120
-
121
- def connection_name_builder
122
- @connection_name_builder ||= begin
123
- clauses = []
124
- clauses << @prefix if @prefix
125
- clauses << @shard_group if @shard_group
126
- clauses << StringProxy.new { DataFabric.active_shard(@shard_group) } if @shard_group
127
- clauses << RAILS_ENV
128
- clauses << StringProxy.new { current_role } if @replicated
129
- clauses
130
- end
131
- end
132
-
133
- def connection
134
- current_pool.connection
135
- end
136
-
137
- def active?
138
- DataFabric.shard_active_for?(@shard_group)
139
- end
140
-
141
- def set_role(role)
142
- Thread.current[:data_fabric_role] = role
143
- end
144
-
145
- def current_role
146
- Thread.current[:data_fabric_role]
147
- end
148
-
149
- def master
150
- with_master { return connection }
151
- end
152
- end
153
-
154
- module ActiveRecordConnectionMethods
155
- def self.included(base)
156
- base.alias_method_chain :reload, :master
157
- end
158
-
159
- def reload_with_master(*args, &block)
160
- connection.with_master { reload_without_master }
161
- end
162
- end
163
-
164
- class StringProxy
165
- def initialize(&block)
166
- @proc = block
167
- end
168
- def to_s
169
- @proc.call
170
- end
171
- end
172
- end
@@ -1,5 +0,0 @@
1
- module DataFabric
2
- module Version
3
- STRING = "1.2.0"
4
- end
5
- end
@@ -1,2 +0,0 @@
1
- // Place your application-specific JavaScript functions and classes here
2
- // This file is automatically included by javascript_include_tag :defaults
@@ -1,963 +0,0 @@
1
- // Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2
- // (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
3
- // (c) 2005-2008 Jon Tirsen (http://www.tirsen.com)
4
- // Contributors:
5
- // Richard Livsey
6
- // Rahul Bhargava
7
- // Rob Wills
8
- //
9
- // script.aculo.us is freely distributable under the terms of an MIT-style license.
10
- // For details, see the script.aculo.us web site: http://script.aculo.us/
11
-
12
- // Autocompleter.Base handles all the autocompletion functionality
13
- // that's independent of the data source for autocompletion. This
14
- // includes drawing the autocompletion menu, observing keyboard
15
- // and mouse events, and similar.
16
- //
17
- // Specific autocompleters need to provide, at the very least,
18
- // a getUpdatedChoices function that will be invoked every time
19
- // the text inside the monitored textbox changes. This method
20
- // should get the text for which to provide autocompletion by
21
- // invoking this.getToken(), NOT by directly accessing
22
- // this.element.value. This is to allow incremental tokenized
23
- // autocompletion. Specific auto-completion logic (AJAX, etc)
24
- // belongs in getUpdatedChoices.
25
- //
26
- // Tokenized incremental autocompletion is enabled automatically
27
- // when an autocompleter is instantiated with the 'tokens' option
28
- // in the options parameter, e.g.:
29
- // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
30
- // will incrementally autocomplete with a comma as the token.
31
- // Additionally, ',' in the above example can be replaced with
32
- // a token array, e.g. { tokens: [',', '\n'] } which
33
- // enables autocompletion on multiple tokens. This is most
34
- // useful when one of the tokens is \n (a newline), as it
35
- // allows smart autocompletion after linebreaks.
36
-
37
- if(typeof Effect == 'undefined')
38
- throw("controls.js requires including script.aculo.us' effects.js library");
39
-
40
- var Autocompleter = { };
41
- Autocompleter.Base = Class.create({
42
- baseInitialize: function(element, update, options) {
43
- element = $(element);
44
- this.element = element;
45
- this.update = $(update);
46
- this.hasFocus = false;
47
- this.changed = false;
48
- this.active = false;
49
- this.index = 0;
50
- this.entryCount = 0;
51
- this.oldElementValue = this.element.value;
52
-
53
- if(this.setOptions)
54
- this.setOptions(options);
55
- else
56
- this.options = options || { };
57
-
58
- this.options.paramName = this.options.paramName || this.element.name;
59
- this.options.tokens = this.options.tokens || [];
60
- this.options.frequency = this.options.frequency || 0.4;
61
- this.options.minChars = this.options.minChars || 1;
62
- this.options.onShow = this.options.onShow ||
63
- function(element, update){
64
- if(!update.style.position || update.style.position=='absolute') {
65
- update.style.position = 'absolute';
66
- Position.clone(element, update, {
67
- setHeight: false,
68
- offsetTop: element.offsetHeight
69
- });
70
- }
71
- Effect.Appear(update,{duration:0.15});
72
- };
73
- this.options.onHide = this.options.onHide ||
74
- function(element, update){ new Effect.Fade(update,{duration:0.15}) };
75
-
76
- if(typeof(this.options.tokens) == 'string')
77
- this.options.tokens = new Array(this.options.tokens);
78
- // Force carriage returns as token delimiters anyway
79
- if (!this.options.tokens.include('\n'))
80
- this.options.tokens.push('\n');
81
-
82
- this.observer = null;
83
-
84
- this.element.setAttribute('autocomplete','off');
85
-
86
- Element.hide(this.update);
87
-
88
- Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
89
- Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this));
90
- },
91
-
92
- show: function() {
93
- if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
94
- if(!this.iefix &&
95
- (Prototype.Browser.IE) &&
96
- (Element.getStyle(this.update, 'position')=='absolute')) {
97
- new Insertion.After(this.update,
98
- '<iframe id="' + this.update.id + '_iefix" '+
99
- 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
100
- 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
101
- this.iefix = $(this.update.id+'_iefix');
102
- }
103
- if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
104
- },
105
-
106
- fixIEOverlapping: function() {
107
- Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
108
- this.iefix.style.zIndex = 1;
109
- this.update.style.zIndex = 2;
110
- Element.show(this.iefix);
111
- },
112
-
113
- hide: function() {
114
- this.stopIndicator();
115
- if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
116
- if(this.iefix) Element.hide(this.iefix);
117
- },
118
-
119
- startIndicator: function() {
120
- if(this.options.indicator) Element.show(this.options.indicator);
121
- },
122
-
123
- stopIndicator: function() {
124
- if(this.options.indicator) Element.hide(this.options.indicator);
125
- },
126
-
127
- onKeyPress: function(event) {
128
- if(this.active)
129
- switch(event.keyCode) {
130
- case Event.KEY_TAB:
131
- case Event.KEY_RETURN:
132
- this.selectEntry();
133
- Event.stop(event);
134
- case Event.KEY_ESC:
135
- this.hide();
136
- this.active = false;
137
- Event.stop(event);
138
- return;
139
- case Event.KEY_LEFT:
140
- case Event.KEY_RIGHT:
141
- return;
142
- case Event.KEY_UP:
143
- this.markPrevious();
144
- this.render();
145
- Event.stop(event);
146
- return;
147
- case Event.KEY_DOWN:
148
- this.markNext();
149
- this.render();
150
- Event.stop(event);
151
- return;
152
- }
153
- else
154
- if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
155
- (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;
156
-
157
- this.changed = true;
158
- this.hasFocus = true;
159
-
160
- if(this.observer) clearTimeout(this.observer);
161
- this.observer =
162
- setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
163
- },
164
-
165
- activate: function() {
166
- this.changed = false;
167
- this.hasFocus = true;
168
- this.getUpdatedChoices();
169
- },
170
-
171
- onHover: function(event) {
172
- var element = Event.findElement(event, 'LI');
173
- if(this.index != element.autocompleteIndex)
174
- {
175
- this.index = element.autocompleteIndex;
176
- this.render();
177
- }
178
- Event.stop(event);
179
- },
180
-
181
- onClick: function(event) {
182
- var element = Event.findElement(event, 'LI');
183
- this.index = element.autocompleteIndex;
184
- this.selectEntry();
185
- this.hide();
186
- },
187
-
188
- onBlur: function(event) {
189
- // needed to make click events working
190
- setTimeout(this.hide.bind(this), 250);
191
- this.hasFocus = false;
192
- this.active = false;
193
- },
194
-
195
- render: function() {
196
- if(this.entryCount > 0) {
197
- for (var i = 0; i < this.entryCount; i++)
198
- this.index==i ?
199
- Element.addClassName(this.getEntry(i),"selected") :
200
- Element.removeClassName(this.getEntry(i),"selected");
201
- if(this.hasFocus) {
202
- this.show();
203
- this.active = true;
204
- }
205
- } else {
206
- this.active = false;
207
- this.hide();
208
- }
209
- },
210
-
211
- markPrevious: function() {
212
- if(this.index > 0) this.index--;
213
- else this.index = this.entryCount-1;
214
- this.getEntry(this.index).scrollIntoView(true);
215
- },
216
-
217
- markNext: function() {
218
- if(this.index < this.entryCount-1) this.index++;
219
- else this.index = 0;
220
- this.getEntry(this.index).scrollIntoView(false);
221
- },
222
-
223
- getEntry: function(index) {
224
- return this.update.firstChild.childNodes[index];
225
- },
226
-
227
- getCurrentEntry: function() {
228
- return this.getEntry(this.index);
229
- },
230
-
231
- selectEntry: function() {
232
- this.active = false;
233
- this.updateElement(this.getCurrentEntry());
234
- },
235
-
236
- updateElement: function(selectedElement) {
237
- if (this.options.updateElement) {
238
- this.options.updateElement(selectedElement);
239
- return;
240
- }
241
- var value = '';
242
- if (this.options.select) {
243
- var nodes = $(selectedElement).select('.' + this.options.select) || [];
244
- if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
245
- } else
246
- value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
247
-
248
- var bounds = this.getTokenBounds();
249
- if (bounds[0] != -1) {
250
- var newValue = this.element.value.substr(0, bounds[0]);
251
- var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
252
- if (whitespace)
253
- newValue += whitespace[0];
254
- this.element.value = newValue + value + this.element.value.substr(bounds[1]);
255
- } else {
256
- this.element.value = value;
257
- }
258
- this.oldElementValue = this.element.value;
259
- this.element.focus();
260
-
261
- if (this.options.afterUpdateElement)
262
- this.options.afterUpdateElement(this.element, selectedElement);
263
- },
264
-
265
- updateChoices: function(choices) {
266
- if(!this.changed && this.hasFocus) {
267
- this.update.innerHTML = choices;
268
- Element.cleanWhitespace(this.update);
269
- Element.cleanWhitespace(this.update.down());
270
-
271
- if(this.update.firstChild && this.update.down().childNodes) {
272
- this.entryCount =
273
- this.update.down().childNodes.length;
274
- for (var i = 0; i < this.entryCount; i++) {
275
- var entry = this.getEntry(i);
276
- entry.autocompleteIndex = i;
277
- this.addObservers(entry);
278
- }
279
- } else {
280
- this.entryCount = 0;
281
- }
282
-
283
- this.stopIndicator();
284
- this.index = 0;
285
-
286
- if(this.entryCount==1 && this.options.autoSelect) {
287
- this.selectEntry();
288
- this.hide();
289
- } else {
290
- this.render();
291
- }
292
- }
293
- },
294
-
295
- addObservers: function(element) {
296
- Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
297
- Event.observe(element, "click", this.onClick.bindAsEventListener(this));
298
- },
299
-
300
- onObserverEvent: function() {
301
- this.changed = false;
302
- this.tokenBounds = null;
303
- if(this.getToken().length>=this.options.minChars) {
304
- this.getUpdatedChoices();
305
- } else {
306
- this.active = false;
307
- this.hide();
308
- }
309
- this.oldElementValue = this.element.value;
310
- },
311
-
312
- getToken: function() {
313
- var bounds = this.getTokenBounds();
314
- return this.element.value.substring(bounds[0], bounds[1]).strip();
315
- },
316
-
317
- getTokenBounds: function() {
318
- if (null != this.tokenBounds) return this.tokenBounds;
319
- var value = this.element.value;
320
- if (value.strip().empty()) return [-1, 0];
321
- var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
322
- var offset = (diff == this.oldElementValue.length ? 1 : 0);
323
- var prevTokenPos = -1, nextTokenPos = value.length;
324
- var tp;
325
- for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
326
- tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
327
- if (tp > prevTokenPos) prevTokenPos = tp;
328
- tp = value.indexOf(this.options.tokens[index], diff + offset);
329
- if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
330
- }
331
- return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
332
- }
333
- });
334
-
335
- Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
336
- var boundary = Math.min(newS.length, oldS.length);
337
- for (var index = 0; index < boundary; ++index)
338
- if (newS[index] != oldS[index])
339
- return index;
340
- return boundary;
341
- };
342
-
343
- Ajax.Autocompleter = Class.create(Autocompleter.Base, {
344
- initialize: function(element, update, url, options) {
345
- this.baseInitialize(element, update, options);
346
- this.options.asynchronous = true;
347
- this.options.onComplete = this.onComplete.bind(this);
348
- this.options.defaultParams = this.options.parameters || null;
349
- this.url = url;
350
- },
351
-
352
- getUpdatedChoices: function() {
353
- this.startIndicator();
354
-
355
- var entry = encodeURIComponent(this.options.paramName) + '=' +
356
- encodeURIComponent(this.getToken());
357
-
358
- this.options.parameters = this.options.callback ?
359
- this.options.callback(this.element, entry) : entry;
360
-
361
- if(this.options.defaultParams)
362
- this.options.parameters += '&' + this.options.defaultParams;
363
-
364
- new Ajax.Request(this.url, this.options);
365
- },
366
-
367
- onComplete: function(request) {
368
- this.updateChoices(request.responseText);
369
- }
370
- });
371
-
372
- // The local array autocompleter. Used when you'd prefer to
373
- // inject an array of autocompletion options into the page, rather
374
- // than sending out Ajax queries, which can be quite slow sometimes.
375
- //
376
- // The constructor takes four parameters. The first two are, as usual,
377
- // the id of the monitored textbox, and id of the autocompletion menu.
378
- // The third is the array you want to autocomplete from, and the fourth
379
- // is the options block.
380
- //
381
- // Extra local autocompletion options:
382
- // - choices - How many autocompletion choices to offer
383
- //
384
- // - partialSearch - If false, the autocompleter will match entered
385
- // text only at the beginning of strings in the
386
- // autocomplete array. Defaults to true, which will
387
- // match text at the beginning of any *word* in the
388
- // strings in the autocomplete array. If you want to
389
- // search anywhere in the string, additionally set
390
- // the option fullSearch to true (default: off).
391
- //
392
- // - fullSsearch - Search anywhere in autocomplete array strings.
393
- //
394
- // - partialChars - How many characters to enter before triggering
395
- // a partial match (unlike minChars, which defines
396
- // how many characters are required to do any match
397
- // at all). Defaults to 2.
398
- //
399
- // - ignoreCase - Whether to ignore case when autocompleting.
400
- // Defaults to true.
401
- //
402
- // It's possible to pass in a custom function as the 'selector'
403
- // option, if you prefer to write your own autocompletion logic.
404
- // In that case, the other options above will not apply unless
405
- // you support them.
406
-
407
- Autocompleter.Local = Class.create(Autocompleter.Base, {
408
- initialize: function(element, update, array, options) {
409
- this.baseInitialize(element, update, options);
410
- this.options.array = array;
411
- },
412
-
413
- getUpdatedChoices: function() {
414
- this.updateChoices(this.options.selector(this));
415
- },
416
-
417
- setOptions: function(options) {
418
- this.options = Object.extend({
419
- choices: 10,
420
- partialSearch: true,
421
- partialChars: 2,
422
- ignoreCase: true,
423
- fullSearch: false,
424
- selector: function(instance) {
425
- var ret = []; // Beginning matches
426
- var partial = []; // Inside matches
427
- var entry = instance.getToken();
428
- var count = 0;
429
-
430
- for (var i = 0; i < instance.options.array.length &&
431
- ret.length < instance.options.choices ; i++) {
432
-
433
- var elem = instance.options.array[i];
434
- var foundPos = instance.options.ignoreCase ?
435
- elem.toLowerCase().indexOf(entry.toLowerCase()) :
436
- elem.indexOf(entry);
437
-
438
- while (foundPos != -1) {
439
- if (foundPos == 0 && elem.length != entry.length) {
440
- ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
441
- elem.substr(entry.length) + "</li>");
442
- break;
443
- } else if (entry.length >= instance.options.partialChars &&
444
- instance.options.partialSearch && foundPos != -1) {
445
- if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
446
- partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
447
- elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
448
- foundPos + entry.length) + "</li>");
449
- break;
450
- }
451
- }
452
-
453
- foundPos = instance.options.ignoreCase ?
454
- elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
455
- elem.indexOf(entry, foundPos + 1);
456
-
457
- }
458
- }
459
- if (partial.length)
460
- ret = ret.concat(partial.slice(0, instance.options.choices - ret.length));
461
- return "<ul>" + ret.join('') + "</ul>";
462
- }
463
- }, options || { });
464
- }
465
- });
466
-
467
- // AJAX in-place editor and collection editor
468
- // Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
469
-
470
- // Use this if you notice weird scrolling problems on some browsers,
471
- // the DOM might be a bit confused when this gets called so do this
472
- // waits 1 ms (with setTimeout) until it does the activation
473
- Field.scrollFreeActivate = function(field) {
474
- setTimeout(function() {
475
- Field.activate(field);
476
- }, 1);
477
- };
478
-
479
- Ajax.InPlaceEditor = Class.create({
480
- initialize: function(element, url, options) {
481
- this.url = url;
482
- this.element = element = $(element);
483
- this.prepareOptions();
484
- this._controls = { };
485
- arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
486
- Object.extend(this.options, options || { });
487
- if (!this.options.formId && this.element.id) {
488
- this.options.formId = this.element.id + '-inplaceeditor';
489
- if ($(this.options.formId))
490
- this.options.formId = '';
491
- }
492
- if (this.options.externalControl)
493
- this.options.externalControl = $(this.options.externalControl);
494
- if (!this.options.externalControl)
495
- this.options.externalControlOnly = false;
496
- this._originalBackground = this.element.getStyle('background-color') || 'transparent';
497
- this.element.title = this.options.clickToEditText;
498
- this._boundCancelHandler = this.handleFormCancellation.bind(this);
499
- this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
500
- this._boundFailureHandler = this.handleAJAXFailure.bind(this);
501
- this._boundSubmitHandler = this.handleFormSubmission.bind(this);
502
- this._boundWrapperHandler = this.wrapUp.bind(this);
503
- this.registerListeners();
504
- },
505
- checkForEscapeOrReturn: function(e) {
506
- if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
507
- if (Event.KEY_ESC == e.keyCode)
508
- this.handleFormCancellation(e);
509
- else if (Event.KEY_RETURN == e.keyCode)
510
- this.handleFormSubmission(e);
511
- },
512
- createControl: function(mode, handler, extraClasses) {
513
- var control = this.options[mode + 'Control'];
514
- var text = this.options[mode + 'Text'];
515
- if ('button' == control) {
516
- var btn = document.createElement('input');
517
- btn.type = 'submit';
518
- btn.value = text;
519
- btn.className = 'editor_' + mode + '_button';
520
- if ('cancel' == mode)
521
- btn.onclick = this._boundCancelHandler;
522
- this._form.appendChild(btn);
523
- this._controls[mode] = btn;
524
- } else if ('link' == control) {
525
- var link = document.createElement('a');
526
- link.href = '#';
527
- link.appendChild(document.createTextNode(text));
528
- link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
529
- link.className = 'editor_' + mode + '_link';
530
- if (extraClasses)
531
- link.className += ' ' + extraClasses;
532
- this._form.appendChild(link);
533
- this._controls[mode] = link;
534
- }
535
- },
536
- createEditField: function() {
537
- var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
538
- var fld;
539
- if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
540
- fld = document.createElement('input');
541
- fld.type = 'text';
542
- var size = this.options.size || this.options.cols || 0;
543
- if (0 < size) fld.size = size;
544
- } else {
545
- fld = document.createElement('textarea');
546
- fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
547
- fld.cols = this.options.cols || 40;
548
- }
549
- fld.name = this.options.paramName;
550
- fld.value = text; // No HTML breaks conversion anymore
551
- fld.className = 'editor_field';
552
- if (this.options.submitOnBlur)
553
- fld.onblur = this._boundSubmitHandler;
554
- this._controls.editor = fld;
555
- if (this.options.loadTextURL)
556
- this.loadExternalText();
557
- this._form.appendChild(this._controls.editor);
558
- },
559
- createForm: function() {
560
- var ipe = this;
561
- function addText(mode, condition) {
562
- var text = ipe.options['text' + mode + 'Controls'];
563
- if (!text || condition === false) return;
564
- ipe._form.appendChild(document.createTextNode(text));
565
- };
566
- this._form = $(document.createElement('form'));
567
- this._form.id = this.options.formId;
568
- this._form.addClassName(this.options.formClassName);
569
- this._form.onsubmit = this._boundSubmitHandler;
570
- this.createEditField();
571
- if ('textarea' == this._controls.editor.tagName.toLowerCase())
572
- this._form.appendChild(document.createElement('br'));
573
- if (this.options.onFormCustomization)
574
- this.options.onFormCustomization(this, this._form);
575
- addText('Before', this.options.okControl || this.options.cancelControl);
576
- this.createControl('ok', this._boundSubmitHandler);
577
- addText('Between', this.options.okControl && this.options.cancelControl);
578
- this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
579
- addText('After', this.options.okControl || this.options.cancelControl);
580
- },
581
- destroy: function() {
582
- if (this._oldInnerHTML)
583
- this.element.innerHTML = this._oldInnerHTML;
584
- this.leaveEditMode();
585
- this.unregisterListeners();
586
- },
587
- enterEditMode: function(e) {
588
- if (this._saving || this._editing) return;
589
- this._editing = true;
590
- this.triggerCallback('onEnterEditMode');
591
- if (this.options.externalControl)
592
- this.options.externalControl.hide();
593
- this.element.hide();
594
- this.createForm();
595
- this.element.parentNode.insertBefore(this._form, this.element);
596
- if (!this.options.loadTextURL)
597
- this.postProcessEditField();
598
- if (e) Event.stop(e);
599
- },
600
- enterHover: function(e) {
601
- if (this.options.hoverClassName)
602
- this.element.addClassName(this.options.hoverClassName);
603
- if (this._saving) return;
604
- this.triggerCallback('onEnterHover');
605
- },
606
- getText: function() {
607
- return this.element.innerHTML.unescapeHTML();
608
- },
609
- handleAJAXFailure: function(transport) {
610
- this.triggerCallback('onFailure', transport);
611
- if (this._oldInnerHTML) {
612
- this.element.innerHTML = this._oldInnerHTML;
613
- this._oldInnerHTML = null;
614
- }
615
- },
616
- handleFormCancellation: function(e) {
617
- this.wrapUp();
618
- if (e) Event.stop(e);
619
- },
620
- handleFormSubmission: function(e) {
621
- var form = this._form;
622
- var value = $F(this._controls.editor);
623
- this.prepareSubmission();
624
- var params = this.options.callback(form, value) || '';
625
- if (Object.isString(params))
626
- params = params.toQueryParams();
627
- params.editorId = this.element.id;
628
- if (this.options.htmlResponse) {
629
- var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
630
- Object.extend(options, {
631
- parameters: params,
632
- onComplete: this._boundWrapperHandler,
633
- onFailure: this._boundFailureHandler
634
- });
635
- new Ajax.Updater({ success: this.element }, this.url, options);
636
- } else {
637
- var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
638
- Object.extend(options, {
639
- parameters: params,
640
- onComplete: this._boundWrapperHandler,
641
- onFailure: this._boundFailureHandler
642
- });
643
- new Ajax.Request(this.url, options);
644
- }
645
- if (e) Event.stop(e);
646
- },
647
- leaveEditMode: function() {
648
- this.element.removeClassName(this.options.savingClassName);
649
- this.removeForm();
650
- this.leaveHover();
651
- this.element.style.backgroundColor = this._originalBackground;
652
- this.element.show();
653
- if (this.options.externalControl)
654
- this.options.externalControl.show();
655
- this._saving = false;
656
- this._editing = false;
657
- this._oldInnerHTML = null;
658
- this.triggerCallback('onLeaveEditMode');
659
- },
660
- leaveHover: function(e) {
661
- if (this.options.hoverClassName)
662
- this.element.removeClassName(this.options.hoverClassName);
663
- if (this._saving) return;
664
- this.triggerCallback('onLeaveHover');
665
- },
666
- loadExternalText: function() {
667
- this._form.addClassName(this.options.loadingClassName);
668
- this._controls.editor.disabled = true;
669
- var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
670
- Object.extend(options, {
671
- parameters: 'editorId=' + encodeURIComponent(this.element.id),
672
- onComplete: Prototype.emptyFunction,
673
- onSuccess: function(transport) {
674
- this._form.removeClassName(this.options.loadingClassName);
675
- var text = transport.responseText;
676
- if (this.options.stripLoadedTextTags)
677
- text = text.stripTags();
678
- this._controls.editor.value = text;
679
- this._controls.editor.disabled = false;
680
- this.postProcessEditField();
681
- }.bind(this),
682
- onFailure: this._boundFailureHandler
683
- });
684
- new Ajax.Request(this.options.loadTextURL, options);
685
- },
686
- postProcessEditField: function() {
687
- var fpc = this.options.fieldPostCreation;
688
- if (fpc)
689
- $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
690
- },
691
- prepareOptions: function() {
692
- this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
693
- Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
694
- [this._extraDefaultOptions].flatten().compact().each(function(defs) {
695
- Object.extend(this.options, defs);
696
- }.bind(this));
697
- },
698
- prepareSubmission: function() {
699
- this._saving = true;
700
- this.removeForm();
701
- this.leaveHover();
702
- this.showSaving();
703
- },
704
- registerListeners: function() {
705
- this._listeners = { };
706
- var listener;
707
- $H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
708
- listener = this[pair.value].bind(this);
709
- this._listeners[pair.key] = listener;
710
- if (!this.options.externalControlOnly)
711
- this.element.observe(pair.key, listener);
712
- if (this.options.externalControl)
713
- this.options.externalControl.observe(pair.key, listener);
714
- }.bind(this));
715
- },
716
- removeForm: function() {
717
- if (!this._form) return;
718
- this._form.remove();
719
- this._form = null;
720
- this._controls = { };
721
- },
722
- showSaving: function() {
723
- this._oldInnerHTML = this.element.innerHTML;
724
- this.element.innerHTML = this.options.savingText;
725
- this.element.addClassName(this.options.savingClassName);
726
- this.element.style.backgroundColor = this._originalBackground;
727
- this.element.show();
728
- },
729
- triggerCallback: function(cbName, arg) {
730
- if ('function' == typeof this.options[cbName]) {
731
- this.options[cbName](this, arg);
732
- }
733
- },
734
- unregisterListeners: function() {
735
- $H(this._listeners).each(function(pair) {
736
- if (!this.options.externalControlOnly)
737
- this.element.stopObserving(pair.key, pair.value);
738
- if (this.options.externalControl)
739
- this.options.externalControl.stopObserving(pair.key, pair.value);
740
- }.bind(this));
741
- },
742
- wrapUp: function(transport) {
743
- this.leaveEditMode();
744
- // Can't use triggerCallback due to backward compatibility: requires
745
- // binding + direct element
746
- this._boundComplete(transport, this.element);
747
- }
748
- });
749
-
750
- Object.extend(Ajax.InPlaceEditor.prototype, {
751
- dispose: Ajax.InPlaceEditor.prototype.destroy
752
- });
753
-
754
- Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
755
- initialize: function($super, element, url, options) {
756
- this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
757
- $super(element, url, options);
758
- },
759
-
760
- createEditField: function() {
761
- var list = document.createElement('select');
762
- list.name = this.options.paramName;
763
- list.size = 1;
764
- this._controls.editor = list;
765
- this._collection = this.options.collection || [];
766
- if (this.options.loadCollectionURL)
767
- this.loadCollection();
768
- else
769
- this.checkForExternalText();
770
- this._form.appendChild(this._controls.editor);
771
- },
772
-
773
- loadCollection: function() {
774
- this._form.addClassName(this.options.loadingClassName);
775
- this.showLoadingText(this.options.loadingCollectionText);
776
- var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
777
- Object.extend(options, {
778
- parameters: 'editorId=' + encodeURIComponent(this.element.id),
779
- onComplete: Prototype.emptyFunction,
780
- onSuccess: function(transport) {
781
- var js = transport.responseText.strip();
782
- if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
783
- throw('Server returned an invalid collection representation.');
784
- this._collection = eval(js);
785
- this.checkForExternalText();
786
- }.bind(this),
787
- onFailure: this.onFailure
788
- });
789
- new Ajax.Request(this.options.loadCollectionURL, options);
790
- },
791
-
792
- showLoadingText: function(text) {
793
- this._controls.editor.disabled = true;
794
- var tempOption = this._controls.editor.firstChild;
795
- if (!tempOption) {
796
- tempOption = document.createElement('option');
797
- tempOption.value = '';
798
- this._controls.editor.appendChild(tempOption);
799
- tempOption.selected = true;
800
- }
801
- tempOption.update((text || '').stripScripts().stripTags());
802
- },
803
-
804
- checkForExternalText: function() {
805
- this._text = this.getText();
806
- if (this.options.loadTextURL)
807
- this.loadExternalText();
808
- else
809
- this.buildOptionList();
810
- },
811
-
812
- loadExternalText: function() {
813
- this.showLoadingText(this.options.loadingText);
814
- var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
815
- Object.extend(options, {
816
- parameters: 'editorId=' + encodeURIComponent(this.element.id),
817
- onComplete: Prototype.emptyFunction,
818
- onSuccess: function(transport) {
819
- this._text = transport.responseText.strip();
820
- this.buildOptionList();
821
- }.bind(this),
822
- onFailure: this.onFailure
823
- });
824
- new Ajax.Request(this.options.loadTextURL, options);
825
- },
826
-
827
- buildOptionList: function() {
828
- this._form.removeClassName(this.options.loadingClassName);
829
- this._collection = this._collection.map(function(entry) {
830
- return 2 === entry.length ? entry : [entry, entry].flatten();
831
- });
832
- var marker = ('value' in this.options) ? this.options.value : this._text;
833
- var textFound = this._collection.any(function(entry) {
834
- return entry[0] == marker;
835
- }.bind(this));
836
- this._controls.editor.update('');
837
- var option;
838
- this._collection.each(function(entry, index) {
839
- option = document.createElement('option');
840
- option.value = entry[0];
841
- option.selected = textFound ? entry[0] == marker : 0 == index;
842
- option.appendChild(document.createTextNode(entry[1]));
843
- this._controls.editor.appendChild(option);
844
- }.bind(this));
845
- this._controls.editor.disabled = false;
846
- Field.scrollFreeActivate(this._controls.editor);
847
- }
848
- });
849
-
850
- //**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
851
- //**** This only exists for a while, in order to let ****
852
- //**** users adapt to the new API. Read up on the new ****
853
- //**** API and convert your code to it ASAP! ****
854
-
855
- Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
856
- if (!options) return;
857
- function fallback(name, expr) {
858
- if (name in options || expr === undefined) return;
859
- options[name] = expr;
860
- };
861
- fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
862
- options.cancelLink == options.cancelButton == false ? false : undefined)));
863
- fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
864
- options.okLink == options.okButton == false ? false : undefined)));
865
- fallback('highlightColor', options.highlightcolor);
866
- fallback('highlightEndColor', options.highlightendcolor);
867
- };
868
-
869
- Object.extend(Ajax.InPlaceEditor, {
870
- DefaultOptions: {
871
- ajaxOptions: { },
872
- autoRows: 3, // Use when multi-line w/ rows == 1
873
- cancelControl: 'link', // 'link'|'button'|false
874
- cancelText: 'cancel',
875
- clickToEditText: 'Click to edit',
876
- externalControl: null, // id|elt
877
- externalControlOnly: false,
878
- fieldPostCreation: 'activate', // 'activate'|'focus'|false
879
- formClassName: 'inplaceeditor-form',
880
- formId: null, // id|elt
881
- highlightColor: '#ffff99',
882
- highlightEndColor: '#ffffff',
883
- hoverClassName: '',
884
- htmlResponse: true,
885
- loadingClassName: 'inplaceeditor-loading',
886
- loadingText: 'Loading...',
887
- okControl: 'button', // 'link'|'button'|false
888
- okText: 'ok',
889
- paramName: 'value',
890
- rows: 1, // If 1 and multi-line, uses autoRows
891
- savingClassName: 'inplaceeditor-saving',
892
- savingText: 'Saving...',
893
- size: 0,
894
- stripLoadedTextTags: false,
895
- submitOnBlur: false,
896
- textAfterControls: '',
897
- textBeforeControls: '',
898
- textBetweenControls: ''
899
- },
900
- DefaultCallbacks: {
901
- callback: function(form) {
902
- return Form.serialize(form);
903
- },
904
- onComplete: function(transport, element) {
905
- // For backward compatibility, this one is bound to the IPE, and passes
906
- // the element directly. It was too often customized, so we don't break it.
907
- new Effect.Highlight(element, {
908
- startcolor: this.options.highlightColor, keepBackgroundImage: true });
909
- },
910
- onEnterEditMode: null,
911
- onEnterHover: function(ipe) {
912
- ipe.element.style.backgroundColor = ipe.options.highlightColor;
913
- if (ipe._effect)
914
- ipe._effect.cancel();
915
- },
916
- onFailure: function(transport, ipe) {
917
- alert('Error communication with the server: ' + transport.responseText.stripTags());
918
- },
919
- onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
920
- onLeaveEditMode: null,
921
- onLeaveHover: function(ipe) {
922
- ipe._effect = new Effect.Highlight(ipe.element, {
923
- startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
924
- restorecolor: ipe._originalBackground, keepBackgroundImage: true
925
- });
926
- }
927
- },
928
- Listeners: {
929
- click: 'enterEditMode',
930
- keydown: 'checkForEscapeOrReturn',
931
- mouseover: 'enterHover',
932
- mouseout: 'leaveHover'
933
- }
934
- });
935
-
936
- Ajax.InPlaceCollectionEditor.DefaultOptions = {
937
- loadingCollectionText: 'Loading options...'
938
- };
939
-
940
- // Delayed observer, like Form.Element.Observer,
941
- // but waits for delay after last key input
942
- // Ideal for live-search fields
943
-
944
- Form.Element.DelayedObserver = Class.create({
945
- initialize: function(element, delay, callback) {
946
- this.delay = delay || 0.5;
947
- this.element = $(element);
948
- this.callback = callback;
949
- this.timer = null;
950
- this.lastValue = $F(this.element);
951
- Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
952
- },
953
- delayedListener: function(event) {
954
- if(this.lastValue == $F(this.element)) return;
955
- if(this.timer) clearTimeout(this.timer);
956
- this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
957
- this.lastValue = $F(this.element);
958
- },
959
- onTimerEvent: function() {
960
- this.timer = null;
961
- this.callback(this.element, $F(this.element));
962
- }
963
- });