Skip to content

Commit

Permalink
Added script.aculo.us Javascripts (controls.js, dragdrop.js, effects.…
Browse files Browse the repository at this point in the history
…js) (NEEDS MORE DESCRIPTION) #1509 [Thomas Fuchs]

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1522 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information
dhh committed Jun 26, 2005
1 parent 66d99c9 commit 7da736f
Show file tree
Hide file tree
Showing 7 changed files with 1,558 additions and 189 deletions.
2 changes: 2 additions & 0 deletions actionpack/CHANGELOG
Original file line number Original file line Diff line number Diff line change
@@ -1,5 +1,7 @@
*SVN* *SVN*


* Added script.aculo.us Javascripts (controls.js, dragdrop.js, effects.js) (NEEDS MORE DESCRIPTION) #1509 [Thomas Fuchs]

* Fixed prototype to consider all fields it doesn't know as text (such as Safari's search) just like the browser in its serialization #1497 [Sean Treadway] * Fixed prototype to consider all fields it doesn't know as text (such as Safari's search) just like the browser in its serialization #1497 [Sean Treadway]


* Improved performance of Routes generation by a factor of 5 #1434 [Nicholas Seckar] * Improved performance of Routes generation by a factor of 5 #1434 [Nicholas Seckar]
Expand Down
123 changes: 117 additions & 6 deletions actionpack/lib/action_view/helpers/javascript_helper.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ module Helpers
# the use of form_remote_tag. # the use of form_remote_tag.
module JavascriptHelper module JavascriptHelper
unless const_defined? :CALLBACKS unless const_defined? :CALLBACKS
CALLBACKS = [:uninitialized, :loading, :loaded, :interactive, :complete] CALLBACKS = [ :uninitialized, :loading, :loaded, :interactive, :complete ]
AJAX_OPTIONS = [ :url, :asynchronous, :method, :insertion, :form, :with ].concat(CALLBACKS)
JAVASCRIPT_PATH = File.join(File.dirname(__FILE__), 'javascripts') JAVASCRIPT_PATH = File.join(File.dirname(__FILE__), 'javascripts')
end end


Expand Down Expand Up @@ -146,21 +147,33 @@ def remote_function(options) #:nodoc: for now
return function return function
end end


# Includes the Action Pack Javascript library inside a single <script> # Includes the Action Pack Javascript libraries inside a single <script>
# tag. # tag. The function first includes prototype.js and then its core extensions,
# (determined by filenames starting with "prototype").
# Afterwards, any additional scripts will be included in random order.
# #
# Note: The recommended approach is to copy the contents of # Note: The recommended approach is to copy the contents of
# lib/action_view/helpers/javascripts/ into your application's # lib/action_view/helpers/javascripts/ into your application's
# public/javascripts/ directory, and use +javascript_include_tag+ to # public/javascripts/ directory, and use +javascript_include_tag+ to
# create remote <script> links. # create remote <script> links.
def define_javascript_functions def define_javascript_functions
javascript = '<script type="text/javascript">' javascript = '<script type="text/javascript">'
Dir.glob(File.join(JAVASCRIPT_PATH, '*')).each { |filename| javascript << "\n" << IO.read(filename) }
# load prototype.js and its extensions first
prototype_libs = Dir.glob(File.join(JAVASCRIPT_PATH, 'prototype*')).sort.reverse
prototype_libs.each do |filename|
javascript << "\n" << IO.read(filename)
end

# load other librairies
(Dir.glob(File.join(JAVASCRIPT_PATH, '*')) - prototype_libs).each do |filename|
javascript << "\n" << IO.read(filename)
end
javascript << '</script>' javascript << '</script>'
end end


# Observes the field with the DOM ID specified by +field_id+ and makes # Observes the field with the DOM ID specified by +field_id+ and makes
# an Ajax when its contents have changed. # an Ajax call when its contents have changed.
# #
# Required +options+ are: # Required +options+ are:
# <tt>:frequency</tt>:: The frequency (in seconds) at which changes to # <tt>:frequency</tt>:: The frequency (in seconds) at which changes to
Expand Down Expand Up @@ -190,13 +203,111 @@ def observe_field(field_id, options = {})
def observe_form(form_id, options = {}) def observe_form(form_id, options = {})
build_observer('Form.Observer', form_id, options) build_observer('Form.Observer', form_id, options)
end end


