actionpack 1.11.2 → 1.12.0

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 (149) hide show
  1. data/CHANGELOG +392 -5
  2. data/lib/action_controller.rb +8 -4
  3. data/lib/action_controller/assertions.rb +9 -10
  4. data/lib/action_controller/base.rb +177 -88
  5. data/lib/action_controller/benchmarking.rb +5 -5
  6. data/lib/action_controller/caching.rb +44 -36
  7. data/lib/action_controller/cgi_ext/cgi_methods.rb +71 -6
  8. data/lib/action_controller/cgi_ext/cookie_performance_fix.rb +1 -1
  9. data/lib/action_controller/cgi_process.rb +36 -24
  10. data/lib/action_controller/components.rb +152 -52
  11. data/lib/action_controller/dependencies.rb +1 -1
  12. data/lib/action_controller/deprecated_redirects.rb +2 -2
  13. data/lib/action_controller/deprecated_request_methods.rb +34 -0
  14. data/lib/action_controller/filters.rb +59 -19
  15. data/lib/action_controller/flash.rb +53 -47
  16. data/lib/action_controller/helpers.rb +2 -2
  17. data/lib/action_controller/integration.rb +524 -0
  18. data/lib/action_controller/layout.rb +58 -23
  19. data/lib/action_controller/mime_responds.rb +163 -0
  20. data/lib/action_controller/mime_type.rb +142 -0
  21. data/lib/action_controller/pagination.rb +13 -7
  22. data/lib/action_controller/request.rb +59 -56
  23. data/lib/action_controller/rescue.rb +1 -1
  24. data/lib/action_controller/routing.rb +29 -10
  25. data/lib/action_controller/scaffolding.rb +8 -0
  26. data/lib/action_controller/session/active_record_store.rb +21 -10
  27. data/lib/action_controller/session/mem_cache_store.rb +18 -12
  28. data/lib/action_controller/session_management.rb +30 -11
  29. data/lib/action_controller/templates/rescues/_trace.rhtml +1 -1
  30. data/lib/action_controller/templates/scaffolds/layout.rhtml +4 -4
  31. data/lib/action_controller/templates/scaffolds/list.rhtml +1 -1
  32. data/lib/action_controller/test_process.rb +189 -118
  33. data/lib/action_controller/vendor/html-scanner/html/node.rb +20 -1
  34. data/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +3 -0
  35. data/lib/action_controller/vendor/html-scanner/html/version.rb +1 -1
  36. data/lib/action_controller/vendor/xml_node.rb +97 -0
  37. data/lib/action_controller/verification.rb +2 -0
  38. data/lib/action_pack/version.rb +3 -3
  39. data/lib/action_view.rb +0 -2
  40. data/lib/action_view/base.rb +109 -36
  41. data/lib/action_view/compiled_templates.rb +1 -1
  42. data/lib/action_view/helpers/active_record_helper.rb +4 -2
  43. data/lib/action_view/helpers/asset_tag_helper.rb +6 -7
  44. data/lib/action_view/helpers/capture_helper.rb +49 -12
  45. data/lib/action_view/helpers/date_helper.rb +14 -4
  46. data/lib/action_view/helpers/form_helper.rb +136 -20
  47. data/lib/action_view/helpers/form_options_helper.rb +29 -7
  48. data/lib/action_view/helpers/form_tag_helper.rb +22 -20
  49. data/lib/action_view/helpers/java_script_macros_helper.rb +29 -9
  50. data/lib/action_view/helpers/javascript_helper.rb +50 -446
  51. data/lib/action_view/helpers/javascripts/controls.js +95 -30
  52. data/lib/action_view/helpers/javascripts/dragdrop.js +161 -21
  53. data/lib/action_view/helpers/javascripts/effects.js +310 -211
  54. data/lib/action_view/helpers/javascripts/prototype.js +228 -28
  55. data/lib/action_view/helpers/number_helper.rb +9 -9
  56. data/lib/action_view/helpers/pagination_helper.rb +1 -1
  57. data/lib/action_view/helpers/prototype_helper.rb +900 -0
  58. data/lib/action_view/helpers/scriptaculous_helper.rb +135 -0
  59. data/lib/action_view/helpers/text_helper.rb +7 -6
  60. data/lib/action_view/helpers/url_helper.rb +23 -14
  61. data/lib/action_view/partials.rb +12 -4
  62. data/rakefile +13 -5
  63. data/test/abstract_unit.rb +4 -3
  64. data/test/active_record_unit.rb +88 -0
  65. data/test/{controller → activerecord}/active_record_assertions_test.rb +7 -50
  66. data/test/{controller → activerecord}/active_record_store_test.rb +27 -4
  67. data/test/activerecord/pagination_test.rb +161 -0
  68. data/test/controller/action_pack_assertions_test.rb +18 -15
  69. data/test/controller/base_test.rb +31 -42
  70. data/test/controller/benchmark_test.rb +8 -11
  71. data/test/controller/capture_test.rb +33 -1
  72. data/test/controller/cgi_test.rb +33 -0
  73. data/test/controller/custom_handler_test.rb +8 -0
  74. data/test/controller/fake_controllers.rb +9 -17
  75. data/test/controller/filters_test.rb +32 -3
  76. data/test/controller/flash_test.rb +26 -41
  77. data/test/controller/fragment_store_setting_test.rb +1 -1
  78. data/test/controller/layout_test.rb +73 -0
  79. data/test/controller/mime_responds_test.rb +257 -0
  80. data/test/controller/mime_type_test.rb +24 -0
  81. data/test/controller/new_render_test.rb +157 -1
  82. data/test/controller/redirect_test.rb +23 -0
  83. data/test/controller/render_test.rb +54 -56
  84. data/test/controller/request_test.rb +25 -0
  85. data/test/controller/routing_test.rb +74 -66
  86. data/test/controller/test_test.rb +66 -1
  87. data/test/controller/verification_test.rb +3 -1
  88. data/test/controller/webservice_test.rb +255 -0
  89. data/test/fixtures/companies.yml +24 -0
  90. data/test/fixtures/company.rb +9 -0
  91. data/test/fixtures/db_definitions/sqlite.sql +42 -0
  92. data/test/fixtures/developer.rb +7 -0
  93. data/test/fixtures/developers.yml +21 -0
  94. data/test/fixtures/developers_projects.yml +13 -0
  95. data/test/fixtures/layout_tests/layouts/controller_name_space/nested.rhtml +1 -0
  96. data/test/fixtures/layout_tests/layouts/item.rhtml +1 -0
  97. data/test/fixtures/layout_tests/layouts/layout_test.rhtml +1 -0
  98. data/test/fixtures/layout_tests/layouts/third_party_template_library.mab +1 -0
  99. data/test/fixtures/layout_tests/views/hello.rhtml +1 -0
  100. data/test/fixtures/multipart/mona_lisa.jpg +0 -0
  101. data/test/fixtures/project.rb +3 -0
  102. data/test/fixtures/projects.yml +7 -0
  103. data/test/fixtures/replies.yml +13 -0
  104. data/test/fixtures/reply.rb +5 -0
  105. data/test/fixtures/respond_to/all_types_with_layout.rhtml +1 -0
  106. data/test/fixtures/respond_to/all_types_with_layout.rjs +1 -0
  107. data/test/fixtures/respond_to/layouts/standard.rhtml +1 -0
  108. data/test/fixtures/respond_to/using_defaults.rhtml +1 -0
  109. data/test/fixtures/respond_to/using_defaults.rjs +1 -0
  110. data/test/fixtures/respond_to/using_defaults.rxml +1 -0
  111. data/test/fixtures/respond_to/using_defaults_with_type_list.rhtml +1 -0
  112. data/test/fixtures/respond_to/using_defaults_with_type_list.rjs +1 -0
  113. data/test/fixtures/respond_to/using_defaults_with_type_list.rxml +1 -0
  114. data/test/fixtures/test/block_content_for.rhtml +2 -0
  115. data/test/fixtures/test/delete_with_js.rjs +2 -0
  116. data/test/fixtures/test/dot.directory/render_file_with_ivar.rhtml +1 -0
  117. data/test/fixtures/test/enum_rjs_test.rjs +6 -0
  118. data/test/fixtures/test/erb_content_for.rhtml +2 -0
  119. data/test/fixtures/test/hello_world.rxml +3 -0
  120. data/test/fixtures/test/hello_world_with_layout_false.rhtml +1 -0
  121. data/test/fixtures/test/non_erb_block_content_for.rxml +4 -0
  122. data/test/fixtures/topic.rb +3 -0
  123. data/test/fixtures/topics.yml +22 -0
  124. data/test/template/active_record_helper_test.rb +4 -0
  125. data/test/template/asset_tag_helper_test.rb +7 -2
  126. data/test/template/date_helper_test.rb +39 -2
  127. data/test/template/form_helper_test.rb +238 -5
  128. data/test/template/form_options_helper_test.rb +78 -0
  129. data/test/template/form_tag_helper_test.rb +11 -0
  130. data/test/template/java_script_macros_helper_test.rb +51 -6
  131. data/test/template/javascript_helper_test.rb +7 -153
  132. data/test/template/number_helper_test.rb +14 -13
  133. data/test/template/prototype_helper_test.rb +423 -0
  134. data/test/template/scriptaculous_helper_test.rb +90 -0
  135. data/test/template/text_helper_test.rb +12 -9
  136. data/test/template/url_helper_test.rb +31 -15
  137. metadata +291 -246
  138. data/lib/action_controller/cgi_ext/multipart_progress.rb +0 -169
  139. data/lib/action_controller/upload_progress.rb +0 -473
  140. data/lib/action_controller/vendor/html-scanner/html/node.rb.rej +0 -17
  141. data/lib/action_view/helpers/upload_progress_helper.rb +0 -433
  142. data/lib/action_view/vendor/builder.rb +0 -13
  143. data/lib/action_view/vendor/builder/blankslate.rb +0 -53
  144. data/lib/action_view/vendor/builder/xmlbase.rb +0 -143
  145. data/lib/action_view/vendor/builder/xmlevents.rb +0 -63
  146. data/lib/action_view/vendor/builder/xmlmarkup.rb +0 -308
  147. data/test/controller/multipart_progress_testx.rb +0 -365
  148. data/test/controller/upload_progress_testx.rb +0 -89
  149. data/test/template/upload_progress_helper_testx.rb +0 -136
