aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionpack/CHANGELOG2
-rw-r--r--actionpack/lib/action_view/helpers/javascript_helper.rb329
-rw-r--r--actionpack/lib/action_view/helpers/javascripts/prototype.js326
3 files changed, 442 insertions, 215 deletions
diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG
index dee0ce136d..9c90b2830d 100644
--- a/actionpack/CHANGELOG
+++ b/actionpack/CHANGELOG
@@ -1,5 +1,7 @@
*SVN*
+* Added a JavascriptHelper and accompanying prototype.js library that opens the world of Ajax to Action Pack with a large array of options for dynamically interacting with an application without reloading the page #884 [Sam Stephenson/David]
+
* Added pagination support through both a controller and helper add-on #817 [Sam Stephenson]
* Fixed routing and helpers to make Rails work on non-vhost setups #826 [Nicholas Seckar/Tobias Luetke]
diff --git a/actionpack/lib/action_view/helpers/javascript_helper.rb b/actionpack/lib/action_view/helpers/javascript_helper.rb
index 814314a4be..1570a363a7 100644
--- a/actionpack/lib/action_view/helpers/javascript_helper.rb
+++ b/actionpack/lib/action_view/helpers/javascript_helper.rb
@@ -2,9 +2,18 @@ require File.dirname(__FILE__) + '/tag_helper'
module ActionView
module Helpers
- # You must call <%= define_javascript_functions %> in your application before using these helpers.
+ # You must call <%= define_javascript_functions %> in your application,
+ # or copy the included Javascript libraries into your application's
+ # public/javascripts/ directory, before using these helpers.
module JavascriptHelper
- # Returns a link that'll trigger a javascript +function+ using the onclick handler and return false after the fact.
+
+ unless const_defined? :CALLBACKS
+ CALLBACKS = [:uninitialized, :loading, :loaded, :interactive, :complete]
+ JAVASCRIPT_PATH = File.join(File.dirname(__FILE__), 'javascripts')
+ end
+
+ # Returns a link that'll trigger a javascript +function+ using the
+ # onclick handler and return false after the fact.
#
# Examples:
# link_to_function "Greeting", "alert('Hello world!')"
@@ -16,17 +25,20 @@ module ActionView
)
end
- # Returns a link to a remote action defined by <tt>options[:url]</tt> (using the url_for format) that's called in the background
- # using XMLHttpRequest. The result of that request can then be inserted into a DOM object who's id can be specified
- # with <tt>options[:update]</tt>. Usually, the result would be a partial prepared by the controller with either render_partial
- # or render_partial_collection.
+ # Returns a link to a remote action defined by <tt>options[:url]</tt>
+ # (using the url_for format) that's called in the background using
+ # XMLHttpRequest. The result of that request can then be inserted into a
+ # DOM object whose id can be specified with <tt>options[:update]</tt>.
+ # Usually, the result would be a partial prepared by the controller with
+ # either render_partial or render_partial_collection.
#
# Examples:
# link_to_remote "Delete this post", :update => "posts", :url => { :action => "destroy", :id => post.id }
# link_to_remote(image_tag("refresh"), :update => "emails", :url => { :action => "list_emails" })
#
- # By default, these remote requests are processed asynchronous during which various callbacks can be triggered (for progress indicators
- # and the likes).
+ # By default, these remote requests are processed asynchronous during
+ # which various callbacks can be triggered (for progress indicators and
+ # the likes).
#
# Example:
# link_to_remote word,
@@ -35,14 +47,20 @@ module ActionView
#
# The complete list of callbacks that may be specified are:
#
- # * <tt>:uninitialized</tt> -- EXPLAIN ME!
- # * <tt>:loading</tt> -- EXPLAIN ME!
- # * <tt>:loaded</tt> -- EXPLAIN ME!
- # * <tt>:interactive</tt> -- EXPLAIN ME!
- # * <tt>:complete</tt> -- EXPLAIN ME!
+ # <tt>:uninitialized</tt>:: Called before the remote document is
+ # initialized with data.
+ # <tt>:loading</tt>:: Called when the remote document is being
+ # loaded with data by the browser.
+ # <tt>:loaded</tt>:: Called when the browser has finished loading
+ # the remote document.
+ # <tt>:interactive</tt>:: Called when the user can interact with the
+ # remote document, even though it has not
+ # finished loading.
+ # <tt>:complete</tt>:: Called when the XMLHttpRequest is complete.
#
- # If you for some reason or another needs synchronous processing (that'll block the browser while the request is happening), you
- # can specify <tt>options[:type] = :sync</tt>.
+ # If you for some reason or another need synchronous processing (that'll
+ # block the browser while the request is happening), you can specify
+ # <tt>options[:type] = :synchronous</tt>.
def link_to_remote(name, options = {}, html_options = {})
link_to_function(name, remote_function(options), html_options)
end
@@ -56,20 +74,16 @@ module ActionView
tag("form", options[:html], true)
end
- def remote_function(options)
- callbacks = build_callbacks(options)
+ def remote_function(options) #:nodoc: for now
+ javascript_options = options_for_ajax(options)
function = options[:update] ?
- "update_with_response('#{options[:update]}', " :
- "xml_request("
+ "new Ajax.Updater('#{options[:update]}', " :
+ "new Ajax.Request("
function << "'#{url_for(options[:url])}'"
- function << ', Form.serialize(this)' if options[:form]
- function << ', null' if !options[:form] && callbacks
- function << ", true" if callbacks || options[:type] != :sync
- function << ", #{callbacks}" if callbacks
- function << ')'
-
+ function << ", #{javascript_options})"
+
function = "#{options[:before]}; #{function}" if options[:before]
function = "#{function}; #{options[:after]}" if options[:after]
function = "if (#{options[:condition]}) { #{function}; }" if options[:condition]
@@ -77,205 +91,90 @@ module ActionView
return function
end
+ # Includes the Action Pack Javascript library inside a single <script>
+ # tag.
+ #
+ # Note: The recommended approach is to copy the contents of
+ # lib/action_view/helpers/javascripts/ into your application's
+ # public/javascripts/ directory, and use +javascript_include_tag+ to
+ # create remote <script> links.
def define_javascript_functions
- <<-EOF
- <script language="JavaScript">
- /* Convenience form methods */
- Field = {
- clear: function() {
- for(i = 0; i < arguments.length; i++) { o(arguments[i]).value = ''; }
- return true;
- },
-
- focus: function(id) {
- o(id).focus();
- return true;
- },
-
- present: function() {
- for(i = 0; i < arguments.length; i++) { if (o(arguments[i]).value == '') { return false; } }
- return true;
- }
- }
-
- /* XMLHttpRequest Methods */
-
- function update_with_response() {
- var container = o(arguments[0]);
- var url = arguments[1];
- var parameters = arguments[2];
- var async = arguments[3];
- var callbacks = arguments[4];
-
- if (async) {
- if(!callbacks) callbacks = {}
- complete = callbacks['complete']
- callbacks['complete'] = function(request) {
- container.innerHTML = request.responseText
- if(complete) complete(request)
- }
- xml_request(url, parameters, true, callbacks)
- } else {
- container.innerHTML = xml_request(url, parameters);
- }
- }
+ javascript = '<script type="text/javascript">'
+ Dir.glob(File.join(JAVASCRIPT_PATH, '*')).each do |filename|
+ javascript << "\n" << IO.read(filename)
+ end
+ javascript << '</script>'
+ end
- function xml_request() {
- var url = arguments[0];
- var parameters = arguments[1];
- var async = arguments[2];
- var callbacks = arguments[3];
- var type = parameters ? "POST" : "GET";
+ # Observes the field with the DOM ID specified by +field_id+ and makes
+ # an Ajax when its contents have changed.
+ #
+ # Required +options+ are:
+ # <tt>:frequency</tt>:: The frequency (in seconds) at which changes to
+ # this field will be detected.
+ # <tt>:url</tt>:: +url_for+-style options for the action to call
+ # when the field has changed.
+ #
+ # Additional options are:
+ # <tt>:update</tt>:: Specifies the DOM ID of the element whose
+ # innerHTML should be updated with the
+ # XMLHttpRequest response text.
+ # <tt>:with</tt>:: A Javascript expression specifying the
+ # parameters for the XMLHttpRequest. This defaults
+ # to 'value', which in the evaluated context
+ # refers to the new field value.
+ #
+ # Additionally, you may specify any of the options documented in
+ # +link_to_remote.
+ def observe_field(field_id, options = {})
+ build_observer('Form.Element.Observer', name, options)
+ end
- req = xml_http_request_object();
- req.open(type, url, async);
-
- if (async) {
- invoke_callback = function(which) {
- if(callbacks && callbacks[which]) callbacks[which](req)
- }
-
- req.onreadystatechange = function() {
- switch(req.readyState) {
- case 0: invoke_callback('uninitialized'); break
- case 1: invoke_callback('loading'); break
- case 2: invoke_callback('loaded'); break
- case 3: invoke_callback('interactive'); break
- case 4: invoke_callback('complete'); break
- }
- }
- }
+ # Like +observe_field+, but operates on an entire form identified by the
+ # DOM ID +form_id+. +options+ are the same as +observe_field+, except
+ # the default value of the <tt>:with</tt> option evaluates to the
+ # serialized (request string) value of the form.
+ def observe_form(form_id, options = {})
+ build_observer('Form.Observer', name, options)
+ end
- req.send(parameters ? parameters + "&_=" : parameters);
+ private
+ def escape_javascript(javascript)
+ (javascript || '').gsub('"', '\"')
+ end
- if(!async) return req.responseText;
- }
-
- function xml_http_request_object() {
- var req = false;
- try {
- req = new ActiveXObject("Msxml2.XMLHTTP");
- } catch (e) {
- try {
- req = new ActiveXObject("Microsoft.XMLHTTP");
- } catch (E) {
- req = false;
- }
- }
-
- if (!req && typeof XMLHttpRequest!='undefined') {
- req = new XMLHttpRequest();
- }
-
- return req;
- }
-
-
- /* Common methods ------------------------------ */
-
- function toggle_display_by_id(id) {
- o(id).style.display = (o(id).style.display == "none") ? "" : "none";
- }
-
- function toggle_display() {
- for(i = 0; i < arguments.length; i++) {
- o(arguments[i]).style.display = (o(arguments[i]).style.display == "none") ? "" : "none";
- }
- }
-
- function o(id) {
- return document.getElementById(id);
- }
-
- function get_elements_by_class(tag_name, class_name) {
- var all = document.all ? document.all : document.getElementsByTagName(tag_name);
- var elements = new Array();
-
- for (var e = 0; e < all.length; e++)
- if (all[e].className == class_name)
- elements[elements.length] = all[e];
-
- return elements;
- }
-
-
- /* Serialize a form by Sam Stephenson ------------------------------ */
-
- Form = {
- Serializers: {
- input: function(element) {
- switch (element.type.toLowerCase()) {
- case 'hidden':
- case 'text':
- return Form.Serializers.textarea(element);
- case 'checkbox':
- case 'radio':
- return Form.Serializers.inputSelector(element);
- }
- },
-
- inputSelector: function(element) {
- if (element.checked)
- return [element.name, element.value];
- },
-
- textarea: function(element) {
- return [element.name, element.value];
- },
-
- select: function(element) {
- var index = element.selectedIndex;
- return [element.name, element.options[index].value];
- }
- },
-
- serialize: function(form) {
- var elements = Form.getFormElements(form);
- var queryComponents = new Array();
-
- for (var i = 0; i < elements.length; i++) {
- var element = elements[i];
- var method = element.tagName.toLowerCase();
+ def options_for_ajax(options)
+ js_options = build_callbacks(options)
+
+ js_options['asynchronous'] = options[:type] != :synchronous
+ js_options['method'] = options[:method] if options[:method]
+
+ if options[:form]
+ js_options['parameters'] = 'Form.serialize(this)'
+ elsif options[:with]
+ js_options['parameters'] = options[:with]
+ end
+
+ '{' + js_options.map {|k, v| "#{k}:#{v}"}.join(', ') + '}'
+ end
- var parameter = Form.Serializers[method](element);
- if (parameter) {
- var queryComponent = encodeURIComponent(parameter[0]) + '=' +
- encodeURIComponent(parameter[1]);
- queryComponents.push(queryComponent);
- }
- }
-
- return queryComponents.join('&');
- },
-
- getFormElements: function(form) {
- var elements = new Array();
- for (tagName in Form.Serializers) {
- var tagElements = form.getElementsByTagName(tagName);
- for (var j = 0; j < tagElements.length; j++)
- elements.push(tagElements[j]);
- }
- return elements;
- }
- }
- </script>
- EOF
+ def build_observer(klass, name, options = {})
+ options[:with] ||= 'value' if options[:update]
+ callback = remote_function(options)
+ javascript = '<script type="text/javascript">'
+ javascript << "new #{klass}('#{name}', "
+ javascript << "#{options[:frequency]}, function(element, value) {"
+ javascript << "#{callback}})</script>"
end
-
- private
- def build_callbacks(options)
- callbacks = nil
- %w{uninitialized loading loaded interactive complete}.each do |cb|
- cb = cb.to_sym
- if options[cb]
- callbacks ? callbacks << "," : callbacks = "{"
- callbacks <<
- "#{cb}:function(request){#{options[cb].gsub(/"/){'\"'}}}"
- end
- end
- callbacks << "}" if callbacks
+
+ def build_callbacks(options)
+ CALLBACKS.inject({}) do |callbacks, callback|
+ name = 'on' + callback.to_s.capitalize
+ code = escape_javascript(options[callback])
+ callbacks[name] = "function(request){#{code}}" if callbacks[name]
callbacks
end
+ end
end
end
end
diff --git a/actionpack/lib/action_view/helpers/javascripts/prototype.js b/actionpack/lib/action_view/helpers/javascripts/prototype.js
new file mode 100644
index 0000000000..f559317b5e
--- /dev/null
+++ b/actionpack/lib/action_view/helpers/javascripts/prototype.js
@@ -0,0 +1,326 @@
+/* Prototype: an object-oriented Javascript library, version 1.0.0
+ * (c) 2005 Sam Stephenson <sam@conio.net>
+ *
+ * Prototype is freely distributable under the terms of an MIT-style license.
+ * For details, see http://prototype.conio.net/
+ */
+
+
+Prototype = {
+ Version = '1.0.0'
+}
+
+Class = {
+ create: function() {
+ return function() {
+ this.initialize.apply(this, arguments);
+ }
+ }
+}
+
+Abstract = new Object();
+
+Object.prototype.extend = function(object) {
+ for (property in object) {
+ this[property] = object[property];
+ }
+ return this;
+}
+
+Function.prototype.bind = function(object) {
+ var method = this;
+ return function() {
+ method.apply(object, arguments);
+ }
+}
+
+Function.prototype.bindAsEventListener = function(object) {
+ var method = this;
+ return function(event) {
+ method.call(object, event || window.event);
+ }
+}
+
+Try = {
+ these: function() {
+ var returnValue;
+
+ for (var i = 0; i < arguments.length; i++) {
+ var lambda = arguments[i];
+ try {
+ returnValue = lambda();
+ break;
+ } catch (e) {}
+ }
+
+ return returnValue;
+ }
+}
+
+Toggle = {
+ visibility: function() {
+ for (i = 0; i < arguments.length; i++) {
+ var element = $(arguments[i]);
+ element.style.display =
+ (element.style.display == 'none' ? '' : 'none');
+ }
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+function $() {
+ var elements = new Array();
+
+ for (i = 0; i < arguments.length; i++) {
+ var element = arguments[i];
+ if (typeof element == 'string')
+ element = document.getElementById(element);
+
+ if (arguments.length == 1)
+ return element;
+
+ elements.push(element);
+ }
+
+ return elements;
+}
+
+function getElementsByClassName(className, element) {
+ var children = (element || document).getElementsByTagName('*');
+ var elements = new Array();
+
+ for (var i = 0; i < children.length; i++) {
+ var child = children[i];
+ var classNames = child.className.split(' ');
+ for (var j = 0; j < classNames.length; j++) {
+ if (classNames[j] == className) {
+ elements.push(child);
+ break;
+ }
+ }
+ }
+
+ return elements;
+}
+
+/*--------------------------------------------------------------------------*/
+
+Ajax = {
+ getTransport: function() {
+ return Try.these(
+ function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+ function() {return new ActiveXObject('Microsoft.XMLHTTP')},
+ function() {return new XMLHttpRequest()}
+ ) || false;
+ },
+
+ emptyFunction: function() {}
+}
+
+Ajax.Base = function() {};
+Ajax.Base.prototype = {
+ setOptions: function(options) {
+ this.options = {
+ method: 'post',
+ asynchronous: true,
+ parameters: ''
+ }.extend(options || {});
+ }
+}
+
+Ajax.Request = Class.create();
+Ajax.Request.Events =
+ ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Request.prototype = (new Ajax.Base()).extend({
+ initialize: function(url, options) {
+ this.transport = Ajax.getTransport();
+ this.setOptions(options);
+
+ try {
+ if (this.options.method == 'get')
+ url += '?' + this.options.parameters + '&_=';
+
+ this.transport.open(this.options.method, url, true);
+
+ if (this.options.asynchronous)
+ this.transport.onreadystatechange = this.onStateChange.bind(this);
+
+ if (this.options.method == 'post') {
+ this.transport.setRequestHeader('Connection', 'close');
+ this.transport.setRequestHeader('Content-type',
+ 'application/x-www-form-urlencoded');
+ }
+
+ this.transport.send(this.options.parameters);
+
+ } catch (e) {
+ }
+ },
+
+ onStateChange: function() {
+ var event = Ajax.Request.Events[this.transport.readyState];
+ (this.options['on' + event] || Ajax.emptyFunction)(this.transport);
+ }
+});
+
+Ajax.Updater = Class.create();
+Ajax.Updater.prototype = (new Ajax.Base()).extend({
+ initialize: function(container, url, options) {
+ this.container = $(container);
+ this.setOptions(options);
+
+ if (this.options.asynchronous) {
+ this.onComplete = this.options.onComplete;
+ this.options.onComplete = this.updateContent.bind(this);
+ }
+
+ this.request = new Ajax.Request(url, this.options);
+
+ if (!this.options.asynchronous)
+ this.updateContent();
+ },
+
+ updateContent: function() {
+ this.container.innerHTML = this.request.transport.responseText;
+ if (this.onComplete) this.onComplete(this.request);
+ }
+});
+
+Field = {
+ clear: function() {
+ for (i = 0; i < arguments.length; i++)
+ $(arguments[i]).value = '';
+ },
+
+ focus: function(element) {
+ $(element).focus();
+ },
+
+ present: function() {
+ for (i = 0; i < arguments.length; i++)
+ if ($(arguments[i]).value == '') return false;
+ return true;
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+Form = {
+ serialize: function(form) {
+ var elements = Form.getElements($(form));
+ var queryComponents = new Array();
+
+ for (var i = 0; i < elements.length; i++) {
+ var queryComponent = Form.Element.serialize(elements[i]);
+ if (queryComponent)
+ queryComponents.push(queryComponent);
+ }
+
+ return queryComponents.join('&');
+ },
+
+ getElements: function(form) {
+ form = $(form);
+ var elements = new Array();
+
+ for (tagName in Form.Element.Serializers) {
+ var tagElements = form.getElementsByTagName(tagName);
+ for (var j = 0; j < tagElements.length; j++)
+ elements.push(tagElements[j]);
+ }
+ return elements;
+ }
+}
+
+Form.Element = {
+ serialize: function(element) {
+ element = $(element);
+ var method = element.tagName.toLowerCase();
+ var parameter = Form.Element.Serializers[method](element);
+
+ if (parameter)
+ return encodeURIComponent(parameter[0]) + '=' +
+ encodeURIComponent(parameter[1]);
+ },
+
+ getValue: function(element) {
+ element = $(element);
+ var method = element.tagName.toLowerCase();
+ var parameter = Form.Element.Serializers[method](element);
+
+ if (parameter)
+ return parameter[1];
+ }
+}
+
+Form.Element.Serializers = {
+ input: function(element) {
+ switch (element.type.toLowerCase()) {
+ case 'hidden':
+ case 'text':
+ return Form.Element.Serializers.textarea(element);
+ case 'checkbox':
+ case 'radio':
+ return Form.Element.Serializers.inputSelector(element);
+ }
+ },
+
+ inputSelector: function(element) {
+ if (element.checked)
+ return [element.name, element.value];
+ },
+
+ textarea: function(element) {
+ return [element.name, element.value];
+ },
+
+ select: function(element) {
+ var index = element.selectedIndex;
+ return [element.name, element.options[index].value];
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.TimedObserver = function() {}
+Abstract.TimedObserver.prototype = {
+ initialize: function(element, frequency, callback) {
+ this.frequency = frequency;
+ this.element = $(element);
+ this.callback = callback;
+
+ this.lastValue = this.getValue();
+ this.registerCallback();
+ },
+
+ registerCallback: function() {
+ setTimeout(this.onTimerEvent.bind(this), this.frequency * 1000);
+ },
+
+ onTimerEvent: function() {
+ var value = this.getValue();
+ if (this.lastValue != value) {
+ this.callback(this.element, value);
+ this.lastValue = value;
+ }
+
+ this.registerCallback();
+ }
+}
+
+Form.Element.Observer = Class.create();
+Form.Element.Observer.prototype = (new Abstract.TimedObserver()).extend({
+ getValue: function() {
+ return Form.Element.getValue(this.element);
+ }
+});
+
+Form.Observer = Class.create();
+Form.Observer.prototype = (new Abstract.TimedObserver()).extend({
+ getValue: function() {
+ return Form.serialize(this.element);
+ }
+});
+