Object.extend(Element.ClassNames.prototype, Enumerable);
/*--------------------------------------------------------------------------*/
+(function() {
+
+ // Converts a CSS percentage value to a decimal.
+ // Ex: toDecimal("30%"); // -> 0.3
+ function toDecimal(pctString) {
+ var match = pctString.match(/^(\d+)%?$/i);
+ if (!match) return null;
+ return (Number(match[1]) / 100);
+ }
+
+ // Can be called like this:
+ // getPixelValue("11px");
+ // Or like this:
+ // getPixelValue(someElement, 'paddingTop');
+ function getPixelValue(value, property) {
+ if (Object.isElement(value)) {
+ element = value;
+ value = element.getStyle(property);
+ }
+ if (value === null) {
+ return null;
+ }
+
+ // Non-IE browsers will always return pixels if possible.
+ // (We use parseFloat instead of parseInt because Firefox can return
+ // non-integer pixel values.)
+ if ((/^\d+(\.\d+)?(px)?$/i).test(value)) {
+ return window.parseFloat(value);
+ }
+
+ // When IE gives us something other than a pixel value, this technique
+ // (invented by Dean Edwards) will convert it to pixels.
+ if (/\d/.test(value) && element.runtimeStyle) {
+ var style = element.style.left, rStyle = element.runtimeStyle.left;
+ element.runtimeStyle.left = element.currentStyle.left;
+ element.style.left = value || 0;
+ value = element.style.pixelLeft;
+ element.style.left = style;
+ element.runtimeStyle.left = rStyle;
+
+ return value;
+ }
+
+ // For other browsers, we have to do a bit of work.
+ if (value.include('%')) {
+ var decimal = toDecimal(value);
+ var whole;
+ if (property.include('left') || property.include('right') ||
+ property.include('width')) {
+ whole = $(element.parentNode).measure('width');
+ } else if (property.include('top') || property.include('bottom') ||
+ property.include('height')) {
+ whole = $(element.parentNode).measure('height');
+ }
+
+ return whole * decimal;
+ }
+
+ // If we get this far, we should probably give up.
+ return 0;
+ }
+
+ // Turns plain numbers into pixel measurements.
+ function toCSSPixels(number) {
+ if (Object.isString(number) && number.endsWith('px')) {
+ return number;
+ }
+ return number + 'px';
+ }
+
+ function isDisplayed(element) {
+ var originalElement = element;
+ while (element && element.parentNode) {
+ var display = element.getStyle('display');
+ if (display === 'none') {
+ return false;
+ }
+ element = $(element.parentNode);
+ }
+ return true;
+ }
+
+ var hasLayout = Prototype.K;
+ if ('currentStyle' in document.documentElement) {
+ hasLayout = function(element) {
+ if (!element.currentStyle.hasLayout) {
+ element.style.zoom = 1;
+ }
+ return element;
+ };
+ }
+
+ // Converts the layout hash property names back to the CSS equivalents.
+ // For now, only the border properties differ.
+ function cssNameFor(key) {
+ if (key.includes('border')) return key + '-width';
+ return key;
+ }
+
+ /**
+ * class Element.Layout < Hash
+ *
+ * A set of key/value pairs representing measurements of various
+ * dimensions of an element.
+ *
+ * <h4>Overview</h4>
+ *
+ * The `Element.Layout` class is a specialized way to measure elements.
+ * It helps mitigate:
+ *
+ * * The convoluted steps often needed to get common measurements for
+ * elements.
+ * * The tendency of browsers to report measurements in non-pixel units.
+ * * The quirks that lead some browsers to report inaccurate measurements.
+ * * The difficulty of measuring elements that are hidden.
+ *
+ * <h4>Usage</h4>
+ *
+ * Instantiate an `Element.Layout` class by passing an element into the
+ * constructor:
+ *
+ * var layout = new Element.Layout(someElement);
+ *
+ * You can also use [[Element.getLayout]], if you prefer.
+ *
+ * Once you have a layout object, retrieve properties using [[Hash]]'s
+ * familiar `get` and `set` syntax.
+ *
+ * layout.get('width'); //-> 400
+ * layout.get('top'); //-> 180
+ *
+ * The following are the CSS-related properties that can be retrieved.
+ * Nearly all of them map directly to their property names in CSS. (The
+ * only exception is for borders — e.g., `border-width` instead of
+ * `border-left-width`.)
+ *
+ * * `height`
+ * * `width`
+ * * `top`
+ * * `left`
+ * * `right`
+ * * `bottom`
+ * * `border-left`
+ * * `border-right`
+ * * `border-top`
+ * * `border-bottom`
+ * * `padding-left`
+ * * `padding-right`
+ * * `padding-top`
+ * * `padding-bottom`
+ * * `margin-top`
+ * * `margin-bottom`
+ * * `margin-left`
+ * * `margin-right`
+ *
+ * In addition, these "composite" properties can be retrieved:
+ *
+ * * `padding-box-width` (width of the content area, from the beginning of
+ * the left padding to the end of the right padding)
+ * * `padding-box-height` (height of the content area, from the beginning
+ * of the top padding to the end of the bottom padding)
+ * * `border-box-width` (width of the content area, from the outer edge of
+ * the left border to the outer edge of the right border)
+ * * `border-box-height` (height of the content area, from the outer edge
+ * of the top border to the outer edge of the bottom border)
+ * * `margin-box-width` (width of the content area, from the beginning of
+ * the left margin to the end of the right margin)
+ * * `margin-box-height` (height of the content area, from the beginning
+ * of the top margin to the end of the bottom margin)
+ *
+ * <h4>Caching</h4>
+ *
+ * Because these properties can be costly to retrieve, `Element.Layout`
+ * behaves differently from an ordinary [[Hash]].
+ *
+ * First: by default, values are "lazy-loaded" — they aren't computed
+ * until they're retrieved. To measure all properties at once, pass
+ * a second argument into the constructor:
+ *
+ * var layout = new Element.Layout(someElement, true);
+ *
+ * Second: once a particular value is computed, it's cached. Asking for
+ * the same property again will return the original value without
+ * re-computation. This means that **an instance of `Element.Layout`
+ * becomes stale when the element's dimensions change**. When this
+ * happens, obtain a new instance.
+ *
+ * <h4>Hidden elements</h4>
+ *
+ * Because it's a common case to want the dimensions of a hidden element
+ * (e.g., for animations), it's possible to measure elements that are
+ * hidden with `display: none`.
+ *
+ * However, **it's only possible to measure a hidden element if its parent
+ * is visible**. If its parent (or any other ancestor) is hidden, any
+ * width and height measurements will return `0`, as will measurements for
+ * `top|bottom|left|right`.
+ *
+ **/
+ Element.Layout = Class.create(Hash, {
+ /**
+ * new Element.Layout(element[, preCompute])
+ * - element (Element): The element to be measured.
+ * - preCompute (Boolean): Whether to compute all values at once.
+ *
+ * Declare a new layout hash.
+ *
+ * The `preCompute` argument determines whether measurements will be
+ * lazy-loaded or not. If you plan to use many different measurements,
+ * it's often more performant to pre-compute, as it minimizes the
+ * amount of overhead needed to measure. If you need only one or two
+ * measurements, it's probably not worth it.
+ **/
+ initialize: function($super, element, preCompute) {
+ $super();
+ this.element = $(element);
+
+ // nullify all properties keys
+ Element.Layout.PROPERTIES.each( function(property) {
+ this._set(property, null);
+ }, this);
+
+ // The 'preCompute' boolean tells us whether we should fetch all values
+ // at once. If so, we should do setup/teardown only once. We set a flag
+ // so that we can ignore calls to `_begin` and `_end` elsewhere.
+ if (preCompute) {
+ this._preComputing = true;
+ this._begin();
+ Element.Layout.PROPERTIES.each( this._compute, this );
+ this._end();
+ this._preComputing = false;
+ }
+ },
+
+ _set: function(property, value) {
+ return Hash.prototype.set.call(this, property, value);
+ },
+
+ // TODO: Investigate.
+ set: function(property, value) {
+ throw "Properties of Element.Layout are read-only.";
+ },
+
+ /**
+ * Element.Layout#get(property) -> Number
+ * - property (String): One of the properties defined in
+ * [[Element.Layout.PROPERTIES]].
+ *
+ * Retrieve the measurement specified by `property`. Will throw an error
+ * if the property is invalid.
+ **/
+ get: function($super, property) {
+ // Try to fetch from the cache.
+ var value = $super(property);
+ return value === null ? this._compute(property) : value;
+ },
+
+ // `_begin` and `_end` are two functions that are called internally
+ // before and after any measurement is done. In certain conditions (e.g.,
+ // when hidden), elements need a "preparation" phase that ensures
+ // accuracy of measurements.
+ _begin: function() {
+ if (this._prepared) return;
+
+ var element = this.element;
+ if (isDisplayed(element)) {
+ this._prepared = true;
+ return;
+ }
+
+ // Remember the original values for some styles we're going to alter.
+ var originalStyles = {
+ position: element.style.position || '',
+ width: element.style.width || '',
+ visibility: element.style.visibility || '',
+ display: element.style.display || ''
+ };
+
+ // We store them so that the `_end` function can retrieve them later.
+ element.store('prototype_original_styles', originalStyles);
+
+ var position = element.getStyle('position'),
+ width = element.getStyle('width');
+
+ element.setStyle({
+ position: 'absolute',
+ visibility: 'hidden',
+ display: 'block'
+ });
+
+ var positionedWidth = element.getStyle('width');
+
+ var newWidth;
+ if (width && (positionedWidth === width)) {
+ // If the element's width is the same both before and after
+ // we set absolute positioning, that means:
+ // (a) it was already absolutely-positioned; or
+ // (b) it has an explicitly-set width, instead of width: auto.
+ // Either way, it means the element is the width it needs to be
+ // in order to report an accurate height.
+ newWidth = getPixelValue(width);
+ } else if (width && (position === 'absolute' || position === 'fixed')) {
+ newWidth = getPixelValue(width);
+ } else {
+ // If not, that means the element's width depends upon the width of
+ // its parent.
+ var parent = element.parentNode, pLayout = $(parent).getLayout();
+
+ newWidth = pLayout.get('width') -
+ this.get('margin-left') -
+ this.get('border-left') -
+ this.get('padding-left') -
+ this.get('padding-right') -
+ this.get('border-right') -
+ this.get('margin-right');
+ }
+
+ element.setStyle({ width: newWidth + 'px' });
+
+ // The element is now ready for measuring.
+ this._prepared = true;
+ },
+
+ _end: function() {
+ var element = this.element;
+ var originalStyles = element.retrieve('prototype_original_styles');
+ element.store('prototype_original_styles', null);
+ element.setStyle(originalStyles);
+ this._prepared = false;
+ },
+
+ _compute: function(property) {
+ var COMPUTATIONS = Element.Layout.COMPUTATIONS;
+ if (!(property in COMPUTATIONS)) {
+ throw "Property not found.";
+ }
+ return this._set(property, COMPUTATIONS[property].call(this, this.element));
+ },
+
+ /**
+ * Element.Layout#toCSS([keys...]) -> Object
+ * - keys (String): A space-separated list of keys to include.
+ *
+ * Converts the layout hash to a plain object of CSS property/value
+ * pairs, optionally including only the given keys.
+ *
+ * Keys can be passed into this method as individual arguments _or_
+ * separated by spaces within a string.
+ *
+ * // Equivalent statements:
+ * someLayout.toCSS('top', 'bottom', 'left', 'right');
+ * someLayout.toCSS('top bottom left right');
+ *
+ * Useful for passing layout properties to [[Element.setStyle]].
+ **/
+ toCSS: function() {
+ var args = $A(arguments);
+ var keys = (args.length === 0) ? Element.Layout.PROPERTIES :
+ args.join(' ').split(' ');
+ var css = {};
+ keys.each( function(key) {
+ // Key needs to be a valid Element.Layout property...
+ if (!Element.Layout.PROPERTIES.include(key)) return;
+ // ...but not a composite property.
+ if (Element.Layout.COMPOSITE_PROPERTIES.include(key)) return;
+
+ var value = this.get(key);
+ // Unless the value is null, add 'px' to the end and add it to the
+ // returned object.
+ if (value) css[cssNameFor(key)] = value + 'px';
+ });
+ return css;
+ },
+
+ inspect: function() {
+ return "#<Element.Layout>";
+ }
+ });
+
+ Object.extend(Element.Layout, {
+ /**
+ * Element.Layout.PROPERTIES = Array
+ *
+ * A list of all measurable properties.
+ **/
+ PROPERTIES: $w('height width top left right bottom border-left border-right border-top border-bottom padding-left padding-right padding-top padding-bottom margin-top margin-bottom margin-left margin-right padding-box-width padding-box-height border-box-width border-box-height margin-box-width margin-box-height'),
+
+ /**
+ * Element.Layout.COMPOSITE_PROPERTIES = Array
+ *
+ * A list of all composite properties. Composite properties don't map
+ * directly to CSS properties — they're combinations of other
+ * properties.
+ **/
+ COMPOSITE_PROPERTIES: $w('padding-box-width padding-box-height margin-box-width margin-box-height border-box-width border-box-height'),
+
+ COMPUTATIONS: {
+ 'height': function(element) {
+ if (!this._preComputing) this._begin();
+
+ var bHeight = this.get('border-box-height');
+ if (bHeight <= 0) return 0;
+
+ var bTop = this.get('border-top'),
+ bBottom = this.get('border-bottom');
+
+ var pTop = this.get('padding-top'),
+ pBottom = this.get('padding-bottom');
+
+ if (!this._preComputing) this._end();
+
+ return bHeight - bTop - bBottom - pTop - pBottom;
+ },
+
+ 'width': function(element) {
+ if (!this._preComputing) this._begin();
+
+ var bWidth = this.get('border-box-width');
+ if (bWidth <= 0) return 0;
+
+ var bLeft = this.get('border-left'),
+ bRight = this.get('border-right');
+
+ var pLeft = this.get('padding-left'),
+ pRight = this.get('padding-right');
+
+ if (!this._preComputing) this._end();
+
+ return bWidth - bLeft - bRight - pLeft - pRight;
+ },
+
+ 'padding-box-height': function(element) {
+ var height = this.get('height'),
+ pTop = this.get('padding-top'),
+ pBottom = this.get('padding-bottom');
+
+ return height + pTop + pBottom;
+ },
+
+ 'padding-box-width': function(element) {
+ var width = this.get('width'),
+ pLeft = this.get('padding-left'),
+ pRight = this.get('padding-right');
+
+ return width + pLeft + pRight;
+ },
+
+ 'border-box-height': function(element) {
+ return element.offsetHeight;
+ },
+
+ 'border-box-width': function(element) {
+ return element.offsetWidth;
+ },
+
+ 'margin-box-height': function(element) {
+ var bHeight = this.get('border-box-height'),
+ mTop = this.get('margin-top'),
+ mBottom = this.get('margin-bottom');
+
+ if (bHeight <= 0) return 0;
+
+ return bHeight + mTop + mBottom;
+ },
+
+ 'margin-box-width': function(element) {
+ var bWidth = this.get('border-box-width'),
+ mLeft = this.get('margin-left'),
+ mRight = this.get('margin-right');
+
+ if (bWidth <= 0) return 0;
+
+ return bWidth + mLeft + mRight;
+ },
+
+ 'top': function(element) {
+ var offset = element.positionedOffset();
+ return offset.top;
+ },
+
+ 'bottom': function(element) {
+ var offset = element.positionedOffset(),
+ parent = element.getOffsetParent(),
+ pHeight = parent.measure('height');
+
+ var mHeight = this.get('border-box-height');
+
+ return pHeight - mHeight - offset.top;
+ //
+ // return getPixelValue(element, 'bottom');
+ },
+
+ 'left': function(element) {
+ var offset = element.positionedOffset();
+ return offset.left;
+ },
+
+ 'right': function(element) {
+ var offset = element.positionedOffset(),
+ parent = element.getOffsetParent(),
+ pWidth = parent.measure('width');
+
+ var mWidth = this.get('border-box-width');
+
+ return pWidth - mWidth - offset.left;
+ //
+ // return getPixelValue(element, 'right');
+ },
+
+ 'padding-top': function(element) {
+ return getPixelValue(element, 'paddingTop');
+ },
+
+ 'padding-bottom': function(element) {
+ return getPixelValue(element, 'paddingBottom');
+ },
+
+ 'padding-left': function(element) {
+ return getPixelValue(element, 'paddingLeft');
+ },
+
+ 'padding-right': function(element) {
+ return getPixelValue(element, 'paddingRight');
+ },
+
+ 'border-top': function(element) {
+ return Object.isNumber(element.clientTop) ? element.clientTop :
+ getPixelValue(element, 'borderTopWidth');
+ },
+
+ 'border-bottom': function(element) {
+ return Object.isNumber(element.clientBottom) ? element.clientBottom :
+ getPixelValue(element, 'borderBottomWidth');
+ },
+
+ 'border-left': function(element) {
+ return Object.isNumber(element.clientLeft) ? element.clientLeft :
+ getPixelValue(element, 'borderLeftWidth');
+ },
+
+ 'border-right': function(element) {
+ return Object.isNumber(element.clientRight) ? element.clientRight :
+ getPixelValue(element, 'borderRightWidth');
+ },
+
+ 'margin-top': function(element) {
+ return getPixelValue(element, 'marginTop');
+ },
+
+ 'margin-bottom': function(element) {
+ return getPixelValue(element, 'marginBottom');
+ },
+
+ 'margin-left': function(element) {
+ return getPixelValue(element, 'marginLeft');
+ },
+
+ 'margin-right': function(element) {
+ return getPixelValue(element, 'marginRight');
+ }
+ }
+ });
+
+ // An easier way to compute right and bottom offsets.
+ if ('getBoundingClientRect' in document.documentElement) {
+ Object.extend(Element.Layout.COMPUTATIONS, {
+ 'right': function(element) {
+ var parent = hasLayout(element.getOffsetParent());
+ var rect = element.getBoundingClientRect(),
+ pRect = parent.getBoundingClientRect();
+
+ return (pRect.right - rect.right).round();
+ },
+
+ 'bottom': function(element) {
+ var parent = hasLayout(element.getOffsetParent());
+ var rect = element.getBoundingClientRect(),
+ pRect = parent.getBoundingClientRect();
+
+ return (pRect.bottom - rect.bottom).round();
+ }
+ });
+ }
+
+ /**
+ * class Element.Offset
+ *
+ * A representation of the top- and left-offsets of an element relative to
+ * another.
+ *
+ * All methods that compute offsets return an instance of `Element.Offset`.
+ *
+ **/
+ Element.Offset = Class.create({
+ /**
+ * new Element.Offset(left, top)
+ *
+ * Instantiates an [[Element.Offset]]. You shouldn't need to call this
+ * directly.
+ **/
+ initialize: function(left, top) {
+ this.left = left.round();
+ this.top = top.round();
+
+ // Act like an array.
+ this[0] = this.left;
+ this[1] = this.top;
+ },
+
+ /**
+ * Element.Offset#relativeTo(offset) -> Element.Offset
+ * - offset (Element.Offset): Another offset to compare to.
+ *
+ * Returns a new [[Element.Offset]] with its origin at the given
+ * `offset`. Useful for determining an element's distance from another
+ * arbitrary element.
+ **/
+ relativeTo: function(offset) {
+ return new Element.Offset(
+ this.left - offset.left,
+ this.top - offset.top
+ );
+ },
+
+ /**
+ * Element.Offset#inspect() -> String
+ **/
+ inspect: function() {
+ return "#<Element.Offset left: #{left} top: #{top}>".interpolate(this);
+ },
+
+ /**
+ * Element.Offset#toString() -> String
+ **/
+ toString: function() {
+ return "[#{left}, #{top}]".interpolate(this);
+ },
+
+ /**
+ * Element.Offset#toArray() -> Array
+ **/
+ toArray: function() {
+ return [this.left, this.top];
+ }
+ });
+
+ /**
+ * Element.getLayout(@element, preCompute) -> Element.Layout
+ *
+ * Returns an instance of [[Element.Layout]] for measuring an element's
+ * dimensions.
+ *
+ * Note that this method returns a _new_ `Element.Layout` object each time
+ * it's called. If you want to take advantage of measurement caching,
+ * retain a reference to one `Element.Layout` object, rather than calling
+ * `Element.getLayout` whenever you need a measurement. You should call
+ * `Element.getLayout` again only when the values in an existing
+ * `Element.Layout` object have become outdated.
+ **/
+ function getLayout(element, preCompute) {
+ return new Element.Layout(element, preCompute);
+ }
+
+ /**
+ * Element.measure(@element, property) -> Number
+ *
+ * Gives the pixel value of `element`'s dimension specified by
+ * `property`.
+ *
+ * Useful for one-off measurements of elements. If you find yourself
+ * calling this method frequently over short spans of code, you might want
+ * to call [[Element.getLayout]] and operate on the [[Element.Layout]]
+ * object itself (thereby taking advantage of measurement caching).
+ **/
+ function measure(element, property) {
+ return $(element).getLayout().get(property);
+ }
+
+ /**
+ * Element.getDimensions(@element) -> Object
+ *
+ * Finds the computed width and height of `element` and returns them as
+ * key/value pairs of an object.
+ *
+ * This method returns correct values on elements whose display is set to
+ * `none` either in an inline style rule or in an CSS stylesheet.
+ *
+ * In order to avoid calling the method twice, you should consider caching
+ * the values returned in a variable as shown below. If you only need
+ * `element`'s width or height, consider using [[Element.getWidth]] or
+ * [[Element.getHeight]] instead.
+ *
+ * Note that all values are returned as _numbers only_ although they are
+ * _expressed in pixels_.
+ *
+ * ##### Examples
+ *
+ * language: html
+ * <div id="rectangle" style="font-size: 10px; width: 20em; height: 10em"></div>
+ *
+ * Then:
+ *
+ * var dimensions = $('rectangle').getDimensions();
+ * // -> {width: 200, height: 100}
+ *
+ * dimensions.width;
+ * // -> 200
+ *
+ * dimensions.height;
+ * // -> 100
+ **/
+ function getDimensions(element) {
+ var layout = $(element).getLayout();
+ return {
+ width: layout.get('width'),
+ height: layout.get('height')
+ };
+ }
+
+ /**
+ * Element.getOffsetParent(@element) -> Element
+ *
+ * Returns `element`'s closest _positioned_ ancestor. If none is found, the
+ * `body` element is returned.
+ **/
+ function getOffsetParent(element) {
+ if (isDetached(element)) return $(document.body);
+
+ // IE reports offset parent incorrectly for inline elements.
+ var isInline = (Element.getStyle(element, 'display') === 'inline');
+ if (!isInline && element.offsetParent) return $(element.offsetParent);
+ if (element === document.body) return $(element);
+
+ while ((element = element.parentNode) && element !== document.body) {
+ if (Element.getStyle(element, 'position') !== 'static') {
+ return (element.nodeName === 'HTML') ? $(document.body) : $(element);
+ }
+ }
+
+ return $(document.body);
+ }
+
+
+ /**
+ * Element.cumulativeOffset(@element) -> Element.Offset
+ *
+ * Returns the offsets of `element` from the top left corner of the
+ * document.
+ **/
+ function cumulativeOffset(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ } while (element);
+ return new Element.Offset(valueL, valueT);
+ }
+
+ /**
+ * Element.positionedOffset(@element) -> Element.Offset
+ *
+ * Returns `element`'s offset relative to its closest positioned ancestor
+ * (the element that would be returned by [[Element.getOffsetParent]]).
+ **/
+ function positionedOffset(element) {
+ // Account for the margin of the element.
+ var layout = element.getLayout();
+
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ if (element) {
+ if (isBody(element)) break;
+ var p = Element.getStyle(element, 'position');
+ if (p !== 'static') break;
+ }
+ } while (element);
+
+ valueL -= layout.get('margin-top');
+ valueT -= layout.get('margin-left');
+
+ return new Element.Offset(valueL, valueT);
+ }
+
+ /**
+ * Element.cumulativeScrollOffset(@element) -> Element.Offset
+ *
+ * Calculates the cumulative scroll offset of an element in nested
+ * scrolling containers.
+ **/
+ function cumulativeScrollOffset(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.scrollTop || 0;
+ valueL += element.scrollLeft || 0;
+ element = element.parentNode;
+ } while (element);
+ return new Element.Offset(valueL, valueT);
+ }
+
+ /**
+ * Element.viewportOffset(@element) -> Array
+ *
+ * Returns the X/Y coordinates of element relative to the viewport.
+ **/
+ function viewportOffset(forElement) {
+ var valueT = 0, valueL = 0, docBody = document.body;
+
+ var element = forElement;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ // Safari fix
+ if (element.offsetParent == docBody &&
+ Element.getStyle(element, 'position') == 'absolute') break;
+ } while (element = element.offsetParent);
+
+ element = forElement;
+ do {
+ // Opera < 9.5 sets scrollTop/Left on both HTML and BODY elements.
+ // Other browsers set it only on the HTML element. The BODY element
+ // can be skipped since its scrollTop/Left should always be 0.
+ if (element != docBody) {
+ valueT -= element.scrollTop || 0;
+ valueL -= element.scrollLeft || 0;
+ }
+ } while (element = element.parentNode);
+ return new Element.Offset(valueL, valueT);
+ }
+
+ /**
+ * Element.absolutize(@element) -> Element
+ *
+ * Turns `element` into an absolutely-positioned element _without_
+ * changing its position in the page layout.
+ **/
+ function absolutize(element) {
+ element = $(element);
+
+ if (Element.getStyle(element, 'position') === 'absolute') {
+ return element;
+ }
+
+ var offsetParent = getOffsetParent(element);
+ var eOffset = element.viewportOffset(), pOffset =
+ offsetParent.viewportOffset();
+
+ var offset = eOffset.relativeTo(pOffset);
+ var layout = element.get('layout');
+
+ element.store('prototype_absolutize_original_styles', {
+ left: element.getStyle('left'),
+ top: element.getStyle('top'),
+ width: element.getStyle('width'),
+ height: element.getStyle('height')
+ });
+
+ element.setStyle({
+ position: 'absolute',
+ top: offset.top + 'px',
+ left: offset.left + 'px',
+ width: layout.get('width') + 'px',
+ height: layout.get('height') + 'px'
+ });
+
+ return element;
+ }
+
+ /**
+ * Element.relativize(@element) -> Element
+ *
+ * Turns `element` into a relatively-positioned element without changing
+ * its position in the page layout.
+ *
+ * Used to undo a call to [[Element.absolutize]].
+ **/
+ function relativize(element) {
+ element = $(element);
+ if (Element.getStyle(element, 'position') === 'relative') {
+ return element;
+ }
+
+ // Restore the original styles as captured by Element#absolutize.
+ var originalStyles =
+ element.retrieve('prototype_absolutize_original_styles');
+
+ if (originalStyles) element.setStyle(originalStyles);
+ return element;
+ }
+
+ Element.addMethods({
+ getLayout: getLayout,
+ measure: measure,
+ getDimensions: getDimensions,
+ getOffsetParent: getOffsetParent,
+ cumulativeOffset: cumulativeOffset,
+ positionedOffset: positionedOffset,
+ cumulativeScrollOffset: cumulativeScrollOffset,
+ viewportOffset: viewportOffset,
+ absolutize: absolutize,
+ relativize: relativize
+ });
+
+ function isBody(element) {
+ return element.nodeName.toUpperCase() === 'BODY';
+ }
+
+ function isDetached(element) {
+ return element !== document.body &&
+ !Element.descendantOf(element, document.body);
+ }
+
+ // If the browser supports the nonstandard `getBoundingClientRect`
+ // (currently only IE and Firefox), it becomes far easier to obtain
+ // true offsets.
+ if ('getBoundingClientRect' in document.documentElement) {
+ Element.addMethods({
+ viewportOffset: function(element) {
+ element = $(element);
+ if (isDetached(element)) return new Element.Offset(0, 0);
+
+ var rect = element.getBoundingClientRect(),
+ docEl = document.documentElement;
+ // The HTML element on IE < 8 has a 2px border by default, giving
+ // an incorrect offset. We correct this by subtracting clientTop
+ // and clientLeft.
+ return new Element.Offset(rect.left - docEl.clientLeft,
+ rect.top - docEl.clientTop);
+ },
+
+ cumulativeOffset: function(element) {
+ element = $(element);
+ if (isDetached(element)) return new Element.Offset(0, 0);
+
+ var docOffset = $(document.body).viewportOffset(),
+ elementOffset = element.viewportOffset();
+ return elementOffset.relativeTo(docOffset);
+ },
+
+ positionedOffset: function(element) {
+ element = $(element);
+ var parent = element.getOffsetParent();
+ if (isDetached(element)) return new Element.Offset(0, 0);
+
+ // When the BODY is the offsetParent, IE6 mistakenly reports the
+ // parent as HTML. Use that as the litmus test to fix another
+ // annoying IE6 quirk.
+ if (element.offsetParent &&
+ element.offsetParent.nodeName.toUpperCase() === 'HTML') {
+ return positionedOffset(element);
+ }
+
+ var eOffset = element.viewportOffset(),
+ pOffset = isBody(parent) ? viewportOffset(parent) :
+ parent.viewportOffset();
+ var retOffset = eOffset.relativeTo(pOffset);
+
+ // Account for the margin of the element.
+ var layout = element.getLayout();
+ var top = retOffset.top - layout.get('margin-top');
+ var left = retOffset.left - layout.get('margin-left');
+
+ return new Element.Offset(left, top);
+ }
+ });
+ }
+})();