Skip to content

Commit

Permalink
Fix IE 30-sec timeout bug, make auto_complete_for even more delicious #…
Browse files Browse the repository at this point in the history
…1572 [Thomas Fuchs]

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1579 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information
dhh committed Jul 1, 2005
1 parent 24a8cb1 commit cadcd9e
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 46 deletions.
11 changes: 11 additions & 0 deletions actionpack/lib/action_controller/auto_complete.rb
Expand Up @@ -8,6 +8,17 @@ module ActionController
# #
# # View # # View
# <%= text_field_with_auto_complete :post, title %> # <%= text_field_with_auto_complete :post, title %>
#
# By default, auto_complete_for limits the results to 10 entries,
# and sorts by the given field.
#
# auto_complete_for takes a third parameter, an options hash to
# the find method used to search for the records:
#
# auto_complete_for :post, :title, :limit => 15, :order => 'created_at DESC'
#
# For help on defining text input fields with autocompletion,
# see ActionView::Helpers::JavascriptHelper.
module AutoComplete module AutoComplete
def self.append_features(base) #:nodoc: def self.append_features(base) #:nodoc:
super super
Expand Down
31 changes: 19 additions & 12 deletions actionpack/lib/action_view/helpers/javascript_helper.rb
Expand Up @@ -3,7 +3,7 @@
module ActionView module ActionView
module Helpers module Helpers
# Provides a set of helpers for calling JavaScript functions and, most importantly, to call remote methods using what has # Provides a set of helpers for calling JavaScript functions and, most importantly, to call remote methods using what has
# been labelled Ajax[http://www.adaptivepath.com/publications/essays/archives/000385.php]. This means that you can call # been labelled AJAX[http://www.adaptivepath.com/publications/essays/archives/000385.php]. This means that you can call
# actions in your controllers without reloading the page, but still update certain parts of it using injections into the # actions in your controllers without reloading the page, but still update certain parts of it using injections into the
# DOM. The common use case is having a form that adds a new element to a list without reloading the page. # DOM. The common use case is having a form that adds a new element to a list without reloading the page.
# #
Expand All @@ -12,7 +12,7 @@ module Helpers
# <tt><%= javascript_include_tag "prototype" %></tt> (which looks for the library in /javascripts/prototype.js). The latter is # <tt><%= javascript_include_tag "prototype" %></tt> (which looks for the library in /javascripts/prototype.js). The latter is
# recommended as the browser can then cache the library instead of fetching all the functions anew on every request. # recommended as the browser can then cache the library instead of fetching all the functions anew on every request.
# #
# If you're the visual type, there's an Ajax movie[http://www.rubyonrails.com/media/video/rails-ajax.mov] demonstrating # If you're the visual type, there's an AJAX movie[http://www.rubyonrails.com/media/video/rails-ajax.mov] demonstrating
# the use of form_remote_tag. # the use of form_remote_tag.
module JavaScriptHelper module JavaScriptHelper
unless const_defined? :CALLBACKS unless const_defined? :CALLBACKS
Expand Down Expand Up @@ -212,7 +212,7 @@ def define_javascript_functions
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 call when its contents have changed. # an AJAX call when its contents have changed.
# #
# Required +options+ are: # Required +options+ are:
# <tt>:url</tt>:: +url_for+-style options for the action to call # <tt>:url</tt>:: +url_for+-style options for the action to call
Expand Down Expand Up @@ -254,7 +254,7 @@ def observe_form(form_id, options = {})
end end




