modalpopup.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751
  1. // Copyright 2011 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Class for showing simple modal popup.
  16. * @author chrishenry@google.com (Chris Henry)
  17. */
  18. goog.provide('goog.ui.ModalPopup');
  19. goog.require('goog.Timer');
  20. goog.require('goog.asserts');
  21. goog.require('goog.dom');
  22. goog.require('goog.dom.TagName');
  23. goog.require('goog.dom.animationFrame');
  24. goog.require('goog.dom.classlist');
  25. goog.require('goog.dom.iframe');
  26. goog.require('goog.events');
  27. goog.require('goog.events.EventType');
  28. goog.require('goog.events.FocusHandler');
  29. goog.require('goog.fx.Transition');
  30. goog.require('goog.string');
  31. goog.require('goog.style');
  32. goog.require('goog.ui.Component');
  33. goog.require('goog.ui.ModalAriaVisibilityHelper');
  34. goog.require('goog.ui.PopupBase');
  35. goog.require('goog.userAgent');
  36. /**
  37. * Base class for modal popup UI components. This can also be used as
  38. * a standalone component to render a modal popup with an empty div.
  39. *
  40. * WARNING: goog.ui.ModalPopup is only guaranteed to work when it is rendered
  41. * directly in the 'body' element.
  42. *
  43. * The Html structure of the modal popup is:
  44. * <pre>
  45. * Element Function Class-name, goog-modalpopup = default
  46. * ----------------------------------------------------------------------------
  47. * - iframe Iframe mask goog-modalpopup-bg
  48. * - div Background mask goog-modalpopup-bg
  49. * - div Modal popup area goog-modalpopup
  50. * - span Tab catcher
  51. * </pre>
  52. * @constructor
  53. * @param {boolean=} opt_useIframeMask Work around windowed controls z-index
  54. * issue by using an iframe instead of a div for bg element.
  55. * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper; see {@link
  56. * goog.ui.Component} for semantics.
  57. * @extends {goog.ui.Component}
  58. */
  59. goog.ui.ModalPopup = function(opt_useIframeMask, opt_domHelper) {
  60. goog.ui.ModalPopup.base(this, 'constructor', opt_domHelper);
  61. /**
  62. * Whether the modal popup should use an iframe as the background
  63. * element to work around z-order issues.
  64. * @type {boolean}
  65. * @private
  66. */
  67. this.useIframeMask_ = !!opt_useIframeMask;
  68. /**
  69. * The element that had focus before the popup was displayed.
  70. * @type {Element}
  71. * @private
  72. */
  73. this.lastFocus_ = null;
  74. /**
  75. * The animation task that resizes the background, scheduled to run in the
  76. * next animation frame.
  77. * @type {function(...?)}
  78. * @private
  79. */
  80. this.resizeBackgroundTask_ = goog.dom.animationFrame.createTask(
  81. {mutate: this.resizeBackground_}, this);
  82. };
  83. goog.inherits(goog.ui.ModalPopup, goog.ui.Component);
  84. goog.tagUnsealableClass(goog.ui.ModalPopup);
  85. /**
  86. * Focus handler. It will be initialized in enterDocument.
  87. * @type {goog.events.FocusHandler}
  88. * @private
  89. */
  90. goog.ui.ModalPopup.prototype.focusHandler_ = null;
  91. /**
  92. * Whether the modal popup is visible.
  93. * @type {boolean}
  94. * @private
  95. */
  96. goog.ui.ModalPopup.prototype.visible_ = false;
  97. /**
  98. * Element for the background which obscures the UI and blocks events.
  99. * @type {Element}
  100. * @private
  101. */
  102. goog.ui.ModalPopup.prototype.bgEl_ = null;
  103. /**
  104. * Iframe element that is only used for IE as a workaround to keep select-type
  105. * elements from burning through background.
  106. * @type {Element}
  107. * @private
  108. */
  109. goog.ui.ModalPopup.prototype.bgIframeEl_ = null;
  110. /**
  111. * Element used to catch focus and prevent the user from tabbing out
  112. * of the popup.
  113. * @type {Element}
  114. * @private
  115. */
  116. goog.ui.ModalPopup.prototype.tabCatcherElement_ = null;
  117. /**
  118. * Whether the modal popup is in the process of wrapping focus from the top of
  119. * the popup to the last tabbable element.
  120. * @type {boolean}
  121. * @private
  122. */
  123. goog.ui.ModalPopup.prototype.backwardTabWrapInProgress_ = false;
  124. /**
  125. * Transition to show the popup.
  126. * @type {goog.fx.Transition}
  127. * @private
  128. */
  129. goog.ui.ModalPopup.prototype.popupShowTransition_;
  130. /**
  131. * Transition to hide the popup.
  132. * @type {goog.fx.Transition}
  133. * @private
  134. */
  135. goog.ui.ModalPopup.prototype.popupHideTransition_;
  136. /**
  137. * Transition to show the background.
  138. * @type {goog.fx.Transition}
  139. * @private
  140. */
  141. goog.ui.ModalPopup.prototype.bgShowTransition_;
  142. /**
  143. * Transition to hide the background.
  144. * @type {goog.fx.Transition}
  145. * @private
  146. */
  147. goog.ui.ModalPopup.prototype.bgHideTransition_;
  148. /**
  149. * Helper object to control aria visibility of the rest of the page.
  150. * @type {goog.ui.ModalAriaVisibilityHelper}
  151. * @private
  152. */
  153. goog.ui.ModalPopup.prototype.modalAriaVisibilityHelper_;
  154. /**
  155. * @return {string} Base CSS class for this component.
  156. * @protected
  157. */
  158. goog.ui.ModalPopup.prototype.getCssClass = function() {
  159. return goog.getCssName('goog-modalpopup');
  160. };
  161. /**
  162. * Returns the background iframe mask element, if any.
  163. * @return {Element} The background iframe mask element, may return
  164. * null/undefined if the modal popup does not use iframe mask.
  165. */
  166. goog.ui.ModalPopup.prototype.getBackgroundIframe = function() {
  167. return this.bgIframeEl_;
  168. };
  169. /**
  170. * Returns the background mask element.
  171. * @return {Element} The background mask element.
  172. */
  173. goog.ui.ModalPopup.prototype.getBackgroundElement = function() {
  174. return this.bgEl_;
  175. };
  176. /**
  177. * Creates the initial DOM representation for the modal popup.
  178. * @override
  179. */
  180. goog.ui.ModalPopup.prototype.createDom = function() {
  181. // Create the modal popup element, and make sure it's hidden.
  182. goog.ui.ModalPopup.base(this, 'createDom');
  183. var element = this.getElement();
  184. goog.asserts.assert(element);
  185. var allClasses = goog.string.trim(this.getCssClass()).split(' ');
  186. goog.dom.classlist.addAll(element, allClasses);
  187. goog.dom.setFocusableTabIndex(element, true);
  188. goog.style.setElementShown(element, false);
  189. // Manages the DOM for background mask elements.
  190. this.manageBackgroundDom_();
  191. this.createTabCatcher_();
  192. };
  193. /**
  194. * Creates and disposes of the DOM for background mask elements.
  195. * @private
  196. */
  197. goog.ui.ModalPopup.prototype.manageBackgroundDom_ = function() {
  198. if (this.useIframeMask_ && !this.bgIframeEl_) {
  199. // IE renders the iframe on top of the select elements while still
  200. // respecting the z-index of the other elements on the page. See
  201. // http://support.microsoft.com/kb/177378 for more information.
  202. // Flash and other controls behave in similar ways for other browsers
  203. this.bgIframeEl_ = goog.dom.iframe.createBlank(this.getDomHelper());
  204. this.bgIframeEl_.className = goog.getCssName(this.getCssClass(), 'bg');
  205. goog.style.setElementShown(this.bgIframeEl_, false);
  206. goog.style.setOpacity(this.bgIframeEl_, 0);
  207. }
  208. // Create the backgound mask, initialize its opacity, and make sure it's
  209. // hidden.
  210. if (!this.bgEl_) {
  211. this.bgEl_ = this.getDomHelper().createDom(
  212. goog.dom.TagName.DIV, goog.getCssName(this.getCssClass(), 'bg'));
  213. goog.style.setElementShown(this.bgEl_, false);
  214. }
  215. };
  216. /**
  217. * Creates the tab catcher element.
  218. * @private
  219. */
  220. goog.ui.ModalPopup.prototype.createTabCatcher_ = function() {
  221. // Creates tab catcher element.
  222. if (!this.tabCatcherElement_) {
  223. this.tabCatcherElement_ =
  224. this.getDomHelper().createElement(goog.dom.TagName.SPAN);
  225. goog.style.setElementShown(this.tabCatcherElement_, false);
  226. goog.dom.setFocusableTabIndex(this.tabCatcherElement_, true);
  227. this.tabCatcherElement_.style.position = 'absolute';
  228. }
  229. };
  230. /**
  231. * Allow a shift-tab from the top of the modal popup to the last tabbable
  232. * element by moving focus to the tab catcher. This should be called after
  233. * catching a wrapping shift-tab event and before allowing it to propagate, so
  234. * that focus will land on the last tabbable element before the tab catcher.
  235. * @protected
  236. */
  237. goog.ui.ModalPopup.prototype.setupBackwardTabWrap = function() {
  238. this.backwardTabWrapInProgress_ = true;
  239. try {
  240. this.tabCatcherElement_.focus();
  241. } catch (e) {
  242. // Swallow this. IE can throw an error if the element can not be focused.
  243. }
  244. // Reset the flag on a timer in case anything goes wrong with the followup
  245. // event.
  246. goog.Timer.callOnce(this.resetBackwardTabWrap_, 0, this);
  247. };
  248. /**
  249. * Resets the backward tab wrap flag.
  250. * @private
  251. */
  252. goog.ui.ModalPopup.prototype.resetBackwardTabWrap_ = function() {
  253. this.backwardTabWrapInProgress_ = false;
  254. };
  255. /**
  256. * Renders the background mask.
  257. * @private
  258. */
  259. goog.ui.ModalPopup.prototype.renderBackground_ = function() {
  260. goog.asserts.assert(!!this.bgEl_, 'Background element must not be null.');
  261. if (this.bgIframeEl_) {
  262. goog.dom.insertSiblingBefore(this.bgIframeEl_, this.getElement());
  263. }
  264. goog.dom.insertSiblingBefore(this.bgEl_, this.getElement());
  265. };
  266. /** @override */
  267. goog.ui.ModalPopup.prototype.canDecorate = function(element) {
  268. // Assume we can decorate any DIV.
  269. return !!element && element.tagName == goog.dom.TagName.DIV;
  270. };
  271. /** @override */
  272. goog.ui.ModalPopup.prototype.decorateInternal = function(element) {
  273. // Decorate the modal popup area element.
  274. goog.ui.ModalPopup.base(this, 'decorateInternal', element);
  275. var allClasses = goog.string.trim(this.getCssClass()).split(' ');
  276. goog.dom.classlist.addAll(goog.asserts.assert(this.getElement()), allClasses);
  277. // Create the background mask...
  278. this.manageBackgroundDom_();
  279. this.createTabCatcher_();
  280. // Make sure the decorated modal popup is focusable and hidden.
  281. goog.dom.setFocusableTabIndex(this.getElement(), true);
  282. goog.style.setElementShown(this.getElement(), false);
  283. };
  284. /** @override */
  285. goog.ui.ModalPopup.prototype.enterDocument = function() {
  286. this.renderBackground_();
  287. goog.ui.ModalPopup.base(this, 'enterDocument');
  288. goog.dom.insertSiblingAfter(this.tabCatcherElement_, this.getElement());
  289. this.focusHandler_ =
  290. new goog.events.FocusHandler(this.getDomHelper().getDocument());
  291. // We need to watch the entire document so that we can detect when the
  292. // focus is moved out of this modal popup.
  293. this.getHandler().listen(
  294. this.focusHandler_, goog.events.FocusHandler.EventType.FOCUSIN,
  295. this.onFocus);
  296. this.setA11YDetectBackground(false);
  297. };
  298. /** @override */
  299. goog.ui.ModalPopup.prototype.exitDocument = function() {
  300. if (this.isVisible()) {
  301. this.setVisible(false);
  302. }
  303. goog.dispose(this.focusHandler_);
  304. goog.ui.ModalPopup.base(this, 'exitDocument');
  305. goog.dom.removeNode(this.bgIframeEl_);
  306. goog.dom.removeNode(this.bgEl_);
  307. goog.dom.removeNode(this.tabCatcherElement_);
  308. };
  309. /**
  310. * Sets the visibility of the modal popup box and focus to the popup.
  311. * @param {boolean} visible Whether the modal popup should be visible.
  312. */
  313. goog.ui.ModalPopup.prototype.setVisible = function(visible) {
  314. goog.asserts.assert(
  315. this.isInDocument(), 'ModalPopup must be rendered first.');
  316. if (visible == this.visible_) {
  317. return;
  318. }
  319. if (this.popupShowTransition_) this.popupShowTransition_.stop();
  320. if (this.bgShowTransition_) this.bgShowTransition_.stop();
  321. if (this.popupHideTransition_) this.popupHideTransition_.stop();
  322. if (this.bgHideTransition_) this.bgHideTransition_.stop();
  323. if (this.isInDocument()) {
  324. this.setA11YDetectBackground(visible);
  325. }
  326. if (visible) {
  327. this.show_();
  328. } else {
  329. this.hide_();
  330. }
  331. };
  332. /**
  333. * Sets aria-hidden on the rest of the page to restrict screen reader focus.
  334. * Top-level elements with an explicit aria-hidden state are not altered.
  335. * @param {boolean} hide Whether to hide or show the rest of the page.
  336. * @protected
  337. */
  338. goog.ui.ModalPopup.prototype.setA11YDetectBackground = function(hide) {
  339. if (!this.modalAriaVisibilityHelper_) {
  340. this.modalAriaVisibilityHelper_ = new goog.ui.ModalAriaVisibilityHelper(
  341. this.getElementStrict(), this.dom_);
  342. }
  343. this.modalAriaVisibilityHelper_.setBackgroundVisibility(hide);
  344. };
  345. /**
  346. * Sets the transitions to show and hide the popup and background.
  347. * @param {!goog.fx.Transition} popupShowTransition Transition to show the
  348. * popup.
  349. * @param {!goog.fx.Transition} popupHideTransition Transition to hide the
  350. * popup.
  351. * @param {!goog.fx.Transition} bgShowTransition Transition to show
  352. * the background.
  353. * @param {!goog.fx.Transition} bgHideTransition Transition to hide
  354. * the background.
  355. */
  356. goog.ui.ModalPopup.prototype.setTransition = function(
  357. popupShowTransition, popupHideTransition, bgShowTransition,
  358. bgHideTransition) {
  359. this.popupShowTransition_ = popupShowTransition;
  360. this.popupHideTransition_ = popupHideTransition;
  361. this.bgShowTransition_ = bgShowTransition;
  362. this.bgHideTransition_ = bgHideTransition;
  363. };
  364. /**
  365. * Shows the popup.
  366. * @private
  367. */
  368. goog.ui.ModalPopup.prototype.show_ = function() {
  369. if (!this.dispatchEvent(goog.ui.PopupBase.EventType.BEFORE_SHOW)) {
  370. return;
  371. }
  372. try {
  373. this.lastFocus_ = this.getDomHelper().getDocument().activeElement;
  374. } catch (e) {
  375. // Focus-related actions often throw exceptions.
  376. // Sample past issue: https://bugzilla.mozilla.org/show_bug.cgi?id=656283
  377. }
  378. this.resizeBackground_();
  379. this.reposition();
  380. // Listen for keyboard and resize events while the modal popup is visible.
  381. this.getHandler()
  382. .listen(
  383. this.getDomHelper().getWindow(), goog.events.EventType.RESIZE,
  384. this.resizeBackground_)
  385. .listen(
  386. this.getDomHelper().getWindow(),
  387. goog.events.EventType.ORIENTATIONCHANGE, this.resizeBackgroundTask_);
  388. this.showPopupElement_(true);
  389. this.focus();
  390. this.visible_ = true;
  391. if (this.popupShowTransition_ && this.bgShowTransition_) {
  392. goog.events.listenOnce(
  393. /** @type {!goog.events.EventTarget} */ (this.popupShowTransition_),
  394. goog.fx.Transition.EventType.END, this.onShow, false, this);
  395. this.bgShowTransition_.play();
  396. this.popupShowTransition_.play();
  397. } else {
  398. this.onShow();
  399. }
  400. };
  401. /**
  402. * Hides the popup.
  403. * @private
  404. */
  405. goog.ui.ModalPopup.prototype.hide_ = function() {
  406. if (!this.dispatchEvent(goog.ui.PopupBase.EventType.BEFORE_HIDE)) {
  407. return;
  408. }
  409. // Stop listening for keyboard and resize events while the modal
  410. // popup is hidden.
  411. this.getHandler()
  412. .unlisten(
  413. this.getDomHelper().getWindow(), goog.events.EventType.RESIZE,
  414. this.resizeBackground_)
  415. .unlisten(
  416. this.getDomHelper().getWindow(),
  417. goog.events.EventType.ORIENTATIONCHANGE, this.resizeBackgroundTask_);
  418. // Set visibility to hidden even if there is a transition. This
  419. // reduces complexity in subclasses who may want to override
  420. // setVisible (such as goog.ui.Dialog).
  421. this.visible_ = false;
  422. if (this.popupHideTransition_ && this.bgHideTransition_) {
  423. goog.events.listenOnce(
  424. /** @type {!goog.events.EventTarget} */ (this.popupHideTransition_),
  425. goog.fx.Transition.EventType.END, this.onHide, false, this);
  426. this.bgHideTransition_.play();
  427. // The transition whose END event you are listening to must be played last
  428. // to prevent errors when disposing on hide event, which occur on browsers
  429. // that do not support CSS3 transitions.
  430. this.popupHideTransition_.play();
  431. } else {
  432. this.onHide();
  433. }
  434. this.returnFocus_();
  435. };
  436. /**
  437. * Attempts to return the focus back to the element that had it before the popup
  438. * was opened.
  439. * @private
  440. */
  441. goog.ui.ModalPopup.prototype.returnFocus_ = function() {
  442. try {
  443. var dom = this.getDomHelper();
  444. var body = dom.getDocument().body;
  445. var active = dom.getDocument().activeElement || body;
  446. if (!this.lastFocus_ || this.lastFocus_ == body) {
  447. this.lastFocus_ = null;
  448. return;
  449. }
  450. // We only want to move the focus if we actually have it, i.e.:
  451. // - if we immediately hid the popup the focus should have moved to the
  452. // body element
  453. // - if there is a hiding transition in progress the focus would still be
  454. // within the dialog and it is safe to move it if the current focused
  455. // element is a child of the dialog
  456. if (active == body || dom.contains(this.getElement(), active)) {
  457. this.lastFocus_.focus();
  458. }
  459. } catch (e) {
  460. // Swallow this. IE can throw an error if the element can not be focused.
  461. }
  462. // Explicitly want to null this out even if there was an error focusing to
  463. // avoid bleed over between dialog invocations.
  464. this.lastFocus_ = null;
  465. };
  466. /**
  467. * Shows or hides the popup element.
  468. * @param {boolean} visible Shows the popup element if true, hides if false.
  469. * @private
  470. */
  471. goog.ui.ModalPopup.prototype.showPopupElement_ = function(visible) {
  472. if (this.bgIframeEl_) {
  473. goog.style.setElementShown(this.bgIframeEl_, visible);
  474. }
  475. if (this.bgEl_) {
  476. goog.style.setElementShown(this.bgEl_, visible);
  477. }
  478. goog.style.setElementShown(this.getElement(), visible);
  479. goog.style.setElementShown(this.tabCatcherElement_, visible);
  480. };
  481. /**
  482. * Called after the popup is shown. If there is a transition, this
  483. * will be called after the transition completed or stopped.
  484. * @protected
  485. */
  486. goog.ui.ModalPopup.prototype.onShow = function() {
  487. this.dispatchEvent(goog.ui.PopupBase.EventType.SHOW);
  488. };
  489. /**
  490. * Called after the popup is hidden. If there is a transition, this
  491. * will be called after the transition completed or stopped.
  492. * @protected
  493. */
  494. goog.ui.ModalPopup.prototype.onHide = function() {
  495. this.showPopupElement_(false);
  496. this.dispatchEvent(goog.ui.PopupBase.EventType.HIDE);
  497. };
  498. /**
  499. * @return {boolean} Whether the modal popup is visible.
  500. */
  501. goog.ui.ModalPopup.prototype.isVisible = function() {
  502. return this.visible_;
  503. };
  504. /**
  505. * Focuses on the modal popup.
  506. */
  507. goog.ui.ModalPopup.prototype.focus = function() {
  508. this.focusElement_();
  509. };
  510. /**
  511. * Make the background element the size of the document.
  512. *
  513. * NOTE(user): We must hide the background element before measuring the
  514. * document, otherwise the size of the background will stop the document from
  515. * shrinking to fit a smaller window. This does cause a slight flicker in Linux
  516. * browsers, but should not be a common scenario.
  517. * @private
  518. */
  519. goog.ui.ModalPopup.prototype.resizeBackground_ = function() {
  520. if (this.bgIframeEl_) {
  521. goog.style.setElementShown(this.bgIframeEl_, false);
  522. }
  523. if (this.bgEl_) {
  524. goog.style.setElementShown(this.bgEl_, false);
  525. }
  526. var doc = this.getDomHelper().getDocument();
  527. var win = goog.dom.getWindow(doc) || window;
  528. // Take the max of document height and view height, in case the document does
  529. // not fill the viewport. Read from both the body element and the html element
  530. // to account for browser differences in treatment of absolutely-positioned
  531. // content.
  532. var viewSize = goog.dom.getViewportSize(win);
  533. var w = Math.max(
  534. viewSize.width,
  535. Math.max(doc.body.scrollWidth, doc.documentElement.scrollWidth));
  536. var h = Math.max(
  537. viewSize.height,
  538. Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight));
  539. if (this.bgIframeEl_) {
  540. goog.style.setElementShown(this.bgIframeEl_, true);
  541. goog.style.setSize(this.bgIframeEl_, w, h);
  542. }
  543. if (this.bgEl_) {
  544. goog.style.setElementShown(this.bgEl_, true);
  545. goog.style.setSize(this.bgEl_, w, h);
  546. }
  547. };
  548. /**
  549. * Centers the modal popup in the viewport, taking scrolling into account.
  550. */
  551. goog.ui.ModalPopup.prototype.reposition = function() {
  552. // TODO(chrishenry): Make this use goog.positioning as in goog.ui.PopupBase?
  553. // Get the current viewport to obtain the scroll offset.
  554. var doc = this.getDomHelper().getDocument();
  555. var win = goog.dom.getWindow(doc) || window;
  556. if (goog.style.getComputedPosition(this.getElement()) == 'fixed') {
  557. var x = 0;
  558. var y = 0;
  559. } else {
  560. var scroll = this.getDomHelper().getDocumentScroll();
  561. var x = scroll.x;
  562. var y = scroll.y;
  563. }
  564. var popupSize = goog.style.getSize(this.getElement());
  565. var viewSize = goog.dom.getViewportSize(win);
  566. // Make sure left and top are non-negatives.
  567. var left = Math.max(x + viewSize.width / 2 - popupSize.width / 2, 0);
  568. var top = Math.max(y + viewSize.height / 2 - popupSize.height / 2, 0);
  569. goog.style.setPosition(this.getElement(), left, top);
  570. // We place the tab catcher at the same position as the dialog to
  571. // prevent IE from scrolling when users try to tab out of the dialog.
  572. goog.style.setPosition(this.tabCatcherElement_, left, top);
  573. };
  574. /**
  575. * Handles focus events. Makes sure that if the user tabs past the
  576. * elements in the modal popup, the focus wraps back to the beginning, and that
  577. * if the user shift-tabs past the front of the modal popup, focus wraps around
  578. * to the end.
  579. * @param {goog.events.BrowserEvent} e Browser's event object.
  580. * @protected
  581. */
  582. goog.ui.ModalPopup.prototype.onFocus = function(e) {
  583. if (this.backwardTabWrapInProgress_) {
  584. this.resetBackwardTabWrap_();
  585. } else if (e.target == this.tabCatcherElement_) {
  586. goog.Timer.callOnce(this.focusElement_, 0, this);
  587. }
  588. };
  589. /**
  590. * Returns the magic tab catcher element used to detect when the user has
  591. * rolled focus off of the popup content. It is automatically created during
  592. * the createDom method() and can be used by subclasses to implement custom
  593. * tab-loop behavior.
  594. * @return {Element} The tab catcher element.
  595. * @protected
  596. */
  597. goog.ui.ModalPopup.prototype.getTabCatcherElement = function() {
  598. return this.tabCatcherElement_;
  599. };
  600. /**
  601. * Moves the focus to the modal popup.
  602. * @private
  603. */
  604. goog.ui.ModalPopup.prototype.focusElement_ = function() {
  605. try {
  606. if (goog.userAgent.IE) {
  607. // In IE, we must first focus on the body or else focussing on a
  608. // sub-element will not work.
  609. this.getDomHelper().getDocument().body.focus();
  610. }
  611. this.getElement().focus();
  612. } catch (e) {
  613. // Swallow this. IE can throw an error if the element can not be focused.
  614. }
  615. };
  616. /** @override */
  617. goog.ui.ModalPopup.prototype.disposeInternal = function() {
  618. goog.dispose(this.popupShowTransition_);
  619. this.popupShowTransition_ = null;
  620. goog.dispose(this.popupHideTransition_);
  621. this.popupHideTransition_ = null;
  622. goog.dispose(this.bgShowTransition_);
  623. this.bgShowTransition_ = null;
  624. goog.dispose(this.bgHideTransition_);
  625. this.bgHideTransition_ = null;
  626. goog.ui.ModalPopup.base(this, 'disposeInternal');
  627. };