bubble.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. // Copyright 2007 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 Definition of the Bubble class.
  16. *
  17. *
  18. * @see ../demos/bubble.html
  19. *
  20. * TODO: support decoration and addChild
  21. */
  22. goog.provide('goog.ui.Bubble');
  23. goog.require('goog.Timer');
  24. goog.require('goog.dom.safe');
  25. goog.require('goog.events');
  26. goog.require('goog.events.EventType');
  27. goog.require('goog.html.SafeHtml');
  28. goog.require('goog.math.Box');
  29. goog.require('goog.positioning');
  30. goog.require('goog.positioning.AbsolutePosition');
  31. goog.require('goog.positioning.AnchoredPosition');
  32. goog.require('goog.positioning.Corner');
  33. goog.require('goog.positioning.CornerBit');
  34. goog.require('goog.string.Const');
  35. goog.require('goog.style');
  36. goog.require('goog.ui.Component');
  37. goog.require('goog.ui.Popup');
  38. goog.scope(function() {
  39. var SafeHtml = goog.html.SafeHtml;
  40. /**
  41. * The Bubble provides a general purpose bubble implementation that can be
  42. * anchored to a particular element and displayed for a period of time.
  43. *
  44. * @param {string|!goog.html.SafeHtml|?Element} message Message or an element
  45. * to display inside the bubble. Strings are treated as plain-text and will
  46. * be HTML escaped.
  47. * @param {Object=} opt_config The configuration
  48. * for the bubble. If not specified, the default configuration will be
  49. * used. {@see goog.ui.Bubble.defaultConfig}.
  50. * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
  51. * @constructor
  52. * @extends {goog.ui.Component}
  53. */
  54. goog.ui.Bubble = function(message, opt_config, opt_domHelper) {
  55. goog.ui.Component.call(this, opt_domHelper);
  56. if (goog.isString(message)) {
  57. message = goog.html.SafeHtml.htmlEscape(message);
  58. }
  59. /**
  60. * The HTML string or element to display inside the bubble.
  61. *
  62. * @type {!goog.html.SafeHtml|Element}
  63. * @private
  64. */
  65. this.message_ = message;
  66. /**
  67. * The Popup element used to position and display the bubble.
  68. *
  69. * @type {goog.ui.Popup}
  70. * @private
  71. */
  72. this.popup_ = new goog.ui.Popup();
  73. /**
  74. * Configuration map that contains bubble's UI elements.
  75. *
  76. * @type {Object}
  77. * @private
  78. */
  79. this.config_ = opt_config || goog.ui.Bubble.defaultConfig;
  80. /**
  81. * Id of the close button for this bubble.
  82. *
  83. * @type {string}
  84. * @private
  85. */
  86. this.closeButtonId_ = this.makeId('cb');
  87. /**
  88. * Id of the div for the embedded element.
  89. *
  90. * @type {string}
  91. * @private
  92. */
  93. this.messageId_ = this.makeId('mi');
  94. };
  95. goog.inherits(goog.ui.Bubble, goog.ui.Component);
  96. goog.tagUnsealableClass(goog.ui.Bubble);
  97. /**
  98. * In milliseconds, timeout after which the button auto-hides. Null means
  99. * infinite.
  100. * @type {?number}
  101. * @private
  102. */
  103. goog.ui.Bubble.prototype.timeout_ = null;
  104. /**
  105. * Key returned by the bubble timer.
  106. * @type {?number}
  107. * @private
  108. */
  109. goog.ui.Bubble.prototype.timerId_ = 0;
  110. /**
  111. * Key returned by the listen function for the close button.
  112. * @type {goog.events.Key}
  113. * @private
  114. */
  115. goog.ui.Bubble.prototype.listener_ = null;
  116. /** @override */
  117. goog.ui.Bubble.prototype.createDom = function() {
  118. goog.ui.Bubble.superClass_.createDom.call(this);
  119. var element = this.getElement();
  120. element.style.position = 'absolute';
  121. element.style.visibility = 'hidden';
  122. this.popup_.setElement(element);
  123. };
  124. /**
  125. * Attaches the bubble to an anchor element. Computes the positioning and
  126. * orientation of the bubble.
  127. *
  128. * @param {Element} anchorElement The element to which we are attaching.
  129. */
  130. goog.ui.Bubble.prototype.attach = function(anchorElement) {
  131. this.setAnchoredPosition_(
  132. anchorElement, this.computePinnedCorner_(anchorElement));
  133. };
  134. /**
  135. * Sets the corner of the bubble to used in the positioning algorithm.
  136. *
  137. * @param {goog.positioning.Corner} corner The bubble corner used for
  138. * positioning constants.
  139. */
  140. goog.ui.Bubble.prototype.setPinnedCorner = function(corner) {
  141. this.popup_.setPinnedCorner(corner);
  142. };
  143. /**
  144. * Sets the position of the bubble. Pass null for corner in AnchoredPosition
  145. * for corner to be computed automatically.
  146. *
  147. * @param {goog.positioning.AbstractPosition} position The position of the
  148. * bubble.
  149. */
  150. goog.ui.Bubble.prototype.setPosition = function(position) {
  151. if (position instanceof goog.positioning.AbsolutePosition) {
  152. this.popup_.setPosition(position);
  153. } else if (position instanceof goog.positioning.AnchoredPosition) {
  154. this.setAnchoredPosition_(position.element, position.corner);
  155. } else {
  156. throw Error('Bubble only supports absolute and anchored positions!');
  157. }
  158. };
  159. /**
  160. * Sets the timeout after which bubble hides itself.
  161. *
  162. * @param {number} timeout Timeout of the bubble.
  163. */
  164. goog.ui.Bubble.prototype.setTimeout = function(timeout) {
  165. this.timeout_ = timeout;
  166. };
  167. /**
  168. * Sets whether the bubble should be automatically hidden whenever user clicks
  169. * outside the bubble element.
  170. *
  171. * @param {boolean} autoHide Whether to hide if user clicks outside the bubble.
  172. */
  173. goog.ui.Bubble.prototype.setAutoHide = function(autoHide) {
  174. this.popup_.setAutoHide(autoHide);
  175. };
  176. /**
  177. * Sets whether the bubble should be visible.
  178. *
  179. * @param {boolean} visible Desired visibility state.
  180. */
  181. goog.ui.Bubble.prototype.setVisible = function(visible) {
  182. if (visible && !this.popup_.isVisible()) {
  183. this.configureElement_();
  184. }
  185. this.popup_.setVisible(visible);
  186. if (!this.popup_.isVisible()) {
  187. this.unconfigureElement_();
  188. }
  189. };
  190. /**
  191. * @return {boolean} Whether the bubble is visible.
  192. */
  193. goog.ui.Bubble.prototype.isVisible = function() {
  194. return this.popup_.isVisible();
  195. };
  196. /** @override */
  197. goog.ui.Bubble.prototype.disposeInternal = function() {
  198. this.unconfigureElement_();
  199. this.popup_.dispose();
  200. this.popup_ = null;
  201. goog.ui.Bubble.superClass_.disposeInternal.call(this);
  202. };
  203. /**
  204. * Creates element's contents and configures all timers. This is called on
  205. * setVisible(true).
  206. * @private
  207. */
  208. goog.ui.Bubble.prototype.configureElement_ = function() {
  209. if (!this.isInDocument()) {
  210. throw Error('You must render the bubble before showing it!');
  211. }
  212. var element = this.getElement();
  213. var corner = this.popup_.getPinnedCorner();
  214. goog.dom.safe.setInnerHtml(
  215. /** @type {!Element} */ (element), this.computeHtmlForCorner_(corner));
  216. if (!(this.message_ instanceof SafeHtml)) {
  217. var messageDiv = this.getDomHelper().getElement(this.messageId_);
  218. this.getDomHelper().appendChild(messageDiv, this.message_);
  219. }
  220. var closeButton = this.getDomHelper().getElement(this.closeButtonId_);
  221. this.listener_ = goog.events.listen(
  222. closeButton, goog.events.EventType.CLICK, this.hideBubble_, false, this);
  223. if (this.timeout_) {
  224. this.timerId_ = goog.Timer.callOnce(this.hideBubble_, this.timeout_, this);
  225. }
  226. };
  227. /**
  228. * Gets rid of the element's contents and all associated timers and listeners.
  229. * This is called on dispose as well as on setVisible(false).
  230. * @private
  231. */
  232. goog.ui.Bubble.prototype.unconfigureElement_ = function() {
  233. if (this.listener_) {
  234. goog.events.unlistenByKey(this.listener_);
  235. this.listener_ = null;
  236. }
  237. if (this.timerId_) {
  238. goog.Timer.clear(this.timerId_);
  239. this.timerId_ = null;
  240. }
  241. var element = this.getElement();
  242. if (element) {
  243. this.getDomHelper().removeChildren(element);
  244. goog.dom.safe.setInnerHtml(element, goog.html.SafeHtml.EMPTY);
  245. }
  246. };
  247. /**
  248. * Computes bubble position based on anchored element.
  249. *
  250. * @param {Element} anchorElement The element to which we are attaching.
  251. * @param {goog.positioning.Corner} corner The bubble corner used for
  252. * positioning.
  253. * @private
  254. */
  255. goog.ui.Bubble.prototype.setAnchoredPosition_ = function(
  256. anchorElement, corner) {
  257. this.popup_.setPinnedCorner(corner);
  258. var margin = this.createMarginForCorner_(corner);
  259. this.popup_.setMargin(margin);
  260. var anchorCorner = goog.positioning.flipCorner(corner);
  261. this.popup_.setPosition(
  262. new goog.positioning.AnchoredPosition(anchorElement, anchorCorner));
  263. };
  264. /**
  265. * Hides the bubble. This is called asynchronously by timer of event processor
  266. * for the mouse click on the close button.
  267. * @private
  268. */
  269. goog.ui.Bubble.prototype.hideBubble_ = function() {
  270. this.setVisible(false);
  271. };
  272. /**
  273. * Returns an AnchoredPosition that will position the bubble optimally
  274. * given the position of the anchor element and the size of the viewport.
  275. *
  276. * @param {Element} anchorElement The element to which the bubble is attached.
  277. * @return {!goog.positioning.AnchoredPosition} The AnchoredPosition
  278. * to give to {@link #setPosition}.
  279. */
  280. goog.ui.Bubble.prototype.getComputedAnchoredPosition = function(anchorElement) {
  281. return new goog.positioning.AnchoredPosition(
  282. anchorElement, this.computePinnedCorner_(anchorElement));
  283. };
  284. /**
  285. * Computes the pinned corner for the bubble.
  286. *
  287. * @param {Element} anchorElement The element to which the button is attached.
  288. * @return {goog.positioning.Corner} The pinned corner.
  289. * @private
  290. */
  291. goog.ui.Bubble.prototype.computePinnedCorner_ = function(anchorElement) {
  292. var doc = this.getDomHelper().getOwnerDocument(anchorElement);
  293. var viewportElement = goog.style.getClientViewportElement(doc);
  294. var viewportWidth = viewportElement.offsetWidth;
  295. var viewportHeight = viewportElement.offsetHeight;
  296. var anchorElementOffset = goog.style.getPageOffset(anchorElement);
  297. var anchorElementSize = goog.style.getSize(anchorElement);
  298. var anchorType = 0;
  299. // right margin or left?
  300. if (viewportWidth - anchorElementOffset.x - anchorElementSize.width >
  301. anchorElementOffset.x) {
  302. anchorType += 1;
  303. }
  304. // attaches to the top or to the bottom?
  305. if (viewportHeight - anchorElementOffset.y - anchorElementSize.height >
  306. anchorElementOffset.y) {
  307. anchorType += 2;
  308. }
  309. return goog.ui.Bubble.corners_[anchorType];
  310. };
  311. /**
  312. * Computes the right offset for a given bubble corner
  313. * and creates a margin element for it. This is done to have the
  314. * button anchor element on its frame rather than on the corner.
  315. *
  316. * @param {goog.positioning.Corner} corner The corner.
  317. * @return {!goog.math.Box} the computed margin. Only left or right fields are
  318. * non-zero, but they may be negative.
  319. * @private
  320. */
  321. goog.ui.Bubble.prototype.createMarginForCorner_ = function(corner) {
  322. var margin = new goog.math.Box(0, 0, 0, 0);
  323. if (corner & goog.positioning.CornerBit.RIGHT) {
  324. margin.right -= this.config_.marginShift;
  325. } else {
  326. margin.left -= this.config_.marginShift;
  327. }
  328. return margin;
  329. };
  330. /**
  331. * Computes the HTML string for a given bubble orientation.
  332. *
  333. * @param {goog.positioning.Corner} corner The corner.
  334. * @return {!goog.html.SafeHtml} The HTML string to place inside the
  335. * bubble's popup.
  336. * @private
  337. */
  338. goog.ui.Bubble.prototype.computeHtmlForCorner_ = function(corner) {
  339. var bubbleTopClass;
  340. var bubbleBottomClass;
  341. switch (corner) {
  342. case goog.positioning.Corner.TOP_LEFT:
  343. bubbleTopClass = this.config_.cssBubbleTopLeftAnchor;
  344. bubbleBottomClass = this.config_.cssBubbleBottomNoAnchor;
  345. break;
  346. case goog.positioning.Corner.TOP_RIGHT:
  347. bubbleTopClass = this.config_.cssBubbleTopRightAnchor;
  348. bubbleBottomClass = this.config_.cssBubbleBottomNoAnchor;
  349. break;
  350. case goog.positioning.Corner.BOTTOM_LEFT:
  351. bubbleTopClass = this.config_.cssBubbleTopNoAnchor;
  352. bubbleBottomClass = this.config_.cssBubbleBottomLeftAnchor;
  353. break;
  354. case goog.positioning.Corner.BOTTOM_RIGHT:
  355. bubbleTopClass = this.config_.cssBubbleTopNoAnchor;
  356. bubbleBottomClass = this.config_.cssBubbleBottomRightAnchor;
  357. break;
  358. default:
  359. throw Error('This corner type is not supported by bubble!');
  360. }
  361. var message = null;
  362. if (this.message_ instanceof SafeHtml) {
  363. message = this.message_;
  364. } else {
  365. message = SafeHtml.create('div', {'id': this.messageId_});
  366. }
  367. var tableRows = goog.html.SafeHtml.concat(
  368. SafeHtml.create(
  369. 'tr', {},
  370. SafeHtml.create('td', {'colspan': 4, 'class': bubbleTopClass})),
  371. SafeHtml.create(
  372. 'tr', {},
  373. SafeHtml.concat(
  374. SafeHtml.create('td', {'class': this.config_.cssBubbleLeft}),
  375. SafeHtml.create(
  376. 'td', {
  377. 'class': this.config_.cssBubbleFont,
  378. 'style':
  379. goog.string.Const.from('padding:0 4px;background:white')
  380. },
  381. message),
  382. SafeHtml.create('td', {
  383. 'id': this.closeButtonId_,
  384. 'class': this.config_.cssCloseButton
  385. }),
  386. SafeHtml.create('td', {'class': this.config_.cssBubbleRight}))),
  387. SafeHtml.create(
  388. 'tr', {},
  389. SafeHtml.create('td', {'colspan': 4, 'class': bubbleBottomClass})));
  390. return SafeHtml.create(
  391. 'table', {
  392. 'border': 0,
  393. 'cellspacing': 0,
  394. 'cellpadding': 0,
  395. 'width': this.config_.bubbleWidth,
  396. 'style': goog.string.Const.from('z-index:1')
  397. },
  398. tableRows);
  399. };
  400. /**
  401. * A default configuration for the bubble.
  402. *
  403. * @type {Object}
  404. */
  405. goog.ui.Bubble.defaultConfig = {
  406. bubbleWidth: 147,
  407. marginShift: 60,
  408. cssBubbleFont: goog.getCssName('goog-bubble-font'),
  409. cssCloseButton: goog.getCssName('goog-bubble-close-button'),
  410. cssBubbleTopRightAnchor: goog.getCssName('goog-bubble-top-right-anchor'),
  411. cssBubbleTopLeftAnchor: goog.getCssName('goog-bubble-top-left-anchor'),
  412. cssBubbleTopNoAnchor: goog.getCssName('goog-bubble-top-no-anchor'),
  413. cssBubbleBottomRightAnchor:
  414. goog.getCssName('goog-bubble-bottom-right-anchor'),
  415. cssBubbleBottomLeftAnchor: goog.getCssName('goog-bubble-bottom-left-anchor'),
  416. cssBubbleBottomNoAnchor: goog.getCssName('goog-bubble-bottom-no-anchor'),
  417. cssBubbleLeft: goog.getCssName('goog-bubble-left'),
  418. cssBubbleRight: goog.getCssName('goog-bubble-right')
  419. };
  420. /**
  421. * An auxiliary array optimizing the corner computation.
  422. *
  423. * @type {Array<goog.positioning.Corner>}
  424. * @private
  425. */
  426. goog.ui.Bubble.corners_ = [
  427. goog.positioning.Corner.BOTTOM_RIGHT, goog.positioning.Corner.BOTTOM_LEFT,
  428. goog.positioning.Corner.TOP_RIGHT, goog.positioning.Corner.TOP_LEFT
  429. ];
  430. }); // goog.scope