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

Benjamin Mako Hill || Want to submit a patch?