From: Jan Schneider Date: Wed, 14 Apr 2010 11:15:24 +0000 (+0200) Subject: Add at least Element.Layout from prototype 1.7. X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=275856b617d08bbdf25755be7373650b676eccdd;p=horde.git Add at least Element.Layout from prototype 1.7. --- diff --git a/horde/js/prototype.js b/horde/js/prototype.js index 8fc14151f..99f454a07 100644 --- a/horde/js/prototype.js +++ b/horde/js/prototype.js @@ -4872,3 +4872,973 @@ Element.ClassNames.prototype = { 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. + * + *

Overview

+ * + * 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. + * + *

Usage

+ * + * 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) + * + *

Caching

+ * + * 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. + * + *

Hidden elements

+ * + * 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 "#"; + } + }); + + 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 "#".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 + *
+ * + * 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); + } + }); + } +})();