actionpack 1.9.1 → 1.10.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (123) hide show
  1. data/CHANGELOG +237 -0
  2. data/README +12 -12
  3. data/lib/action_controller.rb +17 -12
  4. data/lib/action_controller/assertions.rb +119 -67
  5. data/lib/action_controller/base.rb +184 -102
  6. data/lib/action_controller/benchmarking.rb +35 -6
  7. data/lib/action_controller/caching.rb +115 -58
  8. data/lib/action_controller/cgi_ext/cgi_methods.rb +54 -21
  9. data/lib/action_controller/cgi_ext/cookie_performance_fix.rb +39 -35
  10. data/lib/action_controller/cgi_ext/raw_post_data_fix.rb +34 -21
  11. data/lib/action_controller/cgi_process.rb +23 -20
  12. data/lib/action_controller/components.rb +11 -2
  13. data/lib/action_controller/dependencies.rb +0 -5
  14. data/lib/action_controller/deprecated_redirects.rb +17 -0
  15. data/lib/action_controller/filters.rb +13 -9
  16. data/lib/action_controller/flash.rb +7 -7
  17. data/lib/action_controller/helpers.rb +1 -14
  18. data/lib/action_controller/layout.rb +40 -29
  19. data/lib/action_controller/macros/auto_complete.rb +52 -0
  20. data/lib/action_controller/macros/in_place_editing.rb +32 -0
  21. data/lib/action_controller/pagination.rb +44 -28
  22. data/lib/action_controller/request.rb +54 -40
  23. data/lib/action_controller/rescue.rb +8 -6
  24. data/lib/action_controller/routing.rb +77 -28
  25. data/lib/action_controller/scaffolding.rb +10 -14
  26. data/lib/action_controller/session/active_record_store.rb +36 -7
  27. data/lib/action_controller/session_management.rb +126 -0
  28. data/lib/action_controller/streaming.rb +14 -5
  29. data/lib/action_controller/templates/rescues/_request_and_response.rhtml +1 -1
  30. data/lib/action_controller/templates/rescues/_trace.rhtml +24 -0
  31. data/lib/action_controller/templates/rescues/diagnostics.rhtml +2 -13
  32. data/lib/action_controller/templates/rescues/template_error.rhtml +4 -2
  33. data/lib/action_controller/templates/scaffolds/list.rhtml +1 -1
  34. data/lib/action_controller/test_process.rb +35 -17
  35. data/lib/action_controller/upload_progress.rb +52 -0
  36. data/lib/action_controller/url_rewriter.rb +21 -16
  37. data/lib/action_controller/vendor/html-scanner/html/document.rb +2 -2
  38. data/lib/action_controller/vendor/html-scanner/html/node.rb +30 -3
  39. data/lib/action_pack/version.rb +9 -0
  40. data/lib/action_view.rb +1 -1
  41. data/lib/action_view/base.rb +204 -60
  42. data/lib/action_view/compiled_templates.rb +70 -0
  43. data/lib/action_view/helpers/active_record_helper.rb +7 -3
  44. data/lib/action_view/helpers/asset_tag_helper.rb +22 -12
  45. data/lib/action_view/helpers/capture_helper.rb +2 -10
  46. data/lib/action_view/helpers/date_helper.rb +21 -13
  47. data/lib/action_view/helpers/form_helper.rb +14 -10
  48. data/lib/action_view/helpers/form_options_helper.rb +4 -4
  49. data/lib/action_view/helpers/form_tag_helper.rb +59 -25
  50. data/lib/action_view/helpers/java_script_macros_helper.rb +188 -0
  51. data/lib/action_view/helpers/javascript_helper.rb +68 -133
  52. data/lib/action_view/helpers/javascripts/controls.js +427 -165
  53. data/lib/action_view/helpers/javascripts/dragdrop.js +256 -277
  54. data/lib/action_view/helpers/javascripts/effects.js +766 -277
  55. data/lib/action_view/helpers/javascripts/prototype.js +906 -218
  56. data/lib/action_view/helpers/javascripts/slider.js +258 -0
  57. data/lib/action_view/helpers/number_helper.rb +4 -3
  58. data/lib/action_view/helpers/pagination_helper.rb +42 -27
  59. data/lib/action_view/helpers/tag_helper.rb +25 -11
  60. data/lib/action_view/helpers/text_helper.rb +119 -13
  61. data/lib/action_view/helpers/upload_progress_helper.rb +2 -2
  62. data/lib/action_view/helpers/url_helper.rb +68 -21
  63. data/lib/action_view/partials.rb +17 -6
  64. data/lib/action_view/template_error.rb +19 -24
  65. data/rakefile +4 -3
  66. data/test/abstract_unit.rb +2 -1
  67. data/test/controller/action_pack_assertions_test.rb +62 -2
  68. data/test/controller/active_record_assertions_test.rb +5 -6
  69. data/test/controller/active_record_store_test.rb +23 -1
  70. data/test/controller/addresses_render_test.rb +4 -0
  71. data/test/controller/{base_tests.rb → base_test.rb} +4 -3
  72. data/test/controller/benchmark_test.rb +36 -0
  73. data/test/controller/caching_filestore.rb +22 -40
  74. data/test/controller/capture_test.rb +10 -1
  75. data/test/controller/cgi_test.rb +145 -23
  76. data/test/controller/components_test.rb +50 -0
  77. data/test/controller/custom_handler_test.rb +3 -3
  78. data/test/controller/fake_controllers.rb +24 -0
  79. data/test/controller/filters_test.rb +6 -6
  80. data/test/controller/flash_test.rb +6 -6
  81. data/test/controller/fragment_store_setting_test.rb +45 -0
  82. data/test/controller/helper_test.rb +1 -3
  83. data/test/controller/new_render_test.rb +119 -7
  84. data/test/controller/redirect_test.rb +11 -1
  85. data/test/controller/render_test.rb +34 -1
  86. data/test/controller/request_test.rb +14 -5
  87. data/test/controller/routing_test.rb +238 -42
  88. data/test/controller/send_file_test.rb +11 -10
  89. data/test/controller/session_management_test.rb +94 -0
  90. data/test/controller/test_test.rb +194 -5
  91. data/test/controller/url_rewriter_test.rb +46 -0
  92. data/test/fixtures/layouts/talk_from_action.rhtml +2 -0
  93. data/test/fixtures/layouts/yield.rhtml +2 -0
  94. data/test/fixtures/multipart/binary_file +0 -0
  95. data/test/fixtures/multipart/large_text_file +10 -0
  96. data/test/fixtures/multipart/mixed_files +0 -0
  97. data/test/fixtures/multipart/single_parameter +5 -0
  98. data/test/fixtures/multipart/text_file +10 -0
  99. data/test/fixtures/test/_customer_greeting.rhtml +1 -0
  100. data/test/fixtures/test/_hash_object.rhtml +1 -0
  101. data/test/fixtures/test/_person.rhtml +2 -0
  102. data/test/fixtures/test/action_talk_to_layout.rhtml +2 -0
  103. data/test/fixtures/test/content_for.rhtml +2 -0
  104. data/test/fixtures/test/potential_conflicts.rhtml +4 -0
  105. data/test/template/active_record_helper_test.rb +15 -8
  106. data/test/template/asset_tag_helper_test.rb +40 -16
  107. data/test/template/compiled_templates_tests.rb +63 -0
  108. data/test/template/date_helper_test.rb +80 -4
  109. data/test/template/form_helper_test.rb +48 -42
  110. data/test/template/form_options_helper_test.rb +40 -40
  111. data/test/template/form_tag_helper_test.rb +21 -15
  112. data/test/template/java_script_macros_helper_test.rb +56 -0
  113. data/test/template/javascript_helper_test.rb +70 -47
  114. data/test/template/number_helper_test.rb +2 -0
  115. data/test/template/tag_helper_test.rb +9 -0
  116. data/test/template/text_helper_test.rb +146 -1
  117. data/test/template/upload_progress_helper_testx.rb +11 -147
  118. data/test/template/url_helper_test.rb +90 -22
  119. data/test/testing_sandbox.rb +26 -0
  120. metadata +37 -7
  121. data/lib/action_controller/auto_complete.rb +0 -47
  122. data/lib/action_controller/deprecated_renders_and_redirects.rb +0 -76
  123. data/lib/action_controller/session.rb +0 -14
