PageSourceSearch

http://togo-media.com/about/files/lightbox_files/scriptaculous.js

js togo-media.com collected 2026-10-05 09:35:34 UTC 118,598 bytes, 3,433 lines download raw bytes

1// script.aculo.us scriptaculous.js v1.8.0, Tue Nov 06 15:01:40 +0300 2007
2
3// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
4// 
5// Permission is hereby granted, free of charge, to any person obtaining
6// a copy of this software and associated documentation files (the
7// "Software"), to deal in the Software without restriction, including
8// without limitation the rights to use, copy, modify, merge, publish,
9// distribute, sublicense, and/or sell copies of the Software, and to
10// permit persons to whom the Software is furnished to do so, subject to
11// the following conditions:
12// 
13// The above copyright notice and this permission notice shall be
14// included in all copies or substantial portions of the Software.
15//
16// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23//
24// For details, see the script.aculo.us web site: http://script.aculo.us/
25
26var Scriptaculous = {
27  Version: '1.8.0',
28  REQUIRED_PROTOTYPE: '1.6.0'
29}
30
31var Builder = {
32  NODEMAP: {
33    AREA: 'map',
34    CAPTION: 'table',
35    COL: 'table',
36    COLGROUP: 'table',
37    LEGEND: 'fieldset',
38    OPTGROUP: 'select',
39    OPTION: 'select',
40    PARAM: 'object',
41    TBODY: 'table',
42    TD: 'table',
43    TFOOT: 'table',
44    TH: 'table',
45    THEAD: 'table',
46    TR: 'table'
47  },
48  // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken,
49  //       due to a Firefox bug
50  node: function(elementName) {
51    elementName = elementName.toUpperCase();
52    
53    // try innerHTML approach
54    var parentTag = this.NODEMAP[elementName] || 'div';
55    var parentElement = document.createElement(parentTag);
56    try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
57      parentElement.innerHTML = "<" + elementName + "></" + elementName + ">";
58    } catch(e) {}
59    var element = parentElement.firstChild || null;
60      
61    // see if browser added wrapping tags
62    if(element && (element.tagName.toUpperCase() != elementName))
63      element = element.getElementsByTagName(elementName)[0];
64    
65    // fallback to createElement approach
66    if(!element) element = document.createElement(elementName);
67    
68    // abort if nothing could be created
69    if(!element) return;
70
71    // attributes (or text)
72    if(arguments[1])
73      if(this._isStringOrNumber(arguments[1]) ||
74        (arguments[1] instanceof Array) ||
75        arguments[1].tagName) {
76          this._children(element, arguments[1]);
77        } else {
78          var attrs = this._attributes(arguments[1]);
79          if(attrs.length) {
80            try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
81              parentElement.innerHTML = "<" +elementName + " " +
82                attrs + "></" + elementName + ">";
83            } catch(e) {}
84            element = parentElement.firstChild || null;
85            // workaround firefox 1.0.X bug
86            if(!element) {
87              element = document.createElement(elementName);
88              for(attr in arguments[1]) 
89                element[attr == 'class' ? 'className' : attr] = arguments[1][attr];
90            }
91            if(element.tagName.toUpperCase() != elementName)
92              element = parentElement.getElementsByTagName(elementName)[0];
93          }
94        } 
95
96    // text, or array of children
97    if(arguments[2])
98      this._children(element, arguments[2]);
99
100     return element;
101  },
102  _text: function(text) {
103     return document.createTextNode(text);
104  },
105
106  ATTR_MAP: {
107    'className': 'class',
108    'htmlFor': 'for'
109  },
110
111  _attributes: function(attributes) {
112    var attrs = [];
113    for(attribute in attributes)
114      attrs.push((attribute in this.ATTR_MAP ? this.ATTR_MAP[attribute] : attribute) +
115          '="' + attributes[attribute].toString().escapeHTML().gsub(/"/,'&quot;') + '"');
116    return attrs.join(" ");
117  },
118  _children: function(element, children) {
119    if(children.tagName) {
120      element.appendChild(children);
121      return;
122    }
123    if(typeof children=='object') { // array can hold nodes and text
124      children.flatten().each( function(e) {
125        if(typeof e=='object')
126          element.appendChild(e)
127        else
128          if(Builder._isStringOrNumber(e))
129            element.appendChild(Builder._text(e));
130      });
131    } else
132      if(Builder._isStringOrNumber(children))
133        element.appendChild(Builder._text(children));
134  },
135  _isStringOrNumber: function(param) {
136    return(typeof param=='string' || typeof param=='number');
137  },
138  build: function(html) {
139    var element = this.node('div');
140    $(element).update(html.strip());
141    return element.down();
142  },
143  dump: function(scope) { 
144    if(typeof scope != 'object' && typeof scope != 'function') scope = window; //global scope 
145  
146    var tags = ("A ABBR ACRONYM ADDRESS APPLET AREA B BASE BASEFONT BDO BIG BLOCKQUOTE BODY " +
147      "BR BUTTON CAPTION CENTER CITE CODE COL COLGROUP DD DEL DFN DIR DIV DL DT EM FIELDSET " +
148      "FONT FORM FRAME FRAMESET H1 H2 H3 H4 H5 H6 HEAD HR HTML I IFRAME IMG INPUT INS ISINDEX "+
149      "KBD LABEL LEGEND LI LINK MAP MENU META NOFRAMES NOSCRIPT OBJECT OL OPTGROUP OPTION P "+
150      "PARAM PRE Q S SAMP SCRIPT SELECT SMALL SPAN STRIKE STRONG STYLE SUB SUP TABLE TBODY TD "+
151      "TEXTAREA TFOOT TH THEAD TITLE TR TT U UL VAR").split(/\s+/);
152  
153    tags.each( function(tag){ 
154      scope[tag] = function() { 
155        return Builder.node.apply(Builder, [tag].concat($A(arguments)));  
156      } 
157    });
158  }
159}
160
161String.prototype.parseColor = function() {  
162  var color = '#';
163  if (this.slice(0,4) == 'rgb(') {  
164    var cols = this.slice(4,this.length-1).split(',');  
165    var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);  
166  } else {  
167    if (this.slice(0,1) == '#') {  
168      if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();  
169      if (this.length==7) color = this.toLowerCase();  
170    }  
171  }  
172  return (color.length==7 ? color : (arguments[0] || this));  
173};
174
175/*--------------------------------------------------------------------------*/
176
177Element.collectTextNodes = function(element) {  
178  return $A($(element).childNodes).collect( function(node) {
179    return (node.nodeType==3 ? node.nodeValue : 
180      (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
181  }).flatten().join('');
182};
183
184Element.collectTextNodesIgnoreClass = function(element, className) {  
185  return $A($(element).childNodes).collect( function(node) {
186    return (node.nodeType==3 ? node.nodeValue : 
187      ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? 
188        Element.collectTextNodesIgnoreClass(node, className) : ''));
189  }).flatten().join('');
190};
191
192Element.setContentZoom = function(element, percent) {
193  element = $(element);  
194  element.setStyle({fontSize: (percent/100) + 'em'});   
195  if (Prototype.Browser.WebKit) window.scrollBy(0,0);
196  return element;
197};
198
199Element.getInlineOpacity = function(element){
200  return $(element).style.opacity || '';
201};
202
203Element.forceRerendering = function(element) {
204  try {
205    element = $(element);
206    var n = document.createTextNode(' ');
207    element.appendChild(n);
208    element.removeChild(n);
209  } catch(e) { }
210};
211
212/*--------------------------------------------------------------------------*/
213
214var Effect = {
215  _elementDoesNotExistError: {
216    name: 'ElementDoesNotExistError',
217    message: 'The specified DOM element does not exist, but is required for this effect to operate'
218  },
219  Transitions: {
220    linear: Prototype.K,
221    sinoidal: function(pos) {
222      return (-Math.cos(pos*Math.PI)/2) + 0.5;
223    },
224    reverse: function(pos) {
225      return 1-pos;
226    },
227    flicker: function(pos) {
228      var pos = ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
229      return pos > 1 ? 1 : pos;
230    },
231    wobble: function(pos) {
232      return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
233    },
234    pulse: function(pos, pulses) { 
235      pulses = pulses || 5; 
236      return (
237        ((pos % (1/pulses)) * pulses).round() == 0 ? 
238              ((pos * pulses * 2) - (pos * pulses * 2).floor()) : 
239          1 - ((pos * pulses * 2) - (pos * pulses * 2).floor())
240        );
241    },
242    spring: function(pos) { 
243      return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6)); 
244    },
245    none: function(pos) {
246      return 0;
247    },
248    full: function(pos) {
249      return 1;
250    }
251  },
252  DefaultOptions: {
253    duration:   1.0,   // seconds
254    fps:        100,   // 100= assume 66fps max.
255    sync:       false, // true for combining
256    from:       0.0,
257    to:         1.0,
258    delay:      0.0,
259    queue:      'parallel'
260  },
261  tagifyText: function(element) {
262    var tagifyStyle = 'position:relative';
263    if (Prototype.Browser.IE) tagifyStyle += ';zoom:1';
264    
265    element = $(element);
266    $A(element.childNodes).each( function(child) {
267      if (child.nodeType==3) {
268        child.nodeValue.toArray().each( function(character) {
269          element.insertBefore(
270            new Element('span', {style: tagifyStyle}).update(
271              character == ' ' ? String.fromCharCode(160) : character), 
272              child);
273        });
274        Element.remove(child);
275      }
276    });
277  },
278  multiple: function(element, effect) {
279    var elements;
280    if (((typeof element == 'object') || 
281        Object.isFunction(element)) && 
282       (element.length))
283      elements = element;
284    else
285      elements = $(element).childNodes;
286      
287    var options = Object.extend({
288      speed: 0.1,
289      delay: 0.0
290    }, arguments[2] || { });
291    var masterDelay = options.delay;
292
293    $A(elements).each( function(element, index) {
294      new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
295    });
296  },
297  PAIRS: {
298    'slide':  ['SlideDown','SlideUp'],
299    'blind':  ['BlindDown','BlindUp'],
300    'appear': ['Appear','Fade']
301  },
302  toggle: function(element, effect) {
303    element = $(element);
304    effect = (effect || 'appear').toLowerCase();
305    var options = Object.extend({
306      queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
307    }, arguments[2] || { });
308    Effect[element.visible() ? 
309      Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
310  }
311};
312
313Effect.DefaultOptions.transition = Effect.Transitions.sinoidal;
314
315/* ------------- core effects ------------- */
316
317Effect.ScopedQueue = Class.create(Enumerable, {
318  initialize: function() {
319    this.effects  = [];
320    this.interval = null;    
321  },
322  _each: function(iterator) {
323    this.effects._each(iterator);
324  },
325  add: function(effect) {
326    var timestamp = new Date().getTime();
327    
328    var position = Object.isString(effect.options.queue) ? 
329      effect.options.queue : effect.options.queue.position;
330    
331    switch(position) {
332      case 'front':
333        // move unstarted effects after this effect  
334        this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
335            e.startOn  += effect.finishOn;
336            e.finishOn += effect.finishOn;
337          });
338        break;
339      case 'with-last':
340        timestamp = this.effects.pluck('startOn').max() || timestamp;
341        break;
342      case 'end':
343        // start effect after last queued effect has finished
344        timestamp = this.effects.pluck('finishOn').max() || timestamp;
345        break;
346    }
347    
348    effect.startOn  += timestamp;
349    effect.finishOn += timestamp;
350
351    if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
352      this.effects.push(effect);
353    
354    if (!this.interval)
355      this.interval = setInterval(this.loop.bind(this), 15);
356  },
357  remove: function(effect) {
358    this.effects = this.effects.reject(function(e) { return e==effect });
359    if (this.effects.length == 0) {
360      clearInterval(this.interval);
361      this.interval = null;
362    }
363  },
364  loop: function() {
365    var timePos = new Date().getTime();
366    for(var i=0, len=this.effects.length;i<len;i++) 
367      this.effects[i] && this.effects[i].loop(timePos);
368  }
369});
370
371Effect.Queues = {
372  instances: $H(),
373  get: function(queueName) {
374    if (!Object.isString(queueName)) return queueName;
375    
376    return this.instances.get(queueName) ||
377      this.instances.set(queueName, new Effect.ScopedQueue());
378  }
379};
380Effect.Queue = Effect.Queues.get('global');
381
382Effect.Base = Class.create({
383  position: null,
384  start: function(options) {
385    function codeForEvent(options,eventName){
386      return (
387        (options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') +
388        (options[eventName] ? 'this.options.'+eventName+'(this);' : '')
389      );
390    }
391    if (options && options.transition === false) options.transition = Effect.Transitions.linear;
392    this.options      = Object.extend(Object.extend({ },Effect.DefaultOptions), options || { });
393    this.currentFrame = 0;
394    this.state        = 'idle';
395    this.startOn      = this.options.delay*1000;
396    this.finishOn     = this.startOn+(this.options.duration*1000);
397    this.fromToDelta  = this.options.to-this.options.from;
398    this.totalTime    = this.finishOn-this.startOn;
399    this.totalFrames  = this.options.fps*this.options.duration;
400    
401    eval('this.render = function(pos){ '+
402      'if (this.state=="idle"){this.state="running";'+
403      codeForEvent(this.options,'beforeSetup')+
404      (this.setup ? 'this.setup();':'')+ 
405      codeForEvent(this.options,'afterSetup')+
406      '};if (this.state=="running"){'+
407      'pos=this.options.transition(pos)*'+this.fromToDelta+'+'+this.options.from+';'+
408      'this.position=pos;'+
409      codeForEvent(this.options,'beforeUpdate')+
410      (this.update ? 'this.update(pos);':'')+
411      codeForEvent(this.options,'afterUpdate')+
412      '}}');
413    
414    this.event('beforeStart');
415    if (!this.options.sync)
416      Effect.Queues.get(Object.isString(this.options.queue) ? 
417        'global' : this.options.queue.scope).add(this);
418  },
419  loop: function(timePos) {
420    if (timePos >= this.startOn) {
421      if (timePos >= this.finishOn) {
422        this.render(1.0);
423        this.cancel();
424        this.event('beforeFinish');
425        if (this.finish) this.finish(); 
426        this.event('afterFinish');
427        return;  
428      }
429      var pos   = (timePos - this.startOn) / this.totalTime,
430          frame = (pos * this.totalFrames).round();
431      if (frame > this.currentFrame) {
432        this.render(pos);
433        this.currentFrame = frame;
434      }
435    }
436  },
437  cancel: function() {
438    if (!this.options.sync)
439      Effect.Queues.get(Object.isString(this.options.queue) ? 
440        'global' : this.options.queue.scope).remove(this);
441    this.state = 'finished';
442  },
443  event: function(eventName) {
444    if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
445    if (this.options[eventName]) this.options[eventName](this);
446  },
447  inspect: function() {
448    var data = $H();
449    for(property in this)
450      if (!Object.isFunction(this[property])) data.set(property, this[property]);
451    return '#<Effect:' + data.inspect() + ',options:' + $H(this.options).inspect() + '>';
452  }
453});
454
455Effect.Parallel = Class.create(Effect.Base, {
456  initialize: function(effects) {
457    this.effects = effects || [];
458    this.start(arguments[1]);
459  },
460  update: function(position) {
461    this.effects.invoke('render', position);
462  },
463  finish: function(position) {
464    this.effects.each( function(effect) {
465      effect.render(1.0);
466      effect.cancel();
467      effect.event('beforeFinish');
468      if (effect.finish) effect.finish(position);
469      effect.event('afterFinish');
470    });
471  }
472});
473
474Effect.Tween = Class.create(Effect.Base, {
475  initialize: function(object, from, to) {
476    object = Object.isString(object) ? $(object) : object;
477    var args = $A(arguments), method = args.last(), 
478      options = args.length == 5 ? args[3] : null;
479    this.method = Object.isFunction(method) ? method.bind(object) :
480      Object.isFunction(object[method]) ? object[method].bind(object) : 
481      function(value) { object[method] = value };
482    this.start(Object.extend({ from: from, to: to }, options || { }));
483  },
484  update: function(position) {
485    this.method(position);
486  }
487});
488
489Effect.Event = Class.create(Effect.Base, {
490  initialize: function() {
491    this.start(Object.extend({ duration: 0 }, arguments[0] || { }));
492  },
493  update: Prototype.emptyFunction
494});
495
496Effect.Opacity = Class.create(Effect.Base, {
497  initialize: function(element) {
498    this.element = $(element);
499    if (!this.element) throw(Effect._elementDoesNotExistError);
500    // make this work on IE on elements without 'layout'
501    if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
502      this.element.setStyle({zoom: 1});
503    var options = Object.extend({
504      from: this.element.getOpacity() || 0.0,
505      to:   1.0
506    }, arguments[1] || { });
507    this.start(options);
508  },
509  update: function(position) {
510    this.element.setOpacity(position);
511  }
512});
513
514Effect.Move = Class.create(Effect.Base, {
515  initialize: function(element) {
516    this.element = $(element);
517    if (!this.element) throw(Effect._elementDoesNotExistError);
518    var options = Object.extend({
519      x:    0,
520      y:    0,
521      mode: 'relative'
522    }, arguments[1] || { });
523    this.start(options);
524  },
525  setup: function() {
526    this.element.makePositioned();
527    this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
528    this.originalTop  = parseFloat(this.element.getStyle('top')  || '0');
529    if (this.options.mode == 'absolute') {
530      this.options.x = this.options.x - this.originalLeft;
531      this.options.y = this.options.y - this.originalTop;
532    }
533  },
534  update: function(position) {
535    this.element.setStyle({
536      left: (this.options.x  * position + this.originalLeft).round() + 'px',
537      top:  (this.options.y  * position + this.originalTop).round()  + 'px'
538    });
539  }
540});
541
542// for backwards compatibility
543Effect.MoveBy = function(element, toTop, toLeft) {
544  return new Effect.Move(element, 
545    Object.extend({ x: toLeft, y: toTop }, arguments[3] || { }));
546};
547
548Effect.Scale = Class.create(Effect.Base, {
549  initialize: function(element, percent) {
550    this.element = $(element);
551    if (!this.element) throw(Effect._elementDoesNotExistError);
552    var options = Object.extend({
553      scaleX: true,
554      scaleY: true,
555      scaleContent: true,
556      scaleFromCenter: false,
557      scaleMode: 'box',        // 'box' or 'contents' or { } with provided values
558      scaleFrom: 100.0,
559      scaleTo:   percent
560    }, arguments[2] || { });
561    this.start(options);
562  },
563  setup: function() {
564    this.restoreAfterFinish = this.options.restoreAfterFinish || false;
565    this.elementPositioning = this.element.getStyle('position');
566    
567    this.originalStyle = { };
568    ['top','left','width','height','fontSize'].each( function(k) {
569      this.originalStyle[k] = this.element.style[k];
570    }.bind(this));
571      
572    this.originalTop  = this.element.offsetTop;
573    this.originalLeft = this.element.offsetLeft;
574    
575    var fontSize = this.element.getStyle('font-size') || '100%';
576    ['em','px','%','pt'].each( function(fontSizeType) {
577      if (fontSize.indexOf(fontSizeType)>0) {
578        this.fontSize     = parseFloat(fontSize);
579        this.fontSizeType = fontSizeType;
580      }
581    }.bind(this));
582    
583    this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
584    
585    this.dims = null;
586    if (this.options.scaleMode=='box')
587      this.dims = [this.element.offsetHeight, this.element.offsetWidth];
588    if (/^content/.test(this.options.scaleMode))
589      this.dims = [this.element.scrollHeight, this.element.scrollWidth];
590    if (!this.dims)
591      this.dims = [this.options.scaleMode.originalHeight,
592                   this.options.scaleMode.originalWidth];
593  },
594  update: function(position) {
595    var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
596    if (this.options.scaleContent && this.fontSize)
597      this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
598    this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
599  },
600  finish: function(position) {
601    if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
602  },
603  setDimensions: function(height, width) {
604    var d = { };
605    if (this.options.scaleX) d.width = width.round() + 'px';
606    if (this.options.scaleY) d.height = height.round() + 'px';
607    if (this.options.scaleFromCenter) {
608      var topd  = (height - this.dims[0])/2;
609      var leftd = (width  - this.dims[1])/2;
610      if (this.elementPositioning == 'absolute') {
611        if (this.options.scaleY) d.top = this.originalTop-topd + 'px';
612        if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
613      } else {
614        if (this.options.scaleY) d.top = -topd + 'px';
615        if (this.options.scaleX) d.left = -leftd + 'px';
616      }
617    }
618    this.element.setStyle(d);
619  }
620});
621
622Effect.Highlight = Class.create(Effect.Base, {
623  initialize: function(element) {
624    this.element = $(element);
625    if (!this.element) throw(Effect._elementDoesNotExistError);
626    var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { });
627    this.start(options);
628  },
629  setup: function() {
630    // Prevent executing on elements not in the layout flow
631    if (this.element.getStyle('display')=='none') { this.cancel(); return; }
632    // Disable background image during the effect
633    this.oldStyle = { };
634    if (!this.options.keepBackgroundImage) {
635      this.oldStyle.backgroundImage = this.element.getStyle('background-image');
636      this.element.setStyle({backgroundImage: 'none'});
637    }
638    if (!this.options.endcolor)
639      this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
640    if (!this.options.restorecolor)
641      this.options.restorecolor = this.element.getStyle('background-color');
642    // init color calculations
643    this._base  = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
644    this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
645  },
646  update: function(position) {
647    this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
648      return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) });
649  },
650  finish: function() {
651    this.element.setStyle(Object.extend(this.oldStyle, {
652      backgroundColor: this.options.restorecolor
653    }));
654  }
655});
656
657Effect.ScrollTo = function(element) {
658  var options = arguments[1] || { },
659    scrollOffsets = document.viewport.getScrollOffsets(),
660    elementOffsets = $(element).cumulativeOffset(),
661    max = (window.height || document.body.scrollHeight) - document.viewport.getHeight();  
662
663  if (options.offset) elementOffsets[1] += options.offset;
664
665  return new Effect.Tween(null,
666    scrollOffsets.top,
667    elementOffsets[1] > max ? max : elementOffsets[1],
668    options,
669    function(p){ scrollTo(scrollOffsets.left, p.round()) }
670  );
671};
672
673/* ------------- combination effects ------------- */
674
675Effect.Fade = function(element) {
676  element = $(element);
677  var oldOpacity = element.getInlineOpacity();
678  var options = Object.extend({
679    from: element.getOpacity() || 1.0,
680    to:   0.0,
681    afterFinishInternal: function(effect) { 
682      if (effect.options.to!=0) return;
683      effect.element.hide().setStyle({opacity: oldOpacity}); 
684    }
685  }, arguments[1] || { });
686  return new Effect.Opacity(element,options);
687};
688
689Effect.Appear = function(element) {
690  element = $(element);
691  var options = Object.extend({
692  from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
693  to:   1.0,
694  // force Safari to render floated elements properly
695  afterFinishInternal: function(effect) {
696    effect.element.forceRerendering();
697  },
698  beforeSetup: function(effect) {
699    effect.element.setOpacity(effect.options.from).show(); 
700  }}, arguments[1] || { });
701  return new Effect.Opacity(element,options);
702};
703
704Effect.Puff = function(element) {
705  element = $(element);
706  var oldStyle = { 
707    opacity: element.getInlineOpacity(), 
708    position: element.getStyle('position'),
709    top:  element.style.top,
710    left: element.style.left,
711    width: element.style.width,
712    height: element.style.height
713  };
714  return new Effect.Parallel(
715   [ new Effect.Scale(element, 200, 
716      { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), 
717     new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], 
718     Object.extend({ duration: 1.0, 
719      beforeSetupInternal: function(effect) {
720        Position.absolutize(effect.effects[0].element)
721      },
722      afterFinishInternal: function(effect) {
723         effect.effects[0].element.hide().setStyle(oldStyle); }
724     }, arguments[1] || { })
725   );
726};
727
728Effect.BlindUp = function(element) {
729  element = $(element);
730  element.makeClipping();
731  return new Effect.Scale(element, 0,
732    Object.extend({ scaleContent: false, 
733      scaleX: false, 
734      restoreAfterFinish: true,
735      afterFinishInternal: function(effect) {
736        effect.element.hide().undoClipping();
737      } 
738    }, arguments[1] || { })
739  );
740};
741
742Effect.BlindDown = function(element) {
743  element = $(element);
744  var elementDimensions = element.getDimensions();
745  return new Effect.Scale(element, 100, Object.extend({ 
746    scaleContent: false, 
747    scaleX: false,
748    scaleFrom: 0,
749    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
750    restoreAfterFinish: true,
751    afterSetup: function(effect) {
752      effect.element.makeClipping().setStyle({height: '0px'}).show(); 
753    },  
754    afterFinishInternal: function(effect) {
755      effect.element.undoClipping();
756    }
757  }, arguments[1] || { }));
758};
759
760Effect.SwitchOff = function(element) {
761  element = $(element);
762  var oldOpacity = element.getInlineOpacity();
763  return new Effect.Appear(element, Object.extend({
764    duration: 0.4,
765    from: 0,
766    transition: Effect.Transitions.flicker,
767    afterFinishInternal: function(effect) {
768      new Effect.Scale(effect.element, 1, { 
769        duration: 0.3, scaleFromCenter: true,
770        scaleX: false, scaleContent: false, restoreAfterFinish: true,
771        beforeSetup: function(effect) { 
772          effect.element.makePositioned().makeClipping();
773        },
774        afterFinishInternal: function(effect) {
775          effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
776        }
777      })
778    }
779  }, arguments[1] || { }));
780};
781
782Effect.DropOut = function(element) {
783  element = $(element);
784  var oldStyle = {
785    top: element.getStyle('top'),
786    left: element.getStyle('left'),
787    opacity: element.getInlineOpacity() };
788  return new Effect.Parallel(
789    [ new Effect.Move(element, {x: 0, y: 100, sync: true }), 
790      new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
791    Object.extend(
792      { duration: 0.5,
793        beforeSetup: function(effect) {
794          effect.effects[0].element.makePositioned(); 
795        },
796        afterFinishInternal: function(effect) {
797          effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
798        } 
799      }, arguments[1] || { }));
800};
801
802Effect.Shake = function(element) {
803  element = $(element);
804  var options = Object.extend({
805    distance: 20,
806    duration: 0.5
807  }, arguments[1] || {});
808  var distance = parseFloat(options.distance);
809  var split = parseFloat(options.duration) / 10.0;
810  var oldStyle = {
811    top: element.getStyle('top'),
812    left: element.getStyle('left') };
813    return new Effect.Move(element,
814      { x:  distance, y: 0, duration: split, afterFinishInternal: function(effect) {
815    new Effect.Move(effect.element,
816      { x: -distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
817    new Effect.Move(effect.element,
818      { x:  distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
819    new Effect.Move(effect.element,
820      { x: -distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
821    new Effect.Move(effect.element,
822      { x:  distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
823    new Effect.Move(effect.element,
824      { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) {
825        effect.element.undoPositioned().setStyle(oldStyle);
826  }}) }}) }}) }}) }}) }});
827};
828
829Effect.SlideDown = function(element) {
830  element = $(element).cleanWhitespace();
831  // SlideDown need to have the content of the element wrapped in a container element with fixed height!
832  var oldInnerBottom = element.down().getStyle('bottom');
833  var elementDimensions = element.getDimensions();
834  return new Effect.Scale(element, 100, Object.extend({ 
835    scaleContent: false, 
836    scaleX: false, 
837    scaleFrom: window.opera ? 0 : 1,
838    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
839    restoreAfterFinish: true,
840    afterSetup: function(effect) {
841      effect.element.makePositioned();
842      effect.element.down().makePositioned();
843      if (window.opera) effect.element.setStyle({top: ''});
844      effect.element.makeClipping().setStyle({height: '0px'}).show(); 
845    },
846    afterUpdateInternal: function(effect) {
847      effect.element.down().setStyle({bottom:
848        (effect.dims[0] - effect.element.clientHeight) + 'px' }); 
849    },
850    afterFinishInternal: function(effect) {
851      effect.element.undoClipping().undoPositioned();
852      effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
853    }, arguments[1] || { })
854  );
855};
856
857Effect.SlideUp = function(element) {
858  element = $(element).cleanWhitespace();
859  var oldInnerBottom = element.down().getStyle('bottom');
860  var elementDimensions = element.getDimensions();
861  return new Effect.Scale(element, window.opera ? 0 : 1,
862   Object.extend({ scaleContent: false, 
863    scaleX: false, 
864    scaleMode: 'box',
865    scaleFrom: 100,
866    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
867    restoreAfterFinish: true,
868    afterSetup: function(effect) {
869      effect.element.makePositioned();
870      effect.element.down().makePositioned();
871      if (window.opera) effect.element.setStyle({top: ''});
872      effect.element.makeClipping().show();
873    },  
874    afterUpdateInternal: function(effect) {
875      effect.element.down().setStyle({bottom:
876        (effect.dims[0] - effect.element.clientHeight) + 'px' });
877    },
878    afterFinishInternal: function(effect) {
879      effect.element.hide().undoClipping().undoPositioned();
880      effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom});
881    }
882   }, arguments[1] || { })
883  );
884};
885
886// Bug in opera makes the TD containing this element expand for a instance after finish 
887Effect.Squish = function(element) {
888  return new Effect.Scale(element, window.opera ? 1 : 0, { 
889    restoreAfterFinish: true,
890    beforeSetup: function(effect) {
891      effect.element.makeClipping(); 
892    },  
893    afterFinishInternal: function(effect) {
894      effect.element.hide().undoClipping(); 
895    }
896  });
897};
898
899Effect.Grow = function(element) {
900  element = $(element);
901  var options = Object.extend({
902    direction: 'center',
903    moveTransition: Effect.Transitions.sinoidal,
904    scaleTransition: Effect.Transitions.sinoidal,
905    opacityTransition: Effect.Transitions.full
906  }, arguments[1] || { });
907  var oldStyle = {
908    top: element.style.top,
909    left: element.style.left,
910    height: element.style.height,
911    width: element.style.width,
912    opacity: element.getInlineOpacity() };
913
914  var dims = element.getDimensions();    
915  var initialMoveX, initialMoveY;
916  var moveX, moveY;
917  
918  switch (options.direction) {
919    case 'top-left':
920      initialMoveX = initialMoveY = moveX = moveY = 0; 
921      break;
922    case 'top-right':
923      initialMoveX = dims.width;
924      initialMoveY = moveY = 0;
925      moveX = -dims.width;
926      break;
927    case 'bottom-left':
928      initialMoveX = moveX = 0;
929      initialMoveY = dims.height;
930      moveY = -dims.height;
931      break;
932    case 'bottom-right':
933      initialMoveX = dims.width;
934      initialMoveY = dims.height;
935      moveX = -dims.width;
936      moveY = -dims.height;
937      break;
938    case 'center':
939      initialMoveX = dims.width / 2;
940      initialMoveY = dims.height / 2;
941      moveX = -dims.width / 2;
942      moveY = -dims.height / 2;
943      break;
944  }
945  
946  return new Effect.Move(element, {
947    x: initialMoveX,
948    y: initialMoveY,
949    duration: 0.01, 
950    beforeSetup: function(effect) {
951      effect.element.hide().makeClipping().makePositioned();
952    },
953    afterFinishInternal: function(effect) {
954      new Effect.Parallel(
955        [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
956          new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
957          new Effect.Scale(effect.element, 100, {
958            scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, 
959            sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
960        ], Object.extend({
961             beforeSetup: function(effect) {
962               effect.effects[0].element.setStyle({height: '0px'}).show(); 
963             },
964             afterFinishInternal: function(effect) {
965               effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); 
966             }
967           }, options)
968      )
969    }
970  });
971};
972
973Effect.Shrink = function(element) {
974  element = $(element);
975  var options = Object.extend({
976    direction: 'center',
977    moveTransition: Effect.Transitions.sinoidal,
978    scaleTransition: Effect.Transitions.sinoidal,
979    opacityTransition: Effect.Transitions.none
980  }, arguments[1] || { });
981  var oldStyle = {
982    top: element.style.top,
983    left: element.style.left,
984    height: element.style.height,
985    width: element.style.width,
986    opacity: element.getInlineOpacity() };
987
988  var dims = element.getDimensions();
989  var moveX, moveY;
990  
991  switch (options.direction) {
992    case 'top-left':
993      moveX = moveY = 0;
994      break;
995    case 'top-right':
996      moveX = dims.width;
997      moveY = 0;
998      break;
999    case 'bottom-left':
1000      moveX = 0;
1001      moveY = dims.height;
1002      break;
1003    case 'bottom-right':
1004      moveX = dims.width;
1005      moveY = dims.height;
1006      break;
1007    case 'center':  
1008      moveX = dims.width / 2;
1009      moveY = dims.height / 2;
1010      break;
1011  }
1012  
1013  return new Effect.Parallel(
1014    [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
1015      new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
1016      new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
1017    ], Object.extend({            
1018         beforeStartInternal: function(effect) {
1019           effect.effects[0].element.makePositioned().makeClipping(); 
1020         },
1021         afterFinishInternal: function(effect) {
1022           effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
1023       }, options)
1024  );
1025};
1026
1027Effect.Pulsate = function(element) {
1028  element = $(element);
1029  var options    = arguments[1] || { };
1030  var oldOpacity = element.getInlineOpacity();
1031  var transition = options.transition || Effect.Transitions.sinoidal;
1032  var reverser   = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) };
1033  reverser.bind(transition);
1034  return new Effect.Opacity(element, 
1035    Object.extend(Object.extend({  duration: 2.0, from: 0,
1036      afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
1037    }, options), {transition: reverser}));
1038};
1039
1040Effect.Fold = function(element) {
1041  element = $(element);
1042  var oldStyle = {
1043    top: element.style.top,
1044    left: element.style.left,
1045    width: element.style.width,
1046    height: element.style.height };
1047  element.makeClipping();
1048  return new Effect.Scale(element, 5, Object.extend({   
1049    scaleContent: false,
1050    scaleX: false,
1051    afterFinishInternal: function(effect) {
1052    new Effect.Scale(element, 1, { 
1053      scaleContent: false, 
1054      scaleY: false,
1055      afterFinishInternal: function(effect) {
1056        effect.element.hide().undoClipping().setStyle(oldStyle);
1057      } });
1058  }}, arguments[1] || { }));
1059};
1060
1061Effect.Morph = Class.create(Effect.Base, {
1062  initialize: function(element) {
1063    this.element = $(element);
1064    if (!this.element) throw(Effect._elementDoesNotExistError);
1065    var options = Object.extend({
1066      style: { }
1067    }, arguments[1] || { });
1068    
1069    if (!Object.isString(options.style)) this.style = $H(options.style);
1070    else {
1071      if (options.style.include(':'))
1072        this.style = options.style.parseStyle();
1073      else {
1074        this.element.addClassName(options.style);
1075        this.style = $H(this.element.getStyles());
1076        this.element.removeClassName(options.style);
1077        var css = this.element.getStyles();
1078        this.style = this.style.reject(function(style) {
1079          return style.value == css[style.key];
1080        });
1081        options.afterFinishInternal = function(effect) {
1082          effect.element.addClassName(effect.options.style);
1083          effect.transforms.each(function(transform) {
1084            effect.element.style[transform.style] = '';
1085          });
1086        }
1087      }
1088    }
1089    this.start(options);
1090  },
1091  
1092  setup: function(){
1093    function parseColor(color){
1094      if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
1095      color = color.parseColor();
1096      return $R(0,2).map(function(i){
1097        return parseInt( color.slice(i*2+1,i*2+3), 16 ) 
1098      });
1099    }
1100    this.transforms = this.style.map(function(pair){
1101      var property = pair[0], value = pair[1], unit = null;
1102
1103      if (value.parseColor('#zzzzzz') != '#zzzzzz') {
1104        value = value.parseColor();
1105        unit  = 'color';
1106      } else if (property == 'opacity') {
1107        value = parseFloat(value);
1108        if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
1109          this.element.setStyle({zoom: 1});
1110      } else if (Element.CSS_LENGTH.test(value)) {
1111          var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/);
1112          value = parseFloat(components[1]);
1113          unit = (components.length == 3) ? components[2] : null;
1114      }
1115
1116      var originalValue = this.element.getStyle(property);
1117      return { 
1118        style: property.camelize(), 
1119        originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0), 
1120        targetValue: unit=='color' ? parseColor(value) : value,
1121        unit: unit
1122      };
1123    }.bind(this)).reject(function(transform){
1124      return (
1125        (transform.originalValue == transform.targetValue) ||
1126        (
1127          transform.unit != 'color' &&
1128          (isNaN(transform.originalValue) || isNaN(transform.targetValue))
1129        )
1130      )
1131    });
1132  },
1133  update: function(position) {
1134    var style = { }, transform, i = this.transforms.length;
1135    while(i--)
1136      style[(transform = this.transforms[i]).style] = 
1137        transform.unit=='color' ? '#'+
1138          (Math.round(transform.originalValue[0]+
1139            (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() +
1140          (Math.round(transform.originalValue[1]+
1141            (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() +
1142          (Math.round(transform.originalValue[2]+
1143            (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() :
1144        (transform.originalValue +
1145          (transform.targetValue - transform.originalValue) * position).toFixed(3) + 
1146            (transform.unit === null ? '' : transform.unit);
1147    this.element.setStyle(style, true);
1148  }
1149});
1150
1151Effect.Transform = Class.create({
1152  initialize: function(tracks){
1153    this.tracks  = [];
1154    this.options = arguments[1] || { };
1155    this.addTracks(tracks);
1156  },
1157  addTracks: function(tracks){
1158    tracks.each(function(track){
1159      track = $H(track);
1160      var data = track.values().first();
1161      this.tracks.push($H({
1162        ids:     track.keys().first(),
1163        effect:  Effect.Morph,
1164        options: { style: data }
1165      }));
1166    }.bind(this));
1167    return this;
1168  },
1169  play: function(){
1170    return new Effect.Parallel(
1171      this.tracks.map(function(track){
1172        var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options');
1173        var elements = [$(ids) || $$(ids)].flatten();
1174        return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) });
1175      }).flatten(),
1176      this.options
1177    );
1178  }
1179});
1180
1181Element.CSS_PROPERTIES = $w(
1182  'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' + 
1183  'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' +
1184  'borderRightColor borderRightStyle borderRightWidth borderSpacing ' +
1185  'borderTopColor borderTopStyle borderTopWidth bottom clip color ' +
1186  'fontSize fontWeight height left letterSpacing lineHeight ' +
1187  'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+
1188  'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' +
1189  'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' +
1190  'right textIndent top width wordSpacing zIndex');
1191  
1192Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;
1193
1194String.__parseStyleElement = document.createElement('div');
1195String.prototype.parseStyle = function(){
1196  var style, styleRules = $H();
1197  if (Prototype.Browser.WebKit)
1198    style = new Element('div',{style:this}).style;
1199  else {
1200    String.__parseStyleElement.innerHTML = '<div style="' + this + '"></div>';
1201    style = String.__parseStyleElement.childNodes[0].style;
1202  }
1203  
1204  Element.CSS_PROPERTIES.each(function(property){
1205    if (style[property]) styleRules.set(property, style[property]); 
1206  });
1207  
1208  if (Prototype.Browser.IE && this.include('opacity'))
1209    styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]);
1210
1211  return styleRules;
1212};
1213
1214if (document.defaultView && document.defaultView.getComputedStyle) {
1215  Element.getStyles = function(element) {
1216    var css = document.defaultView.getComputedStyle($(element), null);
1217    return Element.CSS_PROPERTIES.inject({ }, function(styles, property) {
1218      styles[property] = css[property];
1219      return styles;
1220    });
1221  };
1222} else {
1223  Element.getStyles = function(element) {
1224    element = $(element);
1225    var css = element.currentStyle, styles;
1226    styles = Element.CSS_PROPERTIES.inject({ }, function(hash, property) {
1227      hash.set(property, css[property]);
1228      return hash;
1229    });
1230    if (!styles.opacity) styles.set('opacity', element.getOpacity());
1231    return styles;
1232  };
1233};
1234
1235Effect.Methods = {
1236  morph: function(element, style) {
1237    element = $(element);
1238    new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { }));
1239    return element;
1240  },
1241  visualEffect: function(element, effect, options) {
1242    element = $(element)
1243    var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1);
1244    new Effect[klass](element, options);
1245    return element;
1246  },
1247  highlight: function(element, options) {
1248    element = $(element);
1249    new Effect.Highlight(element, options);
1250    return element;
1251  }
1252};
1253
1254$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+
1255  'pulsate shake puff squish switchOff dropOut').each(
1256  function(effect) { 
1257    Effect.Methods[effect] = function(element, options){
1258      element = $(element);
1259      Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options);
1260      return element;
1261    }
1262  }
1263);
1264
1265$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each( 
1266  function(f) { Effect.Methods[f] = Element[f]; }
1267);
1268
1269Element.addMethods(Effect.Methods);
1270
1271if(typeof Effect == 'undefined')
1272  throw("controls.js requires including script.aculo.us' effects.js library");
1273
1274var Autocompleter = { }
1275Autocompleter.Base = Class.create({
1276  baseInitialize: function(element, update, options) {
1277    element          = $(element)
1278    this.element     = element; 
1279    this.update      = $(update);  
1280    this.hasFocus    = false; 
1281    this.changed     = false; 
1282    this.active      = false; 
1283    this.index       = 0;     
1284    this.entryCount  = 0;
1285    this.oldElementValue = this.element.value;
1286
1287    if(this.setOptions)
1288      this.setOptions(options);
1289    else
1290      this.options = options || { };
1291
1292    this.options.paramName    = this.options.paramName || this.element.name;
1293    this.options.tokens       = this.options.tokens || [];
1294    this.options.frequency    = this.options.frequency || 0.4;
1295    this.options.minChars     = this.options.minChars || 1;
1296    this.options.onShow       = this.options.onShow || 
1297      function(element, update){ 
1298        if(!update.style.position || update.style.position=='absolute') {
1299          update.style.position = 'absolute';
1300          Position.clone(element, update, {
1301            setHeight: false, 
1302            offsetTop: element.offsetHeight
1303          });
1304        }
1305        Effect.Appear(update,{duration:0.15});
1306      };
1307    this.options.onHide = this.options.onHide || 
1308      function(element, update){ new Effect.Fade(update,{duration:0.15}) };
1309
1310    if(typeof(this.options.tokens) == 'string') 
1311      this.options.tokens = new Array(this.options.tokens);
1312    // Force carriage returns as token delimiters anyway
1313    if (!this.options.tokens.include('\n'))
1314      this.options.tokens.push('\n');
1315
1316    this.observer = null;
1317    
1318    this.element.setAttribute('autocomplete','off');
1319
1320    Element.hide(this.update);
1321
1322    Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
1323    Event.observe(this.element, 'keypress', this.onKeyPress.bindAsEventListener(this));
1324  },
1325
1326  show: function() {
1327    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
1328    if(!this.iefix && 
1329      (Prototype.Browser.IE) &&
1330      (Element.getStyle(this.update, 'position')=='absolute')) {
1331      new Insertion.After(this.update, 
1332       '<iframe id="' + this.update.id + '_iefix" '+
1333       'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
1334       'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
1335      this.iefix = $(this.update.id+'_iefix');
1336    }
1337    if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
1338  },
1339  
1340  fixIEOverlapping: function() {
1341    Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
1342    this.iefix.style.zIndex = 1;
1343    this.update.style.zIndex = 2;
1344    Element.show(this.iefix);
1345  },
1346
1347  hide: function() {
1348    this.stopIndicator();
1349    if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
1350    if(this.iefix) Element.hide(this.iefix);
1351  },
1352
1353  startIndicator: function() {
1354    if(this.options.indicator) Element.show(this.options.indicator);
1355  },
1356
1357  stopIndicator: function() {
1358    if(this.options.indicator) Element.hide(this.options.indicator);
1359  },
1360
1361  onKeyPress: function(event) {
1362    if(this.active)
1363      switch(event.keyCode) {
1364       case Event.KEY_TAB:
1365       case Event.KEY_RETURN:
1366         this.selectEntry();
1367         Event.stop(event);
1368       case Event.KEY_ESC:
1369         this.hide();
1370         this.active = false;
1371         Event.stop(event);
1372         return;
1373       case Event.KEY_LEFT:
1374       case Event.KEY_RIGHT:
1375         return;
1376       case Event.KEY_UP:
1377         this.markPrevious();
1378         this.render();
1379         if(Prototype.Browser.WebKit) Event.stop(event);
1380         return;
1381       case Event.KEY_DOWN:
1382         this.markNext();
1383         this.render();
1384         if(Prototype.Browser.WebKit) Event.stop(event);
1385         return;
1386      }
1387     else 
1388       if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || 
1389         (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;
1390
1391    this.changed = true;
1392    this.hasFocus = true;
1393
1394    if(this.observer) clearTimeout(this.observer);
1395      this.observer = 
1396        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
1397  },
1398
1399  activate: function() {
1400    this.changed = false;
1401    this.hasFocus = true;
1402    this.getUpdatedChoices();
1403  },
1404
1405  onHover: function(event) {
1406    var element = Event.findElement(event, 'LI');
1407    if(this.index != element.autocompleteIndex) 
1408    {
1409        this.index = element.autocompleteIndex;
1410        this.render();
1411    }
1412    Event.stop(event);
1413  },
1414  
1415  onClick: function(event) {
1416    var element = Event.findElement(event, 'LI');
1417    this.index = element.autocompleteIndex;
1418    this.selectEntry();
1419    this.hide();
1420  },
1421  
1422  onBlur: function(event) {
1423    // needed to make click events working
1424    setTimeout(this.hide.bind(this), 250);
1425    this.hasFocus = false;
1426    this.active = false;     
1427  }, 
1428  
1429  render: function() {
1430    if(this.entryCount > 0) {
1431      for (var i = 0; i < this.entryCount; i++)
1432        this.index==i ? 
1433          Element.addClassName(this.getEntry(i),"selected") : 
1434          Element.removeClassName(this.getEntry(i),"selected");
1435      if(this.hasFocus) { 
1436        this.show();
1437        this.active = true;
1438      }
1439    } else {
1440      this.active = false;
1441      this.hide();
1442    }
1443  },
1444  
1445  markPrevious: function() {
1446    if(this.index > 0) this.index--
1447      else this.index = this.entryCount-1;
1448    this.getEntry(this.index).scrollIntoView(true);
1449  },
1450  
1451  markNext: function() {
1452    if(this.index < this.entryCount-1) this.index++
1453      else this.index = 0;
1454    this.getEntry(this.index).scrollIntoView(false);
1455  },
1456  
1457  getEntry: function(index) {
1458    return this.update.firstChild.childNodes[index];
1459  },
1460  
1461  getCurrentEntry: function() {
1462    return this.getEntry(this.index);
1463  },
1464  
1465  selectEntry: function() {
1466    this.active = false;
1467    this.updateElement(this.getCurrentEntry());
1468  },
1469
1470  updateElement: function(selectedElement) {
1471    if (this.options.updateElement) {
1472      this.options.updateElement(selectedElement);
1473      return;
1474    }
1475    var value = '';
1476    if (this.options.select) {
1477      var nodes = $(selectedElement).select('.' + this.options.select) || [];
1478      if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
1479    } else
1480      value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
1481    
1482    var bounds = this.getTokenBounds();
1483    if (bounds[0] != -1) {
1484      var newValue = this.element.value.substr(0, bounds[0]);
1485      var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
1486      if (whitespace)
1487        newValue += whitespace[0];
1488      this.element.value = newValue + value + this.element.value.substr(bounds[1]);
1489    } else {
1490      this.element.value = value;
1491    }
1492    this.oldElementValue = this.element.value;
1493    this.element.focus();
1494    
1495    if (this.options.afterUpdateElement)
1496      this.options.afterUpdateElement(this.element, selectedElement);
1497  },
1498
1499  updateChoices: function(choices) {
1500    if(!this.changed && this.hasFocus) {
1501      this.update.innerHTML = choices;
1502      Element.cleanWhitespace(this.update);
1503      Element.cleanWhitespace(this.update.down());
1504
1505      if(this.update.firstChild && this.update.down().childNodes) {
1506        this.entryCount = 
1507          this.update.down().childNodes.length;
1508        for (var i = 0; i < this.entryCount; i++) {
1509          var entry = this.getEntry(i);
1510          entry.autocompleteIndex = i;
1511          this.addObservers(entry);
1512        }
1513      } else { 
1514        this.entryCount = 0;
1515      }
1516
1517      this.stopIndicator();
1518      this.index = 0;
1519      
1520      if(this.entryCount==1 && this.options.autoSelect) {
1521        this.selectEntry();
1522        this.hide();
1523      } else {
1524        this.render();
1525      }
1526    }
1527  },
1528
1529  addObservers: function(element) {
1530    Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
1531    Event.observe(element, "click", this.onClick.bindAsEventListener(this));
1532  },
1533
1534  onObserverEvent: function() {
1535    this.changed = false;   
1536    this.tokenBounds = null;
1537    if(this.getToken().length>=this.options.minChars) {
1538      this.getUpdatedChoices();
1539    } else {
1540      this.active = false;
1541      this.hide();
1542    }
1543    this.oldElementValue = this.element.value;
1544  },
1545
1546  getToken: function() {
1547    var bounds = this.getTokenBounds();
1548    return this.element.value.substring(bounds[0], bounds[1]).strip();
1549  },
1550
1551  getTokenBounds: function() {
1552    if (null != this.tokenBounds) return this.tokenBounds;
1553    var value = this.element.value;
1554    if (value.strip().empty()) return [-1, 0];
1555    var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
1556    var offset = (diff == this.oldElementValue.length ? 1 : 0);
1557    var prevTokenPos = -1, nextTokenPos = value.length;
1558    var tp;
1559    for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
1560      tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
1561      if (tp > prevTokenPos) prevTokenPos = tp;
1562      tp = value.indexOf(this.options.tokens[index], diff + offset);
1563      if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
1564    }
1565    return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
1566  }
1567});
1568
1569Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
1570  var boundary = Math.min(newS.length, oldS.length);
1571  for (var index = 0; index < boundary; ++index)
1572    if (newS[index] != oldS[index])
1573      return index;
1574  return boundary;
1575};
1576
1577Ajax.Autocompleter = Class.create(Autocompleter.Base, {
1578  initialize: function(element, update, url, options) {
1579    this.baseInitialize(element, update, options);
1580    this.options.asynchronous  = true;
1581    this.options.onComplete    = this.onComplete.bind(this);
1582    this.options.defaultParams = this.options.parameters || null;
1583    this.url                   = url;
1584  },
1585
1586  getUpdatedChoices: function() {
1587    this.startIndicator();
1588    
1589    var entry = encodeURIComponent(this.options.paramName) + '=' + 
1590      encodeURIComponent(this.getToken());
1591
1592    this.options.parameters = this.options.callback ?
1593      this.options.callback(this.element, entry) : entry;
1594
1595    if(this.options.defaultParams) 
1596      this.options.parameters += '&' + this.options.defaultParams;
1597    
1598    new Ajax.Request(this.url, this.options);
1599  },
1600
1601  onComplete: function(request) {
1602    this.updateChoices(request.responseText);
1603  }
1604});
1605
1606// The local array autocompleter. Used when you'd prefer to
1607// inject an array of autocompletion options into the page, rather
1608// than sending out Ajax queries, which can be quite slow sometimes.
1609//
1610// The constructor takes four parameters. The first two are, as usual,
1611// the id of the monitored textbox, and id of the autocompletion menu.
1612// The third is the array you want to autocomplete from, and the fourth
1613// is the options block.
1614//
1615// Extra local autocompletion options:
1616// - choices - How many autocompletion choices to offer
1617//
1618// - partialSearch - If false, the autocompleter will match entered
1619//                    text only at the beginning of strings in the 
1620//                    autocomplete array. Defaults to true, which will
1621//                    match text at the beginning of any *word* in the
1622//                    strings in the autocomplete array. If you want to
1623//                    search anywhere in the string, additionally set
1624//                    the option fullSearch to true (default: off).
1625//
1626// - fullSsearch - Search anywhere in autocomplete array strings.
1627//
1628// - partialChars - How many characters to enter before triggering
1629//                   a partial match (unlike minChars, which defines
1630//                   how many characters are required to do any match
1631//                   at all). Defaults to 2.
1632//
1633// - ignoreCase - Whether to ignore case when autocompleting.
1634//                 Defaults to true.
1635//
1636// It's possible to pass in a custom function as the 'selector' 
1637// option, if you prefer to write your own autocompletion logic.
1638// In that case, the other options above will not apply unless
1639// you support them.
1640
1641Autocompleter.Local = Class.create(Autocompleter.Base, {
1642  initialize: function(element, update, array, options) {
1643    this.baseInitialize(element, update, options);
1644    this.options.array = array;
1645  },
1646
1647  getUpdatedChoices: function() {
1648    this.updateChoices(this.options.selector(this));
1649  },
1650
1651  setOptions: function(options) {
1652    this.options = Object.extend({
1653      choices: 10,
1654      partialSearch: true,
1655      partialChars: 2,
1656      ignoreCase: true,
1657      fullSearch: false,
1658      selector: function(instance) {
1659        var ret       = []; // Beginning matches
1660        var partial   = []; // Inside matches
1661        var entry     = instance.getToken();
1662        var count     = 0;
1663
1664        for (var i = 0; i < instance.options.array.length &&  
1665          ret.length < instance.options.choices ; i++) { 
1666
1667          var elem = instance.options.array[i];
1668          var foundPos = instance.options.ignoreCase ? 
1669            elem.toLowerCase().indexOf(entry.toLowerCase()) : 
1670            elem.indexOf(entry);
1671
1672          while (foundPos != -1) {
1673            if (foundPos == 0 && elem.length != entry.length) { 
1674              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + 
1675                elem.substr(entry.length) + "</li>");
1676              break;
1677            } else if (entry.length >= instance.options.partialChars && 
1678              instance.options.partialSearch && foundPos != -1) {
1679              if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
1680                partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
1681                  elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
1682                  foundPos + entry.length) + "</li>");
1683                break;
1684              }
1685            }
1686
1687            foundPos = instance.options.ignoreCase ? 
1688              elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 
1689              elem.indexOf(entry, foundPos + 1);
1690
1691          }
1692        }
1693        if (partial.length)
1694          ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
1695        return "<ul>" + ret.join('') + "</ul>";
1696      }
1697    }, options || { });
1698  }
1699});
1700
1701// AJAX in-place editor and collection editor
1702// Full rewrite by Christophe Porteneuve <[email protected]> (April 2007).
1703
1704// Use this if you notice weird scrolling problems on some browsers,
1705// the DOM might be a bit confused when this gets called so do this
1706// waits 1 ms (with setTimeout) until it does the activation
1707Field.scrollFreeActivate = function(field) {
1708  setTimeout(function() {
1709    Field.activate(field);
1710  }, 1);
1711}
1712
1713Ajax.InPlaceEditor = Class.create({
1714  initialize: function(element, url, options) {
1715    this.url = url;
1716    this.element = element = $(element);
1717    this.prepareOptions();
1718    this._controls = { };
1719    arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
1720    Object.extend(this.options, options || { });
1721    if (!this.options.formId && this.element.id) {
1722      this.options.formId = this.element.id + '-inplaceeditor';
1723      if ($(this.options.formId))
1724        this.options.formId = '';
1725    }
1726    if (this.options.externalControl)
1727      this.options.externalControl = $(this.options.externalControl);
1728    if (!this.options.externalControl)
1729      this.options.externalControlOnly = false;
1730    this._originalBackground = this.element.getStyle('background-color') || 'transparent';
1731    this.element.title = this.options.clickToEditText;
1732    this._boundCancelHandler = this.handleFormCancellation.bind(this);
1733    this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
1734    this._boundFailureHandler = this.handleAJAXFailure.bind(this);
1735    this._boundSubmitHandler = this.handleFormSubmission.bind(this);
1736    this._boundWrapperHandler = this.wrapUp.bind(this);
1737    this.registerListeners();
1738  },
1739  checkForEscapeOrReturn: function(e) {
1740    if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
1741    if (Event.KEY_ESC == e.keyCode)
1742      this.handleFormCancellation(e);
1743    else if (Event.KEY_RETURN == e.keyCode)
1744      this.handleFormSubmission(e);
1745  },
1746  createControl: function(mode, handler, extraClasses) {
1747    var control = this.options[mode + 'Control'];
1748    var text = this.options[mode + 'Text'];
1749    if ('button' == control) {
1750      var btn = document.createElement('input');
1751      btn.type = 'submit';
1752      btn.value = text;
1753      btn.className = 'editor_' + mode + '_button';
1754      if ('cancel' == mode)
1755        btn.onclick = this._boundCancelHandler;
1756      this._form.appendChild(btn);
1757      this._controls[mode] = btn;
1758    } else if ('link' == control) {
1759      var link = document.createElement('a');
1760      link.href = '#';
1761      link.appendChild(document.createTextNode(text));
1762      link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
1763      link.className = 'editor_' + mode + '_link';
1764      if (extraClasses)
1765        link.className += ' ' + extraClasses;
1766      this._form.appendChild(link);
1767      this._controls[mode] = link;
1768    }
1769  },
1770  createEditField: function() {
1771    var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
1772    var fld;
1773    if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
1774      fld = document.createElement('input');
1775      fld.type = 'text';
1776      var size = this.options.size || this.options.cols || 0;
1777      if (0 < size) fld.size = size;
1778    } else {
1779      fld = document.createElement('textarea');
1780      fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
1781      fld.cols = this.options.cols || 40;
1782    }
1783    fld.name = this.options.paramName;
1784    fld.value = text; // No HTML breaks conversion anymore
1785    fld.className = 'editor_field';
1786    if (this.options.submitOnBlur)
1787      fld.onblur = this._boundSubmitHandler;
1788    this._controls.editor = fld;
1789    if (this.options.loadTextURL)
1790      this.loadExternalText();
1791    this._form.appendChild(this._controls.editor);
1792  },
1793  createForm: function() {
1794    var ipe = this;
1795    function addText(mode, condition) {
1796      var text = ipe.options['text' + mode + 'Controls'];
1797      if (!text || condition === false) return;
1798      ipe._form.appendChild(document.createTextNode(text));
1799    };
1800    this._form = $(document.createElement('form'));
1801    this._form.id = this.options.formId;
1802    this._form.addClassName(this.options.formClassName);
1803    this._form.onsubmit = this._boundSubmitHandler;
1804    this.createEditField();
1805    if ('textarea' == this._controls.editor.tagName.toLowerCase())
1806      this._form.appendChild(document.createElement('br'));
1807    if (this.options.onFormCustomization)
1808      this.options.onFormCustomization(this, this._form);
1809    addText('Before', this.options.okControl || this.options.cancelControl);
1810    this.createControl('ok', this._boundSubmitHandler);
1811    addText('Between', this.options.okControl && this.options.cancelControl);
1812    this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
1813    addText('After', this.options.okControl || this.options.cancelControl);
1814  },
1815  destroy: function() {
1816    if (this._oldInnerHTML)
1817      this.element.innerHTML = this._oldInnerHTML;
1818    this.leaveEditMode();
1819    this.unregisterListeners();
1820  },
1821  enterEditMode: function(e) {
1822    if (this._saving || this._editing) return;
1823    this._editing = true;
1824    this.triggerCallback('onEnterEditMode');
1825    if (this.options.externalControl)
1826      this.options.externalControl.hide();
1827    this.element.hide();
1828    this.createForm();
1829    this.element.parentNode.insertBefore(this._form, this.element);
1830    if (!this.options.loadTextURL)
1831      this.postProcessEditField();
1832    if (e) Event.stop(e);
1833  },
1834  enterHover: function(e) {
1835    if (this.options.hoverClassName)
1836      this.element.addClassName(this.options.hoverClassName);
1837    if (this._saving) return;
1838    this.triggerCallback('onEnterHover');
1839  },
1840  getText: function() {
1841    return this.element.innerHTML;
1842  },
1843  handleAJAXFailure: function(transport) {
1844    this.triggerCallback('onFailure', transport);
1845    if (this._oldInnerHTML) {
1846      this.element.innerHTML = this._oldInnerHTML;
1847      this._oldInnerHTML = null;
1848    }
1849  },
1850  handleFormCancellation: function(e) {
1851    this.wrapUp();
1852    if (e) Event.stop(e);
1853  },
1854  handleFormSubmission: function(e) {
1855    var form = this._form;
1856    var value = $F(this._controls.editor);
1857    this.prepareSubmission();
1858    var params = this.options.callback(form, value) || '';
1859    if (Object.isString(params))
1860      params = params.toQueryParams();
1861    params.editorId = this.element.id;
1862    if (this.options.htmlResponse) {
1863      var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
1864      Object.extend(options, {
1865        parameters: params,
1866        onComplete: this._boundWrapperHandler,
1867        onFailure: this._boundFailureHandler
1868      });
1869      new Ajax.Updater({ success: this.element }, this.url, options);
1870    } else {
1871      var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
1872      Object.extend(options, {
1873        parameters: params,
1874        onComplete: this._boundWrapperHandler,
1875        onFailure: this._boundFailureHandler
1876      });
1877      new Ajax.Request(this.url, options);
1878    }
1879    if (e) Event.stop(e);
1880  },
1881  leaveEditMode: function() {
1882    this.element.removeClassName(this.options.savingClassName);
1883    this.removeForm();
1884    this.leaveHover();
1885    this.element.style.backgroundColor = this._originalBackground;
1886    this.element.show();
1887    if (this.options.externalControl)
1888      this.options.externalControl.show();
1889    this._saving = false;
1890    this._editing = false;
1891    this._oldInnerHTML = null;
1892    this.triggerCallback('onLeaveEditMode');
1893  },
1894  leaveHover: function(e) {
1895    if (this.options.hoverClassName)
1896      this.element.removeClassName(this.options.hoverClassName);
1897    if (this._saving) return;
1898    this.triggerCallback('onLeaveHover');
1899  },
1900  loadExternalText: function() {
1901    this._form.addClassName(this.options.loadingClassName);
1902    this._controls.editor.disabled = true;
1903    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
1904    Object.extend(options, {
1905      parameters: 'editorId=' + encodeURIComponent(this.element.id),
1906      onComplete: Prototype.emptyFunction,
1907      onSuccess: function(transport) {
1908        this._form.removeClassName(this.options.loadingClassName);
1909        var text = transport.responseText;
1910        if (this.options.stripLoadedTextTags)
1911          text = text.stripTags();
1912        this._controls.editor.value = text;
1913        this._controls.editor.disabled = false;
1914        this.postProcessEditField();
1915      }.bind(this),
1916      onFailure: this._boundFailureHandler
1917    });
1918    new Ajax.Request(this.options.loadTextURL, options);
1919  },
1920  postProcessEditField: function() {
1921    var fpc = this.options.fieldPostCreation;
1922    if (fpc)
1923      $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
1924  },
1925  prepareOptions: function() {
1926    this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
1927    Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
1928    [this._extraDefaultOptions].flatten().compact().each(function(defs) {
1929      Object.extend(this.options, defs);
1930    }.bind(this));
1931  },
1932  prepareSubmission: function() {
1933    this._saving = true;
1934    this.removeForm();
1935    this.leaveHover();
1936    this.showSaving();
1937  },
1938  registerListeners: function() {
1939    this._listeners = { };
1940    var listener;
1941    $H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
1942      listener = this[pair.value].bind(this);
1943      this._listeners[pair.key] = listener;
1944      if (!this.options.externalControlOnly)
1945        this.element.observe(pair.key, listener);
1946      if (this.options.externalControl)
1947        this.options.externalControl.observe(pair.key, listener);
1948    }.bind(this));
1949  },
1950  removeForm: function() {
1951    if (!this._form) return;
1952    this._form.remove();
1953    this._form = null;
1954    this._controls = { };
1955  },
1956  showSaving: function() {
1957    this._oldInnerHTML = this.element.innerHTML;
1958    this.element.innerHTML = this.options.savingText;
1959    this.element.addClassName(this.options.savingClassName);
1960    this.element.style.backgroundColor = this._originalBackgroun
1960d;
1961    this.element.show();
1962  },
1963  triggerCallback: function(cbName, arg) {
1964    if ('function' == typeof this.options[cbName]) {
1965      this.options[cbName](this, arg);
1966    }
1967  },
1968  unregisterListeners: function() {
1969    $H(this._listeners).each(function(pair) {
1970      if (!this.options.externalControlOnly)
1971        this.element.stopObserving(pair.key, pair.value);
1972      if (this.options.externalControl)
1973        this.options.externalControl.stopObserving(pair.key, pair.value);
1974    }.bind(this));
1975  },
1976  wrapUp: function(transport) {
1977    this.leaveEditMode();
1978    // Can't use triggerCallback due to backward compatibility: requires
1979    // binding + direct element
1980    this._boundComplete(transport, this.element);
1981  }
1982});
1983
1984Object.extend(Ajax.InPlaceEditor.prototype, {
1985  dispose: Ajax.InPlaceEditor.prototype.destroy
1986});
1987
1988Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
1989  initialize: function($super, element, url, options) {
1990    this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
1991    $super(element, url, options);
1992  },
1993
1994  createEditField: function() {
1995    var list = document.createElement('select');
1996    list.name = this.options.paramName;
1997    list.size = 1;
1998    this._controls.editor = list;
1999    this._collection = this.options.collection || [];
2000    if (this.options.loadCollectionURL)
2001      this.loadCollection();
2002    else
2003      this.checkForExternalText();
2004    this._form.appendChild(this._controls.editor);
2005  },
2006
2007  loadCollection: function() {
2008    this._form.addClassName(this.options.loadingClassName);
2009    this.showLoadingText(this.options.loadingCollectionText);
2010    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
2011    Object.extend(options, {
2012      parameters: 'editorId=' + encodeURIComponent(this.element.id),
2013      onComplete: Prototype.emptyFunction,
2014      onSuccess: function(transport) {
2015        var js = transport.responseText.strip();
2016        if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
2017          throw 'Server returned an invalid collection representation.';
2018        this._collection = eval(js);
2019        this.checkForExternalText();
2020      }.bind(this),
2021      onFailure: this.onFailure
2022    });
2023    new Ajax.Request(this.options.loadCollectionURL, options);
2024  },
2025
2026  showLoadingText: function(text) {
2027    this._controls.editor.disabled = true;
2028    var tempOption = this._controls.editor.firstChild;
2029    if (!tempOption) {
2030      tempOption = document.createElement('option');
2031      tempOption.value = '';
2032      this._controls.editor.appendChild(tempOption);
2033      tempOption.selected = true;
2034    }
2035    tempOption.update((text || '').stripScripts().stripTags());
2036  },
2037
2038  checkForExternalText: function() {
2039    this._text = this.getText();
2040    if (this.options.loadTextURL)
2041      this.loadExternalText();
2042    else
2043      this.buildOptionList();
2044  },
2045
2046  loadExternalText: function() {
2047    this.showLoadingText(this.options.loadingText);
2048    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
2049    Object.extend(options, {
2050      parameters: 'editorId=' + encodeURIComponent(this.element.id),
2051      onComplete: Prototype.emptyFunction,
2052      onSuccess: function(transport) {
2053        this._text = transport.responseText.strip();
2054        this.buildOptionList();
2055      }.bind(this),
2056      onFailure: this.onFailure
2057    });
2058    new Ajax.Request(this.options.loadTextURL, options);
2059  },
2060
2061  buildOptionList: function() {
2062    this._form.removeClassName(this.options.loadingClassName);
2063    this._collection = this._collection.map(function(entry) {
2064      return 2 === entry.length ? entry : [entry, entry].flatten();
2065    });
2066    var marker = ('value' in this.options) ? this.options.value : this._text;
2067    var textFound = this._collection.any(function(entry) {
2068      return entry[0] == marker;
2069    }.bind(this));
2070    this._controls.editor.update('');
2071    var option;
2072    this._collection.each(function(entry, index) {
2073      option = document.createElement('option');
2074      option.value = entry[0];
2075      option.selected = textFound ? entry[0] == marker : 0 == index;
2076      option.appendChild(document.createTextNode(entry[1]));
2077      this._controls.editor.appendChild(option);
2078    }.bind(this));
2079    this._controls.editor.disabled = false;
2080    Field.scrollFreeActivate(this._controls.editor);
2081  }
2082});
2083
2084//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
2085//**** This only  exists for a while,  in order to  let ****
2086//**** users adapt to  the new API.  Read up on the new ****
2087//**** API and convert your code to it ASAP!            ****
2088
2089Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
2090  if (!options) return;
2091  function fallback(name, expr) {
2092    if (name in options || expr === undefined) return;
2093    options[name] = expr;
2094  };
2095  fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
2096    options.cancelLink == options.cancelButton == false ? false : undefined)));
2097  fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
2098    options.okLink == options.okButton == false ? false : undefined)));
2099  fallback('highlightColor', options.highlightcolor);
2100  fallback('highlightEndColor', options.highlightendcolor);
2101};
2102
2103Object.extend(Ajax.InPlaceEditor, {
2104  DefaultOptions: {
2105    ajaxOptions: { },
2106    autoRows: 3,                                // Use when multi-line w/ rows == 1
2107    cancelControl: 'link',                      // 'link'|'button'|false
2108    cancelText: 'cancel',
2109    clickToEditText: 'Click to edit',
2110    externalControl: null,                      // id|elt
2111    externalControlOnly: false,
2112    fieldPostCreation: 'activate',              // 'activate'|'focus'|false
2113    formClassName: 'inplaceeditor-form',
2114    formId: null,                               // id|elt
2115    highlightColor: '#ffff99',
2116    highlightEndColor: '#ffffff',
2117    hoverClassName: '',
2118    htmlResponse: true,
2119    loadingClassName: 'inplaceeditor-loading',
2120    loadingText: 'Loading...',
2121    okControl: 'button',                        // 'link'|'button'|false
2122    okText: 'ok',
2123    paramName: 'value',
2124    rows: 1,                                    // If 1 and multi-line, uses autoRows
2125    savingClassName: 'inplaceeditor-saving',
2126    savingText: 'Saving...',
2127    size: 0,
2128    stripLoadedTextTags: false,
2129    submitOnBlur: false,
2130    textAfterControls: '',
2131    textBeforeControls: '',
2132    textBetweenControls: ''
2133  },
2134  DefaultCallbacks: {
2135    callback: function(form) {
2136      return Form.serialize(form);
2137    },
2138    onComplete: function(transport, element) {
2139      // For backward compatibility, this one is bound to the IPE, and passes
2140      // the element directly.  It was too often customized, so we don't break it.
2141      new Effect.Highlight(element, {
2142        startcolor: this.options.highlightColor, keepBackgroundImage: true });
2143    },
2144    onEnterEditMode: null,
2145    onEnterHover: function(ipe) {
2146      ipe.element.style.backgroundColor = ipe.options.highlightColor;
2147      if (ipe._effect)
2148        ipe._effect.cancel();
2149    },
2150    onFailure: function(transport, ipe) {
2151      alert('Error communication with the server: ' + transport.responseText.stripTags());
2152    },
2153    onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
2154    onLeaveEditMode: null,
2155    onLeaveHover: function(ipe) {
2156      ipe._effect = new Effect.Highlight(ipe.element, {
2157        startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
2158        restorecolor: ipe._originalBackground, keepBackgroundImage: true
2159      });
2160    }
2161  },
2162  Listeners: {
2163    click: 'enterEditMode',
2164    keydown: 'checkForEscapeOrReturn',
2165    mouseover: 'enterHover',
2166    mouseout: 'leaveHover'
2167  }
2168});
2169
2170Ajax.InPlaceCollectionEditor.DefaultOptions = {
2171  loadingCollectionText: 'Loading options...'
2172};
2173
2174// Delayed observer, like Form.Element.Observer, 
2175// but waits for delay after last key input
2176// Ideal for live-search fields
2177
2178Form.Element.DelayedObserver = Class.create({
2179  initialize: function(element, delay, callback) {
2180    this.delay     = delay || 0.5;
2181    this.element   = $(element);
2182    this.callback  = callback;
2183    this.timer     = null;
2184    this.lastValue = $F(this.element); 
2185    Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
2186  },
2187  delayedListener: function(event) {
2188    if(this.lastValue == $F(this.element)) return;
2189    if(this.timer) clearTimeout(this.timer);
2190    this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
2191    this.lastValue = $F(this.element);
2192  },
2193  onTimerEvent: function() {
2194    this.timer = null;
2195    this.callback(this.element, $F(this.element));
2196  }
2197});
2198
2199if (!Control) var Control = { };
2200
2201// options:
2202//  axis: 'vertical', or 'horizontal' (default)
2203//
2204// callbacks:
2205//  onChange(value)
2206//  onSlide(value)
2207Control.Slider = Class.create({
2208  initialize: function(handle, track, options) {
2209    var slider = this;
2210    
2211    if (Object.isArray(handle)) {
2212      this.handles = handle.collect( function(e) { return $(e) });
2213    } else {
2214      this.handles = [$(handle)];
2215    }
2216    
2217    this.track   = $(track);
2218    this.options = options || { };
2219
2220    this.axis      = this.options.axis || 'horizontal';
2221    this.increment = this.options.increment || 1;
2222    this.step      = parseInt(this.options.step || '1');
2223    this.range     = this.options.range || $R(0,1);
2224    
2225    this.value     = 0; // assure backwards compat
2226    this.values    = this.handles.map( function() { return 0 });
2227    this.spans     = this.options.spans ? this.options.spans.map(function(s){ return $(s) }) : false;
2228    this.options.startSpan = $(this.options.startSpan || null);
2229    this.options.endSpan   = $(this.options.endSpan || null);
2230
2231    this.restricted = this.options.restricted || false;
2232
2233    this.maximum   = this.options.maximum || this.range.end;
2234    this.minimum   = this.options.minimum || this.range.start;
2235
2236    // Will be used to align the handle onto the track, if necessary
2237    this.alignX = parseInt(this.options.alignX || '0');
2238    this.alignY = parseInt(this.options.alignY || '0');
2239    
2240    this.trackLength = this.maximumOffset() - this.minimumOffset();
2241
2242    this.handleLength = this.isVertical() ? 
2243      (this.handles[0].offsetHeight != 0 ? 
2244        this.handles[0].offsetHeight : this.handles[0].style.height.replace(/px$/,"")) : 
2245      (this.handles[0].offsetWidth != 0 ? this.handles[0].offsetWidth : 
2246        this.handles[0].style.width.replace(/px$/,""));
2247
2248    this.active   = false;
2249    this.dragging = false;
2250    this.disabled = false;
2251
2252    if (this.options.disabled) this.setDisabled();
2253
2254    // Allowed values array
2255    this.allowedValues = this.options.values ? this.options.values.sortBy(Prototype.K) : false;
2256    if (this.allowedValues) {
2257      this.minimum = this.allowedValues.min();
2258      this.maximum = this.allowedValues.max();
2259    }
2260
2261    this.eventMouseDown = this.startDrag.bindAsEventListener(this);
2262    this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
2263    this.eventMouseMove = this.update.bindAsEventListener(this);
2264
2265    // Initialize handles in reverse (make sure first handle is active)
2266    this.handles.each( function(h,i) {
2267      i = slider.handles.length-1-i;
2268      slider.setValue(parseFloat(
2269        (Object.isArray(slider.options.sliderValue) ? 
2270          slider.options.sliderValue[i] : slider.options.sliderValue) || 
2271         slider.range.start), i);
2272      h.makePositioned().observe("mousedown", slider.eventMouseDown);
2273    });
2274    
2275    this.track.observe("mousedown", this.eventMouseDown);
2276    document.observe("mouseup", this.eventMouseUp);
2277    document.observe("mousemove", this.eventMouseMove);
2278    
2279    this.initialized = true;
2280  },
2281  dispose: function() {
2282    var slider = this;    
2283    Event.stopObserving(this.track, "mousedown", this.eventMouseDown);
2284    Event.stopObserving(document, "mouseup", this.eventMouseUp);
2285    Event.stopObserving(document, "mousemove", this.eventMouseMove);
2286    this.handles.each( function(h) {
2287      Event.stopObserving(h, "mousedown", slider.eventMouseDown);
2288    });
2289  },
2290  setDisabled: function(){
2291    this.disabled = true;
2292  },
2293  setEnabled: function(){
2294    this.disabled = false;
2295  },  
2296  getNearestValue: function(value){
2297    if (this.allowedValues){
2298      if (value >= this.allowedValues.max()) return(this.allowedValues.max());
2299      if (value <= this.allowedValues.min()) return(this.allowedValues.min());
2300      
2301      var offset = Math.abs(this.allowedValues[0] - value);
2302      var newValue = this.allowedValues[0];
2303      this.allowedValues.each( function(v) {
2304        var currentOffset = Math.abs(v - value);
2305        if (currentOffset <= offset){
2306          newValue = v;
2307          offset = currentOffset;
2308        } 
2309      });
2310      return newValue;
2311    }
2312    if (value > this.range.end) return this.range.end;
2313    if (value < this.range.start) return this.range.start;
2314    return value;
2315  },
2316  setValue: function(sliderValue, handleIdx){
2317    if (!this.active) {
2318      this.activeHandleIdx = handleIdx || 0;
2319      this.activeHandle    = this.handles[this.activeHandleIdx];
2320      this.updateStyles();
2321    }
2322    handleIdx = handleIdx || this.activeHandleIdx || 0;
2323    if (this.initialized && this.restricted) {
2324      if ((handleIdx>0) && (sliderValue<this.values[handleIdx-1]))
2325        sliderValue = this.values[handleIdx-1];
2326      if ((handleIdx < (this.handles.length-1)) && (sliderValue>this.values[handleIdx+1]))
2327        sliderValue = this.values[handleIdx+1];
2328    }
2329    sliderValue = this.getNearestValue(sliderValue);
2330    this.values[handleIdx] = sliderValue;
2331    this.value = this.values[0]; // assure backwards compat
2332    
2333    this.handles[handleIdx].style[this.isVertical() ? 'top' : 'left'] = 
2334      this.translateToPx(sliderValue);
2335    
2336    this.drawSpans();
2337    if (!this.dragging || !this.event) this.updateFinished();
2338  },
2339  setValueBy: function(delta, handleIdx) {
2340    this.setValue(this.values[handleIdx || this.activeHandleIdx || 0] + delta, 
2341      handleIdx || this.activeHandleIdx || 0);
2342  },
2343  translateToPx: function(value) {
2344    return Math.round(
2345      ((this.trackLength-this.handleLength)/(this.range.end-this.range.start)) * 
2346      (value - this.range.start)) + "px";
2347  },
2348  translateToValue: function(offset) {
2349    return ((offset/(this.trackLength-this.handleLength) * 
2350      (this.range.end-this.range.start)) + this.range.start);
2351  },
2352  getRange: function(range) {
2353    var v = this.values.sortBy(Prototype.K); 
2354    range = range || 0;
2355    return $R(v[range],v[range+1]);
2356  },
2357  minimumOffset: function(){
2358    return(this.isVertical() ? this.alignY : this.alignX);
2359  },
2360  maximumOffset: function(){
2361    return(this.isVertical() ? 
2362      (this.track.offsetHeight != 0 ? this.track.offsetHeight :
2363        this.track.style.height.replace(/px$/,"")) - this.alignY : 
2364      (this.track.offsetWidth != 0 ? this.track.offsetWidth : 
2365        this.track.style.width.replace(/px$/,"")) - this.alignX);
2366  },  
2367  isVertical:  function(){
2368    return (this.axis == 'vertical');
2369  },
2370  drawSpans: function() {
2371    var slider = this;
2372    if (this.spans)
2373      $R(0, this.spans.length-1).each(function(r) { slider.setSpan(slider.spans[r], slider.getRange(r)) });
2374    if (this.options.startSpan)
2375      this.setSpan(this.options.startSpan,
2376        $R(0, this.values.length>1 ? this.getRange(0).min() : this.value ));
2377    if (this.options.endSpan)
2378      this.setSpan(this.options.endSpan, 
2379        $R(this.values.length>1 ? this.getRange(this.spans.length-1).max() : this.value, this.maximum));
2380  },
2381  setSpan: function(span, range) {
2382    if (this.isVertical()) {
2383      span.style.top = this.translateToPx(range.start);
2384      span.style.height = this.translateToPx(range.end - range.start + this.range.start);
2385    } else {
2386      span.style.left = this.translateToPx(range.start);
2387      span.style.width = this.translateToPx(range.end - range.start + this.range.start);
2388    }
2389  },
2390  updateStyles: function() {
2391    this.handles.each( function(h){ Element.removeClassName(h, 'selected') });
2392    Element.addClassName(this.activeHandle, 'selected');
2393  },
2394  startDrag: function(event) {
2395    if (Event.isLeftClick(event)) {
2396      if (!this.disabled){
2397        this.active = true;
2398        
2399        var handle = Event.element(event);
2400        var pointer  = [Event.pointerX(event), Event.pointerY(event)];
2401        var track = handle;
2402        if (track==this.track) {
2403          var offsets  = Position.cumulativeOffset(this.track); 
2404          this.event = event;
2405          this.setValue(this.translateToValue( 
2406           (this.isVertical() ? pointer[1]-offsets[1] : pointer[0]-offsets[0])-(this.handleLength/2)
2407          ));
2408          var offsets  = Position.cumulativeOffset(this.activeHandle);
2409          this.offsetX = (pointer[0] - offsets[0]);
2410          this.offsetY = (pointer[1] - offsets[1]);
2411        } else {
2412          // find the handle (prevents issues with Safari)
2413          while((this.handles.indexOf(handle) == -1) && handle.parentNode) 
2414            handle = handle.parentNode;
2415            
2416          if (this.handles.indexOf(handle)!=-1) {
2417            this.activeHandle    = handle;
2418            this.activeHandleIdx = this.handles.indexOf(this.activeHandle);
2419            this.updateStyles();
2420            
2421            var offsets  = Position.cumulativeOffset(this.activeHandle);
2422            this.offsetX = (pointer[0] - offsets[0]);
2423            this.offsetY = (pointer[1] - offsets[1]);
2424          }
2425        }
2426      }
2427      Event.stop(event);
2428    }
2429  },
2430  update: function(event) {
2431   if (this.active) {
2432      if (!this.dragging) this.dragging = true;
2433      this.draw(event);
2434      if (Prototype.Browser.WebKit) window.scrollBy(0,0);
2435      Event.stop(event);
2436   }
2437  },
2438  draw: function(event) {
2439    var pointer = [Event.pointerX(event), Event.pointerY(event)];
2440    var offsets = Position.cumulativeOffset(this.track);
2441    pointer[0] -= this.offsetX + offsets[0];
2442    pointer[1] -= this.offsetY + offsets[1];
2443    this.event = event;
2444    this.setValue(this.translateToValue( this.isVertical() ? pointer[1] : pointer[0] ));
2445    if (this.initialized && this.options.onSlide)
2446      this.options.onSlide(this.values.length>1 ? this.values : this.value, this);
2447  },
2448  endDrag: function(event) {
2449    if (this.active && this.dragging) {
2450      this.finishDrag(event, true);
2451      Event.stop(event);
2452    }
2453    this.active = false;
2454    this.dragging = false;
2455  },  
2456  finishDrag: function(event, success) {
2457    this.active = false;
2458    this.dragging = false;
2459    this.updateFinished();
2460  },
2461  updateFinished: function() {
2462    if (this.initialized && this.options.onChange) 
2463      this.options.onChange(this.values.length>1 ? this.values : this.value, this);
2464    this.event = null;
2465  }
2466});
2467
2468if(Object.isUndefined(Effect))
2469  throw("dragdrop.js requires including script.aculo.us' effects.js library");
2470
2471var Droppables = {
2472  drops: [],
2473
2474  remove: function(element) {
2475    this.drops = this.drops.reject(function(d) { return d.element==$(element) });
2476  },
2477
2478  add: function(element) {
2479    element = $(element);
2480    var options = Object.extend({
2481      greedy:     true,
2482      hoverclass: null,
2483      tree:       false
2484    }, arguments[1] || { });
2485
2486    // cache containers
2487    if(options.containment) {
2488      options._containers = [];
2489      var containment = options.containment;
2490      if(Object.isArray(containment)) {
2491        containment.each( function(c) { options._containers.push($(c)) });
2492      } else {
2493        options._containers.push($(containment));
2494      }
2495    }
2496    
2497    if(options.accept) options.accept = [options.accept].flatten();
2498
2499    Element.makePositioned(element); // fix IE
2500    options.element = element;
2501
2502    this.drops.push(options);
2503  },
2504  
2505  findDeepestChild: function(drops) {
2506    deepest = drops[0];
2507      
2508    for (i = 1; i < drops.length; ++i)
2509      if (Element.isParent(drops[i].element, deepest.element))
2510        deepest = drops[i];
2511    
2512    return deepest;
2513  },
2514
2515  isContained: function(element, drop) {
2516    var containmentNode;
2517    if(drop.tree) {
2518      containmentNode = element.treeNode; 
2519    } else {
2520      containmentNode = element.parentNode;
2521    }
2522    return drop._containers.detect(function(c) { return containmentNode == c });
2523  },
2524  
2525  isAffected: function(point, element, drop) {
2526    return (
2527      (drop.element!=element) &&
2528      ((!drop._containers) ||
2529        this.isContained(element, drop)) &&
2530      ((!drop.accept) ||
2531        (Element.classNames(element).detect( 
2532          function(v) { return drop.accept.include(v) } ) )) &&
2533      Position.within(drop.element, point[0], point[1]) );
2534  },
2535
2536  deactivate: function(drop) {
2537    if(drop.hoverclass)
2538      Element.removeClassName(drop.element, drop.hoverclass);
2539    this.last_active = null;
2540  },
2541
2542  activate: function(drop) {
2543    if(drop.hoverclass)
2544      Element.addClassName(drop.element, drop.hoverclass);
2545    this.last_active = drop;
2546  },
2547
2548  show: function(point, element) {
2549    if(!this.drops.length) return;
2550    var drop, affected = [];
2551    
2552    this.drops.each( function(drop) {
2553      if(Droppables.isAffected(point, element, drop))
2554        affected.push(drop);
2555    });
2556        
2557    if(affected.length>0)
2558      drop = Droppables.findDeepestChild(affected);
2559
2560    if(this.last_active && this.last_active != drop) this.deactivate(this.last_active);
2561    if (drop) {
2562      Position.within(drop.element, point[0], point[1]);
2563      if(drop.onHover)
2564        drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
2565      
2566      if (drop != this.last_active) Droppables.activate(drop);
2567    }
2568  },
2569
2570  fire: function(event, element) {
2571    if(!this.last_active) return;
2572    Position.prepare();
2573
2574    if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
2575      if (this.last_active.onDrop) {
2576        this.last_active.onDrop(element, this.last_active.element, event); 
2577        return true; 
2578      }
2579  },
2580
2581  reset: function() {
2582    if(this.last_active)
2583      this.deactivate(this.last_active);
2584  }
2585}
2586
2587var Draggables = {
2588  drags: [],
2589  observers: [],
2590  
2591  register: function(draggable) {
2592    if(this.drags.length == 0) {
2593      this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
2594      this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
2595      this.eventKeypress  = this.keyPress.bindAsEventListener(this);
2596      
2597      Event.observe(document, "mouseup", this.eventMouseUp);
2598      Event.observe(document, "mousemove", this.eventMouseMove);
2599      Event.observe(document, "keypress", this.eventKeypress);
2600    }
2601    this.drags.push(draggable);
2602  },
2603  
2604  unregister: function(draggable) {
2605    this.drags = this.drags.reject(function(d) { return d==draggable });
2606    if(this.drags.length == 0) {
2607      Event.stopObserving(document, "mouseup", this.eventMouseUp);
2608      Event.stopObserving(document, "mousemove", this.eventMouseMove);
2609      Event.stopObserving(document, "keypress", this.eventKeypress);
2610    }
2611  },
2612  
2613  activate: function(draggable) {
2614    if(draggable.options.delay) { 
2615      this._timeout = setTimeout(function() { 
2616        Draggables._timeout = null; 
2617        window.focus(); 
2618        Draggables.activeDraggable = draggable; 
2619      }.bind(this), draggable.options.delay); 
2620    } else {
2621      window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
2622      this.activeDraggable = draggable;
2623    }
2624  },
2625  
2626  deactivate: function() {
2627    this.activeDraggable = null;
2628  },
2629  
2630  updateDrag: function(event) {
2631    if(!this.activeDraggable) return;
2632    var pointer = [Event.pointerX(event), Event.pointerY(event)];
2633    // Mozilla-based browsers fire successive mousemove events with
2634    // the same coordinates, prevent needless redrawing (moz bug?)
2635    if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
2636    this._lastPointer = pointer;
2637    
2638    this.activeDraggable.updateDrag(event, pointer);
2639  },
2640  
2641  endDrag: function(event) {
2642    if(this._timeout) { 
2643      clearTimeout(this._timeout); 
2644      this._timeout = null; 
2645    }
2646    if(!this.activeDraggable) return;
2647    this._lastPointer = null;
2648    this.activeDraggable.endDrag(event);
2649    this.activeDraggable = null;
2650  },
2651  
2652  keyPress: function(event) {
2653    if(this.activeDraggable)
2654      this.activeDraggable.keyPress(event);
2655  },
2656  
2657  addObserver: function(observer) {
2658    this.observers.push(observer);
2659    this._cacheObserverCallbacks();
2660  },
2661  
2662  removeObserver: function(element) {  // element instead of observer fixes mem leaks
2663    this.observers = this.observers.reject( function(o) { return o.element==element });
2664    this._cacheObserverCallbacks();
2665  },
2666  
2667  notify: function(eventName, draggable, event) {  // 'onStart', 'onEnd', 'onDrag'
2668    if(this[eventName+'Count'] > 0)
2669      this.observers.each( function(o) {
2670        if(o[eventName]) o[eventName](eventName, draggable, event);
2671      });
2672    if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
2673  },
2674  
2675  _cacheObserverCallbacks: function() {
2676    ['onStart','onEnd','onDrag'].each( function(eventName) {
2677      Draggables[eventName+'Count'] = Draggables.observers.select(
2678        function(o) { return o[eventName]; }
2679      ).length;
2680    });
2681  }
2682}
2683
2684/*--------------------------------------------------------------------------*/
2685
2686var Draggable = Class.create({
2687  initialize: function(element) {
2688    var defaults = {
2689      handle: false,
2690      reverteffect: function(element, top_offset, left_offset) {
2691        var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
2692        new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
2693          queue: {scope:'_draggable', position:'end'}
2694        });
2695      },
2696      endeffect: function(element) {
2697        var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0;
2698        new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, 
2699          queue: {scope:'_draggable', position:'end'},
2700          afterFinish: function(){ 
2701            Draggable._dragging[element] = false 
2702          }
2703        }); 
2704      },
2705      zindex: 1000,
2706      revert: false,
2707      quiet: false,
2708      scroll: false,
2709      scrollSensitivity: 20,
2710      scrollSpeed: 15,
2711      snap: false,  // false, or xy or [x,y] or function(x,y){ return [x,y] }
2712      delay: 0
2713    };
2714    
2715    if(!arguments[1] || Object.isUndefined(arguments[1].endeffect))
2716      Object.extend(defaults, {
2717        starteffect: function(element) {
2718          element._opacity = Element.getOpacity(element);
2719          Draggable._dragging[element] = true;
2720          new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); 
2721        }
2722      });
2723    
2724    var options = Object.extend(defaults, arguments[1] || { });
2725
2726    this.element = $(element);
2727    
2728    if(options.handle && Object.isString(options.handle))
2729      this.handle = this.element.down('.'+options.handle, 0);
2730    
2731    if(!this.handle) this.handle = $(options.handle);
2732    if(!this.handle) this.handle = this.element;
2733    
2734    if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
2735      options.scroll = $(options.scroll);
2736      this._isScrollChild = Element.childOf(this.element, options.scroll);
2737    }
2738
2739    Element.makePositioned(this.element); // fix IE    
2740
2741    this.options  = options;
2742    this.dragging = false;   
2743
2744    this.eventMouseDown = this.initDrag.bindAsEventListener(this);
2745    Event.observe(this.handle, "mousedown", this.eventMouseDown);
2746    
2747    Draggables.register(this);
2748  },
2749  
2750  destroy: function() {
2751    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
2752    Draggables.unregister(this);
2753  },
2754  
2755  currentDelta: function() {
2756    return([
2757      parseInt(Element.getStyle(this.element,'left') || '0'),
2758      parseInt(Element.getStyle(this.element,'top') || '0')]);
2759  },
2760  
2761  initDrag: function(event) {
2762    if(!Object.isUndefined(Draggable._dragging[this.element]) &&
2763      Draggable._dragging[this.element]) return;
2764    if(Event.isLeftClick(event)) {    
2765      // abort on form elements, fixes a Firefox issue
2766      var src = Event.element(event);
2767      if((tag_name = src.tagName.toUpperCase()) && (
2768        tag_name=='INPUT' ||
2769        tag_name=='SELECT' ||
2770        tag_name=='OPTION' ||
2771        tag_name=='BUTTON' ||
2772        tag_name=='TEXTAREA')) return;
2773        
2774      var pointer = [Event.pointerX(event), Event.pointerY(event)];
2775      var pos     = Position.cumulativeOffset(this.element);
2776      this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
2777      
2778      Draggables.activate(this);
2779      Event.stop(event);
2780    }
2781  },
2782  
2783  startDrag: function(event) {
2784    this.dragging = true;
2785    if(!this.delta)
2786      this.delta = this.currentDelta();
2787    
2788    if(this.options.zindex) {
2789      this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
2790      this.element.style.zIndex = this.options.zindex;
2791    }
2792    
2793    if(this.options.ghosting) {
2794      this._clone = this.element.cloneNode(true);
2795      this.element._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
2796      if (!this.element._originallyAbsolute)
2797        Position.absolutize(this.element);
2798      this.element.parentNode.insertBefore(this._clone, this.element);
2799    }
2800    
2801    if(this.options.scroll) {
2802      if (this.options.scroll == window) {
2803        var where = this._getWindowScroll(this.options.scroll);
2804        this.originalScrollLeft = where.left;
2805        this.originalScrollTop = where.top;
2806      } else {
2807        this.originalScrollLeft = this.options.scroll.scrollLeft;
2808        this.originalScrollTop = this.options.scroll.scrollTop;
2809      }
2810    }
2811    
2812    Draggables.notify('onStart', this, event);
2813        
2814    if(this.options.starteffect) this.options.starteffect(this.element);
2815  },
2816  
2817  updateDrag: function(event, pointer) {
2818    if(!this.dragging) this.startDrag(event);
2819    
2820    if(!this.options.quiet){
2821      Position.prepare();
2822      Droppables.show(pointer, this.element);
2823    }
2824    
2825    Draggables.notify('onDrag', this, event);
2826    
2827    this.draw(pointer);
2828    if(this.options.change) this.options.change(this);
2829    
2830    if(this.options.scroll) {
2831      this.stopScrolling();
2832      
2833      var p;
2834      if (this.options.scroll == window) {
2835        with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
2836      } else {
2837        p = Position.page(this.options.scroll);
2838        p[0] += this.options.scroll.scrollLeft + Position.deltaX;
2839        p[1] += this.options.scroll.scrollTop + Position.deltaY;
2840        p.push(p[0]+this.options.scroll.offsetWidth);
2841        p.push(p[1]+this.options.scroll.offsetHeight);
2842      }
2843      var speed = [0,0];
2844      if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
2845      if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
2846      if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
2847      if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
2848      this.startScrolling(speed);
2849    }
2850    
2851    // fix AppleWebKit rendering
2852    if(Prototype.Browser.WebKit) window.scrollBy(0,0);
2853    
2854    Event.stop(event);
2855  },
2856  
2857  finishDrag: function(event, success) {
2858    this.dragging = false;
2859    
2860    if(this.options.quiet){
2861      Position.prepare();
2862      var pointer = [Event.pointerX(event), Event.pointerY(event)];
2863      Droppables.show(pointer, this.element);
2864    }
2865
2866    if(this.options.ghosting) {
2867      if (!this.element._originallyAbsolute)
2868        Position.relativize(this.element);
2869      delete this.element._originallyAbsolute;
2870      Element.remove(this._clone);
2871      this._clone = null;
2872    }
2873
2874    var dropped = false; 
2875    if(success) { 
2876      dropped = Droppables.fire(event, this.element); 
2877      if (!dropped) dropped = false; 
2878    }
2879    if(dropped && this.options.onDropped) this.options.onDropped(this.element);
2880    Draggables.notify('onEnd', this, event);
2881
2882    var revert = this.options.revert;
2883    if(revert && Object.isFunction(revert)) revert = revert(this.element);
2884    
2885    var d = this.currentDelta();
2886    if(revert && this.options.reverteffect) {
2887      if (dropped == 0 || revert != 'failure')
2888        this.options.reverteffect(this.element,
2889          d[1]-this.delta[1], d[0]-this.delta[0]);
2890    } else {
2891      this.delta = d;
2892    }
2893
2894    if(this.options.zindex)
2895      this.element.style.zIndex = this.originalZ;
2896
2897    if(this.options.endeffect) 
2898      this.options.endeffect(this.element);
2899      
2900    Draggables.deactivate(this);
2901    Droppables.reset();
2902  },
2903  
2904  keyPress: function(event) {
2905    if(event.keyCode!=Event.KEY_ESC) return;
2906    this.finishDrag(event, false);
2907    Event.stop(event);
2908  },
2909  
2910  endDrag: function(event) {
2911    if(!this.dragging) return;
2912    this.stopScrolling();
2913    this.finishDrag(event, true);
2914    Event.stop(event);
2915  },
2916  
2917  draw: function(point) {
2918    var pos = Position.cumulativeOffset(this.element);
2919    if(this.options.ghosting) {
2920      var r   = Position.realOffset(this.element);
2921      pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
2922    }
2923    
2924    var d = this.currentDelta();
2925    pos[0] -= d[0]; pos[1] -= d[1];
2926    
2927    if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
2928      pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
2929      pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
2930    }
2931    
2932    var p = [0,1].map(function(i){ 
2933      return (point[i]-pos[i]-this.offset[i]) 
2934    }.bind(this));
2935    
2936    if(this.options.snap) {
2937      if(Object.isFunction(this.options.snap)) {
2938        p = this.options.snap(p[0],p[1],this);
2939      } else {
2940      if(Object.isArray(this.options.snap)) {
2941        p = p.map( function(v, i) {
2942          return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this))
2943      } else {
2944        p = p.map( function(v) {
2945          return (v/this.options.snap).round()*this.options.snap }.bind(this))
2946      }
2947    }}
2948    
2949    var style = this.element.style;
2950    if((!this.options.constraint) || (this.options.constraint=='horizontal'))
2951      style.left = p[0] + "px";
2952    if((!this.options.constraint) || (this.options.constraint=='vertical'))
2953      style.top  = p[1] + "px";
2954    
2955    if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
2956  },
2957  
2958  stopScrolling: function() {
2959    if(this.scrollInterval) {
2960      clearInterval(this.scrollInterval);
2961      this.scrollInterval = null;
2962      Draggables._lastScrollPointer = null;
2963    }
2964  },
2965  
2966  startScrolling: function(speed) {
2967    if(!(speed[0] || speed[1])) return;
2968    this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
2969    this.lastScrolled = new Date();
2970    this.scrollInterval = setInterval(this.scroll.bind(this), 10);
2971  },
2972  
2973  scroll: function() {
2974    var current = new Date();
2975    var delta = current - this.lastScrolled;
2976    this.lastScrolled = current;
2977    if(this.options.scroll == window) {
2978      with (this._getWindowScroll(this.options.scroll)) {
2979        if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
2980          var d = delta / 1000;
2981          this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
2982        }
2983      }
2984    } else {
2985      this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
2986      this.options.scroll.scrollTop  += this.scrollSpeed[1] * delta / 1000;
2987    }
2988    
2989    Position.prepare();
2990    Droppables.show(Draggables._lastPointer, this.element);
2991    Draggables.notify('onDrag', this);
2992    if (this._isScrollChild) {
2993      Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
2994      Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
2995      Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
2996      if (Draggables._lastScrollPointer[0] < 0)
2997        Draggables._lastScrollPointer[0] = 0;
2998      if (Draggables._lastScrollPointer[1] < 0)
2999        Draggables._lastScrollPointer[1] = 0;
3000      this.draw(Draggables._lastScrollPointer);
3001    }
3002    
3003    if(this.options.change) this.options.change(this);
3004  },
3005  
3006  _getWindowScroll: function(w) {
3007    var T, L, W, H;
3008    with (w.document) {
3009      if (w.document.documentElement && documentElement.scrollTop) {
3010        T = documentElement.scrollTop;
3011        L = documentElement.scrollLeft;
3012      } else if (w.document.body) {
3013        T = body.scrollTop;
3014        L = body.scrollLeft;
3015      }
3016      if (w.innerWidth) {
3017        W = w.innerWidth;
3018        H = w.innerHeight;
3019      } else if (w.document.documentElement && documentElement.clientWidth) {
3020        W = documentElement.clientWidth;
3021        H = documentElement.clientHeight;
3022      } else {
3023        W = body.offsetWidth;
3024        H = body.offsetHeight
3025      }
3026    }
3027    return { top: T, left: L, width: W, height: H };
3028  }
3029});
3030
3031Draggable._dragging = { };
3032
3033/*--------------------------------------------------------------------------*/
3034
3035var SortableObserver = Class.create({
3036  initialize: function(element, observer) {
3037    this.element   = $(element);
3038    this.observer  = observer;
3039    this.lastValue = Sortable.serialize(this.element);
3040  },
3041  
3042  onStart: function() {
3043    this.lastValue = Sortable.serialize(this.element);
3044  },
3045  
3046  onEnd: function() {
3047    Sortable.unmark();
3048    if(this.lastValue != Sortable.serialize(this.element))
3049      this.observer(this.element)
3050  }
3051});
3052
3053var Sortable = {
3054  SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
3055  
3056  sortables: { },
3057  
3058  _findRootElement: function(element) {
3059    while (element.tagName.toUpperCase() != "BODY") {  
3060      if(element.id && Sortable.sortables[element.id]) return element;
3061      element = element.parentNode;
3062    }
3063  },
3064
3065  options: function(element) {
3066    element = Sortable._findRootElement($(element));
3067    if(!element) return;
3068    return Sortable.sortables[element.id];
3069  },
3070  
3071  destroy: function(element){
3072    var s = Sortable.options(element);
3073    
3074    if(s) {
3075      Draggables.removeObserver(s.element);
3076      s.droppables.each(function(d){ Droppables.remove(d) });
3077      s.draggables.invoke('destroy');
3078      
3079      delete Sortable.sortables[s.element.id];
3080    }
3081  },
3082
3083  create: function(element) {
3084    element = $(element);
3085    var options = Object.extend({ 
3086      element:     element,
3087      tag:         'li',       // assumes li children, override with tag: 'tagname'
3088      dropOnEmpty: false,
3089      tree:        false,
3090      treeTag:     'ul',
3091      overlap:     'vertical', // one of 'vertical', 'horizontal'
3092      constraint:  'vertical', // one of 'vertical', 'horizontal', false
3093      containment: element,    // also takes array of elements (or id's); or false
3094      handle:      false,      // or a CSS class
3095      only:        false,
3096      delay:       0,
3097      hoverclass:  null,
3098      ghosting:    false,
3099      quiet:       false, 
3100      scroll:      false,
3101      scrollSensitivity: 20,
3102      scrollSpeed: 15,
3103      format:      this.SERIALIZE_RULE,
3104      
3105      // these take arrays of elements or ids and can be 
3106      // used for better initialization performance
3107      elements:    false,
3108      handles:     false,
3109      
3110      onChange:    Prototype.emptyFunction,
3111      onUpdate:    Prototype.emptyFunction
3112    }, arguments[1] || { });
3113
3114    // clear any old sortable with same element
3115    this.destroy(element);
3116
3117    // build options for the draggables
3118    var options_for_draggable = {
3119      revert:      true,
3120      quiet:       options.quiet,
3121      scroll:      options.scroll,
3122      scrollSpeed: options.scrollSpeed,
3123      scrollSensitivity: options.scrollSensitivity,
3124      delay:       options.delay,
3125      ghosting:    options.ghosting,
3126      constraint:  options.constraint,
3127      handle:      options.handle };
3128
3129    if(options.starteffect)
3130      options_for_draggable.starteffect = options.starteffect;
3131
3132    if(options.reverteffect)
3133      options_for_draggable.reverteffect = options.reverteffect;
3134    else
3135      if(options.ghosting) options_for_draggable.reverteffect = function(element) {
3136        element.style.top  = 0;
3137        element.style.left = 0;
3138      };
3139
3140    if(options.endeffect)
3141      options_for_draggable.endeffect = options.endeffect;
3142
3143    if(options.zindex)
3144      options_for_draggable.zindex = options.zindex;
3145
3146    // build options for the droppables  
3147    var options_for_droppable = {
3148      overlap:     options.overlap,
3149      containment: options.containment,
3150      tree:        options.tree,
3151      hoverclass:  options.hoverclass,
3152      onHover:     Sortable.onHover
3153    }
3154    
3155    var options_for_tree = {
3156      onHover:      Sortable.onEmptyHover,
3157      overlap:      options.overlap,
3158      containment:  options.containment,
3159      hoverclass:   options.hoverclass
3160    }
3161
3162    // fix for gecko engine
3163    Element.cleanWhitespace(element); 
3164
3165    options.draggables = [];
3166    options.droppables = [];
3167
3168    // drop on empty handling
3169    if(options.dropOnEmpty || options.tree) {
3170      Droppables.add(element, options_for_tree);
3171      options.droppables.push(element);
3172    }
3173
3174    (options.elements || this.findElements(element, options) || []).each( function(e,i) {
3175      var handle = options.handles ? $(options.handles[i]) :
3176        (options.handle ? $(e).select('.' + options.handle)[0] : e); 
3177      options.draggables.push(
3178        new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
3179      Droppables.add(e, options_for_droppable);
3180      if(options.tree) e.treeNode = element;
3181      options.droppables.push(e);      
3182    });
3183    
3184    if(options.tree) {
3185      (Sortable.findTreeElements(element, options) || []).each( function(e) {
3186        Droppables.add(e, options_for_tree);
3187        e.treeNode = element;
3188        options.droppables.push(e);
3189      });
3190    }
3191
3192    // keep reference
3193    this.sortables[element.id] = options;
3194
3195    // for onupdate
3196    Draggables.addObserver(new SortableObserver(element, options.onUpdate));
3197
3198  },
3199
3200  // return all suitable-for-sortable elements in a guaranteed order
3201  findElements: function(element, options) {
3202    return Element.findChildren(
3203      element, options.only, options.tree ? true : false, options.tag);
3204  },
3205  
3206  findTreeElements: function(element, options) {
3207    return Element.findChildren(
3208      element, options.only, options.tree ? true : false, options.treeTag);
3209  },
3210
3211  onHover: function(element, dropon, overlap) {
3212    if(Element.isParent(dropon, element)) return;
3213
3214    if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
3215      return;
3216    } else if(overlap>0.5) {
3217      Sortable.mark(dropon, 'before');
3218      if(dropon.previousSibling != element) {
3219        var oldParentNode = element.parentNode;
3220        element.style.visibility = "hidden"; // fix gecko rendering
3221        dropon.parentNode.insertBefore(element, dropon);
3222        if(dropon.parentNode!=oldParentNode) 
3223          Sortable.options(oldParentNode).onChange(element);
3224        Sortable.options(dropon.parentNode).onChange(element);
3225      }
3226    } else {
3227      Sortable.mark(dropon, 'after');
3228      var nextElement = dropon.nextSibling || null;
3229      if(nextElement != element) {
3230        var oldParentNode = element.parentNode;
3231        element.style.visibility = "hidden"; // fix gecko rendering
3232        dropon.parentNode.insertBefore(element, nextElement);
3233        if(dropon.parentNode!=oldParentNode) 
3234          Sortable.options(oldParentNode).onChange(element);
3235        Sortable.options(dropon.parentNode).onChange(element);
3236      }
3237    }
3238  },
3239  
3240  onEmptyHover: function(element, dropon, overlap) {
3241    var oldParentNode = element.parentNode;
3242    var droponOptions = Sortable.options(dropon);
3243        
3244    if(!Element.isParent(dropon, element)) {
3245      var index;
3246      
3247      var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
3248      var child = null;
3249            
3250      if(children) {
3251        var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
3252        
3253        for (index = 0; index < children.length; index += 1) {
3254          if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
3255            offset -= Element.offsetSize (children[index], droponOptions.overlap);
3256          } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
3257            child = index + 1 < children.length ? children[index + 1] : null;
3258            break;
3259          } else {
3260            child = children[index];
3261            break;
3262          }
3263        }
3264      }
3265      
3266      dropon.insertBefore(element, child);
3267      
3268      Sortable.options(oldParentNode).onChange(element);
3269      droponOptions.onChange(element);
3270    }
3271  },
3272
3273  unmark: function() {
3274    if(Sortable._marker) Sortable._marker.hide();
3275  },
3276
3277  mark: function(dropon, position) {
3278    // mark on ghosting only
3279    var sortable = Sortable.options(dropon.parentNode);
3280    if(sortable && !sortable.ghosting) return; 
3281
3282    if(!Sortable._marker) {
3283      Sortable._marker = 
3284        ($('dropmarker') || Element.extend(document.createElement('DIV'))).
3285          hide().addClassName('dropmarker').setStyle({position:'absolute'});
3286      document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
3287    }    
3288    var offsets = Position.cumulativeOffset(dropon);
3289    Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
3290    
3291    if(position=='after')
3292      if(sortable.overlap == 'horizontal') 
3293        Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
3294      else
3295        Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});
3296    
3297    Sortable._marker.show();
3298  },
3299  
3300  _tree: function(element, options, parent) {
3301    var children = Sortable.findElements(element, options) || [];
3302  
3303    for (var i = 0; i < children.length; ++i) {
3304      var match = children[i].id.match(options.format);
3305
3306      if (!match) continue;
3307      
3308      var child = {
3309        id: encodeURIComponent(match ? match[1] : null),
3310        element: element,
3311        parent: parent,
3312        children: [],
3313        position: parent.children.length,
3314        container: $(children[i]).down(options.treeTag)
3315      }
3316      
3317      /* Get the element containing the children and recurse over it */
3318      if (child.container)
3319        this._tree(child.container, options, child)
3320      
3321      parent.children.push (child);
3322    }
3323
3324    return parent; 
3325  },
3326
3327  tree: function(element) {
3328    element = $(element);
3329    var sortableOptions = this.options(element);
3330    var options = Object.extend({
3331      tag: sortableOptions.tag,
3332      treeTag: sortableOptions.treeTag,
3333      only: sortableOptions.only,
3334      name: element.id,
3335      format: sortableOptions.format
3336    }, arguments[1] || { });
3337    
3338    var root = {
3339      id: null,
3340      parent: null,
3341      children: [],
3342      container: element,
3343      position: 0
3344    }
3345    
3346    return Sortable._tree(element, options, root);
3347  },
3348
3349  /* Construct a [i] index for a particular node */
3350  _constructIndex: function(node) {
3351    var index = '';
3352    do {
3353      if (node.id) index = '[' + node.position + ']' + index;
3354    } while ((node = node.parent) != null);
3355    return index;
3356  },
3357
3358  sequence: function(element) {
3359    element = $(element);
3360    var options = Object.extend(this.options(element), arguments[1] || { });
3361    
3362    return $(this.findElements(element, options) || []).map( function(item) {
3363      return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
3364    });
3365  },
3366
3367  setSequence: function(element, new_sequence) {
3368    element = $(element);
3369    var options = Object.extend(this.options(element), arguments[2] || { });
3370    
3371    var nodeMap = { };
3372    this.findElements(element, options).each( function(n) {
3373        if (n.id.match(options.format))
3374            nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
3375        n.parentNode.removeChild(n);
3376    });
3377   
3378    new_sequence.each(function(ident) {
3379      var n = nodeMap[ident];
3380      if (n) {
3381        n[1].appendChild(n[0]);
3382        delete nodeMap[ident];
3383      }
3384    });
3385  },
3386  
3387  serialize: function(element) {
3388    element = $(element);
3389    var options = Object.extend(Sortable.options(element), arguments[1] || { });
3390    var name = encodeURIComponent(
3391      (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
3392    
3393    if (options.tree) {
3394      return Sortable.tree(element, arguments[1]).children.map( function (item) {
3395        return [name + Sortable._constructIndex(item) + "[id]=" + 
3396                encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
3397      }).flatten().join('&');
3398    } else {
3399      return Sortable.sequence(element, arguments[1]).map( function(item) {
3400        return name + "[]=" + encodeURIComponent(item);
3401      }).join('&');
3402    }
3403  }
3404}
3405
3406// Returns true if child is contained within element
3407Element.isParent = function(child, element) {
3408  if (!child.parentNode || child == element) return false;
3409  if (child.parentNode == element) return true;
3410  return Element.isParent(child.parentNode, element);
3411}
3412
3413Element.findChildren = function(element, only, recursive, tagName) {   
3414  if(!element.hasChildNodes()) return null;
3415  tagName = tagName.toUpperCase();
3416  if(only) only = [only].flatten();
3417  var elements = [];
3418  $A(element.childNodes).each( function(e) {
3419    if(e.tagName && e.tagName.toUpperCase()==tagName &&
3420      (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
3421        elements.push(e);
3422    if(recursive) {
3423      var grandchildren = Element.findChildren(e, only, recursive, tagName);
3424      if(grandchildren) elements.push(grandchildren);
3425    }
3426  });
3427
3428  return (elements.length>0 ? elements.flatten() : []);
3429}
3430
3431Element.offsetSize = function (element, type) {
3432  return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
3433}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.