Path: blob/trunk/third_party/closure/goog/positioning/positioning.js
2868 views
// Copyright 2006 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview Common positioning code.16*17* @author [email protected] (Emil A Eklund)18*/1920goog.provide('goog.positioning');21goog.provide('goog.positioning.Corner');22goog.provide('goog.positioning.CornerBit');23goog.provide('goog.positioning.Overflow');24goog.provide('goog.positioning.OverflowStatus');2526goog.require('goog.asserts');27goog.require('goog.dom');28goog.require('goog.dom.TagName');29goog.require('goog.math.Coordinate');30goog.require('goog.math.Rect');31goog.require('goog.math.Size');32goog.require('goog.style');33goog.require('goog.style.bidi');343536/**37* Enum for bits in the {@see goog.positioning.Corner) bitmap.38*39* @enum {number}40*/41goog.positioning.CornerBit = {42BOTTOM: 1,43CENTER: 2,44RIGHT: 4,45FLIP_RTL: 846};474849/**50* Enum for representing an element corner for positioning the popup.51*52* The START constants map to LEFT if element directionality is left53* to right and RIGHT if the directionality is right to left.54* Likewise END maps to RIGHT or LEFT depending on the directionality.55*56* @enum {number}57*/58goog.positioning.Corner = {59TOP_LEFT: 0,60TOP_RIGHT: goog.positioning.CornerBit.RIGHT,61BOTTOM_LEFT: goog.positioning.CornerBit.BOTTOM,62BOTTOM_RIGHT:63goog.positioning.CornerBit.BOTTOM | goog.positioning.CornerBit.RIGHT,64TOP_START: goog.positioning.CornerBit.FLIP_RTL,65TOP_END:66goog.positioning.CornerBit.FLIP_RTL | goog.positioning.CornerBit.RIGHT,67BOTTOM_START:68goog.positioning.CornerBit.BOTTOM | goog.positioning.CornerBit.FLIP_RTL,69BOTTOM_END: goog.positioning.CornerBit.BOTTOM |70goog.positioning.CornerBit.RIGHT | goog.positioning.CornerBit.FLIP_RTL,71TOP_CENTER: goog.positioning.CornerBit.CENTER,72BOTTOM_CENTER:73goog.positioning.CornerBit.BOTTOM | goog.positioning.CornerBit.CENTER74};757677/**78* Enum for representing position handling in cases where the element would be79* positioned outside the viewport.80*81* @enum {number}82*/83goog.positioning.Overflow = {84/** Ignore overflow */85IGNORE: 0,8687/** Try to fit horizontally in the viewport at all costs. */88ADJUST_X: 1,8990/** If the element can't fit horizontally, report positioning failure. */91FAIL_X: 2,9293/** Try to fit vertically in the viewport at all costs. */94ADJUST_Y: 4,9596/** If the element can't fit vertically, report positioning failure. */97FAIL_Y: 8,9899/** Resize the element's width to fit in the viewport. */100RESIZE_WIDTH: 16,101102/** Resize the element's height to fit in the viewport. */103RESIZE_HEIGHT: 32,104105/**106* If the anchor goes off-screen in the x-direction, position the movable107* element off-screen. Otherwise, try to fit horizontally in the viewport.108*/109ADJUST_X_EXCEPT_OFFSCREEN: 64 | 1,110111/**112* If the anchor goes off-screen in the y-direction, position the movable113* element off-screen. Otherwise, try to fit vertically in the viewport.114*/115ADJUST_Y_EXCEPT_OFFSCREEN: 128 | 4116};117118119/**120* Enum for representing the outcome of a positioning call.121*122* @enum {number}123*/124goog.positioning.OverflowStatus = {125NONE: 0,126ADJUSTED_X: 1,127ADJUSTED_Y: 2,128WIDTH_ADJUSTED: 4,129HEIGHT_ADJUSTED: 8,130FAILED_LEFT: 16,131FAILED_RIGHT: 32,132FAILED_TOP: 64,133FAILED_BOTTOM: 128,134FAILED_OUTSIDE_VIEWPORT: 256135};136137138/**139* Shorthand to check if a status code contains any fail code.140* @type {number}141*/142goog.positioning.OverflowStatus.FAILED =143goog.positioning.OverflowStatus.FAILED_LEFT |144goog.positioning.OverflowStatus.FAILED_RIGHT |145goog.positioning.OverflowStatus.FAILED_TOP |146goog.positioning.OverflowStatus.FAILED_BOTTOM |147goog.positioning.OverflowStatus.FAILED_OUTSIDE_VIEWPORT;148149150/**151* Shorthand to check if horizontal positioning failed.152* @type {number}153*/154goog.positioning.OverflowStatus.FAILED_HORIZONTAL =155goog.positioning.OverflowStatus.FAILED_LEFT |156goog.positioning.OverflowStatus.FAILED_RIGHT;157158159/**160* Shorthand to check if vertical positioning failed.161* @type {number}162*/163goog.positioning.OverflowStatus.FAILED_VERTICAL =164goog.positioning.OverflowStatus.FAILED_TOP |165goog.positioning.OverflowStatus.FAILED_BOTTOM;166167168/**169* Positions a movable element relative to an anchor element. The caller170* specifies the corners that should touch. This functions then moves the171* movable element accordingly.172*173* @param {Element} anchorElement The element that is the anchor for where174* the movable element should position itself.175* @param {goog.positioning.Corner} anchorElementCorner The corner of the176* anchorElement for positioning the movable element.177* @param {Element} movableElement The element to move.178* @param {goog.positioning.Corner} movableElementCorner The corner of the179* movableElement that that should be positioned adjacent to the anchor180* element.181* @param {goog.math.Coordinate=} opt_offset An offset specified in pixels.182* After the normal positioning algorithm is applied, the offset is then183* applied. Positive coordinates move the popup closer to the center of the184* anchor element. Negative coordinates move the popup away from the center185* of the anchor element.186* @param {goog.math.Box=} opt_margin A margin specified in pixels.187* After the normal positioning algorithm is applied and any offset, the188* margin is then applied. Positive coordinates move the popup away from the189* spot it was positioned towards its center. Negative coordinates move it190* towards the spot it was positioned away from its center.191* @param {?number=} opt_overflow Overflow handling mode. Defaults to IGNORE if192* not specified. Bitmap, {@see goog.positioning.Overflow}.193* @param {goog.math.Size=} opt_preferredSize The preferred size of the194* movableElement.195* @param {goog.math.Box=} opt_viewport Box object describing the dimensions of196* the viewport. The viewport is specified relative to offsetParent of197* {@code movableElement}. In other words, the viewport can be thought of as198* describing a "position: absolute" element contained in the offsetParent.199* It defaults to visible area of nearest scrollable ancestor of200* {@code movableElement} (see {@code goog.style.getVisibleRectForElement}).201* @return {goog.positioning.OverflowStatus} Status bitmap,202* {@see goog.positioning.OverflowStatus}.203*/204goog.positioning.positionAtAnchor = function(205anchorElement, anchorElementCorner, movableElement, movableElementCorner,206opt_offset, opt_margin, opt_overflow, opt_preferredSize, opt_viewport) {207208goog.asserts.assert(movableElement);209var movableParentTopLeft =210goog.positioning.getOffsetParentPageOffset(movableElement);211212// Get the visible part of the anchor element. anchorRect is213// relative to anchorElement's page.214var anchorRect = goog.positioning.getVisiblePart_(anchorElement);215216// Translate anchorRect to be relative to movableElement's page.217goog.style.translateRectForAnotherFrame(218anchorRect, goog.dom.getDomHelper(anchorElement),219goog.dom.getDomHelper(movableElement));220221// Offset based on which corner of the element we want to position against.222var corner =223goog.positioning.getEffectiveCorner(anchorElement, anchorElementCorner);224var offsetLeft = anchorRect.left;225if (corner & goog.positioning.CornerBit.RIGHT) {226offsetLeft += anchorRect.width;227} else if (corner & goog.positioning.CornerBit.CENTER) {228offsetLeft += anchorRect.width / 2;229}230231// absolutePos is a candidate position relative to the232// movableElement's window.233var absolutePos = new goog.math.Coordinate(234offsetLeft, anchorRect.top +235(corner & goog.positioning.CornerBit.BOTTOM ? anchorRect.height : 0));236237// Translate absolutePos to be relative to the offsetParent.238absolutePos =239goog.math.Coordinate.difference(absolutePos, movableParentTopLeft);240241// Apply offset, if specified242if (opt_offset) {243absolutePos.x +=244(corner & goog.positioning.CornerBit.RIGHT ? -1 : 1) * opt_offset.x;245absolutePos.y +=246(corner & goog.positioning.CornerBit.BOTTOM ? -1 : 1) * opt_offset.y;247}248249// Determine dimension of viewport.250var viewport;251if (opt_overflow) {252if (opt_viewport) {253viewport = opt_viewport;254} else {255viewport = goog.style.getVisibleRectForElement(movableElement);256if (viewport) {257viewport.top -= movableParentTopLeft.y;258viewport.right -= movableParentTopLeft.x;259viewport.bottom -= movableParentTopLeft.y;260viewport.left -= movableParentTopLeft.x;261}262}263}264265return goog.positioning.positionAtCoordinate(266absolutePos, movableElement, movableElementCorner, opt_margin, viewport,267opt_overflow, opt_preferredSize);268};269270271/**272* Calculates the page offset of the given element's273* offsetParent. This value can be used to translate any x- and274* y-offset relative to the page to an offset relative to the275* offsetParent, which can then be used directly with as position276* coordinate for {@code positionWithCoordinate}.277* @param {!Element} movableElement The element to calculate.278* @return {!goog.math.Coordinate} The page offset, may be (0, 0).279*/280goog.positioning.getOffsetParentPageOffset = function(movableElement) {281// Ignore offset for the BODY element unless its position is non-static.282// For cases where the offset parent is HTML rather than the BODY (such as in283// IE strict mode) there's no need to get the position of the BODY as it284// doesn't affect the page offset.285var movableParentTopLeft;286var parent = /** @type {?} */ (movableElement).offsetParent;287if (parent) {288var isBody = parent.tagName == goog.dom.TagName.HTML ||289parent.tagName == goog.dom.TagName.BODY;290if (!isBody || goog.style.getComputedPosition(parent) != 'static') {291// Get the top-left corner of the parent, in page coordinates.292movableParentTopLeft = goog.style.getPageOffset(parent);293294if (!isBody) {295movableParentTopLeft = goog.math.Coordinate.difference(296movableParentTopLeft,297new goog.math.Coordinate(298goog.style.bidi.getScrollLeft(parent), parent.scrollTop));299}300}301}302303return movableParentTopLeft || new goog.math.Coordinate();304};305306307/**308* Returns intersection of the specified element and309* goog.style.getVisibleRectForElement for it.310*311* @param {Element} el The target element.312* @return {!goog.math.Rect} Intersection of getVisibleRectForElement313* and the current bounding rectangle of the element. If the314* intersection is empty, returns the bounding rectangle.315* @private316*/317goog.positioning.getVisiblePart_ = function(el) {318var rect = goog.style.getBounds(el);319var visibleBox = goog.style.getVisibleRectForElement(el);320if (visibleBox) {321rect.intersection(goog.math.Rect.createFromBox(visibleBox));322}323return rect;324};325326327/**328* Positions the specified corner of the movable element at the329* specified coordinate.330*331* @param {goog.math.Coordinate} absolutePos The coordinate to position the332* element at.333* @param {Element} movableElement The element to be positioned.334* @param {goog.positioning.Corner} movableElementCorner The corner of the335* movableElement that that should be positioned.336* @param {goog.math.Box=} opt_margin A margin specified in pixels.337* After the normal positioning algorithm is applied and any offset, the338* margin is then applied. Positive coordinates move the popup away from the339* spot it was positioned towards its center. Negative coordinates move it340* towards the spot it was positioned away from its center.341* @param {goog.math.Box=} opt_viewport Box object describing the dimensions of342* the viewport. Required if opt_overflow is specified.343* @param {?number=} opt_overflow Overflow handling mode. Defaults to IGNORE if344* not specified, {@see goog.positioning.Overflow}.345* @param {goog.math.Size=} opt_preferredSize The preferred size of the346* movableElement. Defaults to the current size.347* @return {goog.positioning.OverflowStatus} Status bitmap.348*/349goog.positioning.positionAtCoordinate = function(350absolutePos, movableElement, movableElementCorner, opt_margin, opt_viewport,351opt_overflow, opt_preferredSize) {352absolutePos = absolutePos.clone();353354// Offset based on attached corner and desired margin.355var corner =356goog.positioning.getEffectiveCorner(movableElement, movableElementCorner);357var elementSize = goog.style.getSize(movableElement);358var size =359opt_preferredSize ? opt_preferredSize.clone() : elementSize.clone();360361var positionResult = goog.positioning.getPositionAtCoordinate(362absolutePos, size, corner, opt_margin, opt_viewport, opt_overflow);363364if (positionResult.status & goog.positioning.OverflowStatus.FAILED) {365return positionResult.status;366}367368goog.style.setPosition(movableElement, positionResult.rect.getTopLeft());369size = positionResult.rect.getSize();370if (!goog.math.Size.equals(elementSize, size)) {371goog.style.setBorderBoxSize(movableElement, size);372}373374return positionResult.status;375};376377378/**379* Computes the position for an element to be placed on-screen at the380* specified coordinates. Returns an object containing both the resulting381* rectangle, and the overflow status bitmap.382*383* @param {!goog.math.Coordinate} absolutePos The coordinate to position the384* element at.385* @param {!goog.math.Size} elementSize The size of the element to be386* positioned.387* @param {goog.positioning.Corner} elementCorner The corner of the388* movableElement that that should be positioned.389* @param {goog.math.Box=} opt_margin A margin specified in pixels.390* After the normal positioning algorithm is applied and any offset, the391* margin is then applied. Positive coordinates move the popup away from the392* spot it was positioned towards its center. Negative coordinates move it393* towards the spot it was positioned away from its center.394* @param {goog.math.Box=} opt_viewport Box object describing the dimensions of395* the viewport. Required if opt_overflow is specified.396* @param {?number=} opt_overflow Overflow handling mode. Defaults to IGNORE397* if not specified, {@see goog.positioning.Overflow}.398* @return {{rect:!goog.math.Rect, status:goog.positioning.OverflowStatus}}399* Object containing the computed position and status bitmap.400*/401goog.positioning.getPositionAtCoordinate = function(402absolutePos, elementSize, elementCorner, opt_margin, opt_viewport,403opt_overflow) {404absolutePos = absolutePos.clone();405elementSize = elementSize.clone();406var status = goog.positioning.OverflowStatus.NONE;407408if (opt_margin || elementCorner != goog.positioning.Corner.TOP_LEFT) {409if (elementCorner & goog.positioning.CornerBit.RIGHT) {410absolutePos.x -= elementSize.width + (opt_margin ? opt_margin.right : 0);411} else if (elementCorner & goog.positioning.CornerBit.CENTER) {412absolutePos.x -= elementSize.width / 2;413} else if (opt_margin) {414absolutePos.x += opt_margin.left;415}416if (elementCorner & goog.positioning.CornerBit.BOTTOM) {417absolutePos.y -=418elementSize.height + (opt_margin ? opt_margin.bottom : 0);419} else if (opt_margin) {420absolutePos.y += opt_margin.top;421}422}423424// Adjust position to fit inside viewport.425if (opt_overflow) {426status = opt_viewport ?427goog.positioning.adjustForViewport_(428absolutePos, elementSize, opt_viewport, opt_overflow) :429goog.positioning.OverflowStatus.FAILED_OUTSIDE_VIEWPORT;430}431432var rect = new goog.math.Rect(0, 0, 0, 0);433rect.left = absolutePos.x;434rect.top = absolutePos.y;435rect.width = elementSize.width;436rect.height = elementSize.height;437return {rect: rect, status: status};438};439440441/**442* Adjusts the position and/or size of an element, identified by its position443* and size, to fit inside the viewport. If the position or size of the element444* is adjusted the pos or size objects, respectively, are modified.445*446* @param {goog.math.Coordinate} pos Position of element, updated if the447* position is adjusted.448* @param {goog.math.Size} size Size of element, updated if the size is449* adjusted.450* @param {goog.math.Box} viewport Bounding box describing the viewport.451* @param {number} overflow Overflow handling mode,452* {@see goog.positioning.Overflow}.453* @return {goog.positioning.OverflowStatus} Status bitmap,454* {@see goog.positioning.OverflowStatus}.455* @private456*/457goog.positioning.adjustForViewport_ = function(pos, size, viewport, overflow) {458var status = goog.positioning.OverflowStatus.NONE;459460var ADJUST_X_EXCEPT_OFFSCREEN =461goog.positioning.Overflow.ADJUST_X_EXCEPT_OFFSCREEN;462var ADJUST_Y_EXCEPT_OFFSCREEN =463goog.positioning.Overflow.ADJUST_Y_EXCEPT_OFFSCREEN;464if ((overflow & ADJUST_X_EXCEPT_OFFSCREEN) == ADJUST_X_EXCEPT_OFFSCREEN &&465(pos.x < viewport.left || pos.x >= viewport.right)) {466overflow &= ~goog.positioning.Overflow.ADJUST_X;467}468if ((overflow & ADJUST_Y_EXCEPT_OFFSCREEN) == ADJUST_Y_EXCEPT_OFFSCREEN &&469(pos.y < viewport.top || pos.y >= viewport.bottom)) {470overflow &= ~goog.positioning.Overflow.ADJUST_Y;471}472473// Left edge outside viewport, try to move it.474if (pos.x < viewport.left && overflow & goog.positioning.Overflow.ADJUST_X) {475pos.x = viewport.left;476status |= goog.positioning.OverflowStatus.ADJUSTED_X;477}478479// Ensure object is inside the viewport width if required.480if (overflow & goog.positioning.Overflow.RESIZE_WIDTH) {481// Move left edge inside viewport.482var originalX = pos.x;483if (pos.x < viewport.left) {484pos.x = viewport.left;485status |= goog.positioning.OverflowStatus.WIDTH_ADJUSTED;486}487488// Shrink width to inside right of viewport.489if (pos.x + size.width > viewport.right) {490// Set the width to be either the new maximum width within the viewport491// or the width originally within the viewport, whichever is less.492size.width = Math.min(493viewport.right - pos.x, originalX + size.width - viewport.left);494size.width = Math.max(size.width, 0);495status |= goog.positioning.OverflowStatus.WIDTH_ADJUSTED;496}497}498499// Right edge outside viewport, try to move it.500if (pos.x + size.width > viewport.right &&501overflow & goog.positioning.Overflow.ADJUST_X) {502pos.x = Math.max(viewport.right - size.width, viewport.left);503status |= goog.positioning.OverflowStatus.ADJUSTED_X;504}505506// Left or right edge still outside viewport, fail if the FAIL_X option was507// specified, ignore it otherwise.508if (overflow & goog.positioning.Overflow.FAIL_X) {509status |=510(pos.x < viewport.left ? goog.positioning.OverflowStatus.FAILED_LEFT :5110) |512(pos.x + size.width > viewport.right ?513goog.positioning.OverflowStatus.FAILED_RIGHT :5140);515}516517// Top edge outside viewport, try to move it.518if (pos.y < viewport.top && overflow & goog.positioning.Overflow.ADJUST_Y) {519pos.y = viewport.top;520status |= goog.positioning.OverflowStatus.ADJUSTED_Y;521}522523// Ensure object is inside the viewport height if required.524if (overflow & goog.positioning.Overflow.RESIZE_HEIGHT) {525// Move top edge inside viewport.526var originalY = pos.y;527if (pos.y < viewport.top) {528pos.y = viewport.top;529status |= goog.positioning.OverflowStatus.HEIGHT_ADJUSTED;530}531532// Shrink height to inside bottom of viewport.533if (pos.y + size.height > viewport.bottom) {534// Set the height to be either the new maximum height within the viewport535// or the height originally within the viewport, whichever is less.536size.height = Math.min(537viewport.bottom - pos.y, originalY + size.height - viewport.top);538size.height = Math.max(size.height, 0);539status |= goog.positioning.OverflowStatus.HEIGHT_ADJUSTED;540}541}542543// Bottom edge outside viewport, try to move it.544if (pos.y + size.height > viewport.bottom &&545overflow & goog.positioning.Overflow.ADJUST_Y) {546pos.y = Math.max(viewport.bottom - size.height, viewport.top);547status |= goog.positioning.OverflowStatus.ADJUSTED_Y;548}549550// Top or bottom edge still outside viewport, fail if the FAIL_Y option was551// specified, ignore it otherwise.552if (overflow & goog.positioning.Overflow.FAIL_Y) {553status |=554(pos.y < viewport.top ? goog.positioning.OverflowStatus.FAILED_TOP :5550) |556(pos.y + size.height > viewport.bottom ?557goog.positioning.OverflowStatus.FAILED_BOTTOM :5580);559}560561return status;562};563564565/**566* Returns an absolute corner (top/bottom left/right) given an absolute567* or relative (top/bottom start/end) corner and the direction of an element.568* Absolute corners remain unchanged.569* @param {Element} element DOM element to test for RTL direction.570* @param {goog.positioning.Corner} corner The popup corner used for571* positioning.572* @return {goog.positioning.Corner} Effective corner.573*/574goog.positioning.getEffectiveCorner = function(element, corner) {575return /** @type {goog.positioning.Corner} */ (576(corner & goog.positioning.CornerBit.FLIP_RTL &&577goog.style.isRightToLeft(element) ?578corner ^ goog.positioning.CornerBit.RIGHT :579corner) &580~goog.positioning.CornerBit.FLIP_RTL);581};582583584/**585* Returns the corner opposite the given one horizontally.586* @param {goog.positioning.Corner} corner The popup corner used to flip.587* @return {goog.positioning.Corner} The opposite corner horizontally.588*/589goog.positioning.flipCornerHorizontal = function(corner) {590return /** @type {goog.positioning.Corner} */ (591corner ^ goog.positioning.CornerBit.RIGHT);592};593594595/**596* Returns the corner opposite the given one vertically.597* @param {goog.positioning.Corner} corner The popup corner used to flip.598* @return {goog.positioning.Corner} The opposite corner vertically.599*/600goog.positioning.flipCornerVertical = function(corner) {601return /** @type {goog.positioning.Corner} */ (602corner ^ goog.positioning.CornerBit.BOTTOM);603};604605606/**607* Returns the corner opposite the given one horizontally and vertically.608* @param {goog.positioning.Corner} corner The popup corner used to flip.609* @return {goog.positioning.Corner} The opposite corner horizontally and610* vertically.611*/612goog.positioning.flipCorner = function(corner) {613return /** @type {goog.positioning.Corner} */ (614corner ^ goog.positioning.CornerBit.BOTTOM ^615goog.positioning.CornerBit.RIGHT);616};617618619