You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by ma...@apache.org on 2006/01/04 21:49:33 UTC

svn commit: r365979 [1/2] - in /myfaces/tomahawk/trunk/src/main/resources/org/apache/myfaces/custom/prototype/resource: controls.js dragdrop.js effects.js prototype.js

Author: matzew
Date: Wed Jan  4 12:49:21 2006
New Revision: 365979

URL: http://svn.apache.org/viewcvs?rev=365979&view=rev
Log:
MYFACES-871

Modified:
    myfaces/tomahawk/trunk/src/main/resources/org/apache/myfaces/custom/prototype/resource/controls.js
    myfaces/tomahawk/trunk/src/main/resources/org/apache/myfaces/custom/prototype/resource/dragdrop.js
    myfaces/tomahawk/trunk/src/main/resources/org/apache/myfaces/custom/prototype/resource/effects.js
    myfaces/tomahawk/trunk/src/main/resources/org/apache/myfaces/custom/prototype/resource/prototype.js

Modified: myfaces/tomahawk/trunk/src/main/resources/org/apache/myfaces/custom/prototype/resource/controls.js
URL: http://svn.apache.org/viewcvs/myfaces/tomahawk/trunk/src/main/resources/org/apache/myfaces/custom/prototype/resource/controls.js?rev=365979&r1=365978&r2=365979&view=diff
==============================================================================
--- myfaces/tomahawk/trunk/src/main/resources/org/apache/myfaces/custom/prototype/resource/controls.js (original)
+++ myfaces/tomahawk/trunk/src/main/resources/org/apache/myfaces/custom/prototype/resource/controls.js Wed Jan  4 12:49:21 2006
@@ -1,52 +1,23 @@
 // Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
 //           (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
-//
-// 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.
-
-Element.collectTextNodesIgnoreClass = function(element, ignoreclass) {
-  var children = $(element).childNodes;
-  var text     = "";
-  var classtest = new RegExp("^([^ ]+ )*" + ignoreclass+ "( [^ ]+)*$","i");
-
-  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);
-    }
-  }
+//           (c) 2005 Jon Tirsen (http://www.tirsen.com)
+// Contributors:
+//  Richard Livsey
+//  Rahul Bhargava
+//  Rob Wills
+// 
+// See scriptaculous.js for full license.
 
-  return text;
-}
-
-// Autocompleter.Base handles all the autocompletion functionality
+// Autocompleter.Base handles all the autocompletion functionality 
 // that's independent of the data source for autocompletion. This
 // includes drawing the autocompletion menu, observing keyboard
 // and mouse events, and similar.
 //
-// Specific autocompleters need to provide, at the very least,
+// Specific autocompleters need to provide, at the very least, 
 // a getUpdatedChoices function that will be invoked every time
-// the text inside the monitored textbox changes. This method
+// the text inside the monitored textbox changes. This method 
 // should get the text for which to provide autocompletion by
-// invoking this.getEntry(), NOT by directly accessing
+// invoking this.getToken(), NOT by directly accessing
 // this.element.value. This is to allow incremental tokenized
 // autocompletion. Specific auto-completion logic (AJAX, etc)
 // belongs in getUpdatedChoices.
@@ -57,52 +28,49 @@
 // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
 // will incrementally autocomplete with a comma as the token.
 // Additionally, ',' in the above example can be replaced with
-// a token array, e.g. { tokens: new Array (',', '\n') } which
-// enables autocompletion on multiple tokens. This is most
-// useful when one of the tokens is \n (a newline), as it
+// a token array, e.g. { tokens: [',', '\n'] } which
+// enables autocompletion on multiple tokens. This is most 
+// useful when one of the tokens is \n (a newline), as it 
 // allows smart autocompletion after linebreaks.
 
 var Autocompleter = {}
 Autocompleter.Base = function() {};
 Autocompleter.Base.prototype = {
-  base_initialize: function(element, update, options) {
-    this.element     = $(element);
-    this.update      = $(update);
-    this.has_focus   = false;
-    this.changed     = false;
-    this.active      = false;
-    this.index       = 0;
-    this.entry_count = 0;
+  baseInitialize: function(element, update, options) {
+    this.element     = $(element); 
+    this.update      = $(update);  
+    this.hasFocus    = false; 
+    this.changed     = false; 
+    this.active      = false; 
+    this.index       = 0;     
+    this.entryCount  = 0;
 
     if (this.setOptions)
       this.setOptions(options);
     else
-      this.options = {}
+      this.options = options || {};
 
-    this.options.tokens       = this.options.tokens || new Array();
+    this.options.paramName    = this.options.paramName || this.element.name;
+    this.options.tokens       = this.options.tokens || [];
     this.options.frequency    = this.options.frequency || 0.4;
-    this.options.min_chars    = this.options.min_chars || 1;
-    this.options.onShow       = this.options.onShow ||
-    function(element, update){
+    this.options.minChars     = this.options.minChars || 1;
+    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';
+        Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
       }
-      new Effect.Appear(update,{duration:0.15});
+      Effect.Appear(update,{duration:0.15});
     };
-    this.options.onHide = this.options.onHide ||
+    this.options.onHide = this.options.onHide || 
     function(element, update){ new Effect.Fade(update,{duration:0.15}) };
 
-    if(this.options.indicator)
-      this.indicator = $(this.options.indicator);
-
-    if (typeof(this.options.tokens) == 'string')
+    if (typeof(this.options.tokens) == 'string') 
       this.options.tokens = new Array(this.options.tokens);
 
     this.observer = null;
+    
+    this.element.setAttribute('autocomplete','off');
 
     Element.hide(this.update);
 
@@ -110,42 +78,40 @@
     Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
   },
 
-  resetWidth: function() {
-    this.update.style.width = this.element.offsetWidth + 'px';
-    if (this.update.scrollWidth - 8 > this.element.offsetWidth) {
-      this.update.style.width = (this.update.scrollWidth - 6) + 'px';
-    }
-  },
-
   show: function() {
-    if(this.update.style.display=='none') this.options.onShow(this.element, this.update);
-    this.resetWidth();
-    if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0) && this.update.style.position=='absolute') {
-      new Insertion.After(this.update,
+    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
+    if(!this.iefix && 
+      (navigator.appVersion.indexOf('MSIE')>0) &&
+      (navigator.userAgent.indexOf('Opera')<0) &&
+      (Element.getStyle(this.update, 'position')=='absolute')) {
+      new Insertion.After(this.update, 
        '<iframe id="' + this.update.id + '_iefix" '+
-       'style="display:none;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
+       'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
        'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
       this.iefix = $(this.update.id+'_iefix');
     }
-    if(this.iefix) {
-      Position.clone(this.update, this.iefix);
-      this.iefix.style.zIndex = 1;
-      this.update.style.zIndex = 2;
-      Element.show(this.iefix);
-    }
+    if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
+  },
+  
+  fixIEOverlapping: function() {
+    Position.clone(this.update, this.iefix);
+    this.iefix.style.zIndex = 1;
+    this.update.style.zIndex = 2;
+    Element.show(this.iefix);
   },
 
   hide: function() {
-    if(this.update.style.display=='') this.options.onHide(this.element, this.update);
+    this.stopIndicator();
+    if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
     if(this.iefix) Element.hide(this.iefix);
   },
 
   startIndicator: function() {
-    if(this.indicator) Element.show(this.indicator);
+    if(this.options.indicator) Element.show(this.options.indicator);
   },
 
   stopIndicator: function() {
-    if(this.indicator) Element.hide(this.indicator);
+    if(this.options.indicator) Element.hide(this.options.indicator);
   },
 
   onKeyPress: function(event) {
@@ -153,133 +119,147 @@
       switch(event.keyCode) {
        case Event.KEY_TAB:
        case Event.KEY_RETURN:
-         this.select_entry();
+         this.selectEntry();
          Event.stop(event);
        case Event.KEY_ESC:
          this.hide();
          this.active = false;
+         Event.stop(event);
          return;
        case Event.KEY_LEFT:
        case Event.KEY_RIGHT:
          return;
        case Event.KEY_UP:
-         this.mark_previous();
+         this.markPrevious();
          this.render();
          if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
          return;
        case Event.KEY_DOWN:
-         this.mark_next();
+         this.markNext();
          this.render();
          if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
          return;
       }
-     else
-      if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN)
+     else 
+      if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) 
         return;
 
     this.changed = true;
-    this.has_focus = true;
+    this.hasFocus = true;
 
     if(this.observer) clearTimeout(this.observer);
-      this.observer =
+      this.observer = 
         setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
   },
 
   onHover: function(event) {
     var element = Event.findElement(event, 'LI');
-    if(this.index != element.autocompleteIndex)
+    if(this.index != element.autocompleteIndex) 
     {
         this.index = element.autocompleteIndex;
         this.render();
     }
     Event.stop(event);
   },
