diff options
author | arv@chromium.org <arv@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-05-13 21:56:54 +0000 |
---|---|---|
committer | arv@chromium.org <arv@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-05-13 21:56:54 +0000 |
commit | 4dc955eb16d38e575aa787ff5b699f1ed6102bf8 (patch) | |
tree | 68d8ee1dc158c63eebb836f104ccb6ca5bdc5e88 /chrome | |
parent | 271eb663e75b2ee3f119b0a719a6ca156175ed57 (diff) | |
download | chromium_src-4dc955eb16d38e575aa787ff5b699f1ed6102bf8.zip chromium_src-4dc955eb16d38e575aa787ff5b699f1ed6102bf8.tar.gz chromium_src-4dc955eb16d38e575aa787ff5b699f1ed6102bf8.tar.bz2 |
Add smarter popup position code to DOM UI Shared Resources.
BUG=None
TEST=js unit tests as well as manually ensuring the menu button in the bookmarks manager flips side when the window gets small.
Review URL: http://codereview.chromium.org/2042017
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@47198 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome')
6 files changed, 506 insertions, 45 deletions
diff --git a/chrome/browser/resources/bookmark_manager/main.html b/chrome/browser/resources/bookmark_manager/main.html index 6630aa9..04d4cc2 100644 --- a/chrome/browser/resources/bookmark_manager/main.html +++ b/chrome/browser/resources/bookmark_manager/main.html @@ -31,6 +31,7 @@ found in the LICENSE file. <script src="chrome://resources/js/cr/ui/command.js"></script> <script src="chrome://resources/js/cr/ui/menu_item.js"></script> <script src="chrome://resources/js/cr/ui/menu.js"></script> +<script src="chrome://resources/js/cr/ui/position_util.js"></script> <script src="chrome://resources/js/cr/ui/menu_button.js"></script> <script src="chrome://resources/js/cr/ui/context_menu_handler.js"></script> diff --git a/chrome/browser/resources/shared/js/cr/ui/context_menu_handler.js b/chrome/browser/resources/shared/js/cr/ui/context_menu_handler.js index 2d96a4c..6b86808 100644 --- a/chrome/browser/resources/shared/js/cr/ui/context_menu_handler.js +++ b/chrome/browser/resources/shared/js/cr/ui/context_menu_handler.js @@ -4,6 +4,8 @@ cr.define('cr.ui', function() { + const positionPopupAtPoint = cr.ui.positionPopupAtPoint; + /** * Handles context menus. * @constructor @@ -86,37 +88,7 @@ cr.define('cr.ui', function() { y = e.clientY; } - var menuRect = menu.getBoundingClientRect(); - var bodyRect = menu.ownerDocument.body.getBoundingClientRect(); - - // Does menu fit below? - if (y + menuRect.height > bodyRect.height) { - // Does menu fit above? - if (y - menuRect.height >= 0) { - y -= menuRect.height; - } else { - // Menu did not fit above nor below. - y = 0; - // We could resize the menu here but lets not worry about that at this - // point. - } - } - - // Does menu fit to the right? - if (x + menuRect.width > bodyRect.width) { - // Does menu fit to the left? - if (x - menuRect.width >= 0) { - x -= menuRect.width; - } else { - // Menu did not fit to the right nor to the left. - x = 0; - // We could resize the menu here but lets not worry about that at this - // point. - } - } - - menu.style.left = x + 'px'; - menu.style.top = y + 'px'; + positionPopupAtPoint(x, y, menu); }, /** diff --git a/chrome/browser/resources/shared/js/cr/ui/menu_button.js b/chrome/browser/resources/shared/js/cr/ui/menu_button.js index cd8defb..b44ed6c 100644 --- a/chrome/browser/resources/shared/js/cr/ui/menu_button.js +++ b/chrome/browser/resources/shared/js/cr/ui/menu_button.js @@ -4,6 +4,7 @@ cr.define('cr.ui', function() { const Menu = cr.ui.Menu; + const positionPopupAroundElement = cr.ui.positionPopupAroundElement; /** * Creates a new menu button element. @@ -86,6 +87,7 @@ cr.define('cr.ui', function() { case 'activate': case 'blur': + case 'resize': this.hideMenu(); break; } @@ -96,10 +98,14 @@ cr.define('cr.ui', function() { */ showMenu: function() { this.menu.style.display = 'block'; + // when the menu is shown we steal all keyboard events. - this.ownerDocument.addEventListener('keydown', this, true); - this.ownerDocument.addEventListener('mousedown', this, true); - this.ownerDocument.addEventListener('blur', this, true); + var doc = this.ownerDocument; + var win = doc.defaultView; + doc.addEventListener('keydown', this, true); + doc.addEventListener('mousedown', this, true); + doc.addEventListener('blur', this, true); + win.addEventListener('resize', this); this.menu.addEventListener('activate', this); this.positionMenu_(); }, @@ -109,9 +115,12 @@ cr.define('cr.ui', function() { */ hideMenu: function() { this.menu.style.display = 'none'; - this.ownerDocument.removeEventListener('keydown', this, true); - this.ownerDocument.removeEventListener('mousedown', this, true); - this.ownerDocument.removeEventListener('blur', this, true); + var doc = this.ownerDocument; + var win = doc.defaultView; + doc.removeEventListener('keydown', this, true); + doc.removeEventListener('mousedown', this, true); + doc.removeEventListener('blur', this, true); + win.removeEventListener('resize', this); this.menu.removeEventListener('activate', this); this.menu.selectedIndex = -1; }, @@ -129,14 +138,7 @@ cr.define('cr.ui', function() { * @private */ positionMenu_: function() { - var buttonRect = this.getBoundingClientRect(); - this.menu.style.top = buttonRect.bottom + 'px'; - if (getComputedStyle(this).direction == 'rtl') { - var menuRect = this.menu.getBoundingClientRect(); - this.menu.style.left = buttonRect.right - menuRect.width + 'px'; - } else { - this.menu.style.left = buttonRect.left + 'px'; - } + positionPopupAroundElement(this, this.menu, cr.ui.AnchorType.BELOW); }, /** diff --git a/chrome/browser/resources/shared/js/cr/ui/position_util.js b/chrome/browser/resources/shared/js/cr/ui/position_util.js new file mode 100644 index 0000000..bc96f49 --- /dev/null +++ b/chrome/browser/resources/shared/js/cr/ui/position_util.js @@ -0,0 +1,203 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview This file provides utility functions for position popups. + */ + +cr.define('cr.ui', function() { + + /** + * Type def for rects as returned by getBoundingClientRect. + * @typedef { {left: number, top: number, width: number, height: number, + * right: number, bottom: number}} + */ + var Rect; + + /** + * Enum for defining how to anchor a popup to an anchor element. + * @enum {number} + */ + const AnchorType = { + /** + * The popop's right edge is aligned with the left edge of the anchor. + * The popup's top edge is aligned with the top edge of the anchor's top + * edge. + */ + BEFORE: 1, // p: right, a: left, p: top, a: top + + /** + * The popop's left edge is aligned with the right edge of the anchor. + * The popup's top edge is aligned with the top edge of the anchor's top + * edge. + */ + AFTER: 2, // p: left a: right, p: top, a: top + + /** + * The popop's bottom edge is aligned with the top edge of the anchor. + * The popup's left edge is aligned with the left edge of the anchor's top + * edge. + */ + ABOVE: 3, // p: bottom, a: top, p: left, a: left + + /** + * The popop's top edge is aligned with the bottom edge of the anchor. + * The popup's left edge is aligned with the left edge of the anchor's top + * edge. + */ + BELOW: 4 // p: top, a: bottom, p: left, a: left + }; + + /** + * Helper function for positionPopupAroundElement and positionPopupAroundRect. + * @param {!Rect} anchorRect The rect for the anchor. + * @param {!HTMLElement} popupElement The element used for the popup. + * @param {AnchorType} type The type of anchoring to do. + */ + function positionPopupAroundRect(anchorRect, popupElement, type) { + var popupRect = popupElement.getBoundingClientRect(); + var popupContainer = popupElement.offsetParent; + var availRect = popupContainer.getBoundingClientRect(); + var rtl = popupElement.ownerDocument.defaultView. + getComputedStyle(popupElement).direction == 'rtl'; + + // Flip BEFORE, AFTER based on RTL. + if (rtl) { + if (type == AnchorType.BEFORE) + type = AnchorType.AFTER; + else if (type == AnchorType.AFTER) + type = AnchorType.BEFORE; + } + + // Flip type based on available size + switch (type) { + case AnchorType.BELOW: + if (anchorRect.bottom + popupRect.height > availRect.height) + type = AnchorType.ABOVE; + break; + case AnchorType.ABOVE: + if (popupRect.height > anchorRect.top) + type = AnchorType.BELOW; + break; + case AnchorType.AFTER: + if (anchorRect.right + popupRect.width > availRect.width) + type = AnchorType.BEFORE; + break; + case AnchorType.BEFORE: + if (popupRect.width > anchorRect.left) + type = AnchorType.AFTER; + break; + } + // flipping done + + var style = popupElement.style; + // Reset all directions. + style.left = style.right = style.top = style.bottom = 'auto' + + // Primary direction + switch (type) { + case AnchorType.BELOW: + style.top = anchorRect.bottom + 'px'; + break; + case AnchorType.ABOVE: + style.bottom = availRect.height - anchorRect.top + 'px'; + break; + case AnchorType.AFTER: + style.left = anchorRect.right + 'px'; + break; + case AnchorType.BEFORE: + style.right = availRect.width - anchorRect.left + 'px'; + break; + } + + // Secondary direction + switch (type) { + case AnchorType.BELOW: + case AnchorType.ABOVE: + if (rtl) { + // align right edges + if (anchorRect.right - popupRect.width >= 0) { + style.right = availRect.width - anchorRect.right + 'px'; + + // align left edges + } else if (anchorRect.left + popupRect.width <= availRect.width) { + style.left = anchorRect.left + 'px'; + + // not enough room on either side + } else { + style.right = '0'; + } + } else { + // align left edges + if (anchorRect.left + popupRect.width <= availRect.width) { + style.left = anchorRect.left + 'px'; + + // align right edges + } else if (anchorRect.right - popupRect.width >= 0) { + style.right = availRect.width - anchorRect.right + 'px'; + + // not enough room on either side + } else { + style.left = '0'; + } + } + break; + + case AnchorType.AFTER: + case AnchorType.BEFORE: + // align top edges + if (anchorRect.top + popupRect.height <= availRect.height) { + style.top = anchorRect.top + 'px'; + + // align bottom edges + } else if (anchorRect.bottom - popupRect.height >= 0) { + style.bottom = availRect.height - anchorRect.bottom + 'px'; + + // not enough room on either side + } else { + style.top = '0'; + } + break; + } + } + + /** + * Positions a popup element relative to an anchor element. The popup element + * should have position set to absolute and it should be a child of the body + * element. + * @param {!HTMLElement} anchorElement The element that the popup is anchored + * to. + * @param {!HTMLElement} popupElement The popup element we are positioning. + * @param {AnchorType} type The type of anchoring we want. + */ + function positionPopupAroundElement(anchorElement, popupElement, type) { + var anchorRect = anchorElement.getBoundingClientRect(); + positionPopupAroundRect(anchorRect, popupElement, type); + } + + /** + * Positions a popup around a point. + * @param {number} x The client x position. + * @param {number} y The client y position. + * @param {!HTMLElement} popupElement The popup element we are positioning. + */ + function positionPopupAtPoint(x, y, popupElement) { + var rect = { + left: x, + top: y, + width: 0, + height: 0, + right: x, + bottom: y + }; + positionPopupAroundRect(rect, popupElement, AnchorType.BELOW); + } + + // Export + return { + AnchorType: AnchorType, + positionPopupAroundElement: positionPopupAroundElement, + positionPopupAtPoint: positionPopupAtPoint + }; +}); diff --git a/chrome/browser/resources/shared/js/cr/ui/position_util_test.html b/chrome/browser/resources/shared/js/cr/ui/position_util_test.html new file mode 100644 index 0000000..5cf28cf --- /dev/null +++ b/chrome/browser/resources/shared/js/cr/ui/position_util_test.html @@ -0,0 +1,282 @@ +<!DOCTYPE html> +<html> +<head> +<!-- TODO(arv): Check in Closue unit tests and make this run as part of the + tests --> +<script src="http://closure-library.googlecode.com/svn/trunk/closure/goog/base.js"></script> +<script src="../../cr.js"></script> +<script src="position_util.js"></script> +<script> + +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.jsunit'); + +</script> +<style> + +html, body { + margin: 0; + width: 100%; + height: 100%; +} + +#anchor { + position: absolute; + width: 10px; + height: 10px; + background: green; +} + +#popup { + position: absolute; + top: 0; + left: 0; + width: 100px; + height: 100px; + background: red; +} + +</style> +</head> +<body> + +<div id=anchor></div> +<div id=popup></div> + +<script> + +var anchor = document.getElementById('anchor'); +var popup = document.getElementById('popup'); +var anchorParent = anchor.offsetParent; +var pr = new goog.testing.PropertyReplacer; +var availRect; + +function MockRect(w, h) { + this.width = w; + this.height = h; + this.right = this.left + w; + this.bottom = this.top + h; +} +MockRect.prototype = { + left: 0, + top: 0 +}; + +function setUp() { + anchor.style.top = '100px'; + anchor.style.left = '100px'; + availRect = new MockRect(200, 200); + pr.set(anchorParent, 'getBoundingClientRect', function() { + return availRect; + }); +} + +function tearDown() { + document.documentElement.dir = 'ltr'; + pr.reset(); +} + +function testAbovePrimary() { + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.ABOVE); + + assertEquals('auto', popup.style.top); + assertEquals('100px', popup.style.bottom); + + anchor.style.top = '90px'; + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.ABOVE); + assertEquals('100px', popup.style.top); + assertEquals('auto', popup.style.bottom); +} + +function testBelowPrimary() { + // ensure enough below + anchor.style.top = '90px'; + + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.BELOW); + + assertEquals('100px', popup.style.top); + assertEquals('auto', popup.style.bottom); + + // ensure not enough below + anchor.style.top = '100px'; + + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.BELOW); + assertEquals('auto', popup.style.top); + assertEquals('100px', popup.style.bottom); +} + +function testBeforePrimary() { + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.BEFORE); + + assertEquals('auto', popup.style.left); + assertEquals('100px', popup.style.right); + + anchor.style.left = '90px'; + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.BEFORE); + assertEquals('100px', popup.style.left); + assertEquals('auto', popup.style.right); +} + +function testBeforePrimaryRtl() { + document.documentElement.dir = 'rtl'; + + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.AFTER); + + assertEquals('auto', popup.style.left); + assertEquals('100px', popup.style.right); + + anchor.style.left = '90px'; + + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.AFTER); + assertEquals('100px', popup.style.left); + assertEquals('auto', popup.style.right); +} + +function testAfterPrimary() { + // ensure enough to the right + anchor.style.left = '90px'; + + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.AFTER); + + assertEquals('100px', popup.style.left); + assertEquals('auto', popup.style.right); + + // ensure not enough below + anchor.style.left = '100px'; + + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.AFTER); + assertEquals('auto', popup.style.left); + assertEquals('100px', popup.style.right); +} + +function testAfterPrimaryRtl() { + document.documentElement.dir = 'rtl'; + + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.AFTER); + + assertEquals('auto', popup.style.left); + assertEquals('100px', popup.style.right); + + // ensure not enough below + anchor.style.left = '90px'; + + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.AFTER); + assertEquals('100px', popup.style.left); + assertEquals('auto', popup.style.right); +} + +function testAboveSecondary() { + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.ABOVE); + + assertEquals('100px', popup.style.left); + assertEquals('auto', popup.style.right); + + anchor.style.left = '110px'; + + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.ABOVE); + + assertEquals('auto', popup.style.left); + assertEquals('80px', popup.style.right); +} + +function testAboveSecondaryRtl() { + document.documentElement.dir = 'rtl'; + + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.ABOVE); + + assertEquals('auto', popup.style.left); + assertEquals('90px', popup.style.right); + + anchor.style.left = '80px'; + + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.ABOVE); + + assertEquals('80px', popup.style.left); + assertEquals('auto', popup.style.right); +} + +function testBelowSecondary() { + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.BELOW); + + assertEquals('100px', popup.style.left); + assertEquals('auto', popup.style.right); + + anchor.style.left = '110px'; + + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.BELOW); + + assertEquals('auto', popup.style.left); + assertEquals('80px', popup.style.right); +} + +function testBelowSecondaryRtl() { + document.documentElement.dir = 'rtl'; + + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.BELOW); + + assertEquals('auto', popup.style.left); + assertEquals('90px', popup.style.right); + + anchor.style.left = '80px'; + + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.BELOW); + + assertEquals('80px', popup.style.left); + assertEquals('auto', popup.style.right); +} + +function testBeforeSecondary() { + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.BEFORE); + + assertEquals('100px', popup.style.top); + assertEquals('auto', popup.style.bottom); + + anchor.style.top = '110px'; + + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.BEFORE); + + assertEquals('auto', popup.style.top); + assertEquals('80px', popup.style.bottom); +} + +function testAfterSecondary() { + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.AFTER); + + assertEquals('100px', popup.style.top); + assertEquals('auto', popup.style.bottom); + + anchor.style.top = '110px'; + + cr.ui.positionPopupAroundElement(anchor, popup, cr.ui.AnchorType.AFTER); + + assertEquals('auto', popup.style.top); + assertEquals('80px', popup.style.bottom); +} + +function testPositionAtPoint() { + cr.ui.positionPopupAtPoint(100, 100, popup); + + assertEquals('100px', popup.style.left); + assertEquals('100px', popup.style.top); + assertEquals('auto', popup.style.right); + assertEquals('auto', popup.style.bottom); + + cr.ui.positionPopupAtPoint(100, 150, popup); + + assertEquals('100px', popup.style.left); + assertEquals('auto', popup.style.top); + assertEquals('auto', popup.style.right); + assertEquals('50px', popup.style.bottom); + + cr.ui.positionPopupAtPoint(150, 150, popup); + + assertEquals('auto', popup.style.left); + assertEquals('auto', popup.style.top); + assertEquals('50px', popup.style.right); + assertEquals('50px', popup.style.bottom); +} + +</script> + +</body> +</html> diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi index 9fe4472..77087148 100644 --- a/chrome/chrome_browser.gypi +++ b/chrome/chrome_browser.gypi @@ -3273,6 +3273,7 @@ 'browser/resources/shared/js/cr/ui/menu.js', 'browser/resources/shared/js/cr/ui/menu_button.js', 'browser/resources/shared/js/cr/ui/menu_item.js', + 'browser/resources/shared/js/cr/ui/position_util.js', 'browser/resources/shared/js/cr/ui/splitter.js', 'browser/resources/shared/js/cr/ui/tree.js', ] |