]> projects.mako.cc - selectricity/blob - public/javascripts/prototype.js
0caf9cd7f0b7d902b0dada3752de348ed27c02cd
[selectricity] / public / javascripts / prototype.js
1 /*  Prototype JavaScript framework, version 1.5.0_rc0
2  *  (c) 2005 Sam Stephenson <sam@conio.net>
3  *
4  *  Prototype is freely distributable under the terms of an MIT-style license.
5  *  For details, see the Prototype web site: http://prototype.conio.net/
6  *
7 /*--------------------------------------------------------------------------*/
8
9 var Prototype = {
10   Version: '1.5.0_rc0',
11   ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)',
12
13   emptyFunction: function() {},
14   K: function(x) {return x}
15 }
16
17 var Class = {
18   create: function() {
19     return function() {
20       this.initialize.apply(this, arguments);
21     }
22   }
23 }
24
25 var Abstract = new Object();
26
27 Object.extend = function(destination, source) {
28   for (var property in source) {
29     destination[property] = source[property];
30   }
31   return destination;
32 }
33
34 Object.inspect = function(object) {
35   try {
36     if (object == undefined) return 'undefined';
37     if (object == null) return 'null';
38     return object.inspect ? object.inspect() : object.toString();
39   } catch (e) {
40     if (e instanceof RangeError) return '...';
41     throw e;
42   }
43 }
44
45 Function.prototype.bind = function() {
46   var __method = this, args = $A(arguments), object = args.shift();
47   return function() {
48     return __method.apply(object, args.concat($A(arguments)));
49   }
50 }
51
52 Function.prototype.bindAsEventListener = function(object) {
53   var __method = this;
54   return function(event) {
55     return __method.call(object, event || window.event);
56   }
57 }
58
59 Object.extend(Number.prototype, {
60   toColorPart: function() {
61     var digits = this.toString(16);
62     if (this < 16) return '0' + digits;
63     return digits;
64   },
65
66   succ: function() {
67     return this + 1;
68   },
69
70   times: function(iterator) {
71     $R(0, this, true).each(iterator);
72     return this;
73   }
74 });
75
76 var Try = {
77   these: function() {
78     var returnValue;
79
80     for (var i = 0; i < arguments.length; i++) {
81       var lambda = arguments[i];
82       try {
83         returnValue = lambda();
84         break;
85       } catch (e) {}
86     }
87
88     return returnValue;
89   }
90 }
91
92 /*--------------------------------------------------------------------------*/
93
94 var PeriodicalExecuter = Class.create();
95 PeriodicalExecuter.prototype = {
96   initialize: function(callback, frequency) {
97     this.callback = callback;
98     this.frequency = frequency;
99     this.currentlyExecuting = false;
100
101     this.registerCallback();
102   },
103
104   registerCallback: function() {
105     setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
106   },
107
108   onTimerEvent: function() {
109     if (!this.currentlyExecuting) {
110       try {
111         this.currentlyExecuting = true;
112         this.callback();
113       } finally {
114         this.currentlyExecuting = false;
115       }
116     }
117   }
118 }
119 Object.extend(String.prototype, {
120   gsub: function(pattern, replacement) {
121     var result = '', source = this, match;
122     replacement = arguments.callee.prepareReplacement(replacement);
123
124     while (source.length > 0) {
125       if (match = source.match(pattern)) {
126         result += source.slice(0, match.index);
127         result += (replacement(match) || '').toString();
128         source  = source.slice(match.index + match[0].length);
129       } else {
130         result += source, source = '';
131       }
132     }
133     return result;
134   },
135
136   sub: function(pattern, replacement, count) {
137     replacement = this.gsub.prepareReplacement(replacement);
138     count = count === undefined ? 1 : count;
139
140     return this.gsub(pattern, function(match) {
141       if (--count < 0) return match[0];
142       return replacement(match);
143     });
144   },
145
146   scan: function(pattern, iterator) {
147     this.gsub(pattern, iterator);
148     return this;
149   },
150
151   truncate: function(length, truncation) {
152     length = length || 30;
153     truncation = truncation === undefined ? '...' : truncation;
154     return this.length > length ?
155       this.slice(0, length - truncation.length) + truncation : this;
156   },
157
158   strip: function() {
159     return this.replace(/^\s+/, '').replace(/\s+$/, '');
160   },
161
162   stripTags: function() {
163     return this.replace(/<\/?[^>]+>/gi, '');
164   },
165
166   stripScripts: function() {
167     return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
168   },
169
170   extractScripts: function() {
171     var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
172     var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
173     return (this.match(matchAll) || []).map(function(scriptTag) {
174       return (scriptTag.match(matchOne) || ['', ''])[1];
175     });
176   },
177
178   evalScripts: function() {
179     return this.extractScripts().map(function(script) { return eval(script) });
180   },
181
182   escapeHTML: function() {
183     var div = document.createElement('div');
184     var text = document.createTextNode(this);
185     div.appendChild(text);
186     return div.innerHTML;
187   },
188
189   unescapeHTML: function() {
190     var div = document.createElement('div');
191     div.innerHTML = this.stripTags();
192     return div.childNodes[0] ? div.childNodes[0].nodeValue : '';
193   },
194
195   toQueryParams: function() {
196     var pairs = this.match(/^\??(.*)$/)[1].split('&');
197     return pairs.inject({}, function(params, pairString) {
198       var pair = pairString.split('=');
199       params[pair[0]] = pair[1];
200       return params;
201     });
202   },
203
204   toArray: function() {
205     return this.split('');
206   },
207
208   camelize: function() {
209     var oStringList = this.split('-');
210     if (oStringList.length == 1) return oStringList[0];
211
212     var camelizedString = this.indexOf('-') == 0
213       ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1)
214       : oStringList[0];
215
216     for (var i = 1, len = oStringList.length; i < len; i++) {
217       var s = oStringList[i];
218       camelizedString += s.charAt(0).toUpperCase() + s.substring(1);
219     }
220
221     return camelizedString;
222   },
223
224   inspect: function() {
225     return "'" + this.replace(/\\/g, '\\\\').replace(/'/g, '\\\'') + "'";
226   }
227 });
228
229 String.prototype.gsub.prepareReplacement = function(replacement) {
230   if (typeof replacement == 'function') return replacement;
231   var template = new Template(replacement);
232   return function(match) { return template.evaluate(match) };
233 }
234
235 String.prototype.parseQuery = String.prototype.toQueryParams;
236
237 var Template = Class.create();
238 Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
239 Template.prototype = {
240   initialize: function(template, pattern) {
241     this.template = template.toString();
242     this.pattern  = pattern || Template.Pattern;
243   },
244
245   evaluate: function(object) {
246     return this.template.gsub(this.pattern, function(match) {
247       var before = match[1];
248       if (before == '\\') return match[2];
249       return before + (object[match[3]] || '').toString();
250     });
251   }
252 }
253
254 var $break    = new Object();
255 var $continue = new Object();
256
257 var Enumerable = {
258   each: function(iterator) {
259     var index = 0;
260     try {
261       this._each(function(value) {
262         try {
263           iterator(value, index++);
264         } catch (e) {
265           if (e != $continue) throw e;
266         }
267       });
268     } catch (e) {
269       if (e != $break) throw e;
270     }
271   },
272
273   all: function(iterator) {
274     var result = true;
275     this.each(function(value, index) {
276       result = result && !!(iterator || Prototype.K)(value, index);
277       if (!result) throw $break;
278     });
279     return result;
280   },
281
282   any: function(iterator) {
283     var result = true;
284     this.each(function(value, index) {
285       if (result = !!(iterator || Prototype.K)(value, index))
286         throw $break;
287     });
288     return result;
289   },
290
291   collect: function(iterator) {
292     var results = [];
293     this.each(function(value, index) {
294       results.push(iterator(value, index));
295     });
296     return results;
297   },
298
299   detect: function (iterator) {
300     var result;
301     this.each(function(value, index) {
302       if (iterator(value, index)) {
303         result = value;
304         throw $break;
305       }
306     });
307     return result;
308   },
309
310   findAll: function(iterator) {
311     var results = [];
312     this.each(function(value, index) {
313       if (iterator(value, index))
314         results.push(value);
315     });
316     return results;
317   },
318
319   grep: function(pattern, iterator) {
320     var results = [];
321     this.each(function(value, index) {
322       var stringValue = value.toString();
323       if (stringValue.match(pattern))
324         results.push((iterator || Prototype.K)(value, index));
325     })
326     return results;
327   },
328
329   include: function(object) {
330     var found = false;
331     this.each(function(value) {
332       if (value == object) {
333         found = true;
334         throw $break;
335       }
336     });
337     return found;
338   },
339
340   inject: function(memo, iterator) {
341     this.each(function(value, index) {
342       memo = iterator(memo, value, index);
343     });
344     return memo;
345   },
346
347   invoke: function(method) {
348     var args = $A(arguments).slice(1);
349     return this.collect(function(value) {
350       return value[method].apply(value, args);
351     });
352   },
353
354   max: function(iterator) {
355     var result;
356     this.each(function(value, index) {
357       value = (iterator || Prototype.K)(value, index);
358       if (result == undefined || value >= result)
359         result = value;
360     });
361     return result;
362   },
363
364   min: function(iterator) {
365     var result;
366     this.each(function(value, index) {
367       value = (iterator || Prototype.K)(value, index);
368       if (result == undefined || value < result)
369         result = value;
370     });
371     return result;
372   },
373
374   partition: function(iterator) {
375     var trues = [], falses = [];
376     this.each(function(value, index) {
377       ((iterator || Prototype.K)(value, index) ?
378         trues : falses).push(value);
379     });
380     return [trues, falses];
381   },
382
383   pluck: function(property) {
384     var results = [];
385     this.each(function(value, index) {
386       results.push(value[property]);
387     });
388     return results;
389   },
390
391   reject: function(iterator) {
392     var results = [];
393     this.each(function(value, index) {
394       if (!iterator(value, index))
395         results.push(value);
396     });
397     return results;
398   },
399
400   sortBy: function(iterator) {
401     return this.collect(function(value, index) {
402       return {value: value, criteria: iterator(value, index)};
403     }).sort(function(left, right) {
404       var a = left.criteria, b = right.criteria;
405       return a < b ? -1 : a > b ? 1 : 0;
406     }).pluck('value');
407   },
408
409   toArray: function() {
410     return this.collect(Prototype.K);
411   },
412
413   zip: function() {
414     var iterator = Prototype.K, args = $A(arguments);
415     if (typeof args.last() == 'function')
416       iterator = args.pop();
417
418     var collections = [this].concat(args).map($A);
419     return this.map(function(value, index) {
420       return iterator(collections.pluck(index));
421     });
422   },
423
424   inspect: function() {
425     return '#<Enumerable:' + this.toArray().inspect() + '>';
426   }
427 }
428
429 Object.extend(Enumerable, {
430   map:     Enumerable.collect,
431   find:    Enumerable.detect,
432   select:  Enumerable.findAll,
433   member:  Enumerable.include,
434   entries: Enumerable.toArray
435 });
436 var $A = Array.from = function(iterable) {
437   if (!iterable) return [];
438   if (iterable.toArray) {
439     return iterable.toArray();
440   } else {
441     var results = [];
442     for (var i = 0; i < iterable.length; i++)
443       results.push(iterable[i]);
444     return results;
445   }
446 }
447
448 Object.extend(Array.prototype, Enumerable);
449
450 if (!Array.prototype._reverse)
451   Array.prototype._reverse = Array.prototype.reverse;
452
453 Object.extend(Array.prototype, {
454   _each: function(iterator) {
455     for (var i = 0; i < this.length; i++)
456       iterator(this[i]);
457   },
458
459   clear: function() {
460     this.length = 0;
461     return this;
462   },
463
464   first: function() {
465     return this[0];
466   },
467
468   last: function() {
469     return this[this.length - 1];
470   },
471
472   compact: function() {
473     return this.select(function(value) {
474       return value != undefined || value != null;
475     });
476   },
477
478   flatten: function() {
479     return this.inject([], function(array, value) {
480       return array.concat(value && value.constructor == Array ?
481         value.flatten() : [value]);
482     });
483   },
484
485   without: function() {
486     var values = $A(arguments);
487     return this.select(function(value) {
488       return !values.include(value);
489     });
490   },
491
492   indexOf: function(object) {
493     for (var i = 0; i < this.length; i++)
494       if (this[i] == object) return i;
495     return -1;
496   },
497
498   reverse: function(inline) {
499     return (inline !== false ? this : this.toArray())._reverse();
500   },
501
502   inspect: function() {
503     return '[' + this.map(Object.inspect).join(', ') + ']';
504   }
505 });
506 var Hash = {
507   _each: function(iterator) {
508     for (var key in this) {
509       var value = this[key];
510       if (typeof value == 'function') continue;
511
512       var pair = [key, value];
513       pair.key = key;
514       pair.value = value;
515       iterator(pair);
516     }
517   },
518
519   keys: function() {
520     return this.pluck('key');
521   },
522
523   values: function() {
524     return this.pluck('value');
525   },
526
527   merge: function(hash) {
528     return $H(hash).inject($H(this), function(mergedHash, pair) {
529       mergedHash[pair.key] = pair.value;
530       return mergedHash;
531     });
532   },
533
534   toQueryString: function() {
535     return this.map(function(pair) {
536       return pair.map(encodeURIComponent).join('=');
537     }).join('&');
538   },
539
540   inspect: function() {
541     return '#<Hash:{' + this.map(function(pair) {
542       return pair.map(Object.inspect).join(': ');
543     }).join(', ') + '}>';
544   }
545 }
546
547 function $H(object) {
548   var hash = Object.extend({}, object || {});
549   Object.extend(hash, Enumerable);
550   Object.extend(hash, Hash);
551   return hash;
552 }
553 ObjectRange = Class.create();
554 Object.extend(ObjectRange.prototype, Enumerable);
555 Object.extend(ObjectRange.prototype, {
556   initialize: function(start, end, exclusive) {
557     this.start = start;
558     this.end = end;
559     this.exclusive = exclusive;
560   },
561
562   _each: function(iterator) {
563     var value = this.start;
564     do {
565       iterator(value);
566       value = value.succ();
567     } while (this.include(value));
568   },
569
570   include: function(value) {
571     if (value < this.start)
572       return false;
573     if (this.exclusive)
574       return value < this.end;
575     return value <= this.end;
576   }
577 });
578
579 var $R = function(start, end, exclusive) {
580   return new ObjectRange(start, end, exclusive);
581 }
582
583 var Ajax = {
584   getTransport: function() {
585     return Try.these(
586       function() {return new XMLHttpRequest()},
587       function() {return new ActiveXObject('Msxml2.XMLHTTP')},
588       function() {return new ActiveXObject('Microsoft.XMLHTTP')}
589     ) || false;
590   },
591
592   activeRequestCount: 0
593 }
594
595 Ajax.Responders = {
596   responders: [],
597
598   _each: function(iterator) {
599     this.responders._each(iterator);
600   },
601
602   register: function(responderToAdd) {
603     if (!this.include(responderToAdd))
604       this.responders.push(responderToAdd);
605   },
606
607   unregister: function(responderToRemove) {
608     this.responders = this.responders.without(responderToRemove);
609   },
610
611   dispatch: function(callback, request, transport, json) {
612     this.each(function(responder) {
613       if (responder[callback] && typeof responder[callback] == 'function') {
614         try {
615           responder[callback].apply(responder, [request, transport, json]);
616         } catch (e) {}
617       }
618     });
619   }
620 };
621
622 Object.extend(Ajax.Responders, Enumerable);
623
624 Ajax.Responders.register({
625   onCreate: function() {
626     Ajax.activeRequestCount++;
627   },
628
629   onComplete: function() {
630     Ajax.activeRequestCount--;
631   }
632 });
633
634 Ajax.Base = function() {};
635 Ajax.Base.prototype = {
636   setOptions: function(options) {
637     this.options = {
638       method:       'post',
639       asynchronous: true,
640       contentType:  'application/x-www-form-urlencoded',
641       parameters:   ''
642     }
643     Object.extend(this.options, options || {});
644   },
645
646   responseIsSuccess: function() {
647     return this.transport.status == undefined
648         || this.transport.status == 0
649         || (this.transport.status >= 200 && this.transport.status < 300);
650   },
651
652   responseIsFailure: function() {
653     return !this.responseIsSuccess();
654   }
655 }
656
657 Ajax.Request = Class.create();
658 Ajax.Request.Events =
659   ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
660
661 Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
662   initialize: function(url, options) {
663     this.transport = Ajax.getTransport();
664     this.setOptions(options);
665     this.request(url);
666   },
667
668   request: function(url) {
669     var parameters = this.options.parameters || '';
670     if (parameters.length > 0) parameters += '&_=';
671
672     try {
673       this.url = url;
674       if (this.options.method == 'get' && parameters.length > 0)
675         this.url += (this.url.match(/\?/) ? '&' : '?') + parameters;
676
677       Ajax.Responders.dispatch('onCreate', this, this.transport);
678
679       this.transport.open(this.options.method, this.url,
680         this.options.asynchronous);
681
682       if (this.options.asynchronous) {
683         this.transport.onreadystatechange = this.onStateChange.bind(this);
684         setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10);
685       }
686
687       this.setRequestHeaders();
688
689       var body = this.options.postBody ? this.options.postBody : parameters;
690       this.transport.send(this.options.method == 'post' ? body : null);
691
692     } catch (e) {
693       this.dispatchException(e);
694     }
695   },
696
697   setRequestHeaders: function() {
698     var requestHeaders =
699       ['X-Requested-With', 'XMLHttpRequest',
700        'X-Prototype-Version', Prototype.Version,
701        'Accept', 'text/javascript, text/html, application/xml, text/xml, */*'];
702
703     if (this.options.method == 'post') {
704       requestHeaders.push('Content-type', this.options.contentType);
705
706       /* Force "Connection: close" for Mozilla browsers to work around
707        * a bug where XMLHttpReqeuest sends an incorrect Content-length
708        * header. See Mozilla Bugzilla #246651.
709        */
710       if (this.transport.overrideMimeType)
711         requestHeaders.push('Connection', 'close');
712     }
713
714     if (this.options.requestHeaders)
715       requestHeaders.push.apply(requestHeaders, this.options.requestHeaders);
716
717     for (var i = 0; i < requestHeaders.length; i += 2)
718       this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]);
719   },
720
721   onStateChange: function() {
722     var readyState = this.transport.readyState;
723     if (readyState != 1)
724       this.respondToReadyState(this.transport.readyState);
725   },
726
727   header: function(name) {
728     try {
729       return this.transport.getResponseHeader(name);
730     } catch (e) {}
731   },
732
733   evalJSON: function() {
734     try {
735       return eval('(' + this.header('X-JSON') + ')');
736     } catch (e) {}
737   },
738
739   evalResponse: function() {
740     try {
741       return eval(this.transport.responseText);
742     } catch (e) {
743       this.dispatchException(e);
744     }
745   },
746
747   respondToReadyState: function(readyState) {
748     var event = Ajax.Request.Events[readyState];
749     var transport = this.transport, json = this.evalJSON();
750
751     if (event == 'Complete') {
752       try {
753         (this.options['on' + this.transport.status]
754          || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')]
755          || Prototype.emptyFunction)(transport, json);
756       } catch (e) {
757         this.dispatchException(e);
758       }
759
760       if ((this.header('Content-type') || '').match(/^text\/javascript/i))
761         this.evalResponse();
762     }
763
764     try {
765       (this.options['on' + event] || Prototype.emptyFunction)(transport, json);
766       Ajax.Responders.dispatch('on' + event, this, transport, json);
767     } catch (e) {
768       this.dispatchException(e);
769     }
770
771     /* Avoid memory leak in MSIE: clean up the oncomplete event handler */
772     if (event == 'Complete')
773       this.transport.onreadystatechange = Prototype.emptyFunction;
774   },
775
776   dispatchException: function(exception) {
777     (this.options.onException || Prototype.emptyFunction)(this, exception);
778     Ajax.Responders.dispatch('onException', this, exception);
779   }
780 });
781
782 Ajax.Updater = Class.create();
783
784 Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
785   initialize: function(container, url, options) {
786     this.containers = {
787       success: container.success ? $(container.success) : $(container),
788       failure: container.failure ? $(container.failure) :
789         (container.success ? null : $(container))
790     }
791
792     this.transport = Ajax.getTransport();
793     this.setOptions(options);
794
795     var onComplete = this.options.onComplete || Prototype.emptyFunction;
796     this.options.onComplete = (function(transport, object) {
797       this.updateContent();
798       onComplete(transport, object);
799     }).bind(this);
800
801     this.request(url);
802   },
803
804   updateContent: function() {
805     var receiver = this.responseIsSuccess() ?
806       this.containers.success : this.containers.failure;
807     var response = this.transport.responseText;
808
809     if (!this.options.evalScripts)
810       response = response.stripScripts();
811
812     if (receiver) {
813       if (this.options.insertion) {
814         new this.options.insertion(receiver, response);
815       } else {
816         Element.update(receiver, response);
817       }
818     }
819
820     if (this.responseIsSuccess()) {
821       if (this.onComplete)
822         setTimeout(this.onComplete.bind(this), 10);
823     }
824   }
825 });
826
827 Ajax.PeriodicalUpdater = Class.create();
828 Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
829   initialize: function(container, url, options) {
830     this.setOptions(options);
831     this.onComplete = this.options.onComplete;
832
833     this.frequency = (this.options.frequency || 2);
834     this.decay = (this.options.decay || 1);
835
836     this.updater = {};
837     this.container = container;
838     this.url = url;
839
840     this.start();
841   },
842
843   start: function() {
844     this.options.onComplete = this.updateComplete.bind(this);
845     this.onTimerEvent();
846   },
847
848   stop: function() {
849     this.updater.onComplete = undefined;
850     clearTimeout(this.timer);
851     (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
852   },
853
854   updateComplete: function(request) {
855     if (this.options.decay) {
856       this.decay = (request.responseText == this.lastText ?
857         this.decay * this.options.decay : 1);
858
859       this.lastText = request.responseText;
860     }
861     this.timer = setTimeout(this.onTimerEvent.bind(this),
862       this.decay * this.frequency * 1000);
863   },
864
865   onTimerEvent: function() {
866     this.updater = new Ajax.Updater(this.container, this.url, this.options);
867   }
868 });
869 function $() {
870   var results = [], element;
871   for (var i = 0; i < arguments.length; i++) {
872     element = arguments[i];
873     if (typeof element == 'string')
874       element = document.getElementById(element);
875     results.push(Element.extend(element));
876   }
877   return results.length < 2 ? results[0] : results;
878 }
879
880 document.getElementsByClassName = function(className, parentElement) {
881   var children = ($(parentElement) || document.body).getElementsByTagName('*');
882   return $A(children).inject([], function(elements, child) {
883     if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
884       elements.push(Element.extend(child));
885     return elements;
886   });
887 }
888
889 /*--------------------------------------------------------------------------*/
890
891 if (!window.Element)
892   var Element = new Object();
893
894 Element.extend = function(element) {
895   if (!element) return;
896   if (_nativeExtensions) return element;
897
898   if (!element._extended && element.tagName && element != window) {
899     var methods = Element.Methods, cache = Element.extend.cache;
900     for (property in methods) {
901       var value = methods[property];
902       if (typeof value == 'function')
903         element[property] = cache.findOrStore(value);
904     }
905   }
906
907   element._extended = true;
908   return element;
909 }
910
911 Element.extend.cache = {
912   findOrStore: function(value) {
913     return this[value] = this[value] || function() {
914       return value.apply(null, [this].concat($A(arguments)));
915     }
916   }
917 }
918
919 Element.Methods = {
920   visible: function(element) {
921     return $(element).style.display != 'none';
922   },
923
924   toggle: function() {
925     for (var i = 0; i < arguments.length; i++) {
926       var element = $(arguments[i]);
927       Element[Element.visible(element) ? 'hide' : 'show'](element);
928     }
929   },
930
931   hide: function() {
932     for (var i = 0; i < arguments.length; i++) {
933       var element = $(arguments[i]);
934       element.style.display = 'none';
935     }
936   },
937
938   show: function() {
939     for (var i = 0; i < arguments.length; i++) {
940       var element = $(arguments[i]);
941       element.style.display = '';
942     }
943   },
944
945   remove: function(element) {
946     element = $(element);
947     element.parentNode.removeChild(element);
948   },
949
950   update: function(element, html) {
951     $(element).innerHTML = html.stripScripts();
952     setTimeout(function() {html.evalScripts()}, 10);
953   },
954
955   replace: function(element, html) {
956     element = $(element);
957     if (element.outerHTML) {
958       element.outerHTML = html.stripScripts();
959     } else {
960       var range = element.ownerDocument.createRange();
961       range.selectNodeContents(element);
962       element.parentNode.replaceChild(
963         range.createContextualFragment(html.stripScripts()), element);
964     }
965     setTimeout(function() {html.evalScripts()}, 10);
966   },
967
968   getHeight: function(element) {
969     element = $(element);
970     return element.offsetHeight;
971   },
972
973   classNames: function(element) {
974     return new Element.ClassNames(element);
975   },
976
977   hasClassName: function(element, className) {
978     if (!(element = $(element))) return;
979     return Element.classNames(element).include(className);
980   },
981
982   addClassName: function(element, className) {
983     if (!(element = $(element))) return;
984     return Element.classNames(element).add(className);
985   },
986
987   removeClassName: function(element, className) {
988     if (!(element = $(element))) return;
989     return Element.classNames(element).remove(className);
990   },
991
992   // removes whitespace-only text node children
993   cleanWhitespace: function(element) {
994     element = $(element);
995     for (var i = 0; i < element.childNodes.length; i++) {
996       var node = element.childNodes[i];
997       if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
998         Element.remove(node);
999     }
1000   },
1001
1002   empty: function(element) {
1003     return $(element).innerHTML.match(/^\s*$/);
1004   },
1005
1006   childOf: function(element, ancestor) {
1007     element = $(element), ancestor = $(ancestor);
1008     while (element = element.parentNode)
1009       if (element == ancestor) return true;
1010     return false;
1011   },
1012
1013   scrollTo: function(element) {
1014     element = $(element);
1015     var x = element.x ? element.x : element.offsetLeft,
1016         y = element.y ? element.y : element.offsetTop;
1017     window.scrollTo(x, y);
1018   },
1019
1020   getStyle: function(element, style) {
1021     element = $(element);
1022     var value = element.style[style.camelize()];
1023     if (!value) {
1024       if (document.defaultView && document.defaultView.getComputedStyle) {
1025         var css = document.defaultView.getComputedStyle(element, null);
1026         value = css ? css.getPropertyValue(style) : null;
1027       } else if (element.currentStyle) {
1028         value = element.currentStyle[style.camelize()];
1029       }
1030     }
1031
1032     if (window.opera && ['left', 'top', 'right', 'bottom'].include(style))
1033       if (Element.getStyle(element, 'position') == 'static') value = 'auto';
1034
1035     return value == 'auto' ? null : value;
1036   },
1037
1038   setStyle: function(element, style) {
1039     element = $(element);
1040     for (var name in style)
1041       element.style[name.camelize()] = style[name];
1042   },
1043
1044   getDimensions: function(element) {
1045     element = $(element);
1046     if (Element.getStyle(element, 'display') != 'none')
1047       return {width: element.offsetWidth, height: element.offsetHeight};
1048
1049     // All *Width and *Height properties give 0 on elements with display none,
1050     // so enable the element temporarily
1051     var els = element.style;
1052     var originalVisibility = els.visibility;
1053     var originalPosition = els.position;
1054     els.visibility = 'hidden';
1055     els.position = 'absolute';
1056     els.display = '';
1057     var originalWidth = element.clientWidth;
1058     var originalHeight = element.clientHeight;
1059     els.display = 'none';
1060     els.position = originalPosition;
1061     els.visibility = originalVisibility;
1062     return {width: originalWidth, height: originalHeight};
1063   },
1064
1065   makePositioned: function(element) {
1066     element = $(element);
1067     var pos = Element.getStyle(element, 'position');
1068     if (pos == 'static' || !pos) {
1069       element._madePositioned = true;
1070       element.style.position = 'relative';
1071       // Opera returns the offset relative to the positioning context, when an
1072       // element is position relative but top and left have not been defined
1073       if (window.opera) {
1074         element.style.top = 0;
1075         element.style.left = 0;
1076       }
1077     }
1078   },
1079
1080   undoPositioned: function(element) {
1081     element = $(element);
1082     if (element._madePositioned) {
1083       element._madePositioned = undefined;
1084       element.style.position =
1085         element.style.top =
1086         element.style.left =
1087         element.style.bottom =
1088         element.style.right = '';
1089     }
1090   },
1091
1092   makeClipping: function(element) {
1093     element = $(element);
1094     if (element._overflow) return;
1095     element._overflow = element.style.overflow;
1096     if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
1097       element.style.overflow = 'hidden';
1098   },
1099
1100   undoClipping: function(element) {
1101     element = $(element);
1102     if (element._overflow) return;
1103     element.style.overflow = element._overflow;
1104     element._overflow = undefined;
1105   }
1106 }
1107
1108 Object.extend(Element, Element.Methods);
1109
1110 var _nativeExtensions = false;
1111
1112 if(!HTMLElement && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
1113   var HTMLElement = {}
1114   HTMLElement.prototype = document.createElement('div').__proto__;
1115 }
1116
1117 Element.addMethods = function(methods) {
1118   Object.extend(Element.Methods, methods || {});
1119
1120   if(typeof HTMLElement != 'undefined') {
1121     var methods = Element.Methods, cache = Element.extend.cache;
1122     for (property in methods) {
1123       var value = methods[property];
1124       if (typeof value == 'function')
1125         HTMLElement.prototype[property] = cache.findOrStore(value);
1126     }
1127     _nativeExtensions = true;
1128   }
1129 }
1130
1131 Element.addMethods();
1132
1133 var Toggle = new Object();
1134 Toggle.display = Element.toggle;
1135
1136 /*--------------------------------------------------------------------------*/
1137
1138 Abstract.Insertion = function(adjacency) {
1139   this.adjacency = adjacency;
1140 }
1141
1142 Abstract.Insertion.prototype = {
1143   initialize: function(element, content) {
1144     this.element = $(element);
1145     this.content = content.stripScripts();
1146
1147     if (this.adjacency && this.element.insertAdjacentHTML) {
1148       try {
1149         this.element.insertAdjacentHTML(this.adjacency, this.content);
1150       } catch (e) {
1151         var tagName = this.element.tagName.toLowerCase();
1152         if (tagName == 'tbody' || tagName == 'tr') {
1153           this.insertContent(this.contentFromAnonymousTable());
1154         } else {
1155           throw e;
1156         }
1157       }
1158     } else {
1159       this.range = this.element.ownerDocument.createRange();
1160       if (this.initializeRange) this.initializeRange();
1161       this.insertContent([this.range.createContextualFragment(this.content)]);
1162     }
1163
1164     setTimeout(function() {content.evalScripts()}, 10);
1165   },
1166
1167   contentFromAnonymousTable: function() {
1168     var div = document.createElement('div');
1169     div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>';
1170     return $A(div.childNodes[0].childNodes[0].childNodes);
1171   }
1172 }
1173
1174 var Insertion = new Object();
1175
1176 Insertion.Before = Class.create();
1177 Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
1178   initializeRange: function() {
1179     this.range.setStartBefore(this.element);
1180   },
1181
1182   insertContent: function(fragments) {
1183     fragments.each((function(fragment) {
1184       this.element.parentNode.insertBefore(fragment, this.element);
1185     }).bind(this));
1186   }
1187 });
1188
1189 Insertion.Top = Class.create();
1190 Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
1191   initializeRange: function() {
1192     this.range.selectNodeContents(this.element);
1193     this.range.collapse(true);
1194   },
1195
1196   insertContent: function(fragments) {
1197     fragments.reverse(false).each((function(fragment) {
1198       this.element.insertBefore(fragment, this.element.firstChild);
1199     }).bind(this));
1200   }
1201 });
1202
1203 Insertion.Bottom = Class.create();
1204 Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
1205   initializeRange: function() {
1206     this.range.selectNodeContents(this.element);
1207     this.range.collapse(this.element);
1208   },
1209
1210   insertContent: function(fragments) {
1211     fragments.each((function(fragment) {
1212       this.element.appendChild(fragment);
1213     }).bind(this));
1214   }
1215 });
1216
1217 Insertion.After = Class.create();
1218 Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
1219   initializeRange: function() {
1220     this.range.setStartAfter(this.element);
1221   },
1222
1223   insertContent: function(fragments) {
1224     fragments.each((function(fragment) {
1225       this.element.parentNode.insertBefore(fragment,
1226         this.element.nextSibling);
1227     }).bind(this));
1228   }
1229 });
1230
1231 /*--------------------------------------------------------------------------*/
1232
1233 Element.ClassNames = Class.create();
1234 Element.ClassNames.prototype = {
1235   initialize: function(element) {
1236     this.element = $(element);
1237   },
1238
1239   _each: function(iterator) {
1240     this.element.className.split(/\s+/).select(function(name) {
1241       return name.length > 0;
1242     })._each(iterator);
1243   },
1244
1245   set: function(className) {
1246     this.element.className = className;
1247   },
1248
1249   add: function(classNameToAdd) {
1250     if (this.include(classNameToAdd)) return;
1251     this.set(this.toArray().concat(classNameToAdd).join(' '));
1252   },
1253
1254   remove: function(classNameToRemove) {
1255     if (!this.include(classNameToRemove)) return;
1256     this.set(this.select(function(className) {
1257       return className != classNameToRemove;
1258     }).join(' '));
1259   },
1260
1261   toString: function() {
1262     return this.toArray().join(' ');
1263   }
1264 }
1265
1266 Object.extend(Element.ClassNames.prototype, Enumerable);
1267 var Selector = Class.create();
1268 Selector.prototype = {
1269   initialize: function(expression) {
1270     this.params = {classNames: []};
1271     this.expression = expression.toString().strip();
1272     this.parseExpression();
1273     this.compileMatcher();
1274   },
1275
1276   parseExpression: function() {
1277     function abort(message) { throw 'Parse error in selector: ' + message; }
1278
1279     if (this.expression == '')  abort('empty expression');
1280
1281     var params = this.params, expr = this.expression, match, modifier, clause, rest;
1282     while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) {
1283       params.attributes = params.attributes || [];
1284       params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''});
1285       expr = match[1];
1286     }
1287
1288     if (expr == '*') return this.params.wildcard = true;
1289
1290     while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) {
1291       modifier = match[1], clause = match[2], rest = match[3];
1292       switch (modifier) {
1293         case '#':       params.id = clause; break;
1294         case '.':       params.classNames.push(clause); break;
1295         case '':
1296         case undefined: params.tagName = clause.toUpperCase(); break;
1297         default:        abort(expr.inspect());
1298       }
1299       expr = rest;
1300     }
1301
1302     if (expr.length > 0) abort(expr.inspect());
1303   },
1304
1305   buildMatchExpression: function() {
1306     var params = this.params, conditions = [], clause;
1307
1308     if (params.wildcard)
1309       conditions.push('true');
1310     if (clause = params.id)
1311       conditions.push('element.id == ' + clause.inspect());
1312     if (clause = params.tagName)
1313       conditions.push('element.tagName.toUpperCase() == ' + clause.inspect());
1314     if ((clause = params.classNames).length > 0)
1315       for (var i = 0; i < clause.length; i++)
1316         conditions.push('Element.hasClassName(element, ' + clause[i].inspect() + ')');
1317     if (clause = params.attributes) {
1318       clause.each(function(attribute) {
1319         var value = 'element.getAttribute(' + attribute.name.inspect() + ')';
1320         var splitValueBy = function(delimiter) {
1321           return value + ' && ' + value + '.split(' + delimiter.inspect() + ')';
1322         }
1323
1324         switch (attribute.operator) {
1325           case '=':       conditions.push(value + ' == ' + attribute.value.inspect()); break;
1326           case '~=':      conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break;
1327           case '|=':      conditions.push(
1328                             splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect()
1329                           ); break;
1330           case '!=':      conditions.push(value + ' != ' + attribute.value.inspect()); break;
1331           case '':
1332           case undefined: conditions.push(value + ' != null'); break;
1333           default:        throw 'Unknown operator ' + attribute.operator + ' in selector';
1334         }
1335       });
1336     }
1337
1338     return conditions.join(' && ');
1339   },
1340
1341   compileMatcher: function() {
1342     this.match = new Function('element', 'if (!element.tagName) return false; \
1343       return ' + this.buildMatchExpression());
1344   },
1345
1346   findElements: function(scope) {
1347     var element;
1348
1349     if (element = $(this.params.id))
1350       if (this.match(element))
1351         if (!scope || Element.childOf(element, scope))
1352           return [element];
1353
1354     scope = (scope || document).getElementsByTagName(this.params.tagName || '*');
1355
1356     var results = [];
1357     for (var i = 0; i < scope.length; i++)
1358       if (this.match(element = scope[i]))
1359         results.push(Element.extend(element));
1360
1361     return results;
1362   },
1363
1364   toString: function() {
1365     return this.expression;
1366   }
1367 }
1368
1369 function $$() {
1370   return $A(arguments).map(function(expression) {
1371     return expression.strip().split(/\s+/).inject([null], function(results, expr) {
1372       var selector = new Selector(expr);
1373       return results.map(selector.findElements.bind(selector)).flatten();
1374     });
1375   }).flatten();
1376 }
1377 var Field = {
1378   clear: function() {
1379     for (var i = 0; i < arguments.length; i++)
1380       $(arguments[i]).value = '';
1381   },
1382
1383   focus: function(element) {
1384     $(element).focus();
1385   },
1386
1387   present: function() {
1388     for (var i = 0; i < arguments.length; i++)
1389       if ($(arguments[i]).value == '') return false;
1390     return true;
1391   },
1392
1393   select: function(element) {
1394     $(element).select();
1395   },
1396
1397   activate: function(element) {
1398     element = $(element);
1399     element.focus();
1400     if (element.select)
1401       element.select();
1402   }
1403 }
1404
1405 /*--------------------------------------------------------------------------*/
1406
1407 var Form = {
1408   serialize: function(form) {
1409     var elements = Form.getElements($(form));
1410     var queryComponents = new Array();
1411
1412     for (var i = 0; i < elements.length; i++) {
1413       var queryComponent = Form.Element.serialize(elements[i]);
1414       if (queryComponent)
1415         queryComponents.push(queryComponent);
1416     }
1417
1418     return queryComponents.join('&');
1419   },
1420
1421   getElements: function(form) {
1422     form = $(form);
1423     var elements = new Array();
1424
1425     for (var tagName in Form.Element.Serializers) {
1426       var tagElements = form.getElementsByTagName(tagName);
1427       for (var j = 0; j < tagElements.length; j++)
1428         elements.push(tagElements[j]);
1429     }
1430     return elements;
1431   },
1432
1433   getInputs: function(form, typeName, name) {
1434     form = $(form);
1435     var inputs = form.getElementsByTagName('input');
1436
1437     if (!typeName && !name)
1438       return inputs;
1439
1440     var matchingInputs = new Array();
1441     for (var i = 0; i < inputs.length; i++) {
1442       var input = inputs[i];
1443       if ((typeName && input.type != typeName) ||
1444           (name && input.name != name))
1445         continue;
1446       matchingInputs.push(input);
1447     }
1448
1449     return matchingInputs;
1450   },
1451
1452   disable: function(form) {
1453     var elements = Form.getElements(form);
1454     for (var i = 0; i < elements.length; i++) {
1455       var element = elements[i];
1456       element.blur();
1457       element.disabled = 'true';
1458     }
1459   },
1460
1461   enable: function(form) {
1462     var elements = Form.getElements(form);
1463     for (var i = 0; i < elements.length; i++) {
1464       var element = elements[i];
1465       element.disabled = '';
1466     }
1467   },
1468
1469   findFirstElement: function(form) {
1470     return Form.getElements(form).find(function(element) {
1471       return element.type != 'hidden' && !element.disabled &&
1472         ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
1473     });
1474   },
1475
1476   focusFirstElement: function(form) {
1477     Field.activate(Form.findFirstElement(form));
1478   },
1479
1480   reset: function(form) {
1481     $(form).reset();
1482   }
1483 }
1484
1485 Form.Element = {
1486   serialize: function(element) {
1487     element = $(element);
1488     var method = element.tagName.toLowerCase();
1489     var parameter = Form.Element.Serializers[method](element);
1490
1491     if (parameter) {
1492       var key = encodeURIComponent(parameter[0]);
1493       if (key.length == 0) return;
1494
1495       if (parameter[1].constructor != Array)
1496         parameter[1] = [parameter[1]];
1497
1498       return parameter[1].map(function(value) {
1499         return key + '=' + encodeURIComponent(value);
1500       }).join('&');
1501     }
1502   },
1503
1504   getValue: function(element) {
1505     element = $(element);
1506     var method = element.tagName.toLowerCase();
1507     var parameter = Form.Element.Serializers[method](element);
1508
1509     if (parameter)
1510       return parameter[1];
1511   }
1512 }
1513
1514 Form.Element.Serializers = {
1515   input: function(element) {
1516     switch (element.type.toLowerCase()) {
1517       case 'submit':
1518       case 'hidden':
1519       case 'password':
1520       case 'text':
1521         return Form.Element.Serializers.textarea(element);
1522       case 'checkbox':
1523       case 'radio':
1524         return Form.Element.Serializers.inputSelector(element);
1525     }
1526     return false;
1527   },
1528
1529   inputSelector: function(element) {
1530     if (element.checked)
1531       return [element.name, element.value];
1532   },
1533
1534   textarea: function(element) {
1535     return [element.name, element.value];
1536   },
1537
1538   select: function(element) {
1539     return Form.Element.Serializers[element.type == 'select-one' ?
1540       'selectOne' : 'selectMany'](element);
1541   },
1542
1543   selectOne: function(element) {
1544     var value = '', opt, index = element.selectedIndex;
1545     if (index >= 0) {
1546       opt = element.options[index];
1547       value = opt.value || opt.text;
1548     }
1549     return [element.name, value];
1550   },
1551
1552   selectMany: function(element) {
1553     var value = [];
1554     for (var i = 0; i < element.length; i++) {
1555       var opt = element.options[i];
1556       if (opt.selected)
1557         value.push(opt.value || opt.text);
1558     }
1559     return [element.name, value];
1560   }
1561 }
1562
1563 /*--------------------------------------------------------------------------*/
1564
1565 var $F = Form.Element.getValue;
1566
1567 /*--------------------------------------------------------------------------*/
1568
1569 Abstract.TimedObserver = function() {}
1570 Abstract.TimedObserver.prototype = {
1571   initialize: function(element, frequency, callback) {
1572     this.frequency = frequency;
1573     this.element   = $(element);
1574     this.callback  = callback;
1575
1576     this.lastValue = this.getValue();
1577     this.registerCallback();
1578   },
1579
1580   registerCallback: function() {
1581     setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
1582   },
1583
1584   onTimerEvent: function() {
1585     var value = this.getValue();
1586     if (this.lastValue != value) {
1587       this.callback(this.element, value);
1588       this.lastValue = value;
1589     }
1590   }
1591 }
1592
1593 Form.Element.Observer = Class.create();
1594 Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
1595   getValue: function() {
1596     return Form.Element.getValue(this.element);
1597   }
1598 });
1599
1600 Form.Observer = Class.create();
1601 Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
1602   getValue: function() {
1603     return Form.serialize(this.element);
1604   }
1605 });
1606
1607 /*--------------------------------------------------------------------------*/
1608
1609 Abstract.EventObserver = function() {}
1610 Abstract.EventObserver.prototype = {
1611   initialize: function(element, callback) {
1612     this.element  = $(element);
1613     this.callback = callback;
1614
1615     this.lastValue = this.getValue();
1616     if (this.element.tagName.toLowerCase() == 'form')
1617       this.registerFormCallbacks();
1618     else
1619       this.registerCallback(this.element);
1620   },
1621
1622   onElementEvent: function() {
1623     var value = this.getValue();
1624     if (this.lastValue != value) {
1625       this.callback(this.element, value);
1626       this.lastValue = value;
1627     }
1628   },
1629
1630   registerFormCallbacks: function() {
1631     var elements = Form.getElements(this.element);
1632     for (var i = 0; i < elements.length; i++)
1633       this.registerCallback(elements[i]);
1634   },
1635
1636   registerCallback: function(element) {
1637     if (element.type) {
1638       switch (element.type.toLowerCase()) {
1639         case 'checkbox':
1640         case 'radio':
1641           Event.observe(element, 'click', this.onElementEvent.bind(this));
1642           break;
1643         case 'password':
1644         case 'text':
1645         case 'textarea':
1646         case 'select-one':
1647         case 'select-multiple':
1648           Event.observe(element, 'change', this.onElementEvent.bind(this));
1649           break;
1650       }
1651     }
1652   }
1653 }
1654
1655 Form.Element.EventObserver = Class.create();
1656 Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
1657   getValue: function() {
1658     return Form.Element.getValue(this.element);
1659   }
1660 });
1661
1662 Form.EventObserver = Class.create();
1663 Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
1664   getValue: function() {
1665     return Form.serialize(this.element);
1666   }
1667 });
1668 if (!window.Event) {
1669   var Event = new Object();
1670 }
1671
1672 Object.extend(Event, {
1673   KEY_BACKSPACE: 8,
1674   KEY_TAB:       9,
1675   KEY_RETURN:   13,
1676   KEY_ESC:      27,
1677   KEY_LEFT:     37,
1678   KEY_UP:       38,
1679   KEY_RIGHT:    39,
1680   KEY_DOWN:     40,
1681   KEY_DELETE:   46,
1682
1683   element: function(event) {
1684     return event.target || event.srcElement;
1685   },
1686
1687   isLeftClick: function(event) {
1688     return (((event.which) && (event.which == 1)) ||
1689             ((event.button) && (event.button == 1)));
1690   },
1691
1692   pointerX: function(event) {
1693     return event.pageX || (event.clientX +
1694       (document.documentElement.scrollLeft || document.body.scrollLeft));
1695   },
1696
1697   pointerY: function(event) {
1698     return event.pageY || (event.clientY +
1699       (document.documentElement.scrollTop || document.body.scrollTop));
1700   },
1701
1702   stop: function(event) {
1703     if (event.preventDefault) {
1704       event.preventDefault();
1705       event.stopPropagation();
1706     } else {
1707       event.returnValue = false;
1708       event.cancelBubble = true;
1709     }
1710   },
1711
1712   // find the first node with the given tagName, starting from the
1713   // node the event was triggered on; traverses the DOM upwards
1714   findElement: function(event, tagName) {
1715     var element = Event.element(event);
1716     while (element.parentNode && (!element.tagName ||
1717         (element.tagName.toUpperCase() != tagName.toUpperCase())))
1718       element = element.parentNode;
1719     return element;
1720   },
1721
1722   observers: false,
1723
1724   _observeAndCache: function(element, name, observer, useCapture) {
1725     if (!this.observers) this.observers = [];
1726     if (element.addEventListener) {
1727       this.observers.push([element, name, observer, useCapture]);
1728       element.addEventListener(name, observer, useCapture);
1729     } else if (element.attachEvent) {
1730       this.observers.push([element, name, observer, useCapture]);
1731       element.attachEvent('on' + name, observer);
1732     }
1733   },
1734
1735   unloadCache: function() {
1736     if (!Event.observers) return;
1737     for (var i = 0; i < Event.observers.length; i++) {
1738       Event.stopObserving.apply(this, Event.observers[i]);
1739       Event.observers[i][0] = null;
1740     }
1741     Event.observers = false;
1742   },
1743
1744   observe: function(element, name, observer, useCapture) {
1745     var element = $(element);
1746     useCapture = useCapture || false;
1747
1748     if (name == 'keypress' &&
1749         (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
1750         || element.attachEvent))
1751       name = 'keydown';
1752
1753     this._observeAndCache(element, name, observer, useCapture);
1754   },
1755
1756   stopObserving: function(element, name, observer, useCapture) {
1757     var element = $(element);
1758     useCapture = useCapture || false;
1759
1760     if (name == 'keypress' &&
1761         (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
1762         || element.detachEvent))
1763       name = 'keydown';
1764
1765     if (element.removeEventListener) {
1766       element.removeEventListener(name, observer, useCapture);
1767     } else if (element.detachEvent) {
1768       element.detachEvent('on' + name, observer);
1769     }
1770   }
1771 });
1772
1773 /* prevent memory leaks in IE */
1774 if (navigator.appVersion.match(/\bMSIE\b/))
1775   Event.observe(window, 'unload', Event.unloadCache, false);
1776 var Position = {
1777   // set to true if needed, warning: firefox performance problems
1778   // NOT neeeded for page scrolling, only if draggable contained in
1779   // scrollable elements
1780   includeScrollOffsets: false,
1781
1782   // must be called before calling withinIncludingScrolloffset, every time the
1783   // page is scrolled
1784   prepare: function() {
1785     this.deltaX =  window.pageXOffset
1786                 || document.documentElement.scrollLeft
1787                 || document.body.scrollLeft
1788                 || 0;
1789     this.deltaY =  window.pageYOffset
1790                 || document.documentElement.scrollTop
1791                 || document.body.scrollTop
1792                 || 0;
1793   },
1794
1795   realOffset: function(element) {
1796     var valueT = 0, valueL = 0;
1797     do {
1798       valueT += element.scrollTop  || 0;
1799       valueL += element.scrollLeft || 0;
1800       element = element.parentNode;
1801     } while (element);
1802     return [valueL, valueT];
1803   },
1804
1805   cumulativeOffset: function(element) {
1806     var valueT = 0, valueL = 0;
1807     do {
1808       valueT += element.offsetTop  || 0;
1809       valueL += element.offsetLeft || 0;
1810       element = element.offsetParent;
1811     } while (element);
1812     return [valueL, valueT];
1813   },
1814
1815   positionedOffset: function(element) {
1816     var valueT = 0, valueL = 0;
1817     do {
1818       valueT += element.offsetTop  || 0;
1819       valueL += element.offsetLeft || 0;
1820       element = element.offsetParent;
1821       if (element) {
1822         p = Element.getStyle(element, 'position');
1823         if (p == 'relative' || p == 'absolute') break;
1824       }
1825     } while (element);
1826     return [valueL, valueT];
1827   },
1828
1829   offsetParent: function(element) {
1830     if (element.offsetParent) return element.offsetParent;
1831     if (element == document.body) return element;
1832
1833     while ((element = element.parentNode) && element != document.body)
1834       if (Element.getStyle(element, 'position') != 'static')
1835         return element;
1836
1837     return document.body;
1838   },
1839
1840   // caches x/y coordinate pair to use with overlap
1841   within: function(element, x, y) {
1842     if (this.includeScrollOffsets)
1843       return this.withinIncludingScrolloffsets(element, x, y);
1844     this.xcomp = x;
1845     this.ycomp = y;
1846     this.offset = this.cumulativeOffset(element);
1847
1848     return (y >= this.offset[1] &&
1849             y <  this.offset[1] + element.offsetHeight &&
1850             x >= this.offset[0] &&
1851             x <  this.offset[0] + element.offsetWidth);
1852   },
1853
1854   withinIncludingScrolloffsets: function(element, x, y) {
1855     var offsetcache = this.realOffset(element);
1856
1857     this.xcomp = x + offsetcache[0] - this.deltaX;
1858     this.ycomp = y + offsetcache[1] - this.deltaY;
1859     this.offset = this.cumulativeOffset(element);
1860
1861     return (this.ycomp >= this.offset[1] &&
1862             this.ycomp <  this.offset[1] + element.offsetHeight &&
1863             this.xcomp >= this.offset[0] &&
1864             this.xcomp <  this.offset[0] + element.offsetWidth);
1865   },
1866
1867   // within must be called directly before
1868   overlap: function(mode, element) {
1869     if (!mode) return 0;
1870     if (mode == 'vertical')
1871       return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
1872         element.offsetHeight;
1873     if (mode == 'horizontal')
1874       return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
1875         element.offsetWidth;
1876   },
1877
1878   clone: function(source, target) {
1879     source = $(source);
1880     target = $(target);
1881     target.style.position = 'absolute';
1882     var offsets = this.cumulativeOffset(source);
1883     target.style.top    = offsets[1] + 'px';
1884     target.style.left   = offsets[0] + 'px';
1885     target.style.width  = source.offsetWidth + 'px';
1886     target.style.height = source.offsetHeight + 'px';
1887   },
1888
1889   page: function(forElement) {
1890     var valueT = 0, valueL = 0;
1891
1892     var element = forElement;
1893     do {
1894       valueT += element.offsetTop  || 0;
1895       valueL += element.offsetLeft || 0;
1896
1897       // Safari fix
1898       if (element.offsetParent==document.body)
1899         if (Element.getStyle(element,'position')=='absolute') break;
1900
1901     } while (element = element.offsetParent);
1902
1903     element = forElement;
1904     do {
1905       valueT -= element.scrollTop  || 0;
1906       valueL -= element.scrollLeft || 0;
1907     } while (element = element.parentNode);
1908
1909     return [valueL, valueT];
1910   },
1911
1912   clone: function(source, target) {
1913     var options = Object.extend({
1914       setLeft:    true,
1915       setTop:     true,
1916       setWidth:   true,
1917       setHeight:  true,
1918       offsetTop:  0,
1919       offsetLeft: 0
1920     }, arguments[2] || {})
1921
1922     // find page position of source
1923     source = $(source);
1924     var p = Position.page(source);
1925
1926     // find coordinate system to use
1927     target = $(target);
1928     var delta = [0, 0];
1929     var parent = null;
1930     // delta [0,0] will do fine with position: fixed elements,
1931     // position:absolute needs offsetParent deltas
1932     if (Element.getStyle(target,'position') == 'absolute') {
1933       parent = Position.offsetParent(target);
1934       delta = Position.page(parent);
1935     }
1936
1937     // correct by body offsets (fixes Safari)
1938     if (parent == document.body) {
1939       delta[0] -= document.body.offsetLeft;
1940       delta[1] -= document.body.offsetTop;
1941     }
1942
1943     // set position
1944     if(options.setLeft)   target.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
1945     if(options.setTop)    target.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
1946     if(options.setWidth)  target.style.width = source.offsetWidth + 'px';
1947     if(options.setHeight) target.style.height = source.offsetHeight + 'px';
1948   },
1949
1950   absolutize: function(element) {
1951     element = $(element);
1952     if (element.style.position == 'absolute') return;
1953     Position.prepare();
1954
1955     var offsets = Position.positionedOffset(element);
1956     var top     = offsets[1];
1957     var left    = offsets[0];
1958     var width   = element.clientWidth;
1959     var height  = element.clientHeight;
1960
1961     element._originalLeft   = left - parseFloat(element.style.left  || 0);
1962     element._originalTop    = top  - parseFloat(element.style.top || 0);
1963     element._originalWidth  = element.style.width;
1964     element._originalHeight = element.style.height;
1965
1966     element.style.position = 'absolute';
1967     element.style.top    = top + 'px';;
1968     element.style.left   = left + 'px';;
1969     element.style.width  = width + 'px';;
1970     element.style.height = height + 'px';;
1971   },
1972
1973   relativize: function(element) {
1974     element = $(element);
1975     if (element.style.position == 'relative') return;
1976     Position.prepare();
1977
1978     element.style.position = 'relative';
1979     var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
1980     var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
1981
1982     element.style.top    = top + 'px';
1983     element.style.left   = left + 'px';
1984     element.style.height = element._originalHeight;
1985     element.style.width  = element._originalWidth;
1986   }
1987 }
1988
1989 // Safari returns margins on body which is incorrect if the child is absolutely
1990 // positioned.  For performance reasons, redefine Position.cumulativeOffset for
1991 // KHTML/WebKit only.
1992 if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
1993   Position.cumulativeOffset = function(element) {
1994     var valueT = 0, valueL = 0;
1995     do {
1996       valueT += element.offsetTop  || 0;
1997       valueL += element.offsetLeft || 0;
1998       if (element.offsetParent == document.body)
1999         if (Element.getStyle(element, 'position') == 'absolute') break;
2000
2001       element = element.offsetParent;
2002     } while (element);
2003
2004     return [valueL, valueT];
2005   }
2006 }

Benjamin Mako Hill || Want to submit a patch?