1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * @fileoverview This file provides utility functions for position popups.
7 */
8
9cr.exportPath('cr.ui');
10
11/**
12 * Type def for rects as returned by getBoundingClientRect.
13 * @typedef {{left: number, top: number, width: number, height: number,
14 *            right: number, bottom: number}}
15 */
16cr.ui.Rect;
17
18/**
19 * Enum for defining how to anchor a popup to an anchor element.
20 * @enum {number}
21 */
22cr.ui.AnchorType = {
23  /**
24   * The popup's right edge is aligned with the left edge of the anchor.
25   * The popup's top edge is aligned with the top edge of the anchor.
26   */
27  BEFORE: 1,  // p: right, a: left, p: top, a: top
28
29  /**
30   * The popop's left edge is aligned with the right edge of the anchor.
31   * The popup's top edge is aligned with the top edge of the anchor.
32   */
33  AFTER: 2,  // p: left a: right, p: top, a: top
34
35  /**
36   * The popop's bottom edge is aligned with the top edge of the anchor.
37   * The popup's left edge is aligned with the left edge of the anchor.
38   */
39  ABOVE: 3,  // p: bottom, a: top, p: left, a: left
40
41  /**
42   * The popop's top edge is aligned with the bottom edge of the anchor.
43   * The popup's left edge is aligned with the left edge of the anchor.
44   */
45  BELOW: 4  // p: top, a: bottom, p: left, a: left
46};
47
48cr.define('cr.ui', function() {
49  /** @const */
50  var AnchorType = cr.ui.AnchorType;
51
52  /**
53   * Helper function for positionPopupAroundElement and positionPopupAroundRect.
54   * @param {!cr.ui.Rect} anchorRect The rect for the anchor.
55   * @param {!HTMLElement} popupElement The element used for the popup.
56   * @param {cr.ui.AnchorType} type The type of anchoring to do.
57   * @param {boolean=} opt_invertLeftRight Whether to invert the right/left
58   *     alignment.
59   */
60  function positionPopupAroundRect(anchorRect, popupElement, type,
61                                   opt_invertLeftRight) {
62    var popupRect = popupElement.getBoundingClientRect();
63    var availRect;
64    var ownerDoc = popupElement.ownerDocument;
65    var cs = ownerDoc.defaultView.getComputedStyle(popupElement);
66    var docElement = ownerDoc.documentElement;
67
68    if (cs.position == 'fixed') {
69      // For 'fixed' positioned popups, the available rectangle should be based
70      // on the viewport rather than the document.
71      availRect = {
72        height: docElement.clientHeight,
73        width: docElement.clientWidth,
74        top: 0,
75        bottom: docElement.clientHeight,
76        left: 0,
77        right: docElement.clientWidth
78      };
79    } else {
80      availRect = popupElement.offsetParent.getBoundingClientRect();
81    }
82
83    if (cs.direction == 'rtl')
84      opt_invertLeftRight = !opt_invertLeftRight;
85
86    // Flip BEFORE, AFTER based on alignment.
87    if (opt_invertLeftRight) {
88      if (type == AnchorType.BEFORE)
89        type = AnchorType.AFTER;
90      else if (type == AnchorType.AFTER)
91        type = AnchorType.BEFORE;
92    }
93
94    // Flip type based on available size
95    switch (type) {
96      case AnchorType.BELOW:
97        if (anchorRect.bottom + popupRect.height > availRect.height &&
98            popupRect.height <= anchorRect.top) {
99          type = AnchorType.ABOVE;
100        }
101        break;
102      case AnchorType.ABOVE:
103        if (popupRect.height > anchorRect.top &&
104            anchorRect.bottom + popupRect.height <= availRect.height) {
105          type = AnchorType.BELOW;
106        }
107        break;
108      case AnchorType.AFTER:
109        if (anchorRect.right + popupRect.width > availRect.width &&
110            popupRect.width <= anchorRect.left) {
111          type = AnchorType.BEFORE;
112        }
113        break;
114      case AnchorType.BEFORE:
115        if (popupRect.width > anchorRect.left &&
116            anchorRect.right + popupRect.width <= availRect.width) {
117          type = AnchorType.AFTER;
118        }
119        break;
120    }
121    // flipping done
122
123    var style = popupElement.style;
124    // Reset all directions.
125    style.left = style.right = style.top = style.bottom = 'auto';
126
127    // Primary direction
128    switch (type) {
129      case AnchorType.BELOW:
130        if (anchorRect.bottom + popupRect.height <= availRect.height)
131          style.top = anchorRect.bottom + 'px';
132        else
133          style.bottom = '0';
134        break;
135      case AnchorType.ABOVE:
136        if (availRect.height - anchorRect.top >= 0)
137          style.bottom = availRect.height - anchorRect.top + 'px';
138        else
139          style.top = '0';
140        break;
141      case AnchorType.AFTER:
142        if (anchorRect.right + popupRect.width <= availRect.width)
143          style.left = anchorRect.right + 'px';
144        else
145          style.right = '0';
146        break;
147      case AnchorType.BEFORE:
148        if (availRect.width - anchorRect.left >= 0)
149          style.right = availRect.width - anchorRect.left + 'px';
150        else
151          style.left = '0';
152        break;
153    }
154
155    // Secondary direction
156    switch (type) {
157      case AnchorType.BELOW:
158      case AnchorType.ABOVE:
159        if (opt_invertLeftRight) {
160          // align right edges
161          if (anchorRect.right - popupRect.width >= 0) {
162            style.right = availRect.width - anchorRect.right + 'px';
163
164          // align left edges
165          } else if (anchorRect.left + popupRect.width <= availRect.width) {
166            style.left = anchorRect.left + 'px';
167
168          // not enough room on either side
169          } else {
170            style.right = '0';
171          }
172        } else {
173          // align left edges
174          if (anchorRect.left + popupRect.width <= availRect.width) {
175            style.left = anchorRect.left + 'px';
176
177          // align right edges
178          } else if (anchorRect.right - popupRect.width >= 0) {
179            style.right = availRect.width - anchorRect.right + 'px';
180
181          // not enough room on either side
182          } else {
183            style.left = '0';
184          }
185        }
186        break;
187
188      case AnchorType.AFTER:
189      case AnchorType.BEFORE:
190        // align top edges
191        if (anchorRect.top + popupRect.height <= availRect.height) {
192          style.top = anchorRect.top + 'px';
193
194        // align bottom edges
195        } else if (anchorRect.bottom - popupRect.height >= 0) {
196          style.bottom = availRect.height - anchorRect.bottom + 'px';
197
198          // not enough room on either side
199        } else {
200          style.top = '0';
201        }
202        break;
203    }
204  }
205
206  /**
207   * Positions a popup element relative to an anchor element. The popup element
208   * should have position set to absolute and it should be a child of the body
209   * element.
210   * @param {!HTMLElement} anchorElement The element that the popup is anchored
211   *     to.
212   * @param {!HTMLElement} popupElement The popup element we are positioning.
213   * @param {cr.ui.AnchorType} type The type of anchoring we want.
214   * @param {boolean=} opt_invertLeftRight Whether to invert the right/left
215   *     alignment.
216   */
217  function positionPopupAroundElement(anchorElement, popupElement, type,
218                                      opt_invertLeftRight) {
219    var anchorRect = anchorElement.getBoundingClientRect();
220    positionPopupAroundRect(anchorRect, popupElement, type,
221                            !!opt_invertLeftRight);
222  }
223
224  /**
225   * Positions a popup around a point.
226   * @param {number} x The client x position.
227   * @param {number} y The client y position.
228   * @param {!HTMLElement} popupElement The popup element we are positioning.
229   */
230  function positionPopupAtPoint(x, y, popupElement) {
231    var rect = {
232      left: x,
233      top: y,
234      width: 0,
235      height: 0,
236      right: x,
237      bottom: y
238    };
239    positionPopupAroundRect(rect, popupElement, AnchorType.BELOW);
240  }
241
242  // Export
243  return {
244    positionPopupAroundElement: positionPopupAroundElement,
245    positionPopupAtPoint: positionPopupAtPoint
246  };
247});
248