aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack
diff options
context:
space:
mode:
authorDavid Heinemeier Hansson <david@loudthinking.com>2005-06-26 12:03:43 +0000
committerDavid Heinemeier Hansson <david@loudthinking.com>2005-06-26 12:03:43 +0000
commit7da736f929e88f4730f160d28afc90667c26b966 (patch)
tree48e46a9dfe62728de3443bc43cdd14db30ef68ce /actionpack
parent66d99c913e1ad549b1880bbceb6f7561040d47b4 (diff)
downloadrails-7da736f929e88f4730f160d28afc90667c26b966.tar.gz
rails-7da736f929e88f4730f160d28afc90667c26b966.tar.bz2
rails-7da736f929e88f4730f160d28afc90667c26b966.zip
Added script.aculo.us Javascripts (controls.js, dragdrop.js, effects.js) (NEEDS MORE DESCRIPTION) #1509 [Thomas Fuchs]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1522 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'actionpack')
-rw-r--r--actionpack/CHANGELOG2
-rw-r--r--actionpack/lib/action_view/helpers/javascript_helper.rb123
-rw-r--r--actionpack/lib/action_view/helpers/javascripts/controls.js218
-rw-r--r--actionpack/lib/action_view/helpers/javascripts/dragdrop.js385
-rw-r--r--actionpack/lib/action_view/helpers/javascripts/effects.js557
-rw-r--r--actionpack/lib/action_view/helpers/javascripts/prototype.js438
-rw-r--r--actionpack/test/template/javascript_helper.rb24
7 files changed, 1558 insertions, 189 deletions
diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG
index c3a922604e..ae8de12988 100644
--- a/actionpack/CHANGELOG
+++ b/actionpack/CHANGELOG
@@ -1,5 +1,7 @@
*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]
* Improved performance of Routes generation by a factor of 5 #1434 [Nicholas Seckar]
diff --git a/actionpack/lib/action_view/helpers/javascript_helper.rb b/actionpack/lib/action_view/helpers/javascript_helper.rb
index d5e2a8b2dc..31c15d222c 100644
--- a/actionpack/lib/action_view/helpers/javascript_helper.rb
+++ b/actionpack/lib/action_view/helpers/javascript_helper.rb
@@ -16,7 +16,8 @@ module ActionView
# the use of form_remote_tag.
module JavascriptHelper
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')
end
@@ -146,8 +147,10 @@ module ActionView
return function
end
- # Includes the Action Pack Javascript library inside a single <script>
- # tag.
+ # Includes the Action Pack Javascript libraries inside a single <script>
+ # 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
# lib/action_view/helpers/javascripts/ into your application's
@@ -155,12 +158,22 @@ module ActionView
# create remote <script> links.
def define_javascript_functions
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>'
end
# 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:
# <tt>:frequency</tt>:: The frequency (in seconds) at which changes to
@@ -190,13 +203,111 @@ module ActionView
def observe_form(form_id, options = {})
build_observer('Form.Observer', form_id, options)
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.
def escape_javascript(javascript)
(javascript || '').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" }
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
+ def options_for_javascript(options)
+ '{' + options.map {|k, v| "#{k}:#{v}"}.join(', ') + '}'
+ end
+
def options_for_ajax(options)
js_options = build_callbacks(options)
@@ -210,7 +321,7 @@ module ActionView
js_options['parameters'] = options[:with]
end
- '{' + js_options.map {|k, v| "#{k}:#{v}"}.join(', ') + '}'
+ options_for_javascript(js_options)
end
def method_option_to_s(method)
diff --git a/actionpack/lib/action_view/helpers/javascripts/controls.js b/actionpack/lib/action_view/helpers/javascripts/controls.js
new file mode 100644
index 0000000000..94d438a784
--- /dev/null
+++ b/actionpack/lib/action_view/helpers/javascripts/controls.js
@@ -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();
+ }
+}); \ No newline at end of file
diff --git a/actionpack/lib/action_view/helpers/javascripts/dragdrop.js b/actionpack/lib/action_view/helpers/javascripts/dragdrop.js
new file mode 100644
index 0000000000..f9ed0b7898
--- /dev/null
+++ b/actionpack/lib/action_view/helpers/javascripts/dragdrop.js
@@ -0,0 +1,385 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//
+// Element.Class part Copyright (c) 2005 by Rick Olson
+//
+// 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.
+
+/*--------------------------------------------------------------------------*/
+
+var Droppables = {
+ drops: false,
+ include_scroll_offsets: false,
+
+ add: function(element) {
+ var element = $(element);
+ var options = {
+ greedy: true,
+ hoverclass: null
+ }.extend(arguments[1] || {});
+
+ // cache containers
+ if(options.containment) {
+ options._containers = new Array();
+ var containment = options.containment;
+ if((typeof containment == 'object') &&
+ (containment.constructor == Array)) {
+ for(var i=0; i<containment.length; i++)
+ options._containers.push($(containment[i]));
+ } else {
+ options._containers.push($(containment));
+ }
+ options._containers_length =
+ options._containers.length-1;
+ }
+
+ if(element.style.position=='') //fix IE
+ element.style.position = 'relative';
+
+ // activate the droppable
+ element.droppable = options;
+
+ if(!this.drops) this.drops = [];
+ this.drops.push(element);
+ },
+
+ is_contained: function(element, drop) {
+ var containers = drop.droppable._containers;
+ var parentNode = element.parentNode;
+ var i = drop.droppable._containers_length;
+ do { if(parentNode==containers[i]) return true; } while (i--);
+ return false;
+ },
+
+ is_affected: function(pX, pY, element, drop) {
+ return (
+ (drop!=element) &&
+ ((!drop.droppable._containers) ||
+ this.is_contained(element, drop)) &&
+ ((!drop.droppable.accept) ||
+ (Element.Class.has_any(element, drop.droppable.accept))) &&
+ Position.within(drop, pX, pY) );
+ },
+
+ deactivate: function(drop) {
+ Element.Class.remove(drop, drop.droppable.hoverclass);
+ this.last_active = null;
+ },
+
+ activate: function(drop) {
+ if(this.last_active) this.deactivate(this.last_active);
+ if(drop.droppable.hoverclass) {
+ Element.Class.add(drop, drop.droppable.hoverclass);
+ this.last_active = drop;
+ }
+ },
+
+ show: function(event, element) {
+ if(!this.drops) return;
+ var pX = Event.pointerX(event);
+ var pY = Event.pointerY(event);
+ if(this.include_scroll_offsets) Position.prepare();
+
+ var i = this.drops.length-1; do {
+ var drop = this.drops[i];
+ if(this.is_affected(pX, pY, element, drop)) {
+ if(drop.droppable.onHover)
+ drop.droppable.onHover(
+ element, drop, Position.overlap(drop.droppable.overlap, drop));
+ if(drop.droppable.greedy) {
+ this.activate(drop);
+ return;
+ }
+ }
+ } while (i--);
+ },
+
+ fire: function(event, element) {
+ if(!this.drops) return;
+ var pX = Event.pointerX(event);
+ var pY = Event.pointerY(event);
+ if(this.include_scroll_offsets) Position.prepare();
+
+ var i = this.drops.length-1; do {
+ var drop = this.drops[i];
+ if(this.is_affected(pX, pY, element, drop))
+ if(drop.droppable.onDrop)
+ drop.droppable.onDrop(element);
+ } while (i--);
+ },
+
+ reset: function() {
+ if(this.last_active)
+ this.deactivate(this.last_active);
+ }
+}
+
+Draggables = {
+ observers: new Array(),
+ addObserver: function(observer) {
+ this.observers.push(observer);
+ },
+ notify: function(eventName) { // 'onStart', 'onEnd'
+ for(var i = 0; i < this.observers.length; i++)
+ this.observers[i][eventName]();
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+Draggable = Class.create();
+Draggable.prototype = {
+ initialize: function(element) {
+ var options = {
+ handle: false,
+ starteffect: function(element) {
+ new Effect2.Opacity(element, {duration:0.2, from:1.0, to:0.7});
+ },
+ reverteffect: function(element, top_offset, left_offset) {
+ new Effect2.MoveBy(element, -top_offset, -left_offset, {duration:0.4});
+ },
+ endeffect: function(element) {
+ new Effect2.Opacity(element, {duration:0.2, from:0.7, to:1.0});
+ },
+ zindex: 1000
+ }.extend(arguments[1] || {});
+
+ this.element = $(element);
+ this.element.drag = this;
+ this.handle = options.handle ? $(options.handle) : this.element;
+
+ this.offsetX = 0;
+ this.offsetY = 0;
+ this.originalLeft = this.currentLeft();
+ this.originalTop = this.currentTop();
+ this.originalX = this.element.offsetLeft;
+ this.originalY = this.element.offsetTop;
+ this.originalZ = parseInt(this.element.style.zIndex || "0");
+
+ this.options = options;
+
+ this.active = false;
+ this.dragging = false;
+
+ Event.observe(this.handle, "mousedown", this.startDrag.bindAsEventListener(this));
+ Event.observe(document, "mouseup", this.endDrag.bindAsEventListener(this));
+ Event.observe(document, "mousemove", this.update.bindAsEventListener(this));
+ },
+ currentLeft: function() {
+ return parseInt(this.element.style.left || '0');
+ },
+ currentTop: function() {
+ return parseInt(this.element.style.top || '0')
+ },
+ startDrag: function(event) {
+ if(Event.isLeftClick(event)) {
+ this.active = true;
+
+ var style = this.element.style;
+ this.originalY = this.element.offsetTop - this.currentTop(); - this.originalTop;
+ this.originalX = this.element.offsetLeft - this.currentLeft(); - this.originalLeft;
+ this.offsetY = event.clientY - this.originalY - this.originalTop;
+ this.offsetX = event.clientX - this.originalX - this.originalLeft;
+
+ Event.stop(event);
+ }
+ },
+ endDrag: function(event) {
+ if(this.active && this.dragging) {
+ this.active = false;
+ this.dragging = false;
+
+ Droppables.fire(event, this.element);
+ Draggables.notify('onEnd');
+
+ if(this.options.revert && this.options.reverteffect) {
+ this.options.reverteffect(this.element,
+ this.currentTop()-this.originalTop,
+ this.currentLeft()-this.originalLeft);
+ } else {
+ this.originalLeft = this.currentLeft();
+ this.originalTop = this.currentTop();
+ }
+ this.element.style.zIndex = this.originalZ;
+
+ if(this.options.endeffect)
+ this.options.endeffect(this.element);
+
+ Droppables.reset();
+ Event.stop(event);
+ }
+ this.active = false;
+ this.dragging = false;
+ },
+ draw: function(event) {
+ var style = this.element.style;
+ this.originalX = this.element.offsetLeft - this.currentLeft() - this.originalLeft;
+ this.originalY = this.element.offsetTop - this.currentTop() - this.originalTop;
+ if((!this.options.constraint) || (this.options.constraint=='horizontal'))
+ style.left = ((event.clientX - this.originalX) - this.offsetX) + "px";
+ if((!this.options.constraint) || (this.options.constraint=='vertical'))
+ style.top = ((event.clientY - this.originalY) - this.offsetY) + "px";
+ if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
+ },
+ update: function(event) {
+ if(this.active) {
+ if(!this.dragging) {
+ var style = this.element.style;
+ this.dragging = true;
+ if(style.position=="") style.position = "relative";
+ style.zIndex = this.options.zindex;
+ Draggables.notify('onStart');
+ if(this.options.starteffect) this.options.starteffect(this.element);
+ }
+
+ Droppables.show(event, this.element);
+ this.draw(event);
+ if(this.options.change) this.options.change(this);
+
+ // fix AppleWebKit rendering
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+
+ Event.stop(event);
+ }
+ }
+}
+
+/*--------------------------------------------------------------------------*/
+
+SortableObserver = Class.create();
+SortableObserver.prototype = {
+ initialize: function(element, observer) {
+ this.element = $(element);
+ this.observer = observer;
+ this.lastValue = Sortable.serialize(this.element);
+ },
+ onStart: function() {
+ this.lastValue = Sortable.serialize(this.element);
+ },
+ onEnd: function() {
+ if(this.lastValue != Sortable.serialize(this.element))
+ this.observer(this.element)
+ }
+}
+
+Sortable = {
+ create: function(element) {
+ var element = $(element);
+ var options = {
+ tag: 'li', // assumes li children, override with tag: 'tagname'
+ overlap: 'vertical', // one of 'vertical', 'horizontal'
+ constraint: 'vertical', // one of 'vertical', 'horizontal', false
+ containment: element, // also takes array of elements (or id's); or false
+ handle: false, // or a CSS class
+ only: false,
+ hoverclass: null,
+ onChange: function() {},
+ onUpdate: function() {}
+ }.extend(arguments[1] || {});
+ element.sortable = options;
+
+ // build options for the draggables
+ var options_for_draggable = {
+ revert: true,
+ constraint: options.constraint,
+ handle: handle };
+ if(options.starteffect)
+ options_for_draggable.starteffect = options.starteffect;
+ if(options.reverteffect)
+ options_for_draggable.reverteffect = options.reverteffect;
+ if(options.endeffect)
+ options_for_draggable.endeffect = options.endeffect;
+ if(options.zindex)
+ options_for_draggable.zindex = options.zindex;
+
+ // build options for the droppables
+ var options_for_droppable = {
+ overlap: options.overlap,
+ containment: options.containment,
+ hoverclass: options.hoverclass,
+ onHover: function(element, dropon, overlap) {
+ if(overlap>0.5) {
+ if(dropon.previousSibling != element) {
+ var oldParentNode = element.parentNode;
+ element.style.visibility = "hidden"; // fix gecko rendering
+ dropon.parentNode.insertBefore(element, dropon);
+ if(dropon.parentNode!=oldParentNode && oldParentNode.sortable)
+ oldParentNode.sortable.onChange(element);
+ if(dropon.parentNode.sortable)
+ dropon.parentNode.sortable.onChange(element);
+ }
+ } else {
+ var nextElement = dropon.nextSibling || null;
+ if(nextElement != element) {
+ var oldParentNode = element.parentNode;
+ element.style.visibility = "hidden"; // fix gecko rendering
+ dropon.parentNode.insertBefore(element, nextElement);
+ if(dropon.parentNode!=oldParentNode && oldParentNode.sortable)
+ oldParentNode.sortable.onChange(element);
+ if(dropon.parentNode.sortable)
+ dropon.parentNode.sortable.onChange(element);
+ }
+ }
+ }
+ }
+
+ // fix for gecko engine
+ Element.cleanWhitespace(element);
+
+ // for onupdate
+ Draggables.addObserver(new SortableObserver(element, options.onUpdate));
+
+ // make it so
+ var elements = element.childNodes;
+ for (var i = 0; i < elements.length; i++)
+ if(elements[i].tagName && elements[i].tagName==options.tag.toUpperCase() &&
+ (!options.only || (Element.Class.has(elements[i], options.only)))) {
+
+ // handles are per-draggable
+ var handle = options.handle ?
+ Element.Class.childrenWith(elements[i], options.handle)[0] : elements[i];
+
+ new Draggable(elements[i], options_for_draggable.extend({ handle: handle }));
+ Droppables.add(elements[i], options_for_droppable);
+ }
+
+ },
+ serialize: function(element) {
+ var element = $(element);
+ var options = {
+ tag: element.sortable.tag,
+ only: element.sortable.only,
+ name: element.id
+ }.extend(arguments[1] || {});
+
+ var items = $(element).childNodes;
+ var queryComponents = new Array();
+
+ for(var i=0; i<items.length; i++)
+ if(items[i].tagName && items[i].tagName==options.tag.toUpperCase() &&
+ (!options.only || (Element.Class.has(items[i], options.only))))
+ queryComponents.push(
+ encodeURIComponent(options.name) + "[]=" +
+ encodeURIComponent(items[i].id.split("_")[1]));
+
+ return queryComponents.join("&");
+ }
+}
+
+/*--------------------------------------------------------------------------*/ \ No newline at end of file
diff --git a/actionpack/lib/action_view/helpers/javascripts/effects.js b/actionpack/lib/action_view/helpers/javascripts/effects.js
new file mode 100644
index 0000000000..82f1fd53ed
--- /dev/null
+++ b/actionpack/lib/action_view/helpers/javascripts/effects.js
@@ -0,0 +1,557 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//
+// Parts (c) 2005 Justin Palmer (http://encytemedia.com/)
+// Parts (c) 2005 Mark Pilgrim (http://diveintomark.org/)
+//
+// 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.
+
+
+Effect = {}
+Effect2 = Effect; // deprecated
+
+/* ------------- transitions ------------- */
+
+Effect.Transitions = {}
+
+Effect.Transitions.linear = function(pos) {
+ return pos;
+}
+Effect.Transitions.sinoidal = function(pos) {
+ return (-Math.cos(pos*Math.PI)/2) + 0.5;
+}
+Effect.Transitions.reverse = function(pos) {
+ return 1-pos;
+}
+Effect.Transitions.flicker = function(pos) {
+ return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random(0.25);
+}
+Effect.Transitions.wobble = function(pos) {
+ return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
+}
+Effect.Transitions.pulse = function(pos) {
+ return (Math.floor(pos*10) % 2 == 0 ?
+ (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10)));
+}
+
+Effect.Transitions.none = function(pos) {
+ return 0;
+}
+Effect.Transitions.full = function(pos) {
+ return 1;
+}
+
+/* ------------- core effects ------------- */
+
+Effect.Base = function() {};
+Effect.Base.prototype = {
+ setOptions: function(options) {
+ this.options = {
+ transition: Effect.Transitions.sinoidal,
+ duration: 1.0, // seconds
+ fps: 25.0, // max. 100fps
+ sync: false, // true for combining
+ from: 0.0,
+ to: 1.0
+ }.extend(options || {});
+ },
+ start: function(options) {
+ this.setOptions(options || {});
+ this.currentFrame = 0;
+ this.startOn = new Date().getTime();
+ this.finishOn = this.startOn + (this.options.duration*1000);
+ if(this.options.beforeStart) this.options.beforeStart(this);
+ if(!this.options.sync) this.loop();
+ },
+ loop: function() {
+ timePos = new Date().getTime();
+ if(timePos >= this.finishOn) {
+ this.render(this.options.to);
+ if(this.finish) this.finish();
+ if(this.options.afterFinish) this.options.afterFinish(this);
+ return;
+ }
+ pos = (timePos - this.startOn) / (this.finishOn - this.startOn);
+ frame = Math.round(pos * this.options.fps * this.options.duration);
+ if(frame > this.currentFrame) {
+ this.render(pos);
+ this.currentFrame = frame;
+ }
+ this.timeout = setTimeout(this.loop.bind(this), 10);
+ },
+ render: function(pos) {
+ if(this.options.transition) pos = this.options.transition(pos);
+ pos = pos * (this.options.to-this.options.from);
+ pos += this.options.from;
+ if(this.options.beforeUpdate) this.options.beforeUpdate(this);
+ if(this.update) this.update(pos);
+ if(this.options.afterUpdate) this.options.afterUpdate(this);
+ },
+ cancel: function() {
+ if(this.timeout) clearTimeout(this.timeout);
+ }
+}
+
+Effect.Parallel = Class.create();
+ Effect.Parallel.prototype = (new Effect.Base()).extend({
+ initialize: function(effects) {
+ this.effects = effects || [];
+ this.start(arguments[1]);
+ },
+ update: function(position) {
+ for (var i = 0; i < this.effects.length; i++)
+ this.effects[i].render(position);
+ },
+ finish: function(position) {
+ for (var i = 0; i < this.effects.length; i++)
+ if(this.effects[i].finish) this.effects[i].finish(position);
+ }
+ });
+
+Effect.Opacity = Class.create();
+Effect.Opacity.prototype = (new Effect.Base()).extend({
+ initialize: function(element) {
+ this.element = $(element);
+ options = {
+ from: 0.0,
+ to: 1.0
+ }.extend(arguments[1] || {});
+ this.start(options);
+ },
+ update: function(position) {
+ this.setOpacity(position);
+ },
+ setOpacity: function(opacity) {
+ opacity = (opacity == 1) ? 0.99999 : opacity;
+ this.element.style.opacity = opacity;
+ this.element.style.filter = "alpha(opacity:"+opacity*100+")";
+ }
+});
+
+Effect.MoveBy = Class.create();
+ Effect.MoveBy.prototype = (new Effect.Base()).extend({
+ initialize: function(element, toTop, toLeft) {
+ this.element = $(element);
+ this.originalTop = parseFloat(this.element.style.top || '0');
+ this.originalLeft = parseFloat(this.element.style.left || '0');
+ this.toTop = toTop;
+ this.toLeft = toLeft;
+ if(this.element.style.position == "")
+ this.element.style.position = "relative";
+ this.start(arguments[3]);
+ },
+ update: function(position) {
+ topd = this.toTop * position + this.originalTop;
+ leftd = this.toLeft * position + this.originalLeft;
+ this.setPosition(topd, leftd);
+ },
+ setPosition: function(topd, leftd) {
+ this.element.style.top = topd + "px";
+ this.element.style.left = leftd + "px";
+ }
+});
+
+Effect.Scale = Class.create();
+Effect.Scale.prototype = (new Effect.Base()).extend({
+ initialize: function(element, percent) {
+ this.element = $(element)
+ options = {
+ scaleX: true,
+ scaleY: true,
+ scaleContent: true,
+ scaleFromCenter: false,
+ scaleMode: 'box', // 'box' or 'contents' or {} with provided values
+ scaleFrom: 100.0
+ }.extend(arguments[2] || {});
+ this.originalTop = this.element.offsetTop;
+ this.originalLeft = this.element.offsetLeft;
+ if (this.element.style.fontSize=="") this.sizeEm = 1.0;
+ if (this.element.style.fontSize && this.element.style.fontSize.indexOf("em")>0)
+ this.sizeEm = parseFloat(this.element.style.fontSize);
+ this.factor = (percent/100.0) - (options.scaleFrom/100.0);
+ if(options.scaleMode=='box') {
+ this.originalHeight = this.element.clientHeight;
+ this.originalWidth = this.element.clientWidth;
+ } else
+ if(options.scaleMode=='contents') {
+ this.originalHeight = this.element.scrollHeight;
+ this.originalWidth = this.element.scrollWidth;
+ } else {
+ this.originalHeight = options.scaleMode.originalHeight;
+ this.originalWidth = options.scaleMode.originalWidth;
+ }
+ this.start(options);
+ },
+
+ update: function(position) {
+ currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
+ if(this.options.scaleContent && this.sizeEm)
+ this.element.style.fontSize = this.sizeEm*currentScale + "em";
+ this.setDimensions(
+ this.originalWidth * currentScale,
+ this.originalHeight * currentScale);
+ },
+
+ setDimensions: function(width, height) {
+ if(this.options.scaleX) this.element.style.width = width + 'px';
+ if(this.options.scaleY) this.element.style.height = height + 'px';
+ if(this.options.scaleFromCenter) {
+ topd = (height - this.originalHeight)/2;
+ leftd = (width - this.originalWidth)/2;
+ if(this.element.style.position=='absolute') {
+ if(this.options.scaleY) this.element.style.top = this.originalTop-topd + "px";
+ if(this.options.scaleX) this.element.style.left = this.originalLeft-leftd + "px";
+ } else {
+ if(this.options.scaleY) this.element.style.top = -topd + "px";
+ if(this.options.scaleX) this.element.style.left = -leftd + "px";
+ }
+ }
+ }
+});
+
+Effect.Highlight = Class.create();
+Effect.Highlight.prototype = (new Effect.Base()).extend({
+ initialize: function(element) {
+ this.element = $(element);
+
+ // try to parse current background color as default for endcolor
+ // browser stores this as: "rgb(255, 255, 255)", convert to "#ffffff" format
+ var endcolor = "#ffffff";
+ var current = this.element.style.backgroundColor;
+ if(current && current.slice(0,4) == "rgb(") {
+ endcolor = "#";
+ var cols = current.slice(4,current.length-1).split(',');
+ var i=0; do { endcolor += parseInt(cols[i]).toColorPart() } while (++i<3); }
+
+ var options = {
+ startcolor: "#ffff99",
+ endcolor: endcolor
+ }.extend(arguments[1] || {});
+
+ // init color calculations
+ this.colors_base = [
+ parseInt(options.startcolor.slice(1,3),16),
+ parseInt(options.startcolor.slice(3,5),16),
+ parseInt(options.startcolor.slice(5),16) ];
+ this.colors_delta = [
+ parseInt(options.endcolor.slice(1,3),16)-this.colors_base[0],
+ parseInt(options.endcolor.slice(3,5),16)-this.colors_base[1],
+ parseInt(options.endcolor.slice(5),16)-this.colors_base[2] ];
+
+ this.start(options);
+ },
+ update: function(position) {
+ var colors = [
+ Math.round(this.colors_base[0]+(this.colors_delta[0]*position)),
+ Math.round(this.colors_base[1]+(this.colors_delta[1]*position)),
+ Math.round(this.colors_base[2]+(this.colors_delta[2]*position)) ];
+ this.element.style.backgroundColor = "#" +
+ colors[0].toColorPart() + colors[1].toColorPart() + colors[2].toColorPart();
+ }
+});
+
+
+/* ------------- prepackaged effects ------------- */
+
+Effect.Fade = function(element) {
+ options = {
+ from: 1.0,
+ to: 0.0,
+ afterFinish: function(effect)
+ { Element.hide(effect.element);
+ effect.setOpacity(1); }
+ }.extend(arguments[1] || {});
+ new Effect.Opacity(element,options);
+}
+
+Effect.Appear = function(element) {
+ options = {
+ from: 0.0,
+ to: 1.0,
+ beforeStart: function(effect)
+ { effect.setOpacity(0);
+ Element.show(effect.element); },
+ afterUpdate: function(effect)
+ { Element.show(effect.element); }
+ }.extend(arguments[1] || {});
+ new Effect.Opacity(element,options);
+}
+
+Effect.Puff = function(element) {
+ new Effect.Parallel(
+ [ new Effect.Scale(element, 200, { sync: true, scaleFromCenter: true }),
+ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0 } ) ],
+ { duration: 1.0,
+ afterUpdate: function(effect)
+ { effect.effects[0].element.style.position = 'absolute'; },
+ afterFinish: function(effect)
+ { Element.hide(effect.effects[0].element); }
+ }
+ );
+}
+
+Effect.BlindUp = function(element) {
+ $(element).style.overflow = 'hidden';
+ new Effect.Scale(element, 0,
+ { scaleContent: false,
+ scaleX: false,
+ afterFinish: function(effect)
+ { Element.hide(effect.element) }
+ }.extend(arguments[1] || {})
+ );
+}
+
+Effect.BlindDown = function(element) {
+ $(element).style.height = '0px';
+ $(element).style.overflow = 'hidden';
+ Element.show(element);
+ new Effect.Scale(element, 100,
+ { scaleContent: false,
+ scaleX: false,
+ scaleMode: 'contents',
+ scaleFrom: 0
+ }.extend(arguments[1] || {})
+ );
+}
+
+Effect.SwitchOff = function(element) {
+ new Effect.Appear(element,
+ { duration: 0.4,
+ transition: Effect.Transitions.flicker,
+ afterFinish: function(effect)
+ { effect.element.style.overflow = 'hidden';
+ new Effect.Scale(effect.element, 1,
+ { duration: 0.3, scaleFromCenter: true,
+ scaleX: false, scaleContent: false,
+ afterUpdate: function(effect) {
+ if(effect.element.style.position=="")
+ effect.element.style.position = 'relative'; },
+ afterFinish: function(effect) { Element.hide(effect.element); }
+ } )
+ }
+ } )
+}
+
+Effect.DropOut = function(element) {
+ new Effect.Parallel(
+ [ new Effect.MoveBy(element, 100, 0, { sync: true }),
+ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0 } ) ],
+ { duration: 0.5,
+ afterFinish: function(effect)
+ { Element.hide(effect.effects[0].element); }
+ });
+}
+
+Effect.Shake = function(element) {
+ new Effect.MoveBy(element, 0, 20,
+ { duration: 0.05, afterFinish: function(effect) {
+ new Effect.MoveBy(effect.element, 0, -40,
+ { duration: 0.1, afterFinish: function(effect) {
+ new Effect.MoveBy(effect.element, 0, 40,
+ { duration: 0.1, afterFinish: function(effect) {
+ new Effect.MoveBy(effect.element, 0, -40,
+ { duration: 0.1, afterFinish: function(effect) {
+ new Effect.MoveBy(effect.element, 0, 40,
+ { duration: 0.1, afterFinish: function(effect) {
+ new Effect.MoveBy(effect.element, 0, -20,
+ { duration: 0.05, afterFinish: function(effect) {
+ }}) }}) }}) }}) }}) }});
+}
+
+Effect.SlideDown = function(element) {
+ $(element).style.height = '0px';
+ $(element).style.overflow = 'hidden';
+ $(element).firstChild.style.position = 'relative';
+ Element.show(element);
+ new Effect.Scale(element, 100,
+ { scaleContent: false,
+ scaleX: false,
+ scaleMode: 'contents',
+ scaleFrom: 0,
+ afterUpdate: function(effect)
+ { effect.element.firstChild.style.bottom =
+ (effect.originalHeight - effect.element.clientHeight) + 'px'; }
+ }.extend(arguments[1] || {})
+ );
+}
+
+Effect.SlideUp = function(element) {
+ $(element).style.overflow = 'hidden';
+ $(element).firstChild.style.position = 'relative';
+ Element.show(element);
+ new Effect.Scale(element, 0,
+ { scaleContent: false,
+ scaleX: false,
+ afterUpdate: function(effect)
+ { effect.element.firstChild.style.bottom =
+ (effect.originalHeight - effect.element.clientHeight) + 'px'; },
+ afterFinish: function(effect)
+ { Element.hide(effect.element); }
+ }.extend(arguments[1] || {})
+ );
+}
+
+Effect.Squish = function(element) {
+ new Effect.Scale(element, 0,
+ { afterFinish: function(effect) { Element.hide(effect.element); } });
+}
+
+Effect.Grow = function(element) {
+ element = $(element);
+ var options = arguments[1] || {};
+
+ var originalWidth = element.clientWidth;
+ var originalHeight = element.clientHeight;
+ element.style.overflow = 'hidden';
+ Element.show(element);
+
+ var direction = options.direction || 'center';
+ var moveTransition = options.moveTransition || Effect.Transitions.sinoidal;
+ var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal;
+ var opacityTransition = options.opacityTransition || Effect.Transitions.full;
+
+ var initialMoveX, initialMoveY;
+ var moveX, moveY;
+
+ switch (direction) {
+ case 'top-left':
+ initialMoveX = initialMoveY = moveX = moveY = 0;
+ break;
+ case 'top-right':
+ initialMoveX = originalWidth;
+ initialMoveY = moveY = 0;
+ moveX = -originalWidth;
+ break;
+ case 'bottom-left':
+ initialMoveX = moveX = 0;
+ initialMoveY = originalHeight;
+ moveY = -originalHeight;
+ break;
+ case 'bottom-right':
+ initialMoveX = originalWidth;
+ initialMoveY = originalHeight;
+ moveX = -originalWidth;
+ moveY = -originalHeight;
+ break;
+ case 'center':
+ initialMoveX = originalWidth / 2;
+ initialMoveY = originalHeight / 2;
+ moveX = -originalWidth / 2;
+ moveY = -originalHeight / 2;
+ break;
+ }
+
+ new Effect.MoveBy(element, initialMoveY, initialMoveX, {
+ duration: 0.01,
+ beforeUpdate: function(effect) { $(element).style.height = '0px'; },
+ afterFinish: function(effect) {
+ new Effect.Parallel(
+ [ new Effect.Opacity(element, { sync: true, to: 1.0, from: 0.0, transition: opacityTransition }),
+ new Effect.MoveBy(element, moveY, moveX, { sync: true, transition: moveTransition }),
+ new Effect.Scale(element, 100, {
+ scaleMode: { originalHeight: originalHeight, originalWidth: originalWidth },
+ sync: true, scaleFrom: 0, scaleTo: 100, transition: scaleTransition })],
+ options); }
+ });
+}
+
+Effect.Shrink = function(element) {
+ element = $(element);
+ var options = arguments[1] || {};
+
+ var originalWidth = element.clientWidth;
+ var originalHeight = element.clientHeight;
+ element.style.overflow = 'hidden';
+ Element.show(element);
+
+ var direction = options.direction || 'center';
+ var moveTransition = options.moveTransition || Effect.Transitions.sinoidal;
+ var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal;
+ var opacityTransition = options.opacityTransition || Effect.Transitions.none;
+
+ var moveX, moveY;
+
+ switch (direction) {
+ case 'top-left':
+ moveX = moveY = 0;
+ break;
+ case 'top-right':
+ moveX = originalWidth;
+ moveY = 0;
+ break;
+ case 'bottom-left':
+ moveX = 0;
+ moveY = originalHeight;
+ break;
+ case 'bottom-right':
+ moveX = originalWidth;
+ moveY = originalHeight;
+ break;
+ case 'center':
+ moveX = originalWidth / 2;
+ moveY = originalHeight / 2;
+ break;
+ }
+
+ new Effect.Parallel(
+ [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: opacityTransition }),
+ new Effect.Scale(element, 0, { sync: true, transition: moveTransition }),
+ new Effect.MoveBy(element, moveY, moveX, { sync: true, transition: scaleTransition }) ],
+ options);
+}
+
+Effect.Pulsate = function(element) {
+ var options = arguments[1] || {};
+ var transition = options.transition || Effect.Transitions.sinoidal;
+ var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) };
+ reverser.bind(transition);
+ new Effect.Opacity(element,
+ { duration: 3.0,
+ afterFinish: function(effect) { Element.show(effect.element); }
+ }.extend(options).extend({transition: reverser}));
+}
+
+Effect.Fold = function(element) {
+ $(element).style.overflow = 'hidden';
+ new Effect2.Scale(element, 5, {
+ scaleContent: false,
+ scaleTo: 100,
+ scaleX: false,
+ afterFinish: function(effect) {
+ new Effect.Scale(element, 1, {
+ scaleContent: false,
+ scaleTo: 0,
+ scaleY: false,
+ afterFinish: function(effect) { Element.hide(effect.element) } });
+ }}.extend(arguments[1] || {}));
+}
+
+// old: new Effect.ContentZoom(element, percent)
+// new: Element.setContentZoom(element, percent)
+
+Element.setContentZoom = function(element, percent) {
+ var element = $(element);
+
+ var sizeEm = 1.0;
+ if (element.style.fontSize.indexOf("em")>0)
+ sizeEm = parseFloat(element.style.fontSize);
+
+ element.style.fontSize = sizeEm*(percent/100) + "em";
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+} \ No newline at end of file
diff --git a/actionpack/lib/action_view/helpers/javascripts/prototype.js b/actionpack/lib/action_view/helpers/javascripts/prototype.js
index ca772aa8fa..1ebb2a46b7 100644
--- a/actionpack/lib/action_view/helpers/javascripts/prototype.js
+++ b/actionpack/lib/action_view/helpers/javascripts/prototype.js
@@ -559,205 +559,279 @@ Insertion.After.prototype = (new Abstract.Insertion('afterEnd')).extend({
}
});
-/*--------------------------------------------------------------------------*/
-
-var Effect = new Object();
-Effect.Highlight = Class.create();
-Effect.Highlight.prototype = {
- initialize: function(element) {
- this.element = $(element);
- this.start = 153;
- this.finish = 255;
- this.current = this.start;
- this.fade();
- },
-
- fade: function() {
- if (this.isFinished()) return;
- if (this.timer) clearTimeout(this.timer);
- this.highlight(this.element, this.current);
- this.current += 17;
- this.timer = setTimeout(this.fade.bind(this), 250);
- },
-
- isFinished: function() {
- return this.current > this.finish;
- },
-
- highlight: function(element, current) {
- element.style.backgroundColor = "#ffff" + current.toColorPart();
+// === Prototype Extension ====================================================
+
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//
+// Element.Class part Copyright (c) 2005 by Rick Olson
+//
+// 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.
+
+var Event = {
+ KEY_BACKSPACE: 8,
+ KEY_TAB: 9,
+ KEY_RETURN: 13,
+ KEY_ESC: 27,
+ KEY_LEFT: 37,
+ KEY_UP: 38,
+ KEY_RIGHT: 39,
+ KEY_DOWN: 40,
+ KEY_DELETE: 46,
+
+ element: function(event) {
+ return event.srcElement || event.currentTarget;
+ },
+
+ isLeftClick: function(event) {
+ return (((event.which) && (event.which == 1)) ||
+ ((event.button) && (event.button == 1)));
+ },
+
+ pointerX: function(event) {
+ return event.pageX || (event.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft));
+ },
+
+ pointerY: function(event) {
+ return event.pageY || (event.clientY + (document.documentElement.scrollTop || document.body.scrollTop));
+ },
+
+ stop: function(event) {
+ if(event.preventDefault)
+ { event.preventDefault(); event.stopPropagation(); }
+ else
+ event.returnValue = false;
+ },
+
+ // find the first node with the given tagName, starting from the
+ // node the event was triggered on, traverses the DOM upwards
+ findElement: function(event, tagName) {
+ element = Event.element(event);
+ while(element.tagName.toUpperCase() != tagName.toUpperCase() && element.parentNode)
+ element = element.parentNode;
+ return element;
+ },
+
+ observe: function(element, name, observer) {
+ if(name=='keypress') {
+ if(navigator.appVersion.indexOf('AppleWebKit')>0)
+ { $(element).addEventListener("keydown",observer,false); return; }
+ if($(element).addEventListener) $(element).addEventListener("keypress",observer,false)
+ else if($(element).attachEvent) $(element).attachEvent("onkeydown",observer);
+ } else {
+ if($(element).addEventListener) $(element).addEventListener(name,observer,false)
+ else if($(element).attachEvent) $(element).attachEvent("on" + name,observer);
+ }
}
}
+/*--------------------------------------------------------------------------*/
-Effect.Fade = Class.create();
-Effect.Fade.prototype = {
- initialize: function(element) {
- this.element = $(element);
- this.start = 100;
- this.finish = 0;
- this.current = this.start;
- this.fade();
- },
-
- fade: function() {
- if (this.isFinished()) { this.element.style.display = 'none'; return; }
- if (this.timer) clearTimeout(this.timer);
- this.setOpacity(this.element, this.current);
- this.current -= 10;
- this.timer = setTimeout(this.fade.bind(this), 50);
- },
-
- isFinished: function() {
- return this.current <= this.finish;
- },
-
- setOpacity: function(element, opacity) {
- opacity = (opacity == 100) ? 99.999 : opacity;
- element.style.filter = "alpha(opacity:"+opacity+")";
- element.style.opacity = opacity/100 /*//*/;
+// removes whitespace-only text node children
+// needed to make Gecko-based browsers happy
+Element.cleanWhitespace = function(element) {
+ var element = $(element);
+ for(var i=0;i<element.childNodes.length;i++) {
+ var node = element.childNodes[i];
+ if(node.nodeType==3 && !/\S/.test(node.nodeValue))
+ Element.remove(node);
}
}
-Effect.Scale = Class.create();
-Effect.Scale.prototype = {
- initialize: function(element, percent) {
- this.element = $(element);
- this.startScale = 1.0;
- this.startHeight = this.element.offsetHeight;
- this.startWidth = this.element.offsetWidth;
- this.currentHeight = this.startHeight;
- this.currentWidth = this.startWidth;
- this.finishScale = (percent/100) /*//*/;
- if (this.element.style.fontSize=="") this.sizeEm = 1.0;
- if (this.element.style.fontSize.indexOf("em")>0)
- this.sizeEm = parseFloat(this.element.style.fontSize);
- if(this.element.effect_scale) {
- clearTimeout(this.element.effect_scale.timer);
- this.startScale = this.element.effect_scale.currentScale;
- this.startHeight = this.element.effect_scale.startHeight;
- this.startWidth = this.element.effect_scale.startWidth;
- if(this.element.effect_scale.sizeEm)
- this.sizeEm = this.element.effect_scale.sizeEm;
- }
- this.element.effect_scale = this;
- this.currentScale = this.startScale;
- this.factor = this.finishScale - this.startScale;
- this.options = arguments[2] || {};
- this.scale();
- },
-
- scale: function() {
- if (this.isFinished()) {
- this.setDimensions(this.element, this.startWidth*this.finishScale, this.startHeight*this.finishScale);
- if(this.sizeEm) this.element.style.fontSize = this.sizeEm*this.finishScale + "em";
- if(this.options.complete) this.options.complete(this);
- return;
- }
- if (this.timer) clearTimeout(this.timer);
- if (this.options.step) this.options.step(this);
- this.setDimensions(this.element, this.currentWidth, this.currentHeight);
- if(this.sizeEm) this.element.style.fontSize = this.sizeEm*this.currentScale + "em";
- this.currentScale += (this.factor/10) /*//*/;
- this.currentWidth = this.startWidth * this.currentScale;
- this.currentHeight = this.startHeight * this.currentScale;
- this.timer = setTimeout(this.scale.bind(this), 50);
- },
-
- isFinished: function() {
- return (this.factor < 0) ?
- this.currentScale <= this.finishScale : this.currentScale >= this.finishScale;
- },
+Element.collectTextNodesIgnoreClass = function(element, ignoreclass) {
+ var children = $(element).childNodes;
+ var text = "";
+ var classtest = new RegExp("^([^ ]+ )*" + ignoreclass+ "( [^ ]+)*$","i");
- setDimensions: function(element, width, height) {
- element.style.width = width + 'px';
- element.style.height = height + 'px';
+ for (var i = 0; i < children.length; i++) {
+ if(children[i].nodeType==3) {
+ text+=children[i].nodeValue;
+ } else {
+ if((!children[i].className.match(classtest)) && children[i].hasChildNodes())
+ text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass);
+ }
}
+
+ return text;
}
-Effect.Squish = Class.create();
-Effect.Squish.prototype = {
- initialize: function(element) {
- this.element = $(element);
- new Effect.Scale(this.element, 1, { complete: this.hide.bind(this) } );
- },
- hide: function() {
- this.element.style.display = 'none';
- }
-}
+/*--------------------------------------------------------------------------*/
-Effect.Puff = Class.create();
-Effect.Puff.prototype = {
- initialize: function(element) {
- this.element = $(element);
- this.opacity = 100;
- this.startTop = this.element.top || this.element.offsetTop;
- this.startLeft = this.element.left || this.element.offsetLeft;
- new Effect.Scale(this.element, 200, { step: this.fade.bind(this), complete: this.hide.bind(this) } );
- },
- fade: function(effect) {
- topd = (((effect.currentScale)*effect.startHeight) - effect.startHeight)/2;
- leftd = (((effect.currentScale)*effect.startWidth) - effect.startWidth)/2;
- this.element.style.position='absolute';
- this.element.style.top = this.startTop-topd + "px";
- this.element.style.left = this.startLeft-leftd + "px";
- this.opacity -= 10;
- this.setOpacity(this.element, this.opacity);
- if(navigator.appVersion.indexOf('AppleWebKit')>0) this.element.innerHTML += ''; //force redraw on safari
- },
- hide: function() {
- this.element.style.display = 'none';
+Text = {
+ stripTags: function(htmlstr) {
+ return htmlstr.replace(/<\/?[^>]+>/gi,"");
},
- setOpacity: function(element, opacity) {
- opacity = (opacity == 100) ? 99.999 : opacity;
- element.style.filter = "alpha(opacity:"+opacity+")";
- element.style.opacity = opacity/100 /*//*/;
+ decodeHTML: function(htmlstr) {
+ return htmlstr.replace(/&lt;/gi,"<").replace(/&gt;/gi,">").replace(/&quot;/gi,'"').replace(/&apos;/gi,"'").replace(/&amp;/gi,"&").replace(/[\n\r]/gi,"");
}
}
-Effect.Appear = Class.create();
-Effect.Appear.prototype = {
- initialize: function(element) {
- this.element = $(element);
- this.start = 0;
- this.finish = 100;
- this.current = this.start;
- this.fade();
- },
-
- fade: function() {
- if (this.isFinished()) return;
- if (this.timer) clearTimeout(this.timer);
- this.setOpacity(this.element, this.current);
- this.current += 10;
- this.timer = setTimeout(this.fade.bind(this), 50);
- },
-
- isFinished: function() {
- return this.current > this.finish;
- },
-
- setOpacity: function(element, opacity) {
- opacity = (opacity == 100) ? 99.999 : opacity;
- element.style.filter = "alpha(opacity:"+opacity+")";
- element.style.opacity = opacity/100 /*//*/;
- element.style.display = '';
- }
-}
+/*--------------------------------------------------------------------------*/
-Effect.ContentZoom = Class.create();
-Effect.ContentZoom.prototype = {
- initialize: function(element, percent) {
- this.element = $(element);
- if (this.element.style.fontSize=="") this.sizeEm = 1.0;
- if (this.element.style.fontSize.indexOf("em")>0)
- this.sizeEm = parseFloat(this.element.style.fontSize);
- if(this.element.effect_contentzoom) {
- this.sizeEm = this.element.effect_contentzoom.sizeEm;
+Element.Class = {
+ // Element.toggleClass(element, className) toggles the class being on/off
+ // Element.toggleClass(element, className1, className2) toggles between both classes,
+ // defaulting to className1 if neither exist
+ toggle: function(element, className) {
+ if(Element.Class.has(element, className)) {
+ Element.Class.remove(element, className);
+ if(arguments.length == 3) Element.Class.add(element, arguments[2]);
+ } else {
+ Element.Class.add(element, className);
+ if(arguments.length == 3) Element.Class.remove(element, arguments[2]);
+ }
+ },
+
+ // gets space-delimited classnames of an element as an array
+ get: function(element) {
+ element = $(element);
+ return element.className.split(' ');
+ },
+
+ // functions adapted from original functions by Gavin Kistner
+ remove: function(element) {
+ element = $(element);
+ var regEx;
+ for(var i = 1; i < arguments.length; i++) {
+ regEx = new RegExp("^" + arguments[i] + "\\b\\s*|\\s*\\b" + arguments[i] + "\\b", 'g');
+ element.className = element.className.replace(regEx, '')
+ }
+ },
+
+ add: function(element) {
+ element = $(element);
+ for(var i = 1; i < arguments.length; i++) {
+ Element.Class.remove(element, arguments[i]);
+ element.className += (element.className.length > 0 ? ' ' : '') + arguments[i];
+ }
+ },
+
+ // returns true if all given classes exist in said element
+ has: function(element) {
+ element = $(element);
+ if(!element || !element.className) return false;
+ var regEx;
+ for(var i = 1; i < arguments.length; i++) {
+ regEx = new RegExp("\\b" + arguments[i] + "\\b");
+ if(!regEx.test(element.className)) return false;
+ }
+ return true;
+ },
+
+ // expects arrays of strings and/or strings as optional paramters
+ // Element.Class.has_any(element, ['classA','classB','classC'], 'classD')
+ has_any: function(element) {
+ element = $(element);
+ if(!element || !element.className) return false;
+ var regEx;
+ for(var i = 1; i < arguments.length; i++) {
+ if((typeof arguments[i] == 'object') &&
+ (arguments[i].constructor == Array)) {
+ for(var j = 0; j < arguments[i].length; j++) {
+ regEx = new RegExp("\\b" + arguments[i][j] + "\\b");
+ if(regEx.test(element.className)) return true;
+ }
+ } else {
+ regEx = new RegExp("\\b" + arguments[i] + "\\b");
+ if(regEx.test(element.className)) return true;
+ }
+ }
+ return false;
+ },
+
+ childrenWith: function(element, className) {
+ var children = $(element).getElementsByTagName('*');
+ var elements = new Array();
+
+ for (var i = 0; i < children.length; i++) {
+ if (Element.Class.has(children[i], className)) {
+ elements.push(children[i]);
+ break;
+ }
+ }
+
+ return elements;
}
- this.element.effect_contentzoom = this;
- this.element.style.fontSize = this.sizeEm*(percent/100) + "em" /*//*/;
- if(navigator.appVersion.indexOf('AppleWebKit')>0) { this.element.scrollTop -= 1; };
- }
}
+
+/*--------------------------------------------------------------------------*/
+
+var Position = {
+ // must be called before calling within_including_scrolloffset, every time the page is scrolled
+ prepare: function() {
+ this.deltaX = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0;
+ this.deltaY = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
+ this.include_scroll_offsets = true;
+ },
+
+ real_offset: function(element) {
+ var valueT = 0; var valueL = 0;
+ do {
+ valueT += element.scrollTop || 0;
+ valueL += element.scrollLeft || 0;
+ element = element.parentNode;
+ } while(element);
+ return [valueL, valueT];
+ },
+
+ // caches x/y coordinate pair to use with overlap
+ within: function(element, x, y) {
+ if(this.include_scroll_offsets)
+ return within_including_scrolloffsets(element, x, y);
+ this.xcomp = x;
+ this.ycomp = y;
+ var offsettop = element.offsetTop;
+ var offsetleft = element.offsetLeft;
+ return (y>=offsettop &&
+ y<offsettop+element.offsetHeight &&
+ x>=offsetleft &&
+ x<offsetleft+element.offsetWidth);
+ },
+
+ within_including_scrolloffsets: function(element, x, y) {
+ var offsetcache = this.real_offset(element);
+ this.xcomp = x + offsetcache[0] - this.deltaX;
+ this.ycomp = y + offsetcache[1] - this.deltaY;
+ this.xcomp = x;
+ this.ycomp = y;
+ var offsettop = element.offsetTop;
+ var offsetleft = element.offsetLeft;
+ return (y>=offsettop &&
+ y<offsettop+element.offsetHeight &&
+ x>=offsetleft &&
+ x<offsetleft+element.offsetWidth);
+ },
+
+ // within must be called directly before
+ overlap: function(mode, element) {
+ if(!mode) return 0;
+ if(mode == 'vertical')
+ return ((element.offsetTop+element.offsetHeight)-this.ycomp) / element.offsetHeight;
+ if(mode == 'horizontal')
+ return ((element.offsetLeft+element.offsetWidth)-this.xcomp) / element.offsetWidth;
+ },
+
+ clone: function(source, target) {
+ $(target).style.top = $(source).style.top;
+ $(target).style.left = $(source).style.left;
+ $(target).style.width = $(source).offsetWidth + "px";
+ $(target).style.height = $(source).offsetHeight + "px";
+ }
+} \ No newline at end of file
diff --git a/actionpack/test/template/javascript_helper.rb b/actionpack/test/template/javascript_helper.rb
index e8395e6753..f70703c819 100644
--- a/actionpack/test/template/javascript_helper.rb
+++ b/actionpack/test/template/javascript_helper.rb
@@ -16,6 +16,11 @@ class JavascriptHelperTest < Test::Unit::TestCase
end
@controller = @controller.new
end
+
+ def test_define_javascript_functions
+ # check if prototype.js is included first
+ assert_not_nil define_javascript_functions.split("\n")[1].match(/Prototype: an object-oriented Javascript library/)
+ end
def test_escape_javascript
assert_equal %(This \\"thing\\" is really\\n netos\\'), escape_javascript(%(This "thing" is really\n netos'))
@@ -39,7 +44,7 @@ class JavascriptHelperTest < Test::Unit::TestCase
end
def test_form_remote_tag
- assert_equal %(<form onsubmit="new Ajax.Updater('glass_of_beer', 'http://www.example.com/fast', {parameters:Form.serialize(this), asynchronous:true}); return false;">),
+ assert_equal %(<form action="http://www.example.com/fast" method="post" onsubmit="new Ajax.Updater('glass_of_beer', 'http://www.example.com/fast', {parameters:Form.serialize(this), asynchronous:true}); return false;">),
form_remote_tag(:update => "glass_of_beer", :url => { :action => :fast })
end
@@ -58,4 +63,21 @@ class JavascriptHelperTest < Test::Unit::TestCase
observe_form("cart", :frequency => 2, :url => { :action => "cart_changed" })
end
+ def test_remote_autocomplete
+ assert_equal %(<script type="text/javascript">new Ajax.Autocompleter('some_input', 'some_input_autocomplete', 'http://www.example.com/autocomplete', {})</script>),
+ remote_autocomplete("some_input", :url => { :action => "autocomplete" });
+ end
+
+ def test_effect
+ assert_equal "new Effect.Highlight('posts',{});", visual_effect(:highlight, "posts")
+ assert_equal "new Effect.Highlight('posts',{});", visual_effect("highlight", :posts)
+ assert_equal "new Effect.Highlight('posts',{});", visual_effect(:highlight, :posts)
+ assert_equal "new Effect.Fade('fademe',{duration:4.0});", visual_effect(:fade, "fademe", :duration => 4.0)
+ end
+
+ def test_remote_sortable
+ assert_equal %(<script type="text/javascript">Sortable.create('mylist',{onUpdate:function(){new Ajax.Request('http://www.example.com/order', {parameters:Sortable.serialize('mylist'), asynchronous:true})}})</script>),
+ remote_sortable("mylist", :url => { :action => "order" })
+ end
+
end