@@ -1,17 +1,13 @@
1
- /* Prototype JavaScript framework, version 1.4.0
1
+ /* Prototype JavaScript framework, version 1.5.0_pre1
2
2
  * (c) 2005 Sam Stephenson <sam@conio.net>
3
3
  *
4
- * THIS FILE IS AUTOMATICALLY GENERATED. When sending patches, please diff
5
- * against the source tree, available from the Prototype darcs repository.
6
- *
7
4
  * Prototype is freely distributable under the terms of an MIT-style license.
8
- *
9
5
  * For details, see the Prototype web site: http://prototype.conio.net/
10
6
  *
11
7
  /*--------------------------------------------------------------------------*/
12
8
 
13
9
  var Prototype = {
14
- Version: '1.4.0',
10
+ Version: '1.5.0_pre1',
15
11
  ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)',
16
12
 
17
13
  emptyFunction: function() {},
@@ -120,26 +116,49 @@ PeriodicalExecuter.prototype = {
120
116
  }
121
117
  }
122
118
  }
119
+ Object.extend(String.prototype, {
120
+ gsub: function(pattern, replacement) {
121
+ var result = '', source = this, match;
122
+ replacement = arguments.callee.prepareReplacement(replacement);
123
+
124
+ while (source.length > 0) {
125
+ if (match = source.match(pattern)) {
126
+ result += source.slice(0, match.index);
127
+ result += (replacement(match) || '').toString();
128
+ source = source.slice(match.index + match[0].length);
129
+ } else {
130
+ result += source, source = '';
131
+ }
132
+ }
133
+ return result;
134
+ },
123
135
 
124
- /*--------------------------------------------------------------------------*/
136
+ sub: function(pattern, replacement, count) {
137
+ replacement = this.gsub.prepareReplacement(replacement);
138
+ count = count === undefined ? 1 : count;
125
139
 
126
- function $() {
127
- var elements = new Array();
140
+ return this.gsub(pattern, function(match) {
141
+ if (--count < 0) return match[0];
142
+ return replacement(match);
143
+ });
144
+ },
128
145
 
129
- for (var i = 0; i < arguments.length; i++) {
130
- var element = arguments[i];
131
- if (typeof element == 'string')
132
- element = document.getElementById(element);
146
+ scan: function(pattern, iterator) {
147
+ this.gsub(pattern, iterator);
148
+ return this;
149
+ },
133
150
 
134
- if (arguments.length == 1)
135
- return element;
151
+ truncate: function(length, truncation) {
152
+ length = length || 30;
153
+ truncation = truncation === undefined ? '...' : truncation;
154
+ return this.length > length ?
155
+ this.slice(0, length - truncation.length) + truncation : this;
156
+ },
136
157
 
137
- elements.push(element);
138
- }
158
+ strip: function() {
159
+ return this.replace(/^\s+/, '').replace(/\s+$/, '');
160
+ },
139
161
 
140
- return elements;
141
- }
142
- Object.extend(String.prototype, {
143
162
  stripTags: function() {
144
163
  return this.replace(/<\/?[^>]+>/gi, '');
145
164
  },
@@ -203,12 +222,35 @@ Object.extend(String.prototype, {
203
222
  },
204
223
 
205
224
  inspect: function() {
206
- return "'" + this.replace('\\', '\\\\').replace("'", '\\\'') + "'";
225
+ return "'" + this.replace(/\\/g, '\\\\').replace(/'/g, '\\\'') + "'";
207
226
  }
208
227
  });
209
228
 
229
+ String.prototype.gsub.prepareReplacement = function(replacement) {
230
+ if (typeof replacement == 'function') return replacement;
231
+ var template = new Template(replacement);
232
+ return function(match) { return template.evaluate(match) };
233
+ }
234
+
210
235
  String.prototype.parseQuery = String.prototype.toQueryParams;
211
236
 
237
+ var Template = Class.create();
238
+ Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
239
+ Template.prototype = {
240
+ initialize: function(template, pattern) {
241
+ this.template = template.toString();
242
+ this.pattern = pattern || Template.Pattern;
243
+ },
244
+
245
+ evaluate: function(object) {
246
+ return this.template.gsub(this.pattern, function(match) {
247
+ var before = match[1];
248
+ if (before == '\\') return match[2];
249
+ return before + (object[match[3]] || '').toString();
250
+ });
251
+ }
252
+ }
253
+
212
254
  var $break = new Object();
213
255
  var $continue = new Object();
214
256
 
@@ -375,8 +417,7 @@ var Enumerable = {
375
417
 
376
418
  var collections = [this].concat(args).map($A);
377
419
  return this.map(function(value, index) {
378
- iterator(value = collections.pluck(index));
379
- return value;
420
+ return iterator(collections.pluck(index));
380
421
  });
381
422
  },
382
423
 
@@ -662,7 +703,8 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
662
703
  setRequestHeaders: function() {
663
704
  var requestHeaders =
664
705
  ['X-Requested-With', 'XMLHttpRequest',
665
- 'X-Prototype-Version', Prototype.Version];
706
+ 'X-Prototype-Version', Prototype.Version,
707
+ 'Accept', 'text/javascript, text/html, application/xml, text/xml, */*'];
666
708
 
667
709
  if (this.options.method == 'post') {
668
710
  requestHeaders.push('Content-type',
@@ -831,22 +873,48 @@ Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
831
873
  this.updater = new Ajax.Updater(this.container, this.url, this.options);
832
874
  }
833
875
  });
876
+ function $() {
877
+ var results = [], element;
878
+ for (var i = 0; i < arguments.length; i++) {
879
+ element = arguments[i];
880
+ if (typeof element == 'string')
881
+ element = document.getElementById(element);
882
+ results.push(Element.extend(element));
883
+ }
884
+ return results.length < 2 ? results[0] : results;
885
+ }
886
+
834
887
  document.getElementsByClassName = function(className, parentElement) {
835
888
  var children = ($(parentElement) || document.body).getElementsByTagName('*');
836
889
  return $A(children).inject([], function(elements, child) {
837
890
  if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
838
- elements.push(child);
891
+ elements.push(Element.extend(child));
839
892
  return elements;
840
893
  });
841
894
  }
842
895
 
843
896
  /*--------------------------------------------------------------------------*/
844
897
 
845
- if (!window.Element) {
898
+ if (!window.Element)
846
899
  var Element = new Object();
900
+
901
+ Element.extend = function(element) {
902
+ if (!element) return;
903
+
904
+ if (!element._extended && element.tagName && element != window) {
905
+ var methods = Element.Methods;
906
+ for (property in methods) {
907
+ var value = methods[property];
908
+ if (typeof value == 'function')
909
+ element[property] = value.bind(null, element);
910
+ }
911
+ }
912
+
913
+ element._extended = true;
914
+ return element;
847
915
  }
848
916
 
849
- Object.extend(Element, {
917
+ Element.Methods = {
850
918
  visible: function(element) {
851
919
  return $(element).style.display != 'none';
852
920
  },
@@ -882,6 +950,19 @@ Object.extend(Element, {
882
950
  setTimeout(function() {html.evalScripts()}, 10);
883
951
  },
884
952
 
953
+ replace: function(element, html) {
954
+ element = $(element);
955
+ if (element.outerHTML) {
956
+ element.outerHTML = html.stripScripts();
957
+ } else {
958
+ var range = element.ownerDocument.createRange();
959
+ range.selectNodeContents(element);
960
+ element.parentNode.replaceChild(
961
+ range.createContextualFragment(html.stripScripts()), element);
962
+ }
963
+ setTimeout(function() {html.evalScripts()}, 10);
964
+ },
965
+
885
966
  getHeight: function(element) {
886
967
  element = $(element);
887
968
  return element.offsetHeight;
@@ -920,6 +1001,13 @@ Object.extend(Element, {
920
1001
  return $(element).innerHTML.match(/^\s*$/);
921
1002
  },
922
1003
 
1004
+ childOf: function(element, ancestor) {
1005
+ element = $(element), ancestor = $(ancestor);
1006
+ while (element = element.parentNode)
1007
+ if (element == ancestor) return true;
1008
+ return false;
1009
+ },
1010
+
923
1011
  scrollTo: function(element) {
924
1012
  element = $(element);
925
1013
  var x = element.x ? element.x : element.offsetLeft,
@@ -1013,7 +1101,9 @@ Object.extend(Element, {
1013
1101
  element.style.overflow = element._overflow;
1014
1102
  element._overflow = undefined;
1015
1103
  }
1016
- });
1104
+ }
1105
+
1106
+ Object.extend(Element, Element.Methods);
1017
1107
 
1018
1108
  var Toggle = new Object();
1019
1109
  Toggle.display = Element.toggle;
@@ -1148,6 +1238,116 @@ Element.ClassNames.prototype = {
1148
1238
  }
1149
1239
 
1150
1240
  Object.extend(Element.ClassNames.prototype, Enumerable);
1241
+ var Selector = Class.create();
1242
+ Selector.prototype = {
1243
+ initialize: function(expression) {
1244
+ this.params = {classNames: []};
1245
+ this.expression = expression.toString().strip();
1246
+ this.parseExpression();
1247
+ this.compileMatcher();
1248
+ },
1249
+
1250
+ parseExpression: function() {
1251
+ function abort(message) { throw 'Parse error in selector: ' + message; }
1252
+
1253
+ if (this.expression == '') abort('empty expression');
1254
+
1255
+ var params = this.params, expr = this.expression, match, modifier, clause, rest;
1256
+ while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) {
1257
+ params.attributes = params.attributes || [];
1258
+ params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''});
1259
+ expr = match[1];
1260
+ }
1261
+
1262
+ if (expr == '*') return this.params.wildcard = true;
1263
+
1264
+ while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) {
1265
+ modifier = match[1], clause = match[2], rest = match[3];
1266
+ switch (modifier) {
1267
+ case '#': params.id = clause; break;
1268
+ case '.': params.classNames.push(clause); break;
1269
+ case '':
1270
+ case undefined: params.tagName = clause.toUpperCase(); break;
1271
+ default: abort(expr.inspect());
1272
+ }
1273
+ expr = rest;
1274
+ }
1275
+
1276
+ if (expr.length > 0) abort(expr.inspect());
1277
+ },
1278
+
1279
+ buildMatchExpression: function() {
1280
+ var params = this.params, conditions = [], clause;
1281
+
1282
+ if (params.wildcard)
1283
+ conditions.push('true');
1284
+ if (clause = params.id)
1285
+ conditions.push('element.id == ' + clause.inspect());
1286
+ if (clause = params.tagName)
1287
+ conditions.push('element.tagName.toUpperCase() == ' + clause.inspect());
1288
+ if ((clause = params.classNames).length > 0)
1289
+ for (var i = 0; i < clause.length; i++)
1290
+ conditions.push('Element.hasClassName(element, ' + clause[i].inspect() + ')');
1291
+ if (clause = params.attributes) {
1292
+ clause.each(function(attribute) {
1293
+ var value = 'element.getAttribute(' + attribute.name.inspect() + ')';
1294
+ var splitValueBy = function(delimiter) {
1295
+ return value + ' && ' + value + '.split(' + delimiter.inspect() + ')';
1296
+ }
1297
+
1298
+ switch (attribute.operator) {
1299
+ case '=': conditions.push(value + ' == ' + attribute.value.inspect()); break;
1300
+ case '~=': conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break;
1301
+ case '|=': conditions.push(
1302
+ splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect()
1303
+ ); break;
1304
+ case '!=': conditions.push(value + ' != ' + attribute.value.inspect()); break;
1305
+ case '':
1306
+ case undefined: conditions.push(value + ' != null'); break;
1307
+ default: throw 'Unknown operator ' + attribute.operator + ' in selector';
1308
+ }
1309
+ });
1310
+ }
1311
+
1312
+ return conditions.join(' && ');
1313
+ },
1314
+
1315
+ compileMatcher: function() {
1316
+ this.match = new Function('element', 'if (!element.tagName) return false; \
1317
+ return ' + this.buildMatchExpression());
1318
+ },
1319
+
1320
+ findElements: function(scope) {
1321
+ var element;
1322
+
1323
+ if (element = $(this.params.id))
1324
+ if (this.match(element))
1325
+ if (!scope || Element.childOf(element, scope))
1326
+ return [element];
1327
+
1328
+ scope = (scope || document).getElementsByTagName(this.params.tagName || '*');
1329
+
1330
+ var results = [];
1331
+ for (var i = 0; i < scope.length; i++)
1332
+ if (this.match(element = scope[i]))
1333
+ results.push(Element.extend(element));
1334
+
1335
+ return results;
1336
+ },
1337
+
1338
+ toString: function() {
1339
+ return this.expression;
1340
+ }
1341
+ }
1342
+
1343
+ function $$() {
1344
+ return $A(arguments).map(function(expression) {
1345
+ return expression.strip().split(/\s+/).inject([null], function(results, expr) {
1346
+ var selector = new Selector(expr);
1347
+ return results.map(selector.findElements.bind(selector)).flatten();
1348
+ });
1349
+ }).flatten();
1350
+ }
1151
1351
  var Field = {
1152
1352
  clear: function() {
1153
1353
  for (var i = 0; i < arguments.length; i++)
@@ -85,15 +85,15 @@ module ActionView
85
85
  # human_size(1234567) => 1.2 MB
86
86
  # human_size(1234567890) => 1.1 GB
87
87
  def number_to_human_size(size)
88
- begin
89
- return "%d Bytes" % size if size < 1.kilobytes
90
- return "%.1f KB" % (size/1.0.kilobytes) if size < 1.megabytes
91
- return "%.1f MB" % (size/1.0.megabytes) if size < 1.gigabytes
92
- return "%.1f GB" % (size/1.0.gigabytes) if size < 1.terabytes
93
- return "%.1f TB" % (size/1.0.terabytes)
94
- rescue
95
- # just return nothing
96
- end
88
+ case
89
+ when size < 1.kilobyte: '%d Bytes' % size
90
+ when size < 1.megabyte: '%.1f KB' % (size / 1.0.kilobyte)
91
+ when size < 1.gigabyte: '%.1f MB' % (size / 1.0.megabyte)
92
+ when size < 1.terabyte: '%.1f GB' % (size / 1.0.gigabyte)
93
+ else '%.1f TB' % (size / 1.0.terabyte)
94
+ end.sub('.0', '')
95
+ rescue
96
+ nil
97
97
  end
98
98
 
99
99
  alias_method :human_size, :number_to_human_size # deprecated alias
@@ -6,7 +6,7 @@ module ActionView
6
6
  #
7
7
  # <%= link_to "Previous page", { :page => paginator.current.previous } if paginator.current.previous %>
8
8
  #
9
- # <%= link_to "Next page", { :page => paginator.current.next } if paginator.current.next =%>
9
+ # <%= link_to "Next page", { :page => paginator.current.next } if paginator.current.next %>
10
10
  module PaginationHelper
11
11
  unless const_defined?(:DEFAULT_OPTIONS)
12
12
  DEFAULT_OPTIONS = {
@@ -0,0 +1,900 @@
1
+ require File.dirname(__FILE__) + '/javascript_helper'
2
+ require 'set'
3
+
4
+ module ActionView
5
+ module Helpers
6
+ # Provides a set of helpers for calling Prototype JavaScript functions,
7
+ # including functionality to call remote methods using
8
+ # Ajax[http://www.adaptivepath.com/publications/essays/archives/000385.php].
9
+ # This means that you can call actions in your controllers without
10
+ # reloading the page, but still update certain parts of it using
11
+ # injections into the DOM. The common use case is having a form that adds
12
+ # a new element to a list without reloading the page.
13
+ #
14
+ # To be able to use these helpers, you must include the Prototype
15
+ # JavaScript framework in your pages. See the documentation for
16
+ # ActionView::Helpers::JavaScriptHelper for more information on including
17
+ # the necessary JavaScript.
18
+ #
19
+ # See link_to_remote for documentation of options common to all Ajax
20
+ # helpers.
21
+ #
22
+ # See also ActionView::Helpers::ScriptaculousHelper for helpers which work
23
+ # with the Scriptaculous controls and visual effects library.
24
+ #
25
+ # See JavaScriptGenerator for information on updating multiple elements
26
+ # on the page in an Ajax response.
27
+ module PrototypeHelper
28
+ unless const_defined? :CALLBACKS
29
+ CALLBACKS = Set.new([ :uninitialized, :loading, :loaded,
30
+ :interactive, :complete, :failure, :success ] +
31
+ (100..599).to_a)
32
+ AJAX_OPTIONS = Set.new([ :before, :after, :condition, :url,
33
+ :asynchronous, :method, :insertion, :position,
34
+ :form, :with, :update, :script ]).merge(CALLBACKS)
35
+ end
36
+
37
+ # Returns a link to a remote action defined by <tt>options[:url]</tt>
38
+ # (using the url_for format) that's called in the background using
39
+ # XMLHttpRequest. The result of that request can then be inserted into a
40
+ # DOM object whose id can be specified with <tt>options[:update]</tt>.
41
+ # Usually, the result would be a partial prepared by the controller with
42
+ # either render_partial or render_partial_collection.
43
+ #
44
+ # Examples:
45
+ # link_to_remote "Delete this post", :update => "posts",
46
+ # :url => { :action => "destroy", :id => post.id }
47
+ # link_to_remote(image_tag("refresh"), :update => "emails",
48
+ # :url => { :action => "list_emails" })
49
+ #
50
+ # You can also specify a hash for <tt>options[:update]</tt> to allow for
51
+ # easy redirection of output to an other DOM element if a server-side
52
+ # error occurs:
53
+ #
54
+ # Example:
55
+ # link_to_remote "Delete this post",
56
+ # :url => { :action => "destroy", :id => post.id },
57
+ # :update => { :success => "posts", :failure => "error" }
58
+ #
59
+ # Optionally, you can use the <tt>options[:position]</tt> parameter to
60
+ # influence how the target DOM element is updated. It must be one of
61
+ # <tt>:before</tt>, <tt>:top</tt>, <tt>:bottom</tt>, or <tt>:after</tt>.
62
+ #
63
+ # By default, these remote requests are processed asynchronous during
64
+ # which various JavaScript callbacks can be triggered (for progress
65
+ # indicators and the likes). All callbacks get access to the
66
+ # <tt>request</tt> object, which holds the underlying XMLHttpRequest.
67
+ #
68
+ # To access the server response, use <tt>request.responseText</tt>, to
69
+ # find out the HTTP status, use <tt>request.status</tt>.
70
+ #
71
+ # Example:
72
+ # link_to_remote word,
73
+ # :url => { :action => "undo", :n => word_counter },
74
+ # :complete => "undoRequestCompleted(request)"
75
+ #
76
+ # The callbacks that may be specified are (in order):
77
+ #
78
+ # <tt>:loading</tt>:: Called when the remote document is being
79
+ # loaded with data by the browser.
80
+ # <tt>:loaded</tt>:: Called when the browser has finished loading
81
+ # the remote document.
82
+ # <tt>:interactive</tt>:: Called when the user can interact with the
83
+ # remote document, even though it has not
84
+ # finished loading.
85
+ # <tt>:success</tt>:: Called when the XMLHttpRequest is completed,
86
+ # and the HTTP status code is in the 2XX range.
87
+ # <tt>:failure</tt>:: Called when the XMLHttpRequest is completed,
88
+ # and the HTTP status code is not in the 2XX
89
+ # range.
90
+ # <tt>:complete</tt>:: Called when the XMLHttpRequest is complete
91
+ # (fires after success/failure if they are
92
+ # present).
93
+ #
94
+ # You can further refine <tt>:success</tt> and <tt>:failure</tt> by
95
+ # adding additional callbacks for specific status codes.
96
+ #
97
+ # Example:
98
+ # link_to_remote word,
99
+ # :url => { :action => "action" },
100
+ # 404 => "alert('Not found...? Wrong URL...?')",
101
+ # :failure => "alert('HTTP Error ' + request.status + '!')"
102
+ #
103
+ # A status code callback overrides the success/failure handlers if
104
+ # present.
105
+ #
106
+ # If you for some reason or another need synchronous processing (that'll
107
+ # block the browser while the request is happening), you can specify
108
+ # <tt>options[:type] = :synchronous</tt>.
109
+ #
110
+ # You can customize further browser side call logic by passing in
111
+ # JavaScript code snippets via some optional parameters. In their order
112
+ # of use these are:
113
+ #
114
+ # <tt>:confirm</tt>:: Adds confirmation dialog.
115
+ # <tt>:condition</tt>:: Perform remote request conditionally
116
+ # by this expression. Use this to
117
+ # describe browser-side conditions when
118
+ # request should not be initiated.
119
+ # <tt>:before</tt>:: Called before request is initiated.
120
+ # <tt>:after</tt>:: Called immediately after request was
121
+ # initiated and before <tt>:loading</tt>.
122
+ # <tt>:submit</tt>:: Specifies the DOM element ID that's used
123
+ # as the parent of the form elements. By
124
+ # default this is the current form, but
125
+ # it could just as well be the ID of a
126
+ # table row or any other DOM element.
127
+ def link_to_remote(name, options = {}, html_options = {})
128
+ link_to_function(name, remote_function(options), html_options)
129
+ end
130
+
131
+ # Periodically calls the specified url (<tt>options[:url]</tt>) every
132
+ # <tt>options[:frequency]</tt> seconds (default is 10). Usually used to
133
+ # update a specified div (<tt>options[:update]</tt>) with the results
134
+ # of the remote call. The options for specifying the target with :url
135
+ # and defining callbacks is the same as link_to_remote.
136
+ def periodically_call_remote(options = {})
137
+ frequency = options[:frequency] || 10 # every ten seconds by default
138
+ code = "new PeriodicalExecuter(function() {#{remote_function(options)}}, #{frequency})"
139
+ javascript_tag(code)
140
+ end
141
+
142
+ # Returns a form tag that will submit using XMLHttpRequest in the
143
+ # background instead of the regular reloading POST arrangement. Even
144
+ # though it's using JavaScript to serialize the form elements, the form
145
+ # submission will work just like a regular submission as viewed by the
146
+ # receiving side (all elements available in @params). The options for
147
+ # specifying the target with :url and defining callbacks is the same as
148
+ # link_to_remote.
149
+ #
150
+ # A "fall-through" target for browsers that doesn't do JavaScript can be
151
+ # specified with the :action/:method options on :html.
152
+ #
153
+ # Example:
154
+ # form_remote_tag :html => { :action =>
155
+ # url_for(:controller => "some", :action => "place") }
156
+ #
157
+ # The Hash passed to the :html key is equivalent to the options (2nd)
158
+ # argument in the FormTagHelper.form_tag method.
159
+ #
160
+ # By default the fall-through action is the same as the one specified in
161
+ # the :url (and the default method is :post).
162
+ def form_remote_tag(options = {})
163
+ options[:form] = true
164
+
165
+ options[:html] ||= {}
166
+ options[:html][:onsubmit] = "#{remote_function(options)}; return false;"
167
+ options[:html][:action] = options[:html][:action] || url_for(options[:url])
168
+ options[:html][:method] = options[:html][:method] || "post"
169
+
170
+ tag("form", options[:html], true)
171
+ end
172
+
173
+ # Works like form_remote_tag, but uses form_for semantics.
174
+ def remote_form_for(object_name, object, options = {}, &proc)
175
+ concat(form_remote_tag(options), proc.binding)
176
+ fields_for(object_name, object, options, &proc)
177
+ concat('</form>', proc.binding)
178
+ end
179
+ alias_method :form_remote_for, :remote_form_for
180
+
181
+ # Returns a button input tag that will submit form using XMLHttpRequest
182
+ # in the background instead of regular reloading POST arrangement.
183
+ # <tt>options</tt> argument is the same as in <tt>form_remote_tag</tt>.
184
+ def submit_to_remote(name, value, options = {})
185
+ options[:with] ||= 'Form.serialize(this.form)'
186
+
187
+ options[:html] ||= {}
188
+ options[:html][:type] = 'button'
189
+ options[:html][:onclick] = "#{remote_function(options)}; return false;"
190
+ options[:html][:name] = name
191
+ options[:html][:value] = value
192
+
193
+ tag("input", options[:html], false)
194
+ end
195
+
196
+ # Returns a JavaScript function (or expression) that'll update a DOM
197
+ # element according to the options passed.
198
+ #
199
+ # * <tt>:content</tt>: The content to use for updating. Can be left out
200
+ # if using block, see example.
201
+ # * <tt>:action</tt>: Valid options are :update (assumed by default),
202
+ # :empty, :remove
203
+ # * <tt>:position</tt> If the :action is :update, you can optionally
204
+ # specify one of the following positions: :before, :top, :bottom,
205
+ # :after.
206
+ #
207
+ # Examples:
208
+ # <%= javascript_tag(update_element_function("products",
209
+ # :position => :bottom, :content => "<p>New product!</p>")) %>
210
+ #
211
+ # <% replacement_function = update_element_function("products") do %>
212
+ # <p>Product 1</p>
213
+ # <p>Product 2</p>
214
+ # <% end %>
215
+ # <%= javascript_tag(replacement_function) %>
216
+ #
217
+ # This method can also be used in combination with remote method call
218
+ # where the result is evaluated afterwards to cause multiple updates on
219
+ # a page. Example:
220
+ #
221
+ # # Calling view
222
+ # <%= form_remote_tag :url => { :action => "buy" },
223
+ # :complete => evaluate_remote_response %>
224
+ # all the inputs here...
225
+ #
226
+ # # Controller action
227
+ # def buy
228
+ # @product = Product.find(1)
229
+ # end
230
+ #
231
+ # # Returning view
232
+ # <%= update_element_function(
233
+ # "cart", :action => :update, :position => :bottom,
234
+ # :content => "<p>New Product: #{@product.name}</p>")) %>
235
+ # <% update_element_function("status", :binding => binding) do %>
236
+ # You've bought a new product!
237
+ # <% end %>
238
+ #
239
+ # Notice how the second call doesn't need to be in an ERb output block
240
+ # since it uses a block and passes in the binding to render directly.
241
+ # This trick will however only work in ERb (not Builder or other
242
+ # template forms).
243
+ #
244
+ # See also JavaScriptGenerator and update_page.
245
+ def update_element_function(element_id, options = {}, &block)
246
+ content = escape_javascript(options[:content] || '')
247
+ content = escape_javascript(capture(&block)) if block
248
+
249
+ javascript_function = case (options[:action] || :update)
250
+ when :update
251
+ if options[:position]
252
+ "new Insertion.#{options[:position].to_s.camelize}('#{element_id}','#{content}')"
253
+ else
254
+ "$('#{element_id}').innerHTML = '#{content}'"
255
+ end
256
+
257
+ when :empty
258
+ "$('#{element_id}').innerHTML = ''"
259
+
260
+ when :remove
261
+ "Element.remove('#{element_id}')"
262
+
263
+ else
264
+ raise ArgumentError, "Invalid action, choose one of :update, :remove, :empty"
265
+ end
266
+
267
+ javascript_function << ";\n"
268
+ options[:binding] ? concat(javascript_function, options[:binding]) : javascript_function
269
+ end
270
+
271
+ # Returns 'eval(request.responseText)' which is the JavaScript function
272
+ # that form_remote_tag can call in :complete to evaluate a multiple
273
+ # update return document using update_element_function calls.
274
+ def evaluate_remote_response
275
+ "eval(request.responseText)"
276
+ end
277
+
278
+ # Returns the JavaScript needed for a remote function.
279
+ # Takes the same arguments as link_to_remote.
280
+ #
281
+ # Example:
282
+ # <select id="options" onchange="<%= remote_function(:update => "options",
283
+ # :url => { :action => :update_options }) %>">
284
+ # <option value="0">Hello</option>
285
+ # <option value="1">World</option>
286
+ # </select>
287
+ def remote_function(options)
288
+ javascript_options = options_for_ajax(options)
289
+
290
+ update = ''
291
+ if options[:update] and options[:update].is_a?Hash
292
+ update = []
293
+ update << "success:'#{options[:update][:success]}'" if options[:update][:success]
294
+ update << "failure:'#{options[:update][:failure]}'" if options[:update][:failure]
295
+ update = '{' + update.join(',') + '}'
296
+ elsif options[:update]
297
+ update << "'#{options[:update]}'"
298
+ end
299
+
300
+ function = update.empty? ?
301
+ "new Ajax.Request(" :
302
+ "new Ajax.Updater(#{update}, "
303
+
304
+ url_options = options[:url]
305
+ url_options = url_options.merge(:escape => false) if url_options.is_a? Hash
306
+ function << "'#{url_for(url_options)}'"
307
+ function << ", #{javascript_options})"
308
+
309
+ function = "#{options[:before]}; #{function}" if options[:before]
310
+ function = "#{function}; #{options[:after]}" if options[:after]
311
+ function = "if (#{options[:condition]}) { #{function}; }" if options[:condition]
312
+ function = "if (confirm('#{escape_javascript(options[:confirm])}')) { #{function}; }" if options[:confirm]
313
+
314
+ return function
315
+ end
316
+
317
+ # Observes the field with the DOM ID specified by +field_id+ and makes
318
+ # an Ajax call when its contents have changed.
319
+ #
320
+ # Required +options+ are either of:
321
+ # <tt>:url</tt>:: +url_for+-style options for the action to call
322
+ # when the field has changed.
323
+ # <tt>:function</tt>:: Instead of making a remote call to a URL, you
324
+ # can specify a function to be called instead.
325
+ #
326
+ # Additional options are:
327
+ # <tt>:frequency</tt>:: The frequency (in seconds) at which changes to
328
+ # this field will be detected. Not setting this
329
+ # option at all or to a value equal to or less than
330
+ # zero will use event based observation instead of
331
+ # time based observation.
332
+ # <tt>:update</tt>:: Specifies the DOM ID of the element whose
333
+ # innerHTML should be updated with the
334
+ # XMLHttpRequest response text.
335
+ # <tt>:with</tt>:: A JavaScript expression specifying the
336
+ # parameters for the XMLHttpRequest. This defaults
337
+ # to 'value', which in the evaluated context
338
+ # refers to the new field value. If you specify a
339
+ # string without a "=", it'll be extended to mean
340
+ # the form key that the value should be assigned to.
341
+ # So :with => "term" gives "'term'=value". If a "=" is
342
+ # present, no extension will happen.
343
+ # <tt>:on</tt>:: Specifies which event handler to observe. By default,
344
+ # it's set to "changed" for text fields and areas and
345
+ # "click" for radio buttons and checkboxes. With this,
346
+ # you can specify it instead to be "blur" or "focus" or
347
+ # any other event.
348
+ #
349
+ # Additionally, you may specify any of the options documented in
350
+ # link_to_remote.
351
+ def observe_field(field_id, options = {})
352
+ if options[:frequency] && options[:frequency] > 0
353
+ build_observer('Form.Element.Observer', field_id, options)
354
+ else
355
+ build_observer('Form.Element.EventObserver', field_id, options)
356
+ end
357
+ end
358
+
359
+ # Like +observe_field+, but operates on an entire form identified by the
360
+ # DOM ID +form_id+. +options+ are the same as +observe_field+, except
361
+ # the default value of the <tt>:with</tt> option evaluates to the
362
+ # serialized (request string) value of the form.
363
+ def observe_form(form_id, options = {})
364
+ if options[:frequency]
365
+ build_observer('Form.Observer', form_id, options)
366
+ else
367
+ build_observer('Form.EventObserver', form_id, options)
368
+ end
369
+ end
370
+
371
+ # All the methods were moved to GeneratorMethods so that
372
+ # #include_helpers_from_context has nothing to overwrite.
373
+ class JavaScriptGenerator #:nodoc:
374
+ def initialize(context, &block) #:nodoc:
375
+ @context, @lines = context, []
376
+ include_helpers_from_context
377
+ @context.instance_exec(self, &block)
378
+ end
379
+
380
+ private
381
+ def include_helpers_from_context
382
+ @context.extended_by.each do |mod|
383
+ extend mod unless mod.name =~ /^ActionView::Helpers/
384
+ end
385
+ extend GeneratorMethods
386
+ end
387
+
388
+ # JavaScriptGenerator generates blocks of JavaScript code that allow you
389
+ # to change the content and presentation of multiple DOM elements. Use
390
+ # this in your Ajax response bodies, either in a <script> tag or as plain
391
+ # JavaScript sent with a Content-type of "text/javascript".
392
+ #
393
+ # Create new instances with PrototypeHelper#update_page or with
394
+ # ActionController::Base#render, then call #insert_html, #replace_html,
395
+ # #remove, #show, #hide, #visual_effect, or any other of the built-in
396
+ # methods on the yielded generator in any order you like to modify the
397
+ # content and appearance of the current page.
398
+ #
399
+ # Example:
400
+ #
401
+ # update_page do |page|
402
+ # page.insert_html :bottom, 'list', "<li>#{@item.name}</li>"
403
+ # page.visual_effect :highlight, 'list'
404
+ # page.hide 'status-indicator', 'cancel-link'
405
+ # end
406
+ #
407
+ # generates the following JavaScript:
408
+ #
409
+ # new Insertion.Bottom("list", "<li>Some item</li>");
410
+ # new Effect.Highlight("list");
411
+ # ["status-indicator", "cancel-link"].each(Element.hide);
412
+ #
413
+ # Helper methods can be used in conjunction with JavaScriptGenerator.
414
+ # When a helper method is called inside an update block on the +page+
415
+ # object, that method will also have access to a +page+ object.
416
+ #
417
+ # Example:
418
+ #
419
+ # module ApplicationHelper
420
+ # def update_time
421
+ # page.replace_html 'time', Time.now.to_s(:db)
422
+ # page.visual_effect :highlight, 'time'
423
+ # end
424
+ # end
425
+ #
426
+ # # Controller action
427
+ # def poll
428
+ # render :update { |page| page.update_time }
429
+ # end
430
+ #
431
+ # You can also use PrototypeHelper#update_page_tag instead of
432
+ # PrototypeHelper#update_page to wrap the generated JavaScript in a
433
+ # <script> tag.
434
+ module GeneratorMethods
435
+ def to_s #:nodoc:
436
+ returning javascript = @lines * $/ do
437
+ if ActionView::Base.debug_rjs
438
+ source = javascript.dup
439
+ javascript.replace "try {\n#{source}\n} catch (e) "
440
+ javascript << "{ alert('RJS error:\\n\\n' + e.toString()); alert('#{source.gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" }}'); throw e }"
441
+ end
442
+ end
443
+ end
444
+
445
+ # Returns a element reference by finding it through +id+ in the DOM. This element can then be
446
+ # used for further method calls. Examples:
447
+ #
448
+ # page['blank_slate'] # => $('blank_slate');
449
+ # page['blank_slate'].show # => $('blank_slate').show();
450
+ # page['blank_slate'].show('first').up # => $('blank_slate').show('first').up();
451
+ def [](id)
452
+ JavaScriptElementProxy.new(self, id)
453
+ end
454
+
455
+ # Returns a collection reference by finding it through a CSS +pattern+ in the DOM. This collection can then be
456
+ # used for further method calls. Examples:
457
+ #
458
+ # page.select('p') # => $$('p');
459
+ # page.select('p.welcome b').first # => $$('p.welcome b').first();
460
+ # page.select('p.welcome b').first.hide # => $$('p.welcome b').first().hide();
461
+ #
462
+ # You can also use prototype enumerations with the collection. Observe:
463
+ #
464
+ # page.select('#items li').each do |value|
465
+ # value.hide
466
+ # end
467
+ # # => $$('#items li').each(function(value) { value.hide(); });
468
+ #
469
+ # Though you can call the block param anything you want, they are always rendered in the
470
+ # javascript as 'value, index.' Other enumerations, like collect() return the last statement:
471
+ #
472
+ # page.select('#items li').collect('hidden') do |item|
473
+ # item.hide
474
+ # end
475
+ # # => var hidden = $$('#items li').collect(function(value, index) { return value.hide(); });
476
+ def select(pattern)
477
+ JavaScriptElementCollectionProxy.new(self, pattern)
478
+ end
479
+
480
+ # Inserts HTML at the specified +position+ relative to the DOM element
481
+ # identified by the given +id+.
482
+ #
483
+ # +position+ may be one of:
484
+ #
485
+ # <tt>:top</tt>:: HTML is inserted inside the element, before the
486
+ # element's existing content.
487
+ # <tt>:bottom</tt>:: HTML is inserted inside the element, after the
488
+ # element's existing content.
489
+ # <tt>:before</tt>:: HTML is inserted immediately preceeding the element.
490
+ # <tt>:after</tt>:: HTML is inserted immediately following the element.
491
+ #
492
+ # +options_for_render+ may be either a string of HTML to insert, or a hash
493
+ # of options to be passed to ActionView::Base#render. For example:
494
+ #
495
+ # # Insert the rendered 'navigation' partial just before the DOM
496
+ # # element with ID 'content'.
497
+ # insert_html :before, 'content', :partial => 'navigation'
498
+ #
499
+ # # Add a list item to the bottom of the <ul> with ID 'list'.
500
+ # insert_html :bottom, 'list', '<li>Last item</li>'
501
+ #
502
+ def insert_html(position, id, *options_for_render)
503
+ insertion = position.to_s.camelize
504
+ call "new Insertion.#{insertion}", id, render(*options_for_render)
505
+ end
506
+
507
+ # Replaces the inner HTML of the DOM element with the given +id+.
508
+ #
509
+ # +options_for_render+ may be either a string of HTML to insert, or a hash
510
+ # of options to be passed to ActionView::Base#render. For example:
511
+ #
512
+ # # Replace the HTML of the DOM element having ID 'person-45' with the
513
+ # # 'person' partial for the appropriate object.
514
+ # replace_html 'person-45', :partial => 'person', :object => @person
515
+ #
516
+ def replace_html(id, *options_for_render)
517
+ call 'Element.update', id, render(*options_for_render)
518
+ end
519
+
520
+ # Replaces the "outer HTML" (i.e., the entire element, not just its
521
+ # contents) of the DOM element with the given +id+.
522
+ #
523
+ # +options_for_render+ may be either a string of HTML to insert, or a hash
524
+ # of options to be passed to ActionView::Base#render. For example:
525
+ #
526
+ # # Replace the DOM element having ID 'person-45' with the
527
+ # # 'person' partial for the appropriate object.
528
+ # replace_html 'person-45', :partial => 'person', :object => @person
529
+ #
530
+ # This allows the same partial that is used for the +insert_html+ to
531
+ # be also used for the input to +replace+ without resorting to
532
+ # the use of wrapper elements.
533
+ #
534
+ # Examples:
535
+ #
536
+ # <div id="people">
537
+ # <%= render :partial => 'person', :collection => @people %>
538
+ # </div>
539
+ #
540
+ # # Insert a new person
541
+ # page.insert_html :bottom, :partial => 'person', :object => @person
542
+ #
543
+ # # Replace an existing person
544
+ # page.replace 'person_45', :partial => 'person', :object => @person
545
+ #
546
+ def replace(id, *options_for_render)
547
+ call 'Element.replace', id, render(*options_for_render)
548
+ end
549
+
550
+ # Removes the DOM elements with the given +ids+ from the page.
551
+ def remove(*ids)
552
+ record "#{javascript_object_for(ids)}.each(Element.remove)"
553
+ end
554
+
555
+ # Shows hidden DOM elements with the given +ids+.
556
+ def show(*ids)
557
+ call 'Element.show', *ids
558
+ end
559
+
560
+ # Hides the visible DOM elements with the given +ids+.
561
+ def hide(*ids)
562
+ call 'Element.hide', *ids
563
+ end
564
+
565
+ # Toggles the visibility of the DOM elements with the given +ids+.
566
+ def toggle(*ids)
567
+ call 'Element.toggle', *ids
568
+ end
569
+
570
+ # Displays an alert dialog with the given +message+.
571
+ def alert(message)
572
+ call 'alert', message
573
+ end
574
+
575
+ # Redirects the browser to the given +location+, in the same form as
576
+ # +url_for+.
577
+ def redirect_to(location)
578
+ assign 'window.location.href', @context.url_for(location)
579
+ end
580
+
581
+ # Calls the JavaScript +function+, optionally with the given
582
+ # +arguments+.
583
+ def call(function, *arguments)
584
+ record "#{function}(#{arguments_for_call(arguments)})"
585
+ end
586
+
587
+ # Assigns the JavaScript +variable+ the given +value+.
588
+ def assign(variable, value)
589
+ record "#{variable} = #{javascript_object_for(value)}"
590
+ end
591
+
592
+ # Writes raw JavaScript to the page.
593
+ def <<(javascript)
594
+ @lines << javascript
595
+ end
596
+
597
+ # Executes the content of the block after a delay of +seconds+. Example:
598
+ #
599
+ # page.delay(20) do
600
+ # page.visual_effect :fade, 'notice'
601
+ # end
602
+ def delay(seconds = 1)
603
+ record "setTimeout(function() {\n\n"
604
+ yield
605
+ record "}, #{(seconds * 1000).to_i})"
606
+ end
607
+
608
+ # Starts a script.aculo.us visual effect. See
609
+ # ActionView::Helpers::ScriptaculousHelper for more information.
610
+ def visual_effect(name, id = nil, options = {})
611
+ record @context.send(:visual_effect, name, id, options)
612
+ end
613
+
614
+ # Creates a script.aculo.us sortable element. Useful
615
+ # to recreate sortable elements after items get added
616
+ # or deleted.
617
+ # See ActionView::Helpers::ScriptaculousHelper for more information.
618
+ def sortable(id, options = {})
619
+ record @context.send(:sortable_element_js, id, options)
620
+ end
621
+
622
+ # Creates a script.aculo.us draggable element.
623
+ # See ActionView::Helpers::ScriptaculousHelper for more information.
624
+ def draggable(id, options = {})
625
+ record @context.send(:draggable_element_js, id, options)
626
+ end
627
+
628
+ # Creates a script.aculo.us drop receiving element.
629
+ # See ActionView::Helpers::ScriptaculousHelper for more information.
630
+ def drop_receiving(id, options = {})
631
+ record @context.send(:drop_receiving_element_js, id, options)
632
+ end
633
+
634
+ private
635
+ def page
636
+ self
637
+ end
638
+
639
+ def record(line)
640
+ returning line = "#{line.to_s.chomp.gsub /\;$/, ''};" do
641
+ self << line
642
+ end
643
+ end
644
+
645
+ def render(*options_for_render)
646
+ Hash === options_for_render.first ?
647
+ @context.render(*options_for_render) :
648
+ options_for_render.first.to_s
649
+ end
650
+
651
+ def javascript_object_for(object)
652
+ object.respond_to?(:to_json) ? object.to_json : object.inspect
653
+ end
654
+
655
+ def arguments_for_call(arguments)
656
+ arguments.map { |argument| javascript_object_for(argument) }.join ', '
657
+ end
658
+
659
+ def method_missing(method, *arguments)
660
+ JavaScriptProxy.new(self, method.to_s.camelize)
661
+ end
662
+ end
663
+ end
664
+
665
+ # Yields a JavaScriptGenerator and returns the generated JavaScript code.
666
+ # Use this to update multiple elements on a page in an Ajax response.
667
+ # See JavaScriptGenerator for more information.
668
+ def update_page(&block)
669
+ JavaScriptGenerator.new(@template, &block).to_s
670
+ end
671
+
672
+ # Works like update_page but wraps the generated JavaScript in a <script>
673
+ # tag. Use this to include generated JavaScript in an ERb template.
674
+ # See JavaScriptGenerator for more information.
675
+ def update_page_tag(&block)
676
+ javascript_tag update_page(&block)
677
+ end
678
+
679
+ protected
680
+ def options_for_ajax(options)
681
+ js_options = build_callbacks(options)
682
+
683
+ js_options['asynchronous'] = options[:type] != :synchronous
684
+ js_options['method'] = method_option_to_s(options[:method]) if options[:method]
685
+ js_options['insertion'] = "Insertion.#{options[:position].to_s.camelize}" if options[:position]
686
+ js_options['evalScripts'] = options[:script].nil? || options[:script]
687
+
688
+ if options[:form]
689
+ js_options['parameters'] = 'Form.serialize(this)'
690
+ elsif options[:submit]
691
+ js_options['parameters'] = "Form.serialize('#{options[:submit]}')"
692
+ elsif options[:with]
693
+ js_options['parameters'] = options[:with]
694
+ end
695
+
696
+ options_for_javascript(js_options)
697
+ end
698
+
699
+ def method_option_to_s(method)
700
+ (method.is_a?(String) and !method.index("'").nil?) ? method : "'#{method}'"
701
+ end
702
+
703
+ def build_observer(klass, name, options = {})
704
+ if options[:with] && !options[:with].include?("=")
705
+ options[:with] = "'#{options[:with]}=' + value"
706
+ else
707
+ options[:with] ||= 'value' if options[:update]
708
+ end
709
+
710
+ callback = options[:function] || remote_function(options)
711
+ javascript = "new #{klass}('#{name}', "
712
+ javascript << "#{options[:frequency]}, " if options[:frequency]
713
+ javascript << "function(element, value) {"
714
+ javascript << "#{callback}}"
715
+ javascript << ", '#{options[:on]}'" if options[:on]
716
+ javascript << ")"
717
+ javascript_tag(javascript)
718
+ end
719
+
720
+ def build_callbacks(options)
721
+ callbacks = {}
722
+ options.each do |callback, code|
723
+ if CALLBACKS.include?(callback)
724
+ name = 'on' + callback.to_s.capitalize
725
+ callbacks[name] = "function(request){#{code}}"
726
+ end
727
+ end
728
+ callbacks
729
+ end
730
+ end
731
+
732
+ # Converts chained method calls on DOM proxy elements into JavaScript chains
733
+ class JavaScriptProxy < Builder::BlankSlate #:nodoc:
734
+ def initialize(generator, root = nil)
735
+ @generator = generator
736
+ @generator << root if root
737
+ end
738
+
739
+ private
740
+ def method_missing(method, *arguments)
741
+ if method.to_s =~ /(.*)=$/
742
+ assign($1, arguments.first)
743
+ else
744
+ call("#{method.to_s.camelize(:lower)}", *arguments)
745
+ end
746
+ end
747
+
748
+ def call(function, *arguments)
749
+ append_to_function_chain!("#{function}(#{@generator.send(:arguments_for_call, arguments)})")
750
+ self
751
+ end
752
+
753
+ def assign(variable, value)
754
+ append_to_function_chain!("#{variable} = #{@generator.send(:javascript_object_for, value)}")
755
+ end
756
+
757
+ def function_chain
758
+ @function_chain ||= @generator.instance_variable_get("@lines")
759
+ end
760
+
761
+ def append_to_function_chain!(call)
762
+ function_chain[-1].chomp!(';')
763
+ function_chain[-1] += ".#{call};"
764
+ end
765
+ end
766
+
767
+ class JavaScriptElementProxy < JavaScriptProxy #:nodoc:
768
+ def initialize(generator, id)
769
+ @id = id
770
+ super(generator, "$(#{id.to_json})")
771
+ end
772
+
773
+ def replace_html(*options_for_render)
774
+ call 'update', @generator.send(:render, *options_for_render)
775
+ end
776
+
777
+ def replace(*options_for_render)
778
+ call 'replace', @generator.send(:render, *options_for_render)
779
+ end
780
+
781
+ def reload
782
+ replace :partial => @id.to_s
783
+ end
784
+
785
+ end
786
+
787
+ class JavaScriptVariableProxy < JavaScriptProxy #:nodoc:
788
+ def initialize(generator, variable)
789
+ @variable = variable
790
+ @empty = true # only record lines if we have to. gets rid of unnecessary linebreaks
791
+ super(generator)
792
+ end
793
+
794
+ # The JSON Encoder calls this to check for the #to_json method
795
+ # Since it's a blank slate object, I suppose it responds to anything.
796
+ def respond_to?(method)
797
+ true
798
+ end
799
+
800
+ def to_json
801
+ @variable
802
+ end
803
+
804
+ private
805
+ def append_to_function_chain!(call)
806
+ @generator << @variable if @empty
807
+ @empty = false
808
+ super
809
+ end
810
+ end
811
+
812
+ class JavaScriptCollectionProxy < JavaScriptProxy #:nodoc:
813
+ ENUMERABLE_METHODS_WITH_RETURN = [:all, :any, :collect, :map, :detect, :find, :find_all, :select, :max, :min, :partition, :reject, :sort_by]
814
+ ENUMERABLE_METHODS = ENUMERABLE_METHODS_WITH_RETURN + [:each]
815
+ attr_reader :generator
816
+ delegate :arguments_for_call, :to => :generator
817
+
818
+ def initialize(generator, pattern)
819
+ super(generator, @pattern = pattern)
820
+ end
821
+
822
+ def grep(variable, pattern, &block)
823
+ enumerate :grep, :variable => variable, :return => true, :method_args => [pattern], :yield_args => %w(value index), &block
824
+ end
825
+
826
+ def inject(variable, memo, &block)
827
+ enumerate :inject, :variable => variable, :method_args => [memo], :yield_args => %w(memo value index), :return => true, &block
828
+ end
829
+
830
+ def pluck(variable, property)
831
+ add_variable_assignment!(variable)
832
+ append_enumerable_function!("pluck(#{property.to_json});")
833
+ end
834
+
835
+ def zip(variable, *arguments, &block)
836
+ add_variable_assignment!(variable)
837
+ append_enumerable_function!("zip(#{arguments_for_call arguments}")
838
+ if block
839
+ function_chain[-1] += ", function(array) {"
840
+ yield ActiveSupport::JSON::Variable.new('array')
841
+ add_return_statement!
842
+ @generator << '});'
843
+ else
844
+ function_chain[-1] += ');'
845
+ end
846
+ end
847
+
848
+ private
849
+ def method_missing(method, *arguments, &block)
850
+ if ENUMERABLE_METHODS.include?(method)
851
+ returnable = ENUMERABLE_METHODS_WITH_RETURN.include?(method)
852
+ variable = arguments.first if returnable
853
+ enumerate(method, {:variable => (arguments.first if returnable), :return => returnable, :yield_args => %w(value index)}, &block)
854
+ else
855
+ super
856
+ end
857
+ end
858
+
859
+ # Options
860
+ # * variable - name of the variable to set the result of the enumeration to
861
+ # * method_args - array of the javascript enumeration method args that occur before the function
862
+ # * yield_args - array of the javascript yield args
863
+ # * return - true if the enumeration should return the last statement
864
+ def enumerate(enumerable, options = {}, &block)
865
+ options[:method_args] ||= []
866
+ options[:yield_args] ||= []
867
+ yield_args = options[:yield_args] * ', '
868
+ method_args = arguments_for_call options[:method_args] # foo, bar, function
869
+ method_args << ', ' unless method_args.blank?
870
+ add_variable_assignment!(options[:variable]) if options[:variable]
871
+ append_enumerable_function!("#{enumerable.to_s.camelize(:lower)}(#{method_args}function(#{yield_args}) {")
872
+ # only yield as many params as were passed in the block
873
+ yield *options[:yield_args].collect { |p| JavaScriptVariableProxy.new(@generator, p) }[0..block.arity-1]
874
+ add_return_statement! if options[:return]
875
+ @generator << '});'
876
+ end
877
+
878
+ def add_variable_assignment!(variable)
879
+ function_chain.push("var #{variable} = #{function_chain.pop}")
880
+ end
881
+
882
+ def add_return_statement!
883
+ unless function_chain.last =~ /return/
884
+ function_chain.push("return #{function_chain.pop.chomp(';')};")
885
+ end
886
+ end
887
+
888
+ def append_enumerable_function!(call)
889
+ function_chain[-1].chomp!(';')
890
+ function_chain[-1] += ".#{call}"
891
+ end
892
+ end
893
+
894
+ class JavaScriptElementCollectionProxy < JavaScriptCollectionProxy #:nodoc:\
895
+ def initialize(generator, pattern)
896
+ super(generator, "$$(#{pattern.to_json})")
897
+ end
898
+ end
899
+ end
900
+ end