# Adds Ajax autocomplete functionality to the text input field with the
# DOM ID specified by +field_id+.
#
# This function expects that the called action returns a HTML <ul> list,
# or nothing if no entries should be displayed for autocompletion.
#
# Required +options+ are:
# <tt>:url</tt>:: Specifies the DOM ID of the element whose
# innerHTML should be updated with the autocomplete
# entries returned by XMLHttpRequest.
#
# Addtional +options+ are:
# <tt>:update</tt>:: Specifies the DOM ID of the element whose
# innerHTML should be updated with the autocomplete
# entries returned by the Ajax request.
# Defaults to field_id + '_autocomplete'
# <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.
# <tt>:indicator</tt>:: Specifies the DOM ID of an elment which will be
# displayed while autocomplete is running.
#
def remote_autocomplete(field_id, options = {})
function = "new Ajax.Autocompleter("
function << "'#{field_id}', "
function << "'" + (options[:update] || "#{field_id}_autocomplete") + "', "
function << "'#{url_for(options[:url])}'"

js_options = {}
js_options[:callback] = "function(element, value) {return #{options[:with]}}" if options[:with]
js_options[:indicator] = "'#{options[:indicator]}'" if options[:indicator]
function << (', ' + options_for_javascript(js_options) + ')')

javascript_tag(function)
end

# Use this method in your view to generate a return for the Ajax automplete requests.
#
# Example Action:
# @items = Item.find(:all, :conditions => [ 'LOWER(description) LIKE ?',
# '%' + params[:for].downcase + '%' ], 'description ASC')
# render :layout => false
#
# Example View:
# <%= autocomplete_responder @items, 'description' %>
def autocomplete_responder(entries, field, phrase = nil)
"<ul>#{entries.map { |entry| '<li>' + (phrase ? highlight(entry[field],phrase) : h(entry[field])) + '</li>' }.join}</ul>" if entries
end

# Returns a JavaScript snippet to be used on the Ajax callbacks for starting
# visual effects.
#
# Example:
# <%= link_to_remote "Reload", :update => "posts",
# :url => { :action => "reload" },
# :complete => visual_effect(:highlight, "posts", :duration => 0.5 )
#
# You can change the behaviour with various options, see
# http://script.aculo.us for more documentation.
def visual_effect(name, element_id, js_options = {})
"new Effect.#{name.to_s.capitalize}('#{element_id}',#{options_for_javascript(js_options)});"
end

# Makes the element with the DOM ID specified by +element_id+ sortable
# by drag-and-drop and make an Ajax call whenever the sort order has
# changed. By default, the action called gets the serialized sortable
# element as parameters.
#
# Example:
# <%= remote_sortable("my_list", :url => { :action => "order" }) %>
#
# In the example, the action gets a "my_list" array parameter
# containing the values of the ids of elements the sortable consists
# of, in the current order.
#
# You can change the behaviour with various options, see
# http://script.aculo.us for more documentation.
#
def remote_sortable(element_id, options = {})
options[:with] ||= "Sortable.serialize('#{element_id}')"
options[:onUpdate] ||= "function(){" + remote_function(options) + "}"
options.delete_if { |key, value| AJAX_OPTIONS.include?(key) }

javascript_tag("Sortable.create('#{element_id}', #{options_for_javascript(options)})")
end


# Escape carrier returns and single and double quotes for Javascript segments. # Escape carrier returns and single and double quotes for Javascript segments.
def escape_javascript(javascript) def escape_javascript(javascript)
(javascript || '').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" } (javascript || '').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" }
end end


# Returns a Javascript tag with the +content+ inside. Example:
# javascript_tag "alert('All is good')" # => <script type="text/javascript">alert('All is good')</script>
def javascript_tag(content)
content_tag("script", content, :type => "text/javascript")
end

private private
def options_for_javascript(options)
'{' + options.map {|k, v| "#{k}:#{v}"}.join(', ') + '}'
end

def options_for_ajax(options) def options_for_ajax(options)
js_options = build_callbacks(options) js_options = build_callbacks(options)


Expand All @@ -210,7 +321,7 @@ def options_for_ajax(options)
js_options['parameters'] = options[:with] js_options['parameters'] = options[:with]
end end


'{' + js_options.map {|k, v| "#{k}:#{v}"}.join(', ') + '}' options_for_javascript(js_options)
end end