@@ -1,41 +1,12 @@
1
1
  // Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2
2
  // (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
3
+ // (c) 2005 Jon Tirsen (http://www.tirsen.com)
4
+ // Contributors:
5
+ // Richard Livsey
6
+ // Rahul Bhargava
7
+ // Rob Wills
3
8
  //
4
- // Permission is hereby granted, free of charge, to any person obtaining
5
- // a copy of this software and associated documentation files (the
6
- // "Software"), to deal in the Software without restriction, including
7
- // without limitation the rights to use, copy, modify, merge, publish,
8
- // distribute, sublicense, and/or sell copies of the Software, and to
9
- // permit persons to whom the Software is furnished to do so, subject to
10
- // the following conditions:
11
- //
12
- // The above copyright notice and this permission notice shall be
13
- // included in all copies or substantial portions of the Software.
14
- //
15
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
- // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
- // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
- // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
- // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
- // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
- // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
-
23
- Element.collectTextNodesIgnoreClass = function(element, ignoreclass) {
24
- var children = $(element).childNodes;
25
- var text = "";
26
- var classtest = new RegExp("^([^ ]+ )*" + ignoreclass+ "( [^ ]+)*$","i");
27
-
28
- for (var i = 0; i < children.length; i++) {
29
- if(children[i].nodeType==3) {
30
- text+=children[i].nodeValue;
31
- } else {
32
- if((!children[i].className.match(classtest)) && children[i].hasChildNodes())
33
- text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass);
34
- }
35
- }
36
-
37
- return text;
38
- }
9
+ // See scriptaculous.js for full license.
39
10
 
40
11
  // Autocompleter.Base handles all the autocompletion functionality
41
12
  // that's independent of the data source for autocompletion. This