# Adds Ajax autocomplete functionality to the text input field with the # Adds AJAX autocomplete functionality to the text input field with the
# DOM ID specified by +field_id+. # DOM ID specified by +field_id+.
# #
# This function expects that the called action returns a HTML <ul> list, # This function expects that the called action returns a HTML <ul> list,
Expand All @@ -272,7 +272,7 @@ def observe_form(form_id, options = {})
# Addtional +options+ are: # Addtional +options+ are:
# <tt>:update</tt>:: Specifies the DOM ID of the element whose # <tt>:update</tt>:: Specifies the DOM ID of the element whose
# innerHTML should be updated with the autocomplete # innerHTML should be updated with the autocomplete
# entries returned by the Ajax request. # entries returned by the AJAX request.
# Defaults to field_id + '_auto_complete' # Defaults to field_id + '_auto_complete'
# <tt>:with</tt>:: A JavaScript expression specifying the # <tt>:with</tt>:: A JavaScript expression specifying the
# parameters for the XMLHttpRequest. This defaults # parameters for the XMLHttpRequest. This defaults
Expand All @@ -294,13 +294,14 @@ def auto_complete_field(field_id, options = {})
javascript_tag(function) javascript_tag(function)
end end


# Use this method in your view to generate a return for the Ajax automplete requests. # Use this method in your view to generate a return for the AJAX automplete requests.
# #
# Example action: # Example action:
# #
# def auto_complete_for_item_title # def auto_complete_for_item_title
# @items = Item.find(:all, :conditions => [ 'LOWER(description) LIKE ?', # @items = Item.find(:all,
# '%' + params[:for].downcase + '%' ], 'description ASC') # :conditions => [ 'LOWER(description) LIKE ?',
# '%' + request.raw_post.downcase + '%' ])
# render :inline => '<%= auto_complete_result(@items, 'description') %>' # render :inline => '<%= auto_complete_result(@items, 'description') %>'
# end # end
# #
Expand All @@ -312,14 +313,20 @@ def auto_complete_result(entries, field, phrase = nil)
content_tag("ul", items) content_tag("ul", items)
end end


# Wrapper for text_field with added AJAX autocompletion functionality.
#
# In your controller, you'll need to define an action called
# auto_complete_for_object_method to respond the AJAX calls,
#
# See the RDoc on ActionController::AutoComplete to learn more about this.
def text_field_with_auto_complete(object, method, tag_options = {}, completion_options = {}) def text_field_with_auto_complete(object, method, tag_options = {}, completion_options = {})
(completion_options[:skip_style] ? "" : auto_complete_stylesheet) + (completion_options[:skip_style] ? "" : auto_complete_stylesheet) +
text_field(object, method, { :autocomplete => "off" }.merge!(tag_options)) + text_field(object, method, { :autocomplete => "off" }.merge!(tag_options)) +
content_tag("div", "", :id => "#{object}_#{method}_auto_complete", :class => "auto_complete") + content_tag("div", "", :id => "#{object}_#{method}_auto_complete", :class => "auto_complete") +
auto_complete_field("#{object}_#{method}", { :url => { :action => "auto_complete_for_#{object}_#{method}" } }.update(completion_options)) auto_complete_field("#{object}_#{method}", { :url => { :action => "auto_complete_for_#{object}_#{method}" } }.update(completion_options))
end end


# Returns a JavaScript snippet to be used on the Ajax callbacks for starting # Returns a JavaScript snippet to be used on the AJAX callbacks for starting
# visual effects. # visual effects.
# #
# Example: # Example:
Expand All @@ -334,12 +341,12 @@ def visual_effect(name, element_id, js_options = {})
end end


# Makes the element with the DOM ID specified by +element_id+ sortable # 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 # by drag-and-drop and make an AJAX call whenever the sort order has
# changed. By default, the action called gets the serialized sortable # changed. By default, the action called gets the serialized sortable
# element as parameters. # element as parameters.
# #
# Example: # Example:
# <%= remote_sortable("my_list", :url => { :action => "order" }) %> # <%= sortable_element("my_list", :url => { :action => "order" }) %>
# #
# In the example, the action gets a "my_list" array parameter # In the example, the action gets a "my_list" array parameter
# containing the values of the ids of elements the sortable consists # containing the values of the ids of elements the sortable consists
Expand Down Expand Up @@ -378,7 +385,7 @@ def options_for_ajax(options)
js_options['method'] = method_option_to_s(options[:method]) if options[:method] js_options['method'] = method_option_to_s(options[:method]) if options[:method]
js_options['insertion'] = "Insertion.#{options[:position].to_s.camelize}" if options[:position] js_options['insertion'] = "Insertion.#{options[:position].to_s.camelize}" if options[:position]
js_options['evalScripts'] = options[:script] == true if options[:script] js_options['evalScripts'] = options[:script] == true if options[:script]