def method_option_to_s(method) def method_option_to_s(method)
Expand Down
218 changes: 218 additions & 0 deletions actionpack/lib/action_view/helpers/javascripts/controls.js
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,218 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Ajax.Autocompleter = Class.create();
Ajax.Autocompleter.prototype = (new Ajax.Base()).extend({
initialize: function(element, update, url, options) {
this.element = $(element);
this.update = $(update);
this.has_focus = false;
this.changed = false;
this.active = false;
this.index = 0;
this.entry_count = 0;
this.url = url;

this.setOptions(options);
this.options.asynchronous = true;
this.options.onComplete = this.onComplete.bind(this)
this.options.frequency = this.options.frequency || 0.4;
this.options.min_chars = this.options.min_chars || 1;
this.options.method = 'post';

if(this.options.indicator)
this.indicator = $(this.options.indicator);

this.observer = null;

Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
Event.observe(document, "click", this.onBlur.bindAsEventListener(this));
},

show: function() {
Element.show(this.update);
if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0)) {
new Insertion.Before(this.update,
'<iframe id="' + this.update.id + '_iefix" style="display:none;" src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
this.iefix = $(this.update.id+'_iefix');
this.iefix.style.position = 'absolute';
this.iefix.style.zIndex = 1;
this.update.style.zIndex = 2;
}
if(this.iefix) {
Position.clone(this.update, this.iefix);
Element.show(this.iefix);
}
},

hide: function() {
if(this.iefix) Element.hide(this.iefix);
Element.hide(this.update);
},

startIndicator: function() {
if(this.indicator) Element.show(this.indicator);
},

stopIndicator: function() {
if(this.indicator) Element.hide(this.indicator);
},

onObserverEvent: function() {
this.changed = false;
if(this.element.value.length>=this.options.min_chars) {
this.startIndicator();
this.options.parameters = this.options.callback ?
this.options.callback(this.element, Form.Element.getValue(this.element)) :
Form.Element.getValue(this.element);
new Ajax.Request(this.url, this.options);
} else {
this.active = false;
this.hide();
}
},

onComplete: function(request) {
if(!this.changed) {
this.update.innerHTML = request.responseText;

if(this.update.firstChild && this.update.firstChild.childNodes) {
this.entry_count =
this.update.firstChild.childNodes.length;
for (var i = 0; i < this.entry_count; i++) {
entry = this.get_entry(i);
entry.autocompleteIndex = i;
Event.observe(entry, "mouseover", this.onHover.bindAsEventListener(this));
Event.observe(entry, "click", this.onClick.bindAsEventListener(this));
}
} else {
this.entry_count = 0;
}

this.stopIndicator();

this.index = 0;
this.render();
}
},

onKeyPress: function(event) {
if(this.active)
switch(event.keyCode) {
case Event.KEY_TAB:
case Event.KEY_RETURN:
this.select_entry();
Event.stop(event);
case Event.KEY_ESC:
this.hide();
this.active = false;
return;
case Event.KEY_LEFT:
case Event.KEY_RIGHT:
return;
case Event.KEY_UP:
this.mark_previous();
this.render();
return;
case Event.KEY_DOWN:
this.mark_next();
this.render();
return;
}
else
if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN)
return;

this.changed = true;
this.has_focus = true;

if(this.observer) clearTimeout(this.observer);
this.observer =
setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
},

onHover: function(event) {
element = Event.findElement(event, 'LI');
if(this.index != element.autocompleteIndex)
{
this.index = element.autocompleteIndex;
this.render();
}
},

onClick: function(event) {
element = Event.findElement(event, 'LI');
this.index = element.autocompleteIndex;
this.select_entry();
},

onBlur: function(event) {
element = Event.element(event);
if(element==this.update) return;
while(element.parentNode)
{ element = element.parentNode; if(element==this.update) return; }
this.hide();
this.active = false;
},

render: function() {
if(this.entry_count > 0) {
for (var i = 0; i < this.entry_count; i++)
this.index==i ?
Element.Class.add(this.get_entry(i),"selected") :
Element.Class.remove(this.get_entry(i),"selected");

if(this.has_focus) {
if(this.get_current_entry().scrollIntoView)
this.get_current_entry().scrollIntoView(false);

this.show();
this.active = true;
}
} else this.hide();
},

mark_previous: function() {
if(this.index > 0) this.index--
else this.index = this.entry_count-1;
},

mark_next: function() {
if(this.index < this.entry_count-1) this.index++
else this.index = 0;
},

get_entry: function(index) {
return this.update.firstChild.childNodes[index];
},

get_current_entry: function() {
return this.get_entry(this.index);
},

select_entry: function() {
this.hide();
this.active = false;
value = Text.decodeHTML(Element.collectTextNodesIgnoreClass(this.get_current_entry(), 'informal'));
this.element.value = value;
this.element.focus();
}
});
Loading

0 comments on commit 7da736f

Please sign in to comment.