@@ -46,7 +17,7 @@ Element.collectTextNodesIgnoreClass = function(element, ignoreclass) {
46
17
  // a getUpdatedChoices function that will be invoked every time
47
18
  // the text inside the monitored textbox changes. This method
48
19
  // should get the text for which to provide autocompletion by
49
- // invoking this.getEntry(), NOT by directly accessing
20
+ // invoking this.getToken(), NOT by directly accessing
50
21
  // this.element.value. This is to allow incremental tokenized
51
22
  // autocompletion. Specific auto-completion logic (AJAX, etc)
52
23
  // belongs in getUpdatedChoices.
@@ -57,7 +28,7 @@ Element.collectTextNodesIgnoreClass = function(element, ignoreclass) {
57
28
  // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
58
29
  // will incrementally autocomplete with a comma as the token.
59
30
  // Additionally, ',' in the above example can be replaced with
60
- // a token array, e.g. { tokens: new Array (',', '\n') } which
31
+ // a token array, e.g. { tokens: [',', '\n'] } which
61
32
  // enables autocompletion on multiple tokens. This is most
62
33
  // useful when one of the tokens is \n (a newline), as it
63
34
  // allows smart autocompletion after linebreaks.
@@ -65,79 +36,79 @@ Element.collectTextNodesIgnoreClass = function(element, ignoreclass) {
65
36
  var Autocompleter = {}
66
37
  Autocompleter.Base = function() {};
67
38
  Autocompleter.Base.prototype = {
68
- base_initialize: function(element, update, options) {
39
+ baseInitialize: function(element, update, options) {
69
40
  this.element = $(element);
70
41
  this.update = $(update);
71
- this.has_focus = false;
42
+ this.hasFocus = false;
72
43
  this.changed = false;
73
44
  this.active = false;
74
45
  this.index = 0;
75
- this.entry_count = 0;
46
+ this.entryCount = 0;
76
47
 
77
48
  if (this.setOptions)
78
49
  this.setOptions(options);
79
50
  else
80
- this.options = {}
81
-
82
- this.options.tokens = this.options.tokens || new Array();
51
+ this.options = options || {};
52
+
53
+ this.options.paramName = this.options.paramName || this.element.name;
54
+ this.options.tokens = this.options.tokens || [];
83
55
  this.options.frequency = this.options.frequency || 0.4;
84
- this.options.min_chars = this.options.min_chars || 1;
56
+ this.options.minChars = this.options.minChars || 1;
85
57
  this.options.onShow = this.options.onShow ||
86
58
  function(element, update){
87
59
  if(!update.style.position || update.style.position=='absolute') {
88
60
  update.style.position = 'absolute';
89
- var offsets = Position.cumulativeOffset(element);
90
- update.style.left = offsets[0] + 'px';
91
- update.style.top = (offsets[1] + element.offsetHeight) + 'px';
92
- update.style.width = element.offsetWidth + 'px';
61
+ Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
93
62
  }
94
- new Effect.Appear(update,{duration:0.15});
63
+ Effect.Appear(update,{duration:0.15});
95
64
  };
96
65
  this.options.onHide = this.options.onHide ||
97
66
  function(element, update){ new Effect.Fade(update,{duration:0.15}) };
98
-
99
- if(this.options.indicator)
100
- this.indicator = $(this.options.indicator);
101
67
 
102
68
  if (typeof(this.options.tokens) == 'string')
103
69
  this.options.tokens = new Array(this.options.tokens);
104
-
70
+
105
71
  this.observer = null;
106
72
 
73
+ this.element.setAttribute('autocomplete','off');
74
+
107
75
  Element.hide(this.update);
108
-
76
+
109
77
  Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
110
78
  Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
111
79
  },
112
80
 
113
81
  show: function() {
114
- if(this.update.style.display=='none') this.options.onShow(this.element, this.update);
115
- if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0) && this.update.style.position=='absolute') {
82
+ if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
83
+ if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0) && (Element.getStyle(this.update, 'position')=='absolute')) {
116
84
  new Insertion.After(this.update,
117
85
  '<iframe id="' + this.update.id + '_iefix" '+
118
- 'style="display:none;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
86
+ 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
119
87
  'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
120
88
  this.iefix = $(this.update.id+'_iefix');
121
89
  }
122
- if(this.iefix) {
123
- Position.clone(this.update, this.iefix);
124
- this.iefix.style.zIndex = 1;
125
- this.update.style.zIndex = 2;
126
- Element.show(this.iefix);
127
- }
90
+ if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
128
91
  },
129
92
 
93
+ fixIEOverlapping: function() {
94
+ Position.clone(this.update, this.iefix);
95
+ this.iefix.style.zIndex = 1;
96
+ this.update.style.zIndex = 2;
97
+ Element.show(this.iefix);
98
+ },
99
+
130
100
  hide: function() {
131
- if(this.update.style.display=='') this.options.onHide(this.element, this.update);
101
+ this.stopIndicator();
102
+ if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
132
103
  if(this.iefix) Element.hide(this.iefix);
133
104
  },
134
-
105
+
135
106
  startIndicator: function() {
136
- if(this.indicator) Element.show(this.indicator);
107
+ if(this.options.indicator) Element.show(this.options.indicator);
137
108
  },
138
-
109
+
139
110
  stopIndicator: function() {
140
- if(this.indicator) Element.hide(this.indicator);
111
+ if(this.options.indicator) Element.hide(this.options.indicator);
141
112
  },
142
113
 
143
114
  onKeyPress: function(event) {
@@ -145,22 +116,23 @@ Autocompleter.Base.prototype = {
145
116
  switch(event.keyCode) {
146
117
  case Event.KEY_TAB:
147
118
  case Event.KEY_RETURN:
148
- this.select_entry();
119
+ this.selectEntry();
149
120
  Event.stop(event);
150
121
  case Event.KEY_ESC:
151
122
  this.hide();
152
123
  this.active = false;
124
+ Event.stop(event);
153
125
  return;
154
126
  case Event.KEY_LEFT:
155
127
  case Event.KEY_RIGHT:
156
128
  return;
157
129
  case Event.KEY_UP:
158
- this.mark_previous();
130
+ this.markPrevious();
159
131
  this.render();
160
132
  if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
161
133
  return;
162
134
  case Event.KEY_DOWN:
163
- this.mark_next();
135
+ this.markNext();
164
136
  this.render();
165
137
  if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
166
138
  return;
@@ -168,15 +140,15 @@ Autocompleter.Base.prototype = {
168
140
  else
169
141
  if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN)
170
142
  return;
171
-
143
+
172
144
  this.changed = true;
173
- this.has_focus = true;
174
-
145
+ this.hasFocus = true;
146
+
175
147
  if(this.observer) clearTimeout(this.observer);
176
148
  this.observer =
177
149
  setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
178
150
  },
179
-
151
+
180
152
  onHover: function(event) {
181
153
  var element = Event.findElement(event, 'LI');
182
154
  if(this.index != element.autocompleteIndex)
@@ -190,92 +162,97 @@ Autocompleter.Base.prototype = {
190
162
  onClick: function(event) {
191
163
  var element = Event.findElement(event, 'LI');
192
164
  this.index = element.autocompleteIndex;
193
- this.select_entry();
194
- Event.stop(event);
165
+ this.selectEntry();
166
+ this.hide();
195
167
  },
196
168
 
197
169
  onBlur: function(event) {
198
170
  // needed to make click events working
199
171
  setTimeout(this.hide.bind(this), 250);
200
- this.has_focus = false;
172
+ this.hasFocus = false;
201
173
  this.active = false;
202
174
  },
203
175
 
204
176
  render: function() {
205
- if(this.entry_count > 0) {
206
- for (var i = 0; i < this.entry_count; i++)
177
+ if(this.entryCount > 0) {
178
+ for (var i = 0; i < this.entryCount; i++)
207
179
  this.index==i ?
208
- Element.addClassName(this.get_entry(i),"selected") :
209
- Element.removeClassName(this.get_entry(i),"selected");
210
-
211
- if(this.has_focus) {
212
- if(this.get_current_entry().scrollIntoView)
213
- this.get_current_entry().scrollIntoView(false);
180
+ Element.addClassName(this.getEntry(i),"selected") :
181
+ Element.removeClassName(this.getEntry(i),"selected");
214
182
 
183
+ if(this.hasFocus) {
215
184
  this.show();
216
185
  this.active = true;
217
186
  }
218
187
  } else this.hide();
219
188
  },
220
189
 
221
- mark_previous: function() {
190
+ markPrevious: function() {
222
191
  if(this.index > 0) this.index--
223
- else this.index = this.entry_count-1;
192
+ else this.index = this.entryCount-1;
224
193
  },
225
194
 
226
- mark_next: function() {
227
- if(this.index < this.entry_count-1) this.index++
195
+ markNext: function() {
196
+ if(this.index < this.entryCount-1) this.index++
228
197
  else this.index = 0;
229
198
  },
230
199
 
231
- get_entry: function(index) {
200
+ getEntry: function(index) {
232
201
  return this.update.firstChild.childNodes[index];
233
202
  },
234
203
 
235
- get_current_entry: function() {
236
- return this.get_entry(this.index);
204
+ getCurrentEntry: function() {
205
+ return this.getEntry(this.index);
237
206
  },
238
207
 
239
- select_entry: function() {
208
+ selectEntry: function() {
240
209
  this.active = false;
241
- value = Element.collectTextNodesIgnoreClass(this.get_current_entry(), 'informal').unescapeHTML();
242
- this.updateElement(value);
243
- this.element.focus();
210
+ this.updateElement(this.getCurrentEntry());
244
211
  },
245
212
 
246
- updateElement: function(value) {
247
- var last_token_pos = this.findLastToken();
248
- if (last_token_pos != -1) {
249
- var new_value = this.element.value.substr(0, last_token_pos + 1);
250
- var whitespace = this.element.value.substr(last_token_pos + 1).match(/^\s+/);
213
+ updateElement: function(selectedElement) {
214
+ if (this.options.updateElement) {
215
+ this.options.updateElement(selectedElement);
216
+ return;
217
+ }
218
+
219
+ var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
220
+ var lastTokenPos = this.findLastToken();
221
+ if (lastTokenPos != -1) {
222
+ var newValue = this.element.value.substr(0, lastTokenPos + 1);
223
+ var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
251
224
  if (whitespace)
252
- new_value += whitespace[0];
253
- this.element.value = new_value + value;
225
+ newValue += whitespace[0];
226
+ this.element.value = newValue + value;
254
227
  } else {
255
228
  this.element.value = value;
256
- }
229
+ }
230
+ this.element.focus();
231
+
232
+ if (this.options.afterUpdateElement)
233
+ this.options.afterUpdateElement(this.element, selectedElement);
257
234
  },
258
-
235
+
259
236
  updateChoices: function(choices) {
260
- if(!this.changed && this.has_focus) {
237
+ if(!this.changed && this.hasFocus) {
261
238
  this.update.innerHTML = choices;
262
239
  Element.cleanWhitespace(this.update);
263
240
  Element.cleanWhitespace(this.update.firstChild);
264
241
 
265
242
  if(this.update.firstChild && this.update.firstChild.childNodes) {
266
- this.entry_count =
243
+ this.entryCount =
267
244
  this.update.firstChild.childNodes.length;
268
- for (var i = 0; i < this.entry_count; i++) {
269
- entry = this.get_entry(i);
245
+ for (var i = 0; i < this.entryCount; i++) {
246
+ var entry = this.getEntry(i);
270
247
  entry.autocompleteIndex = i;
271
248
  this.addObservers(entry);
272
249
  }
273
250
  } else {
274
- this.entry_count = 0;
251
+ this.entryCount = 0;
275
252
  }
276
-
253
+
277
254
  this.stopIndicator();
278
-
255
+
279
256
  this.index = 0;
280
257
  this.render();
281
258
  }
@@ -288,7 +265,7 @@ Autocompleter.Base.prototype = {
288
265
 
289
266
  onObserverEvent: function() {
290
267
  this.changed = false;
291
- if(this.getEntry().length>=this.options.min_chars) {
268
+ if(this.getToken().length>=this.options.minChars) {
292
269
  this.startIndicator();
293
270
  this.getUpdatedChoices();
294
271
  } else {
@@ -297,58 +274,56 @@ Autocompleter.Base.prototype = {
297
274
  }
298
275
  },
299
276
 
300
- getEntry: function() {
301
- var token_pos = this.findLastToken();
302
- if (token_pos != -1)
303
- var ret = this.element.value.substr(token_pos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
277
+ getToken: function() {
278
+ var tokenPos = this.findLastToken();
279
+ if (tokenPos != -1)
280
+ var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
304
281
  else
305
282
  var ret = this.element.value;
306
-
283
+
307
284
  return /\n/.test(ret) ? '' : ret;
308
285
  },
309
286
 
310
287
  findLastToken: function() {
311
- var last_token_pos = -1;
288
+ var lastTokenPos = -1;
312
289
 
313
290
  for (var i=0; i<this.options.tokens.length; i++) {
314
- var this_token_pos = this.element.value.lastIndexOf(this.options.tokens[i]);
315
- if (this_token_pos > last_token_pos)
316
- last_token_pos = this_token_pos;
291
+ var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
292
+ if (thisTokenPos > lastTokenPos)
293
+ lastTokenPos = thisTokenPos;
317
294
  }
318
- return last_token_pos;
295
+ return lastTokenPos;
319
296
  }
320
297
  }
321
298
 
322
299
  Ajax.Autocompleter = Class.create();
323
- Ajax.Autocompleter.prototype = Object.extend(new Autocompleter.Base(),
324
- Object.extend(new Ajax.Base(), {
300
+ Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
325
301
  initialize: function(element, update, url, options) {
326
- this.base_initialize(element, update, options);
302
+ this.baseInitialize(element, update, options);
327
303
  this.options.asynchronous = true;
328
- this.options.onComplete = this.onComplete.bind(this)
329
- this.options.method = 'post';
304
+ this.options.onComplete = this.onComplete.bind(this);
330
305
  this.options.defaultParams = this.options.parameters || null;
331
306
  this.url = url;
332
307
  },
333
-
308
+
334
309
  getUpdatedChoices: function() {
335
- entry = encodeURIComponent(this.element.name) + '=' +
336
- encodeURIComponent(this.getEntry());
337
-
310
+ entry = encodeURIComponent(this.options.paramName) + '=' +
311
+ encodeURIComponent(this.getToken());
312
+
338
313
  this.options.parameters = this.options.callback ?
339
314
  this.options.callback(this.element, entry) : entry;
340
-
315
+
341
316
  if(this.options.defaultParams)
342
317
  this.options.parameters += '&' + this.options.defaultParams;
343
-
318
+
344
319
  new Ajax.Request(this.url, this.options);
345
320
  },
346
-
321
+
347
322
  onComplete: function(request) {
348
323
  this.updateChoices(request.responseText);
349
324
  }
350
325
 
351
- }));
326
+ });
352
327
 
353
328
  // The local array autocompleter. Used when you'd prefer to
354
329
  // inject an array of autocompletion options into the page, rather
@@ -362,22 +337,22 @@ Object.extend(new Ajax.Base(), {
362
337
  // Extra local autocompletion options:
363
338
  // - choices - How many autocompletion choices to offer
364
339
  //
365
- // - partial_search - If false, the autocompleter will match entered
340
+ // - partialSearch - If false, the autocompleter will match entered
366
341
  // text only at the beginning of strings in the
367
342
  // autocomplete array. Defaults to true, which will
368
343
  // match text at the beginning of any *word* in the
369
344
  // strings in the autocomplete array. If you want to
370
345
  // search anywhere in the string, additionally set
371
- // the option full_search to true (default: off).
346
+ // the option fullSearch to true (default: off).
372
347
  //
373
- // - full_search - Search anywhere in autocomplete array strings.
348
+ // - fullSsearch - Search anywhere in autocomplete array strings.
374
349
  //
375
- // - partial_chars - How many characters to enter before triggering
376
- // a partial match (unlike min_chars, which defines
350
+ // - partialChars - How many characters to enter before triggering
351
+ // a partial match (unlike minChars, which defines
377
352
  // how many characters are required to do any match
378
353
  // at all). Defaults to 2.
379
354
  //
380
- // - ignore_case - Whether to ignore case when autocompleting.
355
+ // - ignoreCase - Whether to ignore case when autocompleting.
381
356
  // Defaults to true.
382
357
  //
383
358
  // It's possible to pass in a custom function as the 'selector'
@@ -388,7 +363,7 @@ Object.extend(new Ajax.Base(), {
388
363
  Autocompleter.Local = Class.create();
389
364
  Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
390
365
  initialize: function(element, update, array, options) {
391
- this.base_initialize(element, update, options);
366
+ this.baseInitialize(element, update, options);
392
367
  this.options.array = array;
393
368
  },
394
369
 
@@ -399,41 +374,42 @@ Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
399
374
  setOptions: function(options) {
400
375
  this.options = Object.extend({
401
376
  choices: 10,
402
- partial_search: true,
403
- partial_chars: 2,
404
- ignore_case: true,
405
- full_search: false,
377
+ partialSearch: true,
378
+ partialChars: 2,
379
+ ignoreCase: true,
380
+ fullSearch: false,
406
381
  selector: function(instance) {
407
- var ret = new Array(); // Beginning matches
408
- var partial = new Array(); // Inside matches
409
- var entry = instance.getEntry();
382
+ var ret = []; // Beginning matches
383
+ var partial = []; // Inside matches
384
+ var entry = instance.getToken();
410
385
  var count = 0;
411
-
386
+
412
387
  for (var i = 0; i < instance.options.array.length &&
413
- ret.length < instance.options.choices ; i++) {
388
+ ret.length < instance.options.choices ; i++) {
389
+
414
390
  var elem = instance.options.array[i];
415
- var found_pos = instance.options.ignore_case ?
391
+ var foundPos = instance.options.ignoreCase ?
416
392
  elem.toLowerCase().indexOf(entry.toLowerCase()) :
417
393
  elem.indexOf(entry);
418
394
 
419
- while (found_pos != -1) {
420
- if (found_pos == 0 && elem.length != entry.length) {
395
+ while (foundPos != -1) {
396
+ if (foundPos == 0 && elem.length != entry.length) {
421
397
  ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
422
398
  elem.substr(entry.length) + "</li>");
423
399
  break;
424
- } else if (entry.length >= instance.options.partial_chars &&
425
- instance.options.partial_search && found_pos != -1) {
426
- if (instance.options.full_search || /\s/.test(elem.substr(found_pos-1,1))) {
427
- partial.push("<li>" + elem.substr(0, found_pos) + "<strong>" +
428
- elem.substr(found_pos, entry.length) + "</strong>" + elem.substr(
429
- found_pos + entry.length) + "</li>");
400
+ } else if (entry.length >= instance.options.partialChars &&
401
+ instance.options.partialSearch && foundPos != -1) {
402
+ if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
403
+ partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
404
+ elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
405
+ foundPos + entry.length) + "</li>");
430
406
  break;
431
407
  }
432
408
  }
433
409
 
434
- found_pos = instance.options.ignore_case ?
435
- elem.toLowerCase().indexOf(entry.toLowerCase(), found_pos + 1) :
436
- elem.indexOf(entry, found_pos + 1);
410
+ foundPos = instance.options.ignoreCase ?
411
+ elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
412
+ elem.indexOf(entry, foundPos + 1);
437
413
 
438
414
  }
439
415
  }
@@ -444,3 +420,289 @@ Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
444
420
  }, options || {});
445
421
  }
446
422
  });
423
+
424
+ // AJAX in-place editor
425
+ //
426
+ // see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
427
+
428
+ Ajax.InPlaceEditor = Class.create();
429
+ Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
430
+ Ajax.InPlaceEditor.prototype = {
431
+ initialize: function(element, url, options) {
432
+ this.url = url;
433
+ this.element = $(element);
434
+
435
+ this.options = Object.extend({
436
+ okText: "ok",
437
+ cancelText: "cancel",
438
+ savingText: "Saving...",
439
+ clickToEditText: "Click to edit",
440
+ okText: "ok",
441
+ rows: 1,
442
+ onComplete: function(transport, element) {
443
+ new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
444
+ },
445
+ onFailure: function(transport) {
446
+ alert("Error communicating with the server: " + transport.responseText.stripTags());
447
+ },
448
+ callback: function(form) {
449
+ return Form.serialize(form);
450
+ },
451
+ handleLineBreaks: true,
452
+ loadingText: 'Loading...',
453
+ savingClassName: 'inplaceeditor-saving',
454
+ loadingClassName: 'inplaceeditor-loading',
455
+ formClassName: 'inplaceeditor-form',
456
+ highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
457
+ highlightendcolor: "#FFFFFF",
458
+ externalControl: null,
459
+ ajaxOptions: {}
460
+ }, options || {});
461
+
462
+ if(!this.options.formId && this.element.id) {
463
+ this.options.formId = this.element.id + "-inplaceeditor";
464
+ if ($(this.options.formId)) {
465
+ // there's already a form with that name, don't specify an id
466
+ this.options.formId = null;
467
+ }
468
+ }
469
+
470
+ if (this.options.externalControl) {
471
+ this.options.externalControl = $(this.options.externalControl);
472
+ }
473
+
474
+ this.originalBackground = Element.getStyle(this.element, 'background-color');
475
+ if (!this.originalBackground) {
476
+ this.originalBackground = "transparent";
477
+ }
478
+
479
+ this.element.title = this.options.clickToEditText;
480
+
481
+ this.onclickListener = this.enterEditMode.bindAsEventListener(this);
482
+ this.mouseoverListener = this.enterHover.bindAsEventListener(this);
483
+ this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
484
+ Event.observe(this.element, 'click', this.onclickListener);
485
+ Event.observe(this.element, 'mouseover', this.mouseoverListener);
486
+ Event.observe(this.element, 'mouseout', this.mouseoutListener);
487
+ if (this.options.externalControl) {
488
+ Event.observe(this.options.externalControl, 'click', this.onclickListener);
489
+ Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
490
+ Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
491
+ }
492
+ },
493
+ enterEditMode: function() {
494
+ if (this.saving) return;
495
+ if (this.editing) return;
496
+ this.editing = true;
497
+ this.onEnterEditMode();
498
+ if (this.options.externalControl) {
499
+ Element.hide(this.options.externalControl);
500
+ }
501
+ Element.hide(this.element);
502
+ this.createForm();
503
+ this.element.parentNode.insertBefore(this.form, this.element);
504
+ Field.focus(this.editField);
505
+ // stop the event to avoid a page refresh in Safari
506
+ if (arguments.length > 1) {
507
+ Event.stop(arguments[0]);
508
+ }
509
+ },
510
+ createForm: function() {
511
+ this.form = document.createElement("form");
512
+ this.form.id = this.options.formId;
513
+ Element.addClassName(this.form, this.options.formClassName)
514
+ this.form.onsubmit = this.onSubmit.bind(this);
515
+
516
+ this.createEditField();
517
+
518
+ if (this.options.textarea) {
519
+ var br = document.createElement("br");
520
+ this.form.appendChild(br);
521
+ }
522
+
523
+ okButton = document.createElement("input");
524
+ okButton.type = "submit";
525
+ okButton.value = this.options.okText;
526
+ this.form.appendChild(okButton);
527
+
528
+ cancelLink = document.createElement("a");
529
+ cancelLink.href = "#";
530
+ cancelLink.appendChild(document.createTextNode(this.options.cancelText));
531
+ cancelLink.onclick = this.onclickCancel.bind(this);
532
+ this.form.appendChild(cancelLink);
533
+ },
534
+ hasHTMLLineBreaks: function(string) {
535
+ if (!this.options.handleLineBreaks) return false;
536
+ return string.match(/<br/i) || string.match(/<p>/i);
537
+ },
538
+ convertHTMLLineBreaks: function(string) {
539
+ return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
540
+ },
541
+ createEditField: function() {
542
+ var text;
543
+ if(this.options.loadTextURL) {
544
+ text = this.options.loadingText;
545
+ } else {
546
+ text = this.getText();
547
+ }
548
+
549
+ if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
550
+ this.options.textarea = false;
551
+ var textField = document.createElement("input");
552
+ textField.type = "text";
553
+ textField.name = "value";
554
+ textField.value = text;
555
+ textField.style.backgroundColor = this.options.highlightcolor;
556
+ var size = this.options.size || this.options.cols || 0;
557
+ if (size != 0) textField.size = size;
558
+ this.editField = textField;
559
+ } else {
560
+ this.options.textarea = true;
561
+ var textArea = document.createElement("textarea");
562
+ textArea.name = "value";
563
+ textArea.value = this.convertHTMLLineBreaks(text);
564
+ textArea.rows = this.options.rows;
565
+ textArea.cols = this.options.cols || 40;
566
+ this.editField = textArea;
567
+ }
568
+
569
+ if(this.options.loadTextURL) {
570
+ this.loadExternalText();
571
+ }
572
+ this.form.appendChild(this.editField);
573
+ },
574
+ getText: function() {
575
+ return this.element.innerHTML;
576
+ },
577
+ loadExternalText: function() {
578
+ Element.addClassName(this.form, this.options.loadingClassName);
579
+ this.editField.disabled = true;
580
+ new Ajax.Request(
581
+ this.options.loadTextURL,
582
+ Object.extend({
583
+ asynchronous: true,
584
+ onComplete: this.onLoadedExternalText.bind(this)
585
+ }, this.options.ajaxOptions)
586
+ );
587
+ },
588
+ onLoadedExternalText: function(transport) {
589
+ Element.removeClassName(this.form, this.options.loadingClassName);
590
+ this.editField.disabled = false;
591
+ this.editField.value = transport.responseText.stripTags();
592
+ },
593
+ onclickCancel: function() {
594
+ this.onComplete();
595
+ this.leaveEditMode();
596
+ return false;
597
+ },
598
+ onFailure: function(transport) {
599
+ this.options.onFailure(transport);
600
+ if (this.oldInnerHTML) {
601
+ this.element.innerHTML = this.oldInnerHTML;
602
+ this.oldInnerHTML = null;
603
+ }
604
+ return false;
605
+ },
606
+ onSubmit: function() {
607
+ // onLoading resets these so we need to save them away for the Ajax call
608
+ var form = this.form;
609
+ var value = this.editField.value;
610
+
611
+ // do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
612
+ // which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
613
+ // to be displayed indefinitely
614
+ this.onLoading();
615
+
616
+ new Ajax.Updater(
617
+ {
618
+ success: this.element,
619
+ // don't update on failure (this could be an option)
620
+ failure: null
621
+ },
622
+ this.url,
623
+ Object.extend({
624
+ parameters: this.options.callback(form, value),
625
+ onComplete: this.onComplete.bind(this),
626
+ onFailure: this.onFailure.bind(this)
627
+ }, this.options.ajaxOptions)
628
+ );
629
+ // stop the event to avoid a page refresh in Safari
630
+ if (arguments.length > 1) {
631
+ Event.stop(arguments[0]);
632
+ }
633
+ return false;
634
+ },
635
+ onLoading: function() {
636
+ this.saving = true;
637
+ this.removeForm();
638
+ this.leaveHover();
639
+ this.showSaving();
640
+ },
641
+ showSaving: function() {
642
+ this.oldInnerHTML = this.element.innerHTML;
643
+ this.element.innerHTML = this.options.savingText;
644
+ Element.addClassName(this.element, this.options.savingClassName);
645
+ this.element.style.backgroundColor = this.originalBackground;
646
+ Element.show(this.element);
647
+ },
648
+ removeForm: function() {
649
+ if(this.form) {
650
+ if (this.form.parentNode) Element.remove(this.form);
651
+ this.form = null;
652
+ }
653
+ },
654
+ enterHover: function() {
655
+ if (this.saving) return;
656
+ this.element.style.backgroundColor = this.options.highlightcolor;
657
+ if (this.effect) {
658
+ this.effect.cancel();
659
+ }
660
+ Element.addClassName(this.element, this.options.hoverClassName)
661
+ },
662
+ leaveHover: function() {
663
+ if (this.options.backgroundColor) {
664
+ this.element.style.backgroundColor = this.oldBackground;
665
+ }
666
+ Element.removeClassName(this.element, this.options.hoverClassName)
667
+ if (this.saving) return;
668
+ this.effect = new Effect.Highlight(this.element, {
669
+ startcolor: this.options.highlightcolor,
670
+ endcolor: this.options.highlightendcolor,
671
+ restorecolor: this.originalBackground
672
+ });
673
+ },
674
+ leaveEditMode: function() {
675
+ Element.removeClassName(this.element, this.options.savingClassName);
676
+ this.removeForm();
677
+ this.leaveHover();
678
+ this.element.style.backgroundColor = this.originalBackground;
679
+ Element.show(this.element);
680
+ if (this.options.externalControl) {
681
+ Element.show(this.options.externalControl);
682
+ }
683
+ this.editing = false;
684
+ this.saving = false;
685
+ this.oldInnerHTML = null;
686
+ this.onLeaveEditMode();
687
+ },
688
+ onComplete: function(transport) {
689
+ this.leaveEditMode();
690
+ this.options.onComplete.bind(this)(transport, this.element);
691
+ },
692
+ onEnterEditMode: function() {},
693
+ onLeaveEditMode: function() {},
694
+ dispose: function() {
695
+ if (this.oldInnerHTML) {
696
+ this.element.innerHTML = this.oldInnerHTML;
697
+ }
698
+ this.leaveEditMode();
699
+ Event.stopObserving(this.element, 'click', this.onclickListener);
700
+ Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
701
+ Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
702
+ if (this.options.externalControl) {
703
+ Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
704
+ Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
705
+ Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
706
+ }
707
+ }
708
+ };