if options[:form] if options[:form]
js_options['parameters'] = 'Form.serialize(this)' js_options['parameters'] = 'Form.serialize(this)'
elsif options[:with] elsif options[:with]
Expand Down
47 changes: 29 additions & 18 deletions actionpack/lib/action_view/helpers/javascripts/controls.js
Expand Up @@ -56,35 +56,50 @@ Ajax.Autocompleter.prototype = (new Ajax.Base()).extend({
this.options.min_chars = this.options.min_chars || 1; this.options.min_chars = this.options.min_chars || 1;
this.options.method = 'post'; this.options.method = 'post';


this.options.onShow = this.options.onShow ||
function(element, update){
if(!update.style.position || update.style.position=='absolute') {
update.style.position = 'absolute';
var offsets = Position.cumulativeOffset(element);
update.style.left = offsets[0] + 'px';
update.style.top = (offsets[1] + element.offsetHeight) + 'px';
update.style.width = element.offsetWidth + 'px';
}
new Effect.Appear(update,{duration:0.3});
};
this.options.onHide = this.options.onHide ||
function(element, update){ new Effect.Fade(update,{duration:0.3}) };


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


this.observer = null; this.observer = null;


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


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


hide: function() { hide: function() {
if(this.update.style.display=='') this.options.onHide(this.element, this.update);
if(this.iefix) Element.hide(this.iefix); if(this.iefix) Element.hide(this.iefix);
Element.hide(this.update);
}, },


startIndicator: function() { startIndicator: function() {
Expand Down Expand Up @@ -194,21 +209,18 @@ Ajax.Autocompleter.prototype = (new Ajax.Base()).extend({
}, },


onBlur: function(event) { onBlur: function(event) {
var element = Event.element(event); // needed to make click events working
if(element==this.update) return; setTimeout(this.hide.bind(this), 250);
while(element.parentNode)
{ element = element.parentNode; if(element==this.update) return; }
this.hide();
this.has_focus = false; this.has_focus = false;
this.active = false; this.active = false;
}, },


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


if(this.has_focus) { if(this.has_focus) {
if(this.get_current_entry().scrollIntoView) if(this.get_current_entry().scrollIntoView)
Expand Down Expand Up @@ -239,7 +251,6 @@ Ajax.Autocompleter.prototype = (new Ajax.Base()).extend({
}, },


select_entry: function() { select_entry: function() {
this.hide();
this.active = false; this.active = false;
value = Element.collectTextNodesIgnoreClass(this.get_current_entry(), 'informal').unescapeHTML(); value = Element.collectTextNodesIgnoreClass(this.get_current_entry(), 'informal').unescapeHTML();
this.element.value = value; this.element.value = value;
Expand Down
20 changes: 12 additions & 8 deletions actionpack/lib/action_view/helpers/javascripts/dragdrop.js
Expand Up @@ -221,9 +221,9 @@ Draggables = {
addObserver: function(observer) { addObserver: function(observer) {
this.observers.push(observer); this.observers.push(observer);
}, },
notify: function(eventName) { // 'onStart', 'onEnd' notify: function(eventName, draggable) { // 'onStart', 'onEnd'
for(var i = 0; i < this.observers.length; i++) for(var i = 0; i < this.observers.length; i++)
this.observers[i][eventName](); this.observers[i][eventName](draggable);
} }
} }


Expand All @@ -243,7 +243,8 @@ Draggable.prototype = {
endeffect: function(element) { endeffect: function(element) {
new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0});
}, },
zindex: 1000 zindex: 1000,
revert: false
}.extend(arguments[1] || {}); }.extend(arguments[1] || {});


this.element = $(element); this.element = $(element);
Expand Down Expand Up @@ -278,8 +279,8 @@ Draggable.prototype = {
this.active = true; this.active = true;


var style = this.element.style; var style = this.element.style;
this.originalY = this.element.offsetTop - this.currentTop(); - this.originalTop; this.originalY = this.element.offsetTop - this.currentTop() - this.originalTop;
this.originalX = this.element.offsetLeft - this.currentLeft(); - this.originalLeft; this.originalX = this.element.offsetLeft - this.currentLeft() - this.originalLeft;
this.offsetY = event.clientY - this.originalY - this.originalTop; this.offsetY = event.clientY - this.originalY - this.originalTop;
this.offsetX = event.clientX - this.originalX - this.originalLeft; this.offsetX = event.clientX - this.originalX - this.originalLeft;


Expand All @@ -292,9 +293,12 @@ Draggable.prototype = {
this.dragging = false; this.dragging = false;


Droppables.fire(event, this.element); Droppables.fire(event, this.element);
Draggables.notify('onEnd'); Draggables.notify('onEnd', this);


if(this.options.revert && this.options.reverteffect) { var revert = this.options.revert;
if(revert && typeof revert == 'function') revert = revert(this.element);

if(revert && this.options.reverteffect) {
this.options.reverteffect(this.element, this.options.reverteffect(this.element,
this.currentTop()-this.originalTop, this.currentTop()-this.originalTop,
this.currentLeft()-this.originalLeft); this.currentLeft()-this.originalLeft);
Expand Down Expand Up @@ -330,7 +334,7 @@ Draggable.prototype = {
this.dragging = true; this.dragging = true;
if(style.position=="") style.position = "relative"; if(style.position=="") style.position = "relative";
style.zIndex = this.options.zindex; style.zIndex = this.options.zindex;
Draggables.notify('onStart'); Draggables.notify('onStart', this);
if(this.options.starteffect) this.options.starteffect(this.element); if(this.options.starteffect) this.options.starteffect(this.element);
} }


Expand Down
23 changes: 19 additions & 4 deletions actionpack/lib/action_view/helpers/javascripts/effects.js
Expand Up @@ -307,25 +307,33 @@ Effect.Puff = function(element) {
} }


Effect.BlindUp = function(element) { Effect.BlindUp = function(element) {
$(element)._overflow = $(element).style.overflow || 'visible';
$(element).style.overflow = 'hidden'; $(element).style.overflow = 'hidden';
new Effect.Scale(element, 0, new Effect.Scale(element, 0,
{ scaleContent: false, { scaleContent: false,
scaleX: false, scaleX: false,
afterFinish: function(effect) afterFinish: function(effect)
{ Element.hide(effect.element) } {
Element.hide(effect.element);
effect.element.style.overflow = effect.element._overflow;
}
}.extend(arguments[1] || {}) }.extend(arguments[1] || {})
); );
} }


Effect.BlindDown = function(element) { Effect.BlindDown = function(element) {
$(element).style.height = '0px'; $(element).style.height = '0px';
$(element)._overflow = $(element).style.overflow || 'visible';
$(element).style.overflow = 'hidden'; $(element).style.overflow = 'hidden';
Element.show(element); Element.show(element);
new Effect.Scale(element, 100, new Effect.Scale(element, 100,
{ scaleContent: false, { scaleContent: false,
scaleX: false, scaleX: false,
scaleMode: 'contents', scaleMode: 'contents',
scaleFrom: 0 scaleFrom: 0,
afterFinish: function(effect) {
effect.element.style.overflow = effect.element._overflow;
}
}.extend(arguments[1] || {}) }.extend(arguments[1] || {})
); );
} }
Expand Down Expand Up @@ -375,6 +383,7 @@ Effect.Shake = function(element) {
} }


Effect.SlideDown = function(element) { Effect.SlideDown = function(element) {
$(element)._overflow = $(element).style.overflow || 'visible';
$(element).style.height = '0px'; $(element).style.height = '0px';
$(element).style.overflow = 'hidden'; $(element).style.overflow = 'hidden';
$(element).firstChild.style.position = 'relative'; $(element).firstChild.style.position = 'relative';
Expand All @@ -386,12 +395,15 @@ Effect.SlideDown = function(element) {
scaleFrom: 0, scaleFrom: 0,
afterUpdate: function(effect) afterUpdate: function(effect)
{ effect.element.firstChild.style.bottom = { effect.element.firstChild.style.bottom =
(effect.originalHeight - effect.element.clientHeight) + 'px'; } (effect.originalHeight - effect.element.clientHeight) + 'px'; },
afterFinish: function(effect)
{ effect.element.style.overflow = effect.element._overflow; }
}.extend(arguments[1] || {}) }.extend(arguments[1] || {})
); );
} }


Effect.SlideUp = function(element) { Effect.SlideUp = function(element) {
$(element)._overflow = $(element).style.overflow || 'visible';
$(element).style.overflow = 'hidden'; $(element).style.overflow = 'hidden';
$(element).firstChild.style.position = 'relative'; $(element).firstChild.style.position = 'relative';
Element.show(element); Element.show(element);
Expand All @@ -402,7 +414,10 @@ Effect.SlideUp = function(element) {
{ effect.element.firstChild.style.bottom = { effect.element.firstChild.style.bottom =
(effect.originalHeight - effect.element.clientHeight) + 'px'; }, (effect.originalHeight - effect.element.clientHeight) + 'px'; },
afterFinish: function(effect) afterFinish: function(effect)
{ Element.hide(effect.element); } {
Element.hide(effect.element);
effect.element.style.overflow = effect.element._overflow;
}
}.extend(arguments[1] || {}) }.extend(arguments[1] || {})
); );
} }
Expand Down
11 changes: 7 additions & 4 deletions actionpack/lib/action_view/helpers/javascripts/prototype.js
Expand Up @@ -221,11 +221,12 @@ Ajax.Request.prototype = (new Ajax.Base()).extend({
}, },


