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