bubble.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. // Copyright 2005 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 Bubble component - handles display, hiding, etc. of the
  16. * actual bubble UI.
  17. *
  18. * This is used exclusively by code within the editor package, and should not
  19. * be used directly.
  20. *
  21. * @author robbyw@google.com (Robby Walker)
  22. */
  23. goog.provide('goog.ui.editor.Bubble');
  24. goog.require('goog.asserts');
  25. goog.require('goog.dom');
  26. goog.require('goog.dom.TagName');
  27. goog.require('goog.dom.ViewportSizeMonitor');
  28. goog.require('goog.dom.classlist');
  29. goog.require('goog.editor.style');
  30. goog.require('goog.events.EventHandler');
  31. goog.require('goog.events.EventTarget');
  32. goog.require('goog.events.EventType');
  33. goog.require('goog.functions');
  34. goog.require('goog.log');
  35. goog.require('goog.math.Box');
  36. goog.require('goog.object');
  37. goog.require('goog.positioning');
  38. goog.require('goog.positioning.Corner');
  39. goog.require('goog.positioning.Overflow');
  40. goog.require('goog.positioning.OverflowStatus');
  41. goog.require('goog.string');
  42. goog.require('goog.style');
  43. goog.require('goog.ui.Component');
  44. goog.require('goog.ui.PopupBase');
  45. goog.require('goog.userAgent');
  46. /**
  47. * Property bubble UI element.
  48. * @param {Element} parent The parent element for this bubble.
  49. * @param {number} zIndex The z index to draw the bubble at.
  50. * @constructor
  51. * @extends {goog.events.EventTarget}
  52. */
  53. goog.ui.editor.Bubble = function(parent, zIndex) {
  54. goog.ui.editor.Bubble.base(this, 'constructor');
  55. /**
  56. * Dom helper for the document the bubble should be shown in.
  57. * @type {!goog.dom.DomHelper}
  58. * @private
  59. */
  60. this.dom_ = goog.dom.getDomHelper(parent);
  61. /**
  62. * Event handler for this bubble.
  63. * @type {goog.events.EventHandler<!goog.ui.editor.Bubble>}
  64. * @private
  65. */
  66. this.eventHandler_ = new goog.events.EventHandler(this);
  67. /**
  68. * Object that monitors the application window for size changes.
  69. * @type {goog.dom.ViewportSizeMonitor}
  70. * @private
  71. */
  72. this.viewPortSizeMonitor_ =
  73. new goog.dom.ViewportSizeMonitor(this.dom_.getWindow());
  74. /**
  75. * Maps panel ids to panels.
  76. * @type {Object<goog.ui.editor.Bubble.Panel_>}
  77. * @private
  78. */
  79. this.panels_ = {};
  80. /**
  81. * Container element for the entire bubble. This may contain elements related
  82. * to look and feel or styling of the bubble.
  83. * @type {Element}
  84. * @private
  85. */
  86. this.bubbleContainer_ = this.dom_.createDom(
  87. goog.dom.TagName.DIV,
  88. {'className': goog.ui.editor.Bubble.BUBBLE_CLASSNAME});
  89. goog.style.setElementShown(this.bubbleContainer_, false);
  90. goog.dom.appendChild(parent, this.bubbleContainer_);
  91. goog.style.setStyle(this.bubbleContainer_, 'zIndex', zIndex);
  92. /**
  93. * Container element for the bubble panels - this should be some inner element
  94. * within (or equal to) bubbleContainer.
  95. * @type {Element}
  96. * @private
  97. */
  98. this.bubbleContents_ = this.createBubbleDom(this.dom_, this.bubbleContainer_);
  99. /**
  100. * Element showing the close box.
  101. * @type {!Element}
  102. * @private
  103. */
  104. this.closeBox_ = this.dom_.createDom(goog.dom.TagName.DIV, {
  105. 'className': goog.getCssName('tr_bubble_closebox'),
  106. 'innerHTML': '&nbsp;'
  107. });
  108. this.bubbleContents_.appendChild(this.closeBox_);
  109. // We make bubbles unselectable so that clicking on them does not steal focus
  110. // or move the cursor away from the element the bubble is attached to.
  111. goog.editor.style.makeUnselectable(this.bubbleContainer_, this.eventHandler_);
  112. /**
  113. * Popup that controls showing and hiding the bubble at the appropriate
  114. * position.
  115. * @type {goog.ui.PopupBase}
  116. * @private
  117. */
  118. this.popup_ = new goog.ui.PopupBase(this.bubbleContainer_);
  119. };
  120. goog.inherits(goog.ui.editor.Bubble, goog.events.EventTarget);
  121. /**
  122. * The css class name of the bubble container element.
  123. * @type {string}
  124. */
  125. goog.ui.editor.Bubble.BUBBLE_CLASSNAME = goog.getCssName('tr_bubble');
  126. /**
  127. * Creates and adds DOM for the bubble UI to the given container. This default
  128. * implementation just returns the container itself.
  129. * @param {!goog.dom.DomHelper} dom DOM helper to use.
  130. * @param {!Element} container Element to add the new elements to.
  131. * @return {!Element} The element where bubble content should be added.
  132. * @protected
  133. */
  134. goog.ui.editor.Bubble.prototype.createBubbleDom = function(dom, container) {
  135. return container;
  136. };
  137. /**
  138. * A logger for goog.ui.editor.Bubble.
  139. * @type {goog.log.Logger}
  140. * @protected
  141. */
  142. goog.ui.editor.Bubble.prototype.logger =
  143. goog.log.getLogger('goog.ui.editor.Bubble');
  144. /** @override */
  145. goog.ui.editor.Bubble.prototype.disposeInternal = function() {
  146. goog.ui.editor.Bubble.base(this, 'disposeInternal');
  147. goog.dom.removeNode(this.bubbleContainer_);
  148. this.bubbleContainer_ = null;
  149. this.eventHandler_.dispose();
  150. this.eventHandler_ = null;
  151. this.viewPortSizeMonitor_.dispose();
  152. this.viewPortSizeMonitor_ = null;
  153. };
  154. /**
  155. * @return {Element} The element that where the bubble's contents go.
  156. */
  157. goog.ui.editor.Bubble.prototype.getContentElement = function() {
  158. return this.bubbleContents_;
  159. };
  160. /**
  161. * @return {Element} The element that contains the bubble.
  162. * @protected
  163. */
  164. goog.ui.editor.Bubble.prototype.getContainerElement = function() {
  165. return this.bubbleContainer_;
  166. };
  167. /**
  168. * @return {goog.events.EventHandler<T>} The event handler.
  169. * @protected
  170. * @this {T}
  171. * @template T
  172. */
  173. goog.ui.editor.Bubble.prototype.getEventHandler = function() {
  174. return this.eventHandler_;
  175. };
  176. /**
  177. * Handles user resizing of window.
  178. * @private
  179. */
  180. goog.ui.editor.Bubble.prototype.handleWindowResize_ = function() {
  181. if (this.isVisible()) {
  182. this.reposition();
  183. }
  184. };
  185. /**
  186. * Sets whether the bubble dismisses itself when the user clicks outside of it.
  187. * @param {boolean} autoHide Whether to autohide on an external click.
  188. */
  189. goog.ui.editor.Bubble.prototype.setAutoHide = function(autoHide) {
  190. this.popup_.setAutoHide(autoHide);
  191. };
  192. /**
  193. * Returns whether there is already a panel of the given type.
  194. * @param {string} type Type of panel to check.
  195. * @return {boolean} Whether there is already a panel of the given type.
  196. */
  197. goog.ui.editor.Bubble.prototype.hasPanelOfType = function(type) {
  198. return goog.object.some(
  199. this.panels_, function(panel) { return panel.type == type; });
  200. };
  201. /**
  202. * Adds a panel to the bubble.
  203. * @param {string} type The type of bubble panel this is. Should usually be
  204. * the same as the tagName of the targetElement. This ensures multiple
  205. * bubble panels don't appear for the same element.
  206. * @param {string} title The title of the panel.
  207. * @param {Element} targetElement The target element of the bubble.
  208. * @param {function(Element): void} contentFn Function that when called with
  209. * a container element, will add relevant panel content to it.
  210. * @param {boolean=} opt_preferTopPosition Whether to prefer placing the bubble
  211. * above the element instead of below it. Defaults to preferring below.
  212. * If any panel prefers the top position, the top position is used.
  213. * @return {string} The id of the panel.
  214. */
  215. goog.ui.editor.Bubble.prototype.addPanel = function(
  216. type, title, targetElement, contentFn, opt_preferTopPosition) {
  217. var id = goog.string.createUniqueString();
  218. var panel = new goog.ui.editor.Bubble.Panel_(
  219. this.dom_, id, type, title, targetElement, !opt_preferTopPosition);
  220. this.panels_[id] = panel;
  221. // Insert the panel in string order of type. Technically we could use binary
  222. // search here but n is really small (probably 0 - 2) so it's not worth it.
  223. // The last child of bubbleContents_ is the close box so we take care not
  224. // to treat it as a panel element, and we also ensure it stays as the last
  225. // element. The intention here is not to create any artificial order, but
  226. // just to ensure that it is always consistent.
  227. var nextElement;
  228. for (var i = 0, len = this.bubbleContents_.childNodes.length - 1; i < len;
  229. i++) {
  230. var otherChild = this.bubbleContents_.childNodes[i];
  231. var otherPanel = this.panels_[otherChild.id];
  232. if (otherPanel.type > type) {
  233. nextElement = otherChild;
  234. break;
  235. }
  236. }
  237. goog.dom.insertSiblingBefore(
  238. panel.element, nextElement || this.bubbleContents_.lastChild);
  239. contentFn(panel.getContentElement());
  240. goog.editor.style.makeUnselectable(panel.element, this.eventHandler_);
  241. var numPanels = goog.object.getCount(this.panels_);
  242. if (numPanels == 1) {
  243. this.openBubble_();
  244. } else if (numPanels == 2) {
  245. goog.dom.classlist.add(
  246. goog.asserts.assert(this.bubbleContainer_),
  247. goog.getCssName('tr_multi_bubble'));
  248. }
  249. this.reposition();
  250. return id;
  251. };
  252. /**
  253. * Removes the panel with the given id.
  254. * @param {string} id The id of the panel.
  255. */
  256. goog.ui.editor.Bubble.prototype.removePanel = function(id) {
  257. var panel = this.panels_[id];
  258. goog.dom.removeNode(panel.element);
  259. delete this.panels_[id];
  260. var numPanels = goog.object.getCount(this.panels_);
  261. if (numPanels <= 1) {
  262. goog.dom.classlist.remove(
  263. goog.asserts.assert(this.bubbleContainer_),
  264. goog.getCssName('tr_multi_bubble'));
  265. }
  266. if (numPanels == 0) {
  267. this.closeBubble_();
  268. } else {
  269. this.reposition();
  270. }
  271. };
  272. /**
  273. * Opens the bubble.
  274. * @private
  275. */
  276. goog.ui.editor.Bubble.prototype.openBubble_ = function() {
  277. this.eventHandler_
  278. .listen(this.closeBox_, goog.events.EventType.CLICK, this.closeBubble_)
  279. .listen(
  280. this.viewPortSizeMonitor_, goog.events.EventType.RESIZE,
  281. this.handleWindowResize_)
  282. .listen(
  283. this.popup_, goog.ui.PopupBase.EventType.HIDE, this.handlePopupHide);
  284. this.popup_.setVisible(true);
  285. this.reposition();
  286. };
  287. /**
  288. * Closes the bubble.
  289. * @private
  290. */
  291. goog.ui.editor.Bubble.prototype.closeBubble_ = function() {
  292. this.popup_.setVisible(false);
  293. };
  294. /**
  295. * Handles the popup's hide event by removing all panels and dispatching a
  296. * HIDE event.
  297. * @protected
  298. */
  299. goog.ui.editor.Bubble.prototype.handlePopupHide = function() {
  300. // Remove the panel elements.
  301. for (var panelId in this.panels_) {
  302. goog.dom.removeNode(this.panels_[panelId].element);
  303. }
  304. // Update the state to reflect no panels.
  305. this.panels_ = {};
  306. goog.dom.classlist.remove(
  307. goog.asserts.assert(this.bubbleContainer_),
  308. goog.getCssName('tr_multi_bubble'));
  309. this.eventHandler_.removeAll();
  310. this.dispatchEvent(goog.ui.Component.EventType.HIDE);
  311. };
  312. /**
  313. * Returns the visibility of the bubble.
  314. * @return {boolean} True if visible false if not.
  315. */
  316. goog.ui.editor.Bubble.prototype.isVisible = function() {
  317. return this.popup_.isVisible();
  318. };
  319. /**
  320. * The vertical clearance in pixels between the bottom of the targetElement
  321. * and the edge of the bubble.
  322. * @type {number}
  323. * @private
  324. */
  325. goog.ui.editor.Bubble.VERTICAL_CLEARANCE_ = goog.userAgent.IE ? 4 : 2;
  326. /**
  327. * Bubble's margin box to be passed to goog.positioning.
  328. * @type {goog.math.Box}
  329. * @private
  330. */
  331. goog.ui.editor.Bubble.MARGIN_BOX_ = new goog.math.Box(
  332. goog.ui.editor.Bubble.VERTICAL_CLEARANCE_, 0,
  333. goog.ui.editor.Bubble.VERTICAL_CLEARANCE_, 0);
  334. /**
  335. * Returns the margin box.
  336. * @return {goog.math.Box}
  337. * @protected
  338. */
  339. goog.ui.editor.Bubble.prototype.getMarginBox = function() {
  340. return goog.ui.editor.Bubble.MARGIN_BOX_;
  341. };
  342. /**
  343. * Positions and displays this bubble below its targetElement. Assumes that
  344. * the bubbleContainer is already contained in the document object it applies
  345. * to.
  346. */
  347. goog.ui.editor.Bubble.prototype.reposition = function() {
  348. var targetElement = null;
  349. var preferBottomPosition = true;
  350. for (var panelId in this.panels_) {
  351. var panel = this.panels_[panelId];
  352. // We don't care which targetElement we get, so we just take the last one.
  353. targetElement = panel.targetElement;
  354. preferBottomPosition = preferBottomPosition && panel.preferBottomPosition;
  355. }
  356. var status = goog.positioning.OverflowStatus.FAILED;
  357. // Fix for bug when bubbleContainer and targetElement have
  358. // opposite directionality, the bubble should anchor to the END of
  359. // the targetElement instead of START.
  360. var reverseLayout =
  361. (goog.style.isRightToLeft(this.bubbleContainer_) !=
  362. goog.style.isRightToLeft(targetElement));
  363. // Try to put the bubble at the bottom of the target unless the plugin has
  364. // requested otherwise.
  365. if (preferBottomPosition) {
  366. status = this.positionAtAnchor_(
  367. reverseLayout ? goog.positioning.Corner.BOTTOM_END :
  368. goog.positioning.Corner.BOTTOM_START,
  369. goog.positioning.Corner.TOP_START,
  370. goog.positioning.Overflow.ADJUST_X | goog.positioning.Overflow.FAIL_Y);
  371. }
  372. if (status & goog.positioning.OverflowStatus.FAILED) {
  373. // Try to put it at the top of the target if there is not enough
  374. // space at the bottom.
  375. status = this.positionAtAnchor_(
  376. reverseLayout ? goog.positioning.Corner.TOP_END :
  377. goog.positioning.Corner.TOP_START,
  378. goog.positioning.Corner.BOTTOM_START,
  379. goog.positioning.Overflow.ADJUST_X | goog.positioning.Overflow.FAIL_Y);
  380. }
  381. if (status & goog.positioning.OverflowStatus.FAILED) {
  382. // Put it at the bottom again with adjustment if there is no
  383. // enough space at the top.
  384. status = this.positionAtAnchor_(
  385. reverseLayout ? goog.positioning.Corner.BOTTOM_END :
  386. goog.positioning.Corner.BOTTOM_START,
  387. goog.positioning.Corner.TOP_START, goog.positioning.Overflow.ADJUST_X |
  388. goog.positioning.Overflow.ADJUST_Y);
  389. if (status & goog.positioning.OverflowStatus.FAILED) {
  390. goog.log.warning(
  391. this.logger,
  392. 'reposition(): positionAtAnchor() failed with ' + status);
  393. }
  394. }
  395. };
  396. /**
  397. * A helper for reposition() - positions the bubble in regards to the position
  398. * of the elements the bubble is attached to.
  399. * @param {goog.positioning.Corner} targetCorner The corner of
  400. * the target element.
  401. * @param {goog.positioning.Corner} bubbleCorner The corner of the bubble.
  402. * @param {number} overflow Overflow handling mode bitmap,
  403. * {@see goog.positioning.Overflow}.
  404. * @return {number} Status bitmap, {@see goog.positioning.OverflowStatus}.
  405. * @private
  406. */
  407. goog.ui.editor.Bubble.prototype.positionAtAnchor_ = function(
  408. targetCorner, bubbleCorner, overflow) {
  409. var targetElement = null;
  410. for (var panelId in this.panels_) {
  411. // For now, we use the outermost element. This assumes the multiple
  412. // elements this panel is showing for contain each other - in the event
  413. // that is not generally the case this may need to be updated to pick
  414. // the lowest or highest element depending on targetCorner.
  415. var candidate = this.panels_[panelId].targetElement;
  416. if (!targetElement || goog.dom.contains(candidate, targetElement)) {
  417. targetElement = this.panels_[panelId].targetElement;
  418. }
  419. }
  420. return goog.positioning.positionAtAnchor(
  421. targetElement, targetCorner, this.bubbleContainer_, bubbleCorner, null,
  422. this.getMarginBox(), overflow, null, this.getViewportBox());
  423. };
  424. /**
  425. * Returns the viewport box to use when positioning the bubble.
  426. * @return {goog.math.Box}
  427. * @protected
  428. */
  429. goog.ui.editor.Bubble.prototype.getViewportBox = goog.functions.NULL;
  430. /**
  431. * Private class used to describe a bubble panel.
  432. * @param {goog.dom.DomHelper} dom DOM helper used to create the panel.
  433. * @param {string} id ID of the panel.
  434. * @param {string} type Type of the panel.
  435. * @param {string} title Title of the panel.
  436. * @param {Element} targetElement Element the panel is showing for.
  437. * @param {boolean} preferBottomPosition Whether this panel prefers to show
  438. * below the target element.
  439. * @constructor
  440. * @private
  441. */
  442. goog.ui.editor.Bubble.Panel_ = function(
  443. dom, id, type, title, targetElement, preferBottomPosition) {
  444. /**
  445. * The type of bubble panel.
  446. * @type {string}
  447. */
  448. this.type = type;
  449. /**
  450. * The target element of this bubble panel.
  451. * @type {Element}
  452. */
  453. this.targetElement = targetElement;
  454. /**
  455. * Whether the panel prefers to be placed below the target element.
  456. * @type {boolean}
  457. */
  458. this.preferBottomPosition = preferBottomPosition;
  459. /**
  460. * The element containing this panel.
  461. */
  462. this.element = dom.createDom(
  463. goog.dom.TagName.DIV,
  464. {className: goog.getCssName('tr_bubble_panel'), id: id},
  465. dom.createDom(
  466. goog.dom.TagName.DIV,
  467. {className: goog.getCssName('tr_bubble_panel_title')},
  468. title ? title + ':' : ''), // TODO(robbyw): Does this work in bidi?
  469. dom.createDom(
  470. goog.dom.TagName.DIV,
  471. {className: goog.getCssName('tr_bubble_panel_content')}));
  472. };
  473. /**
  474. * @return {Element} The element in the panel where content should go.
  475. */
  476. goog.ui.editor.Bubble.Panel_.prototype.getContentElement = function() {
  477. return /** @type {Element} */ (this.element.lastChild);
  478. };