-
+  
   onClick: function(event) {
     var element = Event.findElement(event, 'LI');
     this.index = element.autocompleteIndex;
-    this.select_entry();
-    Event.stop(event);
+    this.selectEntry();
+    this.hide();
   },
-
+  
   onBlur: function(event) {
     // needed to make click events working
     setTimeout(this.hide.bind(this), 250);
-    this.has_focus = false;
-    this.active = false;
-  },
-
+    this.hasFocus = false;
+    this.active = false;     
+  }, 
+  
   render: function() {
-    if(this.entry_count > 0) {
-      for (var i = 0; i < this.entry_count; i++)
-        this.index==i ?
-          Element.addClassName(this.get_entry(i),"selected") :
-          Element.removeClassName(this.get_entry(i),"selected");
-
-      if(this.has_focus) {
-        if(this.get_current_entry().scrollIntoView)
-          this.get_current_entry().scrollIntoView(false);
-
+    if(this.entryCount > 0) {
+      for (var i = 0; i < this.entryCount; i++)
+        this.index==i ? 
+          Element.addClassName(this.getEntry(i),"selected") : 
+          Element.removeClassName(this.getEntry(i),"selected");
+        
+      if(this.hasFocus) { 
         this.show();
         this.active = true;
       }
-    } else this.hide();
+    } else {
+      this.active = false;
+      this.hide();
+    }
   },
-
-  mark_previous: function() {
+  
+  markPrevious: function() {
     if(this.index > 0) this.index--
-      else this.index = this.entry_count-1;
+      else this.index = this.entryCount-1;
   },
-
-  mark_next: function() {
-    if(this.index < this.entry_count-1) this.index++
+  
+  markNext: function() {
+    if(this.index < this.entryCount-1) this.index++
       else this.index = 0;
   },
-
-  get_entry: function(index) {
+  
+  getEntry: function(index) {
     return this.update.firstChild.childNodes[index];
   },
-
-  get_current_entry: function() {
-    return this.get_entry(this.index);
+  
+  getCurrentEntry: function() {
+    return this.getEntry(this.index);
   },
-
-  select_entry: function() {
+  
+  selectEntry: function() {
     this.active = false;
-    value = Element.collectTextNodesIgnoreClass(this.get_current_entry(), 'informal').unescapeHTML();
-    this.updateElement(value);
-    this.element.focus();
+    this.updateElement(this.getCurrentEntry());
   },
 
-  updateElement: function(value) {
-    var last_token_pos = this.findLastToken();
-    if (last_token_pos != -1) {
-      var new_value = this.element.value.substr(0, last_token_pos + 1);
-      var whitespace = this.element.value.substr(last_token_pos + 1).match(/^\s+/);
+  updateElement: function(selectedElement) {
+    if (this.options.updateElement) {
+      this.options.updateElement(selectedElement);
+      return;
+    }
+    var value = '';
+    if (this.options.select) {
+      var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
+      if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
+    } else
+      value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
+    
+    var lastTokenPos = this.findLastToken();
+    if (lastTokenPos != -1) {
+      var newValue = this.element.value.substr(0, lastTokenPos + 1);
+      var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
       if (whitespace)
-        new_value += whitespace[0];
-      this.element.value = new_value + value;
+        newValue += whitespace[0];
+      this.element.value = newValue + value;
     } else {
       this.element.value = value;
     }
+    this.element.focus();
+    
+    if (this.options.afterUpdateElement)
+      this.options.afterUpdateElement(this.element, selectedElement);
   },
 
   updateChoices: function(choices) {
-    if(!this.changed && this.has_focus) {
+    if(!this.changed && this.hasFocus) {
       this.update.innerHTML = choices;
       Element.cleanWhitespace(this.update);
       Element.cleanWhitespace(this.update.firstChild);
 
       if(this.update.firstChild && this.update.firstChild.childNodes) {
-        this.entry_count =
+        this.entryCount = 
           this.update.firstChild.childNodes.length;
-        for (var i = 0; i < this.entry_count; i++) {
-          entry = this.get_entry(i);
+        for (var i = 0; i < this.entryCount; i++) {
+          var entry = this.getEntry(i);
           entry.autocompleteIndex = i;
           this.addObservers(entry);
         }
-      } else {
-        this.entry_count = 0;
+      } else { 
+        this.entryCount = 0;
       }
 
       this.stopIndicator();
@@ -295,8 +275,8 @@
   },
 
   onObserverEvent: function() {
-    this.changed = false;
-    if(this.getEntry().length>=this.options.min_chars) {
+    this.changed = false;   
+    if(this.getToken().length>=this.options.minChars) {
       this.startIndicator();
       this.getUpdatedChoices();
     } else {
@@ -305,10 +285,10 @@
     }
   },
 
-  getEntry: function() {
-    var token_pos = this.findLastToken();
-    if (token_pos != -1)
-      var ret = this.element.value.substr(token_pos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
+  getToken: function() {
+    var tokenPos = this.findLastToken();
+    if (tokenPos != -1)
+      var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
     else
       var ret = this.element.value;
 
@@ -316,34 +296,36 @@
   },
 
   findLastToken: function() {
-    var last_token_pos = -1;
+    var lastTokenPos = -1;
 
     for (var i=0; i<this.options.tokens.length; i++) {
-      var this_token_pos = this.element.value.lastIndexOf(this.options.tokens[i]);
-      if (this_token_pos > last_token_pos)
-        last_token_pos = this_token_pos;
+      var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
+      if (thisTokenPos > lastTokenPos)
+        lastTokenPos = thisTokenPos;
     }
-    return last_token_pos;
+    return lastTokenPos;
   }
 }
 
 Ajax.Autocompleter = Class.create();
-Ajax.Autocompleter.prototype = Object.extend(new Autocompleter.Base(),
-Object.extend(new Ajax.Base(), {
+Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
   initialize: function(element, update, url, options) {
-	  this.base_initialize(element, update, options);
-    this.options.asynchronous = true;
-    this.options.onComplete   = this.onComplete.bind(this)
-    this.options.method       = 'post';
-    this.url                  = url;
+	  this.baseInitialize(element, update, options);
+    this.options.asynchronous  = true;
+    this.options.onComplete    = this.onComplete.bind(this);
+    this.options.defaultParams = this.options.parameters || null;
+    this.url                   = url;
   },
 
   getUpdatedChoices: function() {
-    entry = encodeURIComponent(this.element.name) + '=' +
-      encodeURIComponent(this.getEntry());
+    entry = encodeURIComponent(this.options.paramName) + '=' + 
+      encodeURIComponent(this.getToken());
+
+    this.options.parameters = this.options.callback ?
+      this.options.callback(this.element, entry) : entry;
 
-      this.options.parameters = this.options.callback ?
-        this.options.callback(this.element, entry) : entry;
+    if(this.options.defaultParams) 
+      this.options.parameters += '&' + this.options.defaultParams;
 
     new Ajax.Request(this.url, this.options);
   },
@@ -352,7 +334,7 @@
     this.updateChoices(request.responseText);
   }
 
-}));
+});
 
 // The local array autocompleter. Used when you'd prefer to
 // inject an array of autocompletion options into the page, rather
@@ -366,25 +348,25 @@
 // Extra local autocompletion options:
 // - choices - How many autocompletion choices to offer
 //
-// - partial_search - If false, the autocompleter will match entered
-//                    text only at the beginning of strings in the
+// - partialSearch - If false, the autocompleter will match entered
+//                    text only at the beginning of strings in the 
 //                    autocomplete array. Defaults to true, which will
 //                    match text at the beginning of any *word* in the
 //                    strings in the autocomplete array. If you want to
 //                    search anywhere in the string, additionally set
-//                    the option full_search to true (default: off).
+//                    the option fullSearch to true (default: off).
 //
-// - full_search - Search anywhere in autocomplete array strings.
+// - fullSsearch - Search anywhere in autocomplete array strings.
 //
-// - partial_chars - How many characters to enter before triggering
-//                   a partial match (unlike min_chars, which defines
+// - partialChars - How many characters to enter before triggering
+//                   a partial match (unlike minChars, which defines
 //                   how many characters are required to do any match
 //                   at all). Defaults to 2.
 //
-// - ignore_case - Whether to ignore case when autocompleting.
+// - ignoreCase - Whether to ignore case when autocompleting.
 //                 Defaults to true.
 //
-// It's possible to pass in a custom function as the 'selector'
+// It's possible to pass in a custom function as the 'selector' 
 // option, if you prefer to write your own autocompletion logic.
 // In that case, the other options above will not apply unless
 // you support them.
@@ -392,7 +374,7 @@
 Autocompleter.Local = Class.create();
 Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
   initialize: function(element, update, array, options) {
-    this.base_initialize(element, update, options);
+    this.baseInitialize(element, update, options);
     this.options.array = array;
   },
 
@@ -403,35 +385,43 @@
   setOptions: function(options) {
     this.options = Object.extend({
       choices: 10,
-      partial_search: true,
-      partial_chars: 2,
-      ignore_case: true,
-      full_search: false,
+      partialSearch: true,
+      partialChars: 2,
+      ignoreCase: true,
+      fullSearch: false,
       selector: function(instance) {
-        var ret       = new Array(); // Beginning matches
-        var partial   = new Array(); // Inside matches
-        var entry     = instance.getEntry();
+        var ret       = []; // Beginning matches
+        var partial   = []; // Inside matches
+        var entry     = instance.getToken();
         var count     = 0;
 
-        for (var i = 0; i < instance.options.array.length &&
-            ret.length < instance.options.choices ; i++) {
+        for (var i = 0; i < instance.options.array.length &&  
+          ret.length < instance.options.choices ; i++) { 
+
           var elem = instance.options.array[i];
-          var found_pos = instance.options.ignore_case ?
-            elem.toLowerCase().indexOf(entry.toLowerCase()) :
+          var foundPos = instance.options.ignoreCase ? 
+            elem.toLowerCase().indexOf(entry.toLowerCase()) : 
             elem.indexOf(entry);
 
-          if (found_pos == 0 && elem.length == entry.length)
-            continue;
-          else if (found_pos == 0)
-            ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
-              elem.substr(entry.length) + "</li>");
-          else if (entry.length >= instance.options.partial_chars &&
-            instance.options.partial_search && found_pos != -1) {
-            if (!instance.options.full_search && !/\s/.test(elem.substr(found_pos-1,1)))
-              continue;
-            partial.push("<li>" + elem.substr(0, found_pos) + "<strong>" +
-              elem.substr(found_pos, entry.length) + "</strong>" + elem.substr(
-              found_pos + entry.length) + "</li>")
+          while (foundPos != -1) {
+            if (foundPos == 0 && elem.length != entry.length) { 
+              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + 
+                elem.substr(entry.length) + "</li>");
+              break;
+            } else if (entry.length >= instance.options.partialChars && 
+              instance.options.partialSearch && foundPos != -1) {
+              if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
+                partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
+                  elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
+                  foundPos + entry.length) + "</li>");
+                break;
+              }
+            }
+
+            foundPos = instance.options.ignoreCase ? 
+              elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 
+              elem.indexOf(entry, foundPos + 1);
+
           }
         }
         if (partial.length)
@@ -441,3 +431,340 @@
     }, options || {});
   }
 });
+
+// AJAX in-place editor
+//
+// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
+
+// Use this if you notice weird scrolling problems on some browsers,
+// the DOM might be a bit confused when this gets called so do this
+// waits 1 ms (with setTimeout) until it does the activation
+Field.scrollFreeActivate = function(field) {
+  setTimeout(function() {
+    Field.activate(field);
+  }, 1);
+}
+
+Ajax.InPlaceEditor = Class.create();
+Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
+Ajax.InPlaceEditor.prototype = {
+  initialize: function(element, url, options) {
+    this.url = url;
+    this.element = $(element);
+
+    this.options = Object.extend({
+      okButton: true,
+      okText: "ok",
+      cancelLink: true,
+      cancelText: "cancel",
+      savingText: "Saving...",
+      clickToEditText: "Click to edit",
+      okText: "ok",
+      rows: 1,
+      onComplete: function(transport, element) {
+        new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
+      },
+      onFailure: function(transport) {
+        alert("Error communicating with the server: " + transport.responseText.stripTags());
+      },
+      callback: function(form) {
+        return Form.serialize(form);
+      },
+      handleLineBreaks: true,
+      loadingText: 'Loading...',
+      savingClassName: 'inplaceeditor-saving',
+      loadingClassName: 'inplaceeditor-loading',
+      formClassName: 'inplaceeditor-form',
+      highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
+      highlightendcolor: "#FFFFFF",
+      externalControl:	null,
+      submitOnBlur: false,
+      ajaxOptions: {}
+    }, options || {});
+
+    if(!this.options.formId && this.element.id) {
+      this.options.formId = this.element.id + "-inplaceeditor";
+      if ($(this.options.formId)) {
+        // there's already a form with that name, don't specify an id
+        this.options.formId = null;
+      }
+    }
+    
+    if (this.options.externalControl) {
+      this.options.externalControl = $(this.options.externalControl);
+    }
+    
+    this.originalBackground = Element.getStyle(this.element, 'background-color');
+    if (!this.originalBackground) {
+      this.originalBackground = "transparent";
+    }
+    
+    this.element.title = this.options.clickToEditText;
+    
+    this.onclickListener = this.enterEditMode.bindAsEventListener(this);
+    this.mouseoverListener = this.enterHover.bindAsEventListener(this);
+    this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
+    Event.observe(this.element, 'click', this.onclickListener);
+    Event.observe(this.element, 'mouseover', this.mouseoverListener);
+    Event.observe(this.element, 'mouseout', this.mouseoutListener);
+    if (this.options.externalControl) {
+      Event.observe(this.options.externalControl, 'click', this.onclickListener);
+      Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
+      Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
+    }
+  },
+  enterEditMode: function(evt) {
+    if (this.saving) return;
+    if (this.editing) return;
+    this.editing = true;
+    this.onEnterEditMode();
+    if (this.options.externalControl) {
+      Element.hide(this.options.externalControl);
+    }
+    Element.hide(this.element);
+    this.createForm();
+    this.element.parentNode.insertBefore(this.form, this.element);
+    Field.scrollFreeActivate(this.editField);
+    // stop the event to avoid a page refresh in Safari
+    if (evt) {
+      Event.stop(evt);
+    }
+    return false;
+  },
+  createForm: function() {
+    this.form = document.createElement("form");
+    this.form.id = this.options.formId;
+    Element.addClassName(this.form, this.options.formClassName)
+    this.form.onsubmit = this.onSubmit.bind(this);
+
+    this.createEditField();
+
+    if (this.options.textarea) {
+      var br = document.createElement("br");
+      this.form.appendChild(br);
+    }
+
+    if (this.options.okButton) {
+      okButton = document.createElement("input");
+      okButton.type = "submit";
+      okButton.value = this.options.okText;
+      this.form.appendChild(okButton);
+    }
+
+    if (this.options.cancelLink) {
+      cancelLink = document.createElement("a");
+      cancelLink.href = "#";
+      cancelLink.appendChild(document.createTextNode(this.options.cancelText));
+      cancelLink.onclick = this.onclickCancel.bind(this);
+      this.form.appendChild(cancelLink);
+    }
+  },
+  hasHTMLLineBreaks: function(string) {
+    if (!this.options.handleLineBreaks) return false;
+    return string.match(/<br/i) || string.match(/<p>/i);
+  },
+  convertHTMLLineBreaks: function(string) {
+    return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
+  },
+  createEditField: function() {
+    var text;
+    if(this.options.loadTextURL) {
+      text = this.options.loadingText;
+    } else {
+      text = this.getText();
+    }
+
+    var obj = this;
+    
+    if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
+      this.options.textarea = false;
+      var textField = document.createElement("input");
+      textField.obj = this;
+      textField.type = "text";
+      textField.name = "value";
+      textField.value = text;
+      textField.style.backgroundColor = this.options.highlightcolor;
+      var size = this.options.size || this.options.cols || 0;
+      if (size != 0) textField.size = size;
+      if (this.options.submitOnBlur)
+        textField.onblur = this.onSubmit.bind(this);
+      this.editField = textField;
+    } else {
+      this.options.textarea = true;
+      var textArea = document.createElement("textarea");
+      textArea.obj = this;
+      textArea.name = "value";
+      textArea.value = this.convertHTMLLineBreaks(text);
+      textArea.rows = this.options.rows;
+      textArea.cols = this.options.cols || 40;
+      if (this.options.submitOnBlur)
+        textArea.onblur = this.onSubmit.bind(this);
+      this.editField = textArea;
+    }
+    
+    if(this.options.loadTextURL) {
+      this.loadExternalText();
+    }
+    this.form.appendChild(this.editField);
+  },
+  getText: function() {
+    return this.element.innerHTML;
+  },
+  loadExternalText: function() {
+    Element.addClassName(this.form, this.options.loadingClassName);
+    this.editField.disabled = true;
+    new Ajax.Request(
+      this.options.loadTextURL,
+      Object.extend({
+        asynchronous: true,
+        onComplete: this.onLoadedExternalText.bind(this)
+      }, this.options.ajaxOptions)
+    );
+  },
+  onLoadedExternalText: function(transport) {
+    Element.removeClassName(this.form, this.options.loadingClassName);
+    this.editField.disabled = false;
+    this.editField.value = transport.responseText.stripTags();
+  },
+  onclickCancel: function() {
+    this.onComplete();
+    this.leaveEditMode();
+    return false;
+  },
+  onFailure: function(transport) {
+    this.options.onFailure(transport);
+    if (this.oldInnerHTML) {
+      this.element.innerHTML = this.oldInnerHTML;
+      this.oldInnerHTML = null;
+    }
+    return false;
+  },
+  onSubmit: function() {
+    // onLoading resets these so we need to save them away for the Ajax call
+    var form = this.form;
+    var value = this.editField.value;
+    
+    // do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
+    // which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
+    // to be displayed indefinitely
+    this.onLoading();
+    
+    new Ajax.Updater(
+      { 
+        success: this.element,
+         // don't update on failure (this could be an option)
+        failure: null
+      },
+      this.url,
+      Object.extend({
+        parameters: this.options.callback(form, value),
+        onComplete: this.onComplete.bind(this),
+        onFailure: this.onFailure.bind(this)
+      }, this.options.ajaxOptions)
+    );
+    // stop the event to avoid a page refresh in Safari
+    if (arguments.length > 1) {
+      Event.stop(arguments[0]);
+    }
+    return false;
+  },
+  onLoading: function() {
+    this.saving = true;
+    this.removeForm();
+    this.leaveHover();
+    this.showSaving();
+  },
+  showSaving: function() {
+    this.oldInnerHTML = this.element.innerHTML;
+    this.element.innerHTML = this.options.savingText;
+    Element.addClassName(this.element, this.options.savingClassName);
+    this.element.style.backgroundColor = this.originalBackground;
+    Element.show(this.element);
+  },
+  removeForm: function() {
+    if(this.form) {
+      if (this.form.parentNode) Element.remove(this.form);
+      this.form = null;
+    }
+  },
+  enterHover: function() {
+    if (this.saving) return;
+    this.element.style.backgroundColor = this.options.highlightcolor;
+    if (this.effect) {
+      this.effect.cancel();
+    }
+    Element.addClassName(this.element, this.options.hoverClassName)
+  },
+  leaveHover: function() {
+    if (this.options.backgroundColor) {
+      this.element.style.backgroundColor = this.oldBackground;
+    }
+    Element.removeClassName(this.element, this.options.hoverClassName)
+    if (this.saving) return;
+    this.effect = new Effect.Highlight(this.element, {
+      startcolor: this.options.highlightcolor,
+      endcolor: this.options.highlightendcolor,
+      restorecolor: this.originalBackground
+    });
+  },
+  leaveEditMode: function() {
+    Element.removeClassName(this.element, this.options.savingClassName);
+    this.removeForm();
+    this.leaveHover();
+    this.element.style.backgroundColor = this.originalBackground;
+    Element.show(this.element);
+    if (this.options.externalControl) {
+      Element.show(this.options.externalControl);
+    }
+    this.editing = false;
+    this.saving = false;
+    this.oldInnerHTML = null;
+    this.onLeaveEditMode();
+  },
+  onComplete: function(transport) {
+    this.leaveEditMode();
+    this.options.onComplete.bind(this)(transport, this.element);
+  },
+  onEnterEditMode: function() {},
+  onLeaveEditMode: function() {},
+  dispose: function() {
+    if (this.oldInnerHTML) {
+      this.element.innerHTML = this.oldInnerHTML;
+    }
+    this.leaveEditMode();
+    Event.stopObserving(this.element, 'click', this.onclickListener);
+    Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
+    Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
+    if (this.options.externalControl) {
+      Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
+      Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
+      Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
+    }
+  }
+};
+
+// Delayed observer, like Form.Element.Observer, 
+// but waits for delay after last key input
+// Ideal for live-search fields
+
+Form.Element.DelayedObserver = Class.create();
+Form.Element.DelayedObserver.prototype = {
+  initialize: function(element, delay, callback) {
+    this.delay     = delay || 0.5;
+    this.element   = $(element);
+    this.callback  = callback;
+    this.timer     = null;
+    this.lastValue = $F(this.element); 
+    Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
+  },
+  delayedListener: function(event) {
+    if(this.lastValue == $F(this.element)) return;
+    if(this.timer) clearTimeout(this.timer);
+    this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
+    this.lastValue = $F(this.element);
+  },
+  onTimerEvent: function() {
+    this.timer = null;
+    this.callback(this.element, $F(this.element));
+  }
+};

Modified: myfaces/tomahawk/trunk/src/main/resources/org/apache/myfaces/custom/prototype/resource/dragdrop.js
URL: http://svn.apache.org/viewcvs/myfaces/tomahawk/trunk/src/main/resources/org/apache/myfaces/custom/prototype/resource/dragdrop.js?rev=365979&r1=365978&r2=365979&view=diff
==============================================================================
--- myfaces/tomahawk/trunk/src/main/resources/org/apache/myfaces/custom/prototype/resource/dragdrop.js (original)
+++ myfaces/tomahawk/trunk/src/main/resources/org/apache/myfaces/custom/prototype/resource/dragdrop.js Wed Jan  4 12:49:21 2006
@@ -1,212 +1,94 @@
 // 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.
-
-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;
-    }
-}
+// 
+// See scriptaculous.js for full license.
 
 /*--------------------------------------------------------------------------*/
 
 var Droppables = {
-  drops: false,
+  drops: [],
 
   remove: function(element) {
-    for(var i = 0; i < this.drops.length; i++)
-      if(this.drops[i].element == element)
-        this.drops.splice(i,1);
+    this.drops = this.drops.reject(function(d) { return d.element==$(element) });
   },
 
   add: function(element) {
-    var element = $(element);
+    element = $(element);
     var options = Object.extend({
       greedy:     true,
-      hoverclass: null
+      hoverclass: null  
     }, arguments[1] || {});
 
     // cache containers
     if(options.containment) {
-      options._containers = new Array();
+      options._containers = [];
       var containment = options.containment;
-      if((typeof containment == 'object') &&
+      if((typeof containment == 'object') && 
         (containment.constructor == Array)) {
-        for(var i=0; i<containment.length; i++)
-          options._containers.push($(containment[i]));
+        containment.each( function(c) { options._containers.push($(c)) });
       } else {
         options._containers.push($(containment));
       }
-      options._containers_length =
-        options._containers.length-1;
     }
+    
+    if(options.accept) options.accept = [options.accept].flatten();
 
     Element.makePositioned(element); // fix IE
-
     options.element = element;
 
-    // activate the droppable
-    if(!this.drops) this.drops = [];
     this.drops.push(options);
   },
 
-  is_contained: function(element, drop) {
-    var containers = drop._containers;
+  isContained: function(element, drop) {
     var parentNode = element.parentNode;
-    var i = drop._containers_length;
-    do { if(parentNode==containers[i]) return true; } while (i--);
-    return false;
+    return drop._containers.detect(function(c) { return parentNode == c });
   },
 
-  is_affected: function(pX, pY, element, drop) {
+  isAffected: function(point, element, drop) {
     return (
       (drop.element!=element) &&
       ((!drop._containers) ||
-        this.is_contained(element, drop)) &&
+        this.isContained(element, drop)) &&
       ((!drop.accept) ||
-        (Element.Class.has_any(element, drop.accept))) &&
-      Position.within(drop.element, pX, pY) );
+        (Element.classNames(element).detect( 
+          function(v) { return drop.accept.include(v) } ) )) &&
+      Position.within(drop.element, point[0], point[1]) );
   },
 
   deactivate: function(drop) {
-    Element.Class.remove(drop.element, drop.hoverclass);
+    if(drop.hoverclass)
+      Element.removeClassName(drop.element, drop.hoverclass);
     this.last_active = null;
   },
 
   activate: function(drop) {
-    if(this.last_active) this.deactivate(this.last_active);
-    if(drop.hoverclass) {
-      Element.Class.add(drop.element, drop.hoverclass);
-      this.last_active = drop;
-    }
+    if(drop.hoverclass)
+      Element.addClassName(drop.element, drop.hoverclass);
+    this.last_active = drop;
   },
 
-  show: function(event, element) {
-    if(!this.drops) return;
-    var pX = Event.pointerX(event);
-    var pY = Event.pointerY(event);
-    Position.prepare();
-
-    var i = this.drops.length-1; do {
-      var drop = this.drops[i];
-      if(this.is_affected(pX, pY, element, drop)) {
+  show: function(point, element) {
+    if(!this.drops.length) return;
+    
+    if(this.last_active) this.deactivate(this.last_active);
+    this.drops.each( function(drop) {
+      if(Droppables.isAffected(point, element, drop)) {
         if(drop.onHover)
            drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
-        if(drop.greedy) {
-          this.activate(drop);
-          return;
+        if(drop.greedy) { 
+          Droppables.activate(drop);
+          throw $break;
         }
       }
-    } while (i--);
+    });
   },
 
   fire: function(event, element) {
     if(!this.last_active) return;
     Position.prepare();
 
-    if (this.is_affected(Event.pointerX(event), Event.pointerY(event), element, this.last_active))
-      if (this.last_active.onDrop)
-        this.last_active.onDrop(element, this.last_active);
-
+    if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
+      if (this.last_active.onDrop) 
+        this.last_active.onDrop(element, this.last_active.element, event);
   },
 
   reset: function() {
@@ -215,224 +97,326 @@
   }
 }
 
-Draggables = {
-  observers: new Array(),
+var Draggables = {
+  drags: [],
+  observers: [],
+  
+  register: function(draggable) {
+    if(this.drags.length == 0) {
+      this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
+      this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
+      this.eventKeypress  = this.keyPress.bindAsEventListener(this);
+      
+      Event.observe(document, "mouseup", this.eventMouseUp);
+      Event.observe(document, "mousemove", this.eventMouseMove);
+      Event.observe(document, "keypress", this.eventKeypress);
+    }
+    this.drags.push(draggable);
+  },
+  
+  unregister: function(draggable) {
+    this.drags = this.drags.reject(function(d) { return d==draggable });
+    if(this.drags.length == 0) {
+      Event.stopObserving(document, "mouseup", this.eventMouseUp);
+      Event.stopObserving(document, "mousemove", this.eventMouseMove);
+      Event.stopObserving(document, "keypress", this.eventKeypress);
+    }
+  },
+  
+  activate: function(draggable) {
+    window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
+    this.activeDraggable = draggable;
+  },
+  
+  deactivate: function(draggbale) {
+    this.activeDraggable = null;
+  },
+  
+  updateDrag: function(event) {
+    if(!this.activeDraggable) return;
+    var pointer = [Event.pointerX(event), Event.pointerY(event)];
+    // Mozilla-based browsers fire successive mousemove events with
+    // the same coordinates, prevent needless redrawing (moz bug?)
+    if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
+    this._lastPointer = pointer;
+    this.activeDraggable.updateDrag(event, pointer);
+  },
+  
+  endDrag: function(event) {
+    if(!this.activeDraggable) return;
+    this._lastPointer = null;
+    this.activeDraggable.endDrag(event);
+    this.activeDraggable = null;
+  },
+  
+  keyPress: function(event) {
+    if(this.activeDraggable)
+      this.activeDraggable.keyPress(event);
+  },
+  
   addObserver: function(observer) {
     this.observers.push(observer);
+    this._cacheObserverCallbacks();
   },
-  removeObserver: function(element) {  // element instead of obsever fixes mem leaks
-    for(var i = 0; i < this.observers.length; i++)
-      if(this.observers[i].element && (this.observers[i].element == element))
-        this.observers.splice(i,1);
-  },
-  notify: function(eventName, draggable) {  // 'onStart', 'onEnd'
-    for(var i = 0; i < this.observers.length; i++)
-      this.observers[i][eventName](draggable);
+  
+  removeObserver: function(element) {  // element instead of observer fixes mem leaks
+    this.observers = this.observers.reject( function(o) { return o.element==element });
+    this._cacheObserverCallbacks();
+  },
+  
+  notify: function(eventName, draggable, event) {  // 'onStart', 'onEnd', 'onDrag'
+    if(this[eventName+'Count'] > 0)
+      this.observers.each( function(o) {
+        if(o[eventName]) o[eventName](eventName, draggable, event);
+      });
+  },
+  
+  _cacheObserverCallbacks: function() {
+    ['onStart','onEnd','onDrag'].each( function(eventName) {
+      Draggables[eventName+'Count'] = Draggables.observers.select(
+        function(o) { return o[eventName]; }
+      ).length;
+    });
   }
 }
 
 /*--------------------------------------------------------------------------*/
 
-Draggable = Class.create();
+var Draggable = Class.create();
 Draggable.prototype = {
   initialize: function(element) {
     var options = Object.extend({
       handle: false,
-      starteffect: function(element) {
-        new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7});
+      starteffect: function(element) { 
+        new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7}); 
       },
       reverteffect: function(element, top_offset, left_offset) {
-        new Effect.MoveBy(element, -top_offset, -left_offset, {duration:0.4});
+        var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
+        element._revert = new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur});
       },
-      endeffect: function(element) {
-         new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0});
+      endeffect: function(element) { 
+        new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); 
       },
       zindex: 1000,
-      revert: false
+      revert: false,
+      snap: false   // false, or xy or [x,y] or function(x,y){ return [x,y] }
     }, arguments[1] || {});
 
-    this.element      = $(element);
-    this.handle       = options.handle ? $(options.handle) : this.element;
-
-    Element.makePositioned(this.element); // fix IE
-
-    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;
-
-    this.eventMouseDown = this.startDrag.bindAsEventListener(this);
-    this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
-    this.eventMouseMove = this.update.bindAsEventListener(this);
-    this.eventKeypress  = this.keyPress.bindAsEventListener(this);
+    this.element = $(element);
+    
+    if(options.handle && (typeof options.handle == 'string'))
+      this.handle = Element.childrenWithClassName(this.element, options.handle)[0];  
+    if(!this.handle) this.handle = $(options.handle);
+    if(!this.handle) this.handle = this.element;
+
+    Element.makePositioned(this.element); // fix IE    
+
+    this.delta    = this.currentDelta();
+    this.options  = options;
+    this.dragging = false;   
 
+    this.eventMouseDown = this.initDrag.bindAsEventListener(this);
     Event.observe(this.handle, "mousedown", this.eventMouseDown);
-    Event.observe(document, "mouseup", this.eventMouseUp);
-    Event.observe(document, "mousemove", this.eventMouseMove);
-    Event.observe(document, "keypress", this.eventKeypress);
+    
+    Draggables.register(this);
   },
+  
   destroy: function() {
     Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
-    Event.stopObserving(document, "mouseup", this.eventMouseUp);
-    Event.stopObserving(document, "mousemove", this.eventMouseMove);
-    Event.stopObserving(document, "keypress", this.eventKeypress);
-  },
-  currentLeft: function() {
-    return parseInt(this.element.style.left || '0');
+    Draggables.unregister(this);
   },
-  currentTop: function() {
-    return parseInt(this.element.style.top || '0')
+  
+  currentDelta: function() {
+    return([
+      parseInt(Element.getStyle(this.element,'left') || '0'),
+      parseInt(Element.getStyle(this.element,'top') || '0')]);
+  },
+  
+  initDrag: function(event) {
+    if(Event.isLeftClick(event)) {    
+      // abort on form elements, fixes a Firefox issue
+      var src = Event.element(event);
+      if(src.tagName && (
+        src.tagName=='INPUT' ||
+        src.tagName=='SELECT' ||
+        src.tagName=='BUTTON' ||
+        src.tagName=='TEXTAREA')) return;
+        
+      if(this.element._revert) {
+        this.element._revert.cancel();
+        this.element._revert = null;
+      }
+      
+      var pointer = [Event.pointerX(event), Event.pointerY(event)];
+      var pos     = Position.cumulativeOffset(this.element);
+      this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
+      
+      Draggables.activate(this);
+      Event.stop(event);
+    }
   },
+  
   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);
+    this.dragging = true;
+    
+    if(this.options.zindex) {
+      this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
+      this.element.style.zIndex = this.options.zindex;
+    }
+    
+    if(this.options.ghosting) {
+      this._clone = this.element.cloneNode(true);
+      Position.absolutize(this.element);
+      this.element.parentNode.insertBefore(this._clone, this.element);
     }
+    
+    Draggables.notify('onStart', this, event);
+    if(this.options.starteffect) this.options.starteffect(this.element);
+  },
+  
+  updateDrag: function(event, pointer) {
+    if(!this.dragging) this.startDrag(event);
+    Position.prepare();
+    Droppables.show(pointer, this.element);
+    Draggables.notify('onDrag', this, event);
+    this.draw(pointer);
+    if(this.options.change) this.options.change(this);
+    
+    // fix AppleWebKit rendering
+    if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+    Event.stop(event);
   },
+  
   finishDrag: function(event, success) {
-    this.active = false;
     this.dragging = false;
 
+    if(this.options.ghosting) {
+      Position.relativize(this.element);
+      Element.remove(this._clone);
+      this._clone = null;
+    }
+
     if(success) Droppables.fire(event, this.element);
-    Draggables.notify('onEnd', this);
+    Draggables.notify('onEnd', this, event);
 
     var revert = this.options.revert;
     if(revert && typeof revert == 'function') revert = revert(this.element);
-
+    
+    var d = this.currentDelta();
     if(revert && this.options.reverteffect) {
-      this.options.reverteffect(this.element,
-      this.currentTop()-this.originalTop,
-      this.currentLeft()-this.originalLeft);
+      this.options.reverteffect(this.element, 
+        d[1]-this.delta[1], d[0]-this.delta[0]);
     } else {
-      this.originalLeft = this.currentLeft();
-      this.originalTop  = this.currentTop();
+      this.delta = d;
     }
 
-    this.element.style.zIndex = this.originalZ;
+    if(this.options.zindex)
+      this.element.style.zIndex = this.originalZ;
 
-    if(this.options.endeffect)
+    if(this.options.endeffect) 
       this.options.endeffect(this.element);
 
+    Draggables.deactivate(this);
     Droppables.reset();
   },
+  
   keyPress: function(event) {
-    if(this.active) {
-      if(event.keyCode==Event.KEY_ESC) {
-        this.finishDrag(event, false);
-        Event.stop(event);
-      }
-    }
+    if(!event.keyCode==Event.KEY_ESC) return;
+    this.finishDrag(event, false);
+    Event.stop(event);
   },
+  
   endDrag: function(event) {
-    if(this.active && this.dragging) {
-      this.finishDrag(event, true);
-      Event.stop(event);
-    }
-    this.active = false;
-    this.dragging = false;
-  },
-  draw: function(event) {
+    if(!this.dragging) return;
+    this.finishDrag(event, true);
+    Event.stop(event);
+  },
+  
+  draw: function(point) {
+    var pos = Position.cumulativeOffset(this.element);
+    var d = this.currentDelta();
+    pos[0] -= d[0]; pos[1] -= d[1];
+    
+    var p = [0,1].map(function(i){ return (point[i]-pos[i]-this.offset[i]) }.bind(this));
+    
+    if(this.options.snap) {
+      if(typeof this.options.snap == 'function') {
+        p = this.options.snap(p[0],p[1]);
+      } else {
+      if(this.options.snap instanceof Array) {
+        p = p.map( function(v, i) {
+          return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
+      } else {
+        p = p.map( function(v) {
+          return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
+      }
+    }}
+    
     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";
+      style.left = p[0] + "px";
     if((!this.options.constraint) || (this.options.constraint=='vertical'))
-      style.top  = ((event.clientY - this.originalY) - this.offsetY) + "px";
+      style.top  = p[1] + "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', this);
-        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();
+var 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() {
+    Sortable.unmark();
     if(this.lastValue != Sortable.serialize(this.element))
       this.observer(this.element)
   }
 }
 
-Sortable = {
+var Sortable = {
   sortables: new Array(),
+  
   options: function(element){
-    var element = $(element);
-    for(var i=0;i<this.sortables.length;i++)
-      if(this.sortables[i].element == element)
-        return this.sortables[i];
-    return null;
+    element = $(element);
+    return this.sortables.detect(function(s) { return s.element == element });
   },
+  
   destroy: function(element){
-    var element = $(element);
-    for(var i=0;i<this.sortables.length;i++) {
-      if(this.sortables[i].element == element) {
-        var s = this.sortables[i];
-        Draggables.removeObserver(s.element);
-        for(var j=0;j<s.droppables.length;j++)
-          Droppables.remove(s.droppables[j]);
-        for(var j=0;j<s.draggables.length;j++)
-          s.draggables[j].destroy();
-        this.sortables.splice(i,1);
-      }
-    }
+    element = $(element);
+    this.sortables.findAll(function(s) { return s.element == element }).each(function(s){
+      Draggables.removeObserver(s.element);
+      s.droppables.each(function(d){ Droppables.remove(d) });
+      s.draggables.invoke('destroy');
+    });
+    this.sortables = this.sortables.reject(function(s) { return s.element == element });
   },
+  
   create: function(element) {
-    var element = $(element);
-    var options = Object.extend({
+    element = $(element);
+    var options = Object.extend({ 
       element:     element,
       tag:         'li',       // assumes li children, override with tag: 'tagname'
+      dropOnEmpty: false,
+      tree:        false,      // fixme: unimplemented
       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() {}
+      ghosting:    false,
+      format:      null,
+      onChange:    Prototype.emptyFunction,
+      onUpdate:    Prototype.emptyFunction
     }, arguments[1] || {});
 
     // clear any old sortable with same element
@@ -441,70 +425,60 @@
     // build options for the draggables
     var options_for_draggable = {
       revert:      true,
+      ghosting:    options.ghosting,
       constraint:  options.constraint,
-      handle:      handle };
+      handle:      options.handle };
+
     if(options.starteffect)
       options_for_draggable.starteffect = options.starteffect;
+
     if(options.reverteffect)
       options_for_draggable.reverteffect = options.reverteffect;
+    else
+      if(options.ghosting) options_for_draggable.reverteffect = function(element) {
+        element.style.top  = 0;
+        element.style.left = 0;
+      };
+
     if(options.endeffect)
       options_for_draggable.endeffect = options.endeffect;
+
     if(options.zindex)
       options_for_draggable.zindex = options.zindex;
 
-    // build options for the droppables
+    // 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);
-          }
-        }
-      }
+      onHover:     Sortable.onHover,
+      greedy:      !options.dropOnEmpty
     }
 
     // fix for gecko engine
-    Element.cleanWhitespace(element);
+    Element.cleanWhitespace(element); 
 
     options.draggables = [];
     options.droppables = [];
 
     // 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];
-
-        options.draggables.push(new Draggable(elements[i], Object.extend(options_for_draggable, { handle: handle })));
 
-        Droppables.add(elements[i], options_for_droppable);
-        options.droppables.push(elements[i]);
+    // drop on empty handling
+    if(options.dropOnEmpty) {
+      Droppables.add(element,
+        {containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false});
+      options.droppables.push(element);
+    }
 
-      }
+    (this.findElements(element, options) || []).each( function(e) {
+      // handles are per-draggable
+      var handle = options.handle ? 
+        Element.childrenWithClassName(e, options.handle)[0] : e;    
+      options.draggables.push(
+        new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
+      Droppables.add(e, options_for_droppable);
+      options.droppables.push(e);      
+    });
 
     // keep reference
     this.sortables.push(options);
@@ -513,25 +487,99 @@
     Draggables.addObserver(new SortableObserver(element, options.onUpdate));
 
   },
+
+  // return all suitable-for-sortable elements in a guaranteed order
+  findElements: function(element, options) {
+    if(!element.hasChildNodes()) return null;
+    var elements = [];
+    $A(element.childNodes).each( function(e) {
+      if(e.tagName && e.tagName.toUpperCase()==options.tag.toUpperCase() &&
+        (!options.only || (Element.hasClassName(e, options.only))))
+          elements.push(e);
+      if(options.tree) {
+        var grandchildren = this.findElements(e, options);
+        if(grandchildren) elements.push(grandchildren);
+      }
+    });
+
+    return (elements.length>0 ? elements.flatten() : null);
+  },
+
+  onHover: function(element, dropon, overlap) {
+    if(overlap>0.5) {
+      Sortable.mark(dropon, 'before');
+      if(dropon.previousSibling != element) {
+        var oldParentNode = element.parentNode;
+        element.style.visibility = "hidden"; // fix gecko rendering
+        dropon.parentNode.insertBefore(element, dropon);
+        if(dropon.parentNode!=oldParentNode) 
+          Sortable.options(oldParentNode).onChange(element);
+        Sortable.options(dropon.parentNode).onChange(element);
+      }
+    } else {
+      Sortable.mark(dropon, 'after');
+      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) 
+          Sortable.options(oldParentNode).onChange(element);
+        Sortable.options(dropon.parentNode).onChange(element);
+      }
+    }
+  },
+
+  onEmptyHover: function(element, dropon) {
+    if(element.parentNode!=dropon) {
+      var oldParentNode = element.parentNode;
+      dropon.appendChild(element);
+      Sortable.options(oldParentNode).onChange(element);
+      Sortable.options(dropon).onChange(element);
+    }
+  },
+
+  unmark: function() {
+    if(Sortable._marker) Element.hide(Sortable._marker);
+  },
+
+  mark: function(dropon, position) {
+    // mark on ghosting only
+    var sortable = Sortable.options(dropon.parentNode);
+    if(sortable && !sortable.ghosting) return; 
+
+    if(!Sortable._marker) {
+      Sortable._marker = $('dropmarker') || document.createElement('DIV');
+      Element.hide(Sortable._marker);
+      Element.addClassName(Sortable._marker, 'dropmarker');
+      Sortable._marker.style.position = 'absolute';
+      document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
+    }    
+    var offsets = Position.cumulativeOffset(dropon);
+    Sortable._marker.style.left = offsets[0] + 'px';
+    Sortable._marker.style.top = offsets[1] + 'px';
+    
+    if(position=='after')
+      if(sortable.overlap == 'horizontal') 
+        Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px';
+      else
+        Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
+    
+    Element.show(Sortable._marker);
+  },
+
   serialize: function(element) {
-    var element = $(element);
+    element = $(element);
     var sortableOptions = this.options(element);
     var options = Object.extend({
       tag:  sortableOptions.tag,
       only: sortableOptions.only,
-      name: element.id
+      name: element.id,
+      format: sortableOptions.format || /^[^_]*_(.*)$/
     }, 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("&");
+    return $(this.findElements(element, options) || []).map( function(item) {
+      return (encodeURIComponent(options.name) + "[]=" + 
+              encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : ''));
+    }).join("&");
   }
-} 
\ No newline at end of file
+}
\ No newline at end of file