setRequestHeaders: function() { setRequestHeaders: function() {
var requestHeaders = ['X-Requested-With', 'XMLHttpRequest', var requestHeaders = [
'X-Requested-With', 'XMLHttpRequest',
'X-Prototype-Version', Prototype.Version]; 'X-Prototype-Version', Prototype.Version];


if (this.options.method == 'post') if (this.options.method == 'post')
requestHeaders.push('Connection', 'close', requestHeaders.push(//'Connection', 'close',
'Content-type', 'application/x-www-form-urlencoded'); 'Content-type', 'application/x-www-form-urlencoded');


if (this.options.requestHeaders) if (this.options.requestHeaders)
Expand Down Expand Up @@ -987,8 +988,10 @@ var Position = {
clone: function(source, target) { clone: function(source, target) {
source = $(source); source = $(source);
target = $(target); target = $(target);
target.style.top = source.style.top; target.style.position = 'absolute';
target.style.left = source.style.left; var offsets = this.cumulativeOffset(source);
target.style.top = offsets[1] + 'px';
target.style.left = offsets[0] + 'px';
target.style.width = source.offsetWidth + 'px'; target.style.width = source.offsetWidth + 'px';
target.style.height = source.offsetHeight + 'px'; target.style.height = source.offsetHeight + 'px';
} }
Expand Down

0 comments on commit cadcd9e

Please sign in to comment.