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.
- data/CHANGELOG +392 -5
- data/lib/action_controller.rb +8 -4
- data/lib/action_controller/assertions.rb +9 -10
- data/lib/action_controller/base.rb +177 -88
- data/lib/action_controller/benchmarking.rb +5 -5
- data/lib/action_controller/caching.rb +44 -36
- data/lib/action_controller/cgi_ext/cgi_methods.rb +71 -6
- data/lib/action_controller/cgi_ext/cookie_performance_fix.rb +1 -1
- data/lib/action_controller/cgi_process.rb +36 -24
- data/lib/action_controller/components.rb +152 -52
- data/lib/action_controller/dependencies.rb +1 -1
- data/lib/action_controller/deprecated_redirects.rb +2 -2
- data/lib/action_controller/deprecated_request_methods.rb +34 -0
- data/lib/action_controller/filters.rb +59 -19
- data/lib/action_controller/flash.rb +53 -47
- data/lib/action_controller/helpers.rb +2 -2
- data/lib/action_controller/integration.rb +524 -0
- data/lib/action_controller/layout.rb +58 -23
- data/lib/action_controller/mime_responds.rb +163 -0
- data/lib/action_controller/mime_type.rb +142 -0
- data/lib/action_controller/pagination.rb +13 -7
- data/lib/action_controller/request.rb +59 -56
- data/lib/action_controller/rescue.rb +1 -1
- data/lib/action_controller/routing.rb +29 -10
- data/lib/action_controller/scaffolding.rb +8 -0
- data/lib/action_controller/session/active_record_store.rb +21 -10
- data/lib/action_controller/session/mem_cache_store.rb +18 -12
- data/lib/action_controller/session_management.rb +30 -11
- data/lib/action_controller/templates/rescues/_trace.rhtml +1 -1
- data/lib/action_controller/templates/scaffolds/layout.rhtml +4 -4
- data/lib/action_controller/templates/scaffolds/list.rhtml +1 -1
- data/lib/action_controller/test_process.rb +189 -118
- data/lib/action_controller/vendor/html-scanner/html/node.rb +20 -1
- data/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +3 -0
- data/lib/action_controller/vendor/html-scanner/html/version.rb +1 -1
- data/lib/action_controller/vendor/xml_node.rb +97 -0
- data/lib/action_controller/verification.rb +2 -0
- data/lib/action_pack/version.rb +3 -3
- data/lib/action_view.rb +0 -2
- data/lib/action_view/base.rb +109 -36
- data/lib/action_view/compiled_templates.rb +1 -1
- data/lib/action_view/helpers/active_record_helper.rb +4 -2
- data/lib/action_view/helpers/asset_tag_helper.rb +6 -7
- data/lib/action_view/helpers/capture_helper.rb +49 -12
- data/lib/action_view/helpers/date_helper.rb +14 -4
- data/lib/action_view/helpers/form_helper.rb +136 -20
- data/lib/action_view/helpers/form_options_helper.rb +29 -7
- data/lib/action_view/helpers/form_tag_helper.rb +22 -20
- data/lib/action_view/helpers/java_script_macros_helper.rb +29 -9
- data/lib/action_view/helpers/javascript_helper.rb +50 -446
- data/lib/action_view/helpers/javascripts/controls.js +95 -30
- data/lib/action_view/helpers/javascripts/dragdrop.js +161 -21
- data/lib/action_view/helpers/javascripts/effects.js +310 -211
- data/lib/action_view/helpers/javascripts/prototype.js +228 -28
- data/lib/action_view/helpers/number_helper.rb +9 -9
- data/lib/action_view/helpers/pagination_helper.rb +1 -1
- data/lib/action_view/helpers/prototype_helper.rb +900 -0
- data/lib/action_view/helpers/scriptaculous_helper.rb +135 -0
- data/lib/action_view/helpers/text_helper.rb +7 -6
- data/lib/action_view/helpers/url_helper.rb +23 -14
- data/lib/action_view/partials.rb +12 -4
- data/rakefile +13 -5
- data/test/abstract_unit.rb +4 -3
- data/test/active_record_unit.rb +88 -0
- data/test/{controller → activerecord}/active_record_assertions_test.rb +7 -50
- data/test/{controller → activerecord}/active_record_store_test.rb +27 -4
- data/test/activerecord/pagination_test.rb +161 -0
- data/test/controller/action_pack_assertions_test.rb +18 -15
- data/test/controller/base_test.rb +31 -42
- data/test/controller/benchmark_test.rb +8 -11
- data/test/controller/capture_test.rb +33 -1
- data/test/controller/cgi_test.rb +33 -0
- data/test/controller/custom_handler_test.rb +8 -0
- data/test/controller/fake_controllers.rb +9 -17
- data/test/controller/filters_test.rb +32 -3
- data/test/controller/flash_test.rb +26 -41
- data/test/controller/fragment_store_setting_test.rb +1 -1
- data/test/controller/layout_test.rb +73 -0
- data/test/controller/mime_responds_test.rb +257 -0
- data/test/controller/mime_type_test.rb +24 -0
- data/test/controller/new_render_test.rb +157 -1
- data/test/controller/redirect_test.rb +23 -0
- data/test/controller/render_test.rb +54 -56
- data/test/controller/request_test.rb +25 -0
- data/test/controller/routing_test.rb +74 -66
- data/test/controller/test_test.rb +66 -1
- data/test/controller/verification_test.rb +3 -1
- data/test/controller/webservice_test.rb +255 -0
- data/test/fixtures/companies.yml +24 -0
- data/test/fixtures/company.rb +9 -0
- data/test/fixtures/db_definitions/sqlite.sql +42 -0
- data/test/fixtures/developer.rb +7 -0
- data/test/fixtures/developers.yml +21 -0
- data/test/fixtures/developers_projects.yml +13 -0
- data/test/fixtures/layout_tests/layouts/controller_name_space/nested.rhtml +1 -0
- data/test/fixtures/layout_tests/layouts/item.rhtml +1 -0
- data/test/fixtures/layout_tests/layouts/layout_test.rhtml +1 -0
- data/test/fixtures/layout_tests/layouts/third_party_template_library.mab +1 -0
- data/test/fixtures/layout_tests/views/hello.rhtml +1 -0
- data/test/fixtures/multipart/mona_lisa.jpg +0 -0
- data/test/fixtures/project.rb +3 -0
- data/test/fixtures/projects.yml +7 -0
- data/test/fixtures/replies.yml +13 -0
- data/test/fixtures/reply.rb +5 -0
- data/test/fixtures/respond_to/all_types_with_layout.rhtml +1 -0
- data/test/fixtures/respond_to/all_types_with_layout.rjs +1 -0
- data/test/fixtures/respond_to/layouts/standard.rhtml +1 -0
- data/test/fixtures/respond_to/using_defaults.rhtml +1 -0
- data/test/fixtures/respond_to/using_defaults.rjs +1 -0
- data/test/fixtures/respond_to/using_defaults.rxml +1 -0
- data/test/fixtures/respond_to/using_defaults_with_type_list.rhtml +1 -0
- data/test/fixtures/respond_to/using_defaults_with_type_list.rjs +1 -0
- data/test/fixtures/respond_to/using_defaults_with_type_list.rxml +1 -0
- data/test/fixtures/test/block_content_for.rhtml +2 -0
- data/test/fixtures/test/delete_with_js.rjs +2 -0
- data/test/fixtures/test/dot.directory/render_file_with_ivar.rhtml +1 -0
- data/test/fixtures/test/enum_rjs_test.rjs +6 -0
- data/test/fixtures/test/erb_content_for.rhtml +2 -0
- data/test/fixtures/test/hello_world.rxml +3 -0
- data/test/fixtures/test/hello_world_with_layout_false.rhtml +1 -0
- data/test/fixtures/test/non_erb_block_content_for.rxml +4 -0
- data/test/fixtures/topic.rb +3 -0
- data/test/fixtures/topics.yml +22 -0
- data/test/template/active_record_helper_test.rb +4 -0
- data/test/template/asset_tag_helper_test.rb +7 -2
- data/test/template/date_helper_test.rb +39 -2
- data/test/template/form_helper_test.rb +238 -5
- data/test/template/form_options_helper_test.rb +78 -0
- data/test/template/form_tag_helper_test.rb +11 -0
- data/test/template/java_script_macros_helper_test.rb +51 -6
- data/test/template/javascript_helper_test.rb +7 -153
- data/test/template/number_helper_test.rb +14 -13
- data/test/template/prototype_helper_test.rb +423 -0
- data/test/template/scriptaculous_helper_test.rb +90 -0
- data/test/template/text_helper_test.rb +12 -9
- data/test/template/url_helper_test.rb +31 -15
- metadata +291 -246
- data/lib/action_controller/cgi_ext/multipart_progress.rb +0 -169
- data/lib/action_controller/upload_progress.rb +0 -473
- data/lib/action_controller/vendor/html-scanner/html/node.rb.rej +0 -17
- data/lib/action_view/helpers/upload_progress_helper.rb +0 -433
- data/lib/action_view/vendor/builder.rb +0 -13
- data/lib/action_view/vendor/builder/blankslate.rb +0 -53
- data/lib/action_view/vendor/builder/xmlbase.rb +0 -143
- data/lib/action_view/vendor/builder/xmlevents.rb +0 -63
- data/lib/action_view/vendor/builder/xmlmarkup.rb +0 -308
- data/test/controller/multipart_progress_testx.rb +0 -365
- data/test/controller/upload_progress_testx.rb +0 -89
- data/test/template/upload_progress_helper_testx.rb +0 -136
@@ -1,17 +1,13 @@
|
|
1
|
-
/* Prototype JavaScript framework, version 1.
|
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.
|
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
|
-
|
127
|
-
|
140
|
+
return this.gsub(pattern, function(match) {
|
141
|
+
if (--count < 0) return match[0];
|
142
|
+
return replacement(match);
|
143
|
+
});
|
144
|
+
},
|
128
145
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
146
|
+
scan: function(pattern, iterator) {
|
147
|
+
this.gsub(pattern, iterator);
|
148
|
+
return this;
|
149
|
+
},
|
133
150
|
|
134
|
-
|
135
|
-
|
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
|
-
|
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(
|
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(
|
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
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|