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