tooltip.js 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052
  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 Tooltip widget implementation.
  16. *
  17. * @author eae@google.com (Emil A Eklund)
  18. * @see ../demos/tooltip.html
  19. */
  20. goog.provide('goog.ui.Tooltip');
  21. goog.provide('goog.ui.Tooltip.CursorTooltipPosition');
  22. goog.provide('goog.ui.Tooltip.ElementTooltipPosition');
  23. goog.provide('goog.ui.Tooltip.State');
  24. goog.require('goog.Timer');
  25. goog.require('goog.array');
  26. goog.require('goog.asserts');
  27. goog.require('goog.dom');
  28. goog.require('goog.dom.TagName');
  29. goog.require('goog.dom.safe');
  30. goog.require('goog.events');
  31. goog.require('goog.events.EventType');
  32. goog.require('goog.events.FocusHandler');
  33. goog.require('goog.math.Box');
  34. goog.require('goog.math.Coordinate');
  35. goog.require('goog.positioning');
  36. goog.require('goog.positioning.AnchoredPosition');
  37. goog.require('goog.positioning.Corner');
  38. goog.require('goog.positioning.Overflow');
  39. goog.require('goog.positioning.OverflowStatus');
  40. goog.require('goog.positioning.ViewportPosition');
  41. goog.require('goog.structs.Set');
  42. goog.require('goog.style');
  43. goog.require('goog.ui.Popup');
  44. goog.require('goog.ui.PopupBase');
  45. /**
  46. * Tooltip widget. Can be attached to one or more elements and is shown, with a
  47. * slight delay, when the the cursor is over the element or the element gains
  48. * focus.
  49. *
  50. * @param {Element|string=} opt_el Element to display tooltip for, either
  51. * element reference or string id.
  52. * @param {?string=} opt_str Text message to display in tooltip.
  53. * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
  54. * @constructor
  55. * @extends {goog.ui.Popup}
  56. */
  57. goog.ui.Tooltip = function(opt_el, opt_str, opt_domHelper) {
  58. /**
  59. * Dom Helper
  60. * @type {goog.dom.DomHelper}
  61. * @private
  62. */
  63. this.dom_ = opt_domHelper ||
  64. (opt_el ? goog.dom.getDomHelper(goog.dom.getElement(opt_el)) :
  65. goog.dom.getDomHelper());
  66. goog.ui.Popup.call(
  67. this,
  68. this.dom_.createDom(
  69. goog.dom.TagName.DIV, {'style': 'position:absolute;display:none;'}));
  70. /**
  71. * Cursor position relative to the page.
  72. * @type {!goog.math.Coordinate}
  73. * @protected
  74. */
  75. this.cursorPosition = new goog.math.Coordinate(1, 1);
  76. /**
  77. * Elements this widget is attached to.
  78. * @type {goog.structs.Set}
  79. * @private
  80. */
  81. this.elements_ = new goog.structs.Set();
  82. /**
  83. * Keyboard focus event handler for elements inside the tooltip.
  84. * @private {goog.events.FocusHandler}
  85. */
  86. this.tooltipFocusHandler_ = null;
  87. // Attach to element, if specified
  88. if (opt_el) {
  89. this.attach(opt_el);
  90. }
  91. // Set message, if specified.
  92. if (opt_str != null) {
  93. this.setText(opt_str);
  94. }
  95. };
  96. goog.inherits(goog.ui.Tooltip, goog.ui.Popup);
  97. goog.tagUnsealableClass(goog.ui.Tooltip);
  98. /**
  99. * List of active (open) tooltip widgets. Used to prevent multiple tooltips
  100. * from appearing at once.
  101. *
  102. * @type {!Array<goog.ui.Tooltip>}
  103. * @private
  104. */
  105. goog.ui.Tooltip.activeInstances_ = [];
  106. /**
  107. * Active element reference. Used by the delayed show functionality to keep
  108. * track of the element the mouse is over or the element with focus.
  109. * @type {Element}
  110. * @private
  111. */
  112. goog.ui.Tooltip.prototype.activeEl_ = null;
  113. /**
  114. * CSS class name for tooltip.
  115. *
  116. * @type {string}
  117. */
  118. goog.ui.Tooltip.prototype.className = goog.getCssName('goog-tooltip');
  119. /**
  120. * Delay in milliseconds since the last mouseover or mousemove before the
  121. * tooltip is displayed for an element.
  122. *
  123. * @type {number}
  124. * @private
  125. */
  126. goog.ui.Tooltip.prototype.showDelayMs_ = 500;
  127. /**
  128. * Timer for when to show.
  129. *
  130. * @type {number|undefined}
  131. * @protected
  132. */
  133. goog.ui.Tooltip.prototype.showTimer;
  134. /**
  135. * Delay in milliseconds before tooltips are hidden.
  136. *
  137. * @type {number}
  138. * @private
  139. */
  140. goog.ui.Tooltip.prototype.hideDelayMs_ = 0;
  141. /**
  142. * Timer for when to hide.
  143. *
  144. * @type {number|undefined}
  145. * @protected
  146. */
  147. goog.ui.Tooltip.prototype.hideTimer;
  148. /**
  149. * Element that triggered the tooltip. Note that if a second element triggers
  150. * this tooltip, anchor becomes that second element, even if its show is
  151. * cancelled and the original tooltip survives.
  152. *
  153. * @type {Element|undefined}
  154. * @protected
  155. */
  156. goog.ui.Tooltip.prototype.anchor;
  157. /**
  158. * Possible states for the tooltip to be in.
  159. * @enum {number}
  160. */
  161. goog.ui.Tooltip.State = {
  162. INACTIVE: 0,
  163. WAITING_TO_SHOW: 1,
  164. SHOWING: 2,
  165. WAITING_TO_HIDE: 3,
  166. UPDATING: 4 // waiting to show new hovercard while old one still showing.
  167. };
  168. /**
  169. * Popup activation types. Used to select a positioning strategy.
  170. * @enum {number}
  171. */
  172. goog.ui.Tooltip.Activation = {
  173. CURSOR: 0,
  174. FOCUS: 1
  175. };
  176. /**
  177. * Whether the anchor has seen the cursor move or has received focus since the
  178. * tooltip was last shown. Used to ignore mouse over events triggered by view
  179. * changes and UI updates.
  180. * @type {boolean|undefined}
  181. * @private
  182. */
  183. goog.ui.Tooltip.prototype.seenInteraction_;
  184. /**
  185. * Whether the cursor must have moved before the tooltip will be shown.
  186. * @type {boolean|undefined}
  187. * @private
  188. */
  189. goog.ui.Tooltip.prototype.requireInteraction_;
  190. /**
  191. * If this tooltip's element contains another tooltip that becomes active, this
  192. * property identifies that tooltip so that we can check if this tooltip should
  193. * not be hidden because the nested tooltip is active.
  194. * @type {goog.ui.Tooltip}
  195. * @private
  196. */
  197. goog.ui.Tooltip.prototype.childTooltip_;
  198. /**
  199. * If this tooltip is inside another tooltip's element, then it may have
  200. * prevented that tooltip from hiding. When this tooltip hides, we'll need
  201. * to check if the parent should be hidden as well.
  202. * @type {goog.ui.Tooltip}
  203. * @private
  204. */
  205. goog.ui.Tooltip.prototype.parentTooltip_;
  206. /**
  207. * Returns the dom helper that is being used on this component.
  208. * @return {goog.dom.DomHelper} The dom helper used on this component.
  209. */
  210. goog.ui.Tooltip.prototype.getDomHelper = function() {
  211. return this.dom_;
  212. };
  213. /**
  214. * @return {goog.ui.Tooltip} Active tooltip in a child element, or null if none.
  215. * @protected
  216. */
  217. goog.ui.Tooltip.prototype.getChildTooltip = function() {
  218. return this.childTooltip_;
  219. };
  220. /**
  221. * Attach to element. Tooltip will be displayed when the cursor is over the
  222. * element or when the element has been active for a few milliseconds.
  223. *
  224. * @param {Element|string} el Element to display tooltip for, either element
  225. * reference or string id.
  226. */
  227. goog.ui.Tooltip.prototype.attach = function(el) {
  228. el = goog.dom.getElement(el);
  229. this.elements_.add(el);
  230. goog.events.listen(
  231. el, goog.events.EventType.MOUSEOVER, this.handleMouseOver, false, this);
  232. goog.events.listen(
  233. el, goog.events.EventType.MOUSEOUT, this.handleMouseOutAndBlur, false,
  234. this);
  235. goog.events.listen(
  236. el, goog.events.EventType.MOUSEMOVE, this.handleMouseMove, false, this);
  237. goog.events.listen(
  238. el, goog.events.EventType.FOCUS, this.handleFocus, false, this);
  239. goog.events.listen(
  240. el, goog.events.EventType.BLUR, this.handleMouseOutAndBlur, false, this);
  241. };
  242. /**
  243. * Detach from element(s).
  244. *
  245. * @param {Element|string=} opt_el Element to detach from, either element
  246. * reference or string id. If no element is
  247. * specified all are detached.
  248. */
  249. goog.ui.Tooltip.prototype.detach = function(opt_el) {
  250. if (opt_el) {
  251. var el = goog.dom.getElement(opt_el);
  252. this.detachElement_(el);
  253. this.elements_.remove(el);
  254. } else {
  255. var a = this.elements_.getValues();
  256. for (var el, i = 0; el = a[i]; i++) {
  257. this.detachElement_(el);
  258. }
  259. this.elements_.clear();
  260. }
  261. };
  262. /**
  263. * Detach from element.
  264. *
  265. * @param {Element} el Element to detach from.
  266. * @private
  267. */
  268. goog.ui.Tooltip.prototype.detachElement_ = function(el) {
  269. goog.events.unlisten(
  270. el, goog.events.EventType.MOUSEOVER, this.handleMouseOver, false, this);
  271. goog.events.unlisten(
  272. el, goog.events.EventType.MOUSEOUT, this.handleMouseOutAndBlur, false,
  273. this);
  274. goog.events.unlisten(
  275. el, goog.events.EventType.MOUSEMOVE, this.handleMouseMove, false, this);
  276. goog.events.unlisten(
  277. el, goog.events.EventType.FOCUS, this.handleFocus, false, this);
  278. goog.events.unlisten(
  279. el, goog.events.EventType.BLUR, this.handleMouseOutAndBlur, false, this);
  280. };
  281. /**
  282. * Sets delay in milliseconds before tooltip is displayed for an element.
  283. *
  284. * @param {number} delay The delay in milliseconds.
  285. */
  286. goog.ui.Tooltip.prototype.setShowDelayMs = function(delay) {
  287. this.showDelayMs_ = delay;
  288. };
  289. /**
  290. * @return {number} The delay in milliseconds before tooltip is displayed for an
  291. * element.
  292. */
  293. goog.ui.Tooltip.prototype.getShowDelayMs = function() {
  294. return this.showDelayMs_;
  295. };
  296. /**
  297. * Sets delay in milliseconds before tooltip is hidden once the cursor leavs
  298. * the element.
  299. *
  300. * @param {number} delay The delay in milliseconds.
  301. */
  302. goog.ui.Tooltip.prototype.setHideDelayMs = function(delay) {
  303. this.hideDelayMs_ = delay;
  304. };
  305. /**
  306. * @return {number} The delay in milliseconds before tooltip is hidden once the
  307. * cursor leaves the element.
  308. */
  309. goog.ui.Tooltip.prototype.getHideDelayMs = function() {
  310. return this.hideDelayMs_;
  311. };
  312. /**
  313. * Sets tooltip message as plain text.
  314. *
  315. * @param {string} str Text message to display in tooltip.
  316. */
  317. goog.ui.Tooltip.prototype.setText = function(str) {
  318. goog.dom.setTextContent(this.getElement(), str);
  319. };
  320. /**
  321. * Sets tooltip message as HTML markup.
  322. * @param {!goog.html.SafeHtml} html HTML message to display in tooltip.
  323. */
  324. goog.ui.Tooltip.prototype.setSafeHtml = function(html) {
  325. var element = this.getElement();
  326. if (element) {
  327. goog.dom.safe.setInnerHtml(element, html);
  328. }
  329. };
  330. /**
  331. * Sets tooltip element.
  332. *
  333. * @param {Element} el HTML element to use as the tooltip.
  334. * @override
  335. */
  336. goog.ui.Tooltip.prototype.setElement = function(el) {
  337. var oldElement = this.getElement();
  338. if (oldElement) {
  339. goog.dom.removeNode(oldElement);
  340. }
  341. goog.ui.Tooltip.superClass_.setElement.call(this, el);
  342. if (el) {
  343. var body = this.dom_.getDocument().body;
  344. body.insertBefore(el, body.lastChild);
  345. this.registerContentFocusEvents_();
  346. } else {
  347. goog.dispose(this.tooltipFocusHandler_);
  348. this.tooltipFocusHandler_ = null;
  349. }
  350. };
  351. /**
  352. * Handler for keyboard focus events of elements inside the tooltip's content
  353. * element. This should only be invoked if this.getElement() != null.
  354. * @private
  355. */
  356. goog.ui.Tooltip.prototype.registerContentFocusEvents_ = function() {
  357. goog.dispose(this.tooltipFocusHandler_);
  358. this.tooltipFocusHandler_ =
  359. new goog.events.FocusHandler(goog.asserts.assert(this.getElement()));
  360. this.registerDisposable(this.tooltipFocusHandler_);
  361. goog.events.listen(
  362. this.tooltipFocusHandler_, goog.events.FocusHandler.EventType.FOCUSIN,
  363. this.clearHideTimer, undefined /* opt_capt */, this);
  364. goog.events.listen(
  365. this.tooltipFocusHandler_, goog.events.FocusHandler.EventType.FOCUSOUT,
  366. this.startHideTimer, undefined /* opt_capt */, this);
  367. };
  368. /**
  369. * @return {string} The tooltip message as plain text.
  370. */
  371. goog.ui.Tooltip.prototype.getText = function() {
  372. return goog.dom.getTextContent(this.getElement());
  373. };
  374. /**
  375. * @return {string} The tooltip message as HTML as plain string.
  376. */
  377. goog.ui.Tooltip.prototype.getHtml = function() {
  378. return this.getElement().innerHTML;
  379. };
  380. /**
  381. * @return {goog.ui.Tooltip.State} Current state of tooltip.
  382. */
  383. goog.ui.Tooltip.prototype.getState = function() {
  384. return this.showTimer ?
  385. (this.isVisible() ? goog.ui.Tooltip.State.UPDATING :
  386. goog.ui.Tooltip.State.WAITING_TO_SHOW) :
  387. this.hideTimer ? goog.ui.Tooltip.State.WAITING_TO_HIDE :
  388. this.isVisible() ? goog.ui.Tooltip.State.SHOWING :
  389. goog.ui.Tooltip.State.INACTIVE;
  390. };
  391. /**
  392. * Sets whether tooltip requires the mouse to have moved or the anchor receive
  393. * focus before the tooltip will be shown.
  394. * @param {boolean} requireInteraction Whether tooltip should require some user
  395. * interaction before showing tooltip.
  396. */
  397. goog.ui.Tooltip.prototype.setRequireInteraction = function(requireInteraction) {
  398. this.requireInteraction_ = requireInteraction;
  399. };
  400. /**
  401. * Returns true if the coord is in the tooltip.
  402. * @param {goog.math.Coordinate} coord Coordinate being tested.
  403. * @return {boolean} Whether the coord is in the tooltip.
  404. */
  405. goog.ui.Tooltip.prototype.isCoordinateInTooltip = function(coord) {
  406. // Check if coord is inside the the tooltip
  407. if (!this.isVisible()) {
  408. return false;
  409. }
  410. var offset = goog.style.getPageOffset(this.getElement());
  411. var size = goog.style.getSize(this.getElement());
  412. return offset.x <= coord.x && coord.x <= offset.x + size.width &&
  413. offset.y <= coord.y && coord.y <= offset.y + size.height;
  414. };
  415. /**
  416. * Called before the popup is shown.
  417. *
  418. * @return {boolean} Whether tooltip should be shown.
  419. * @protected
  420. * @override
  421. */
  422. goog.ui.Tooltip.prototype.onBeforeShow = function() {
  423. if (!goog.ui.PopupBase.prototype.onBeforeShow.call(this)) {
  424. return false;
  425. }
  426. // Hide all open tooltips except if this tooltip is triggered by an element
  427. // inside another tooltip.
  428. if (this.anchor) {
  429. for (var tt, i = 0; tt = goog.ui.Tooltip.activeInstances_[i]; i++) {
  430. if (!goog.dom.contains(tt.getElement(), this.anchor)) {
  431. tt.setVisible(false);
  432. }
  433. }
  434. }
  435. goog.array.insert(goog.ui.Tooltip.activeInstances_, this);
  436. var element = this.getElement();
  437. element.className = this.className;
  438. this.clearHideTimer();
  439. // Register event handlers for tooltip. Used to prevent the tooltip from
  440. // closing if the cursor is over the tooltip rather then the element that
  441. // triggered it.
  442. goog.events.listen(
  443. element, goog.events.EventType.MOUSEOVER, this.handleTooltipMouseOver,
  444. false, this);
  445. goog.events.listen(
  446. element, goog.events.EventType.MOUSEOUT, this.handleTooltipMouseOut,
  447. false, this);
  448. this.clearShowTimer();
  449. return true;
  450. };
  451. /** @override */
  452. goog.ui.Tooltip.prototype.onHide = function() {
  453. goog.array.remove(goog.ui.Tooltip.activeInstances_, this);
  454. // Hide all open tooltips triggered by an element inside this tooltip.
  455. var element = this.getElement();
  456. for (var tt, i = 0; tt = goog.ui.Tooltip.activeInstances_[i]; i++) {
  457. if (tt.anchor && goog.dom.contains(element, tt.anchor)) {
  458. tt.setVisible(false);
  459. }
  460. }
  461. // If this tooltip is inside another tooltip, start hide timer for that
  462. // tooltip in case this tooltip was the only reason it was still showing.
  463. if (this.parentTooltip_) {
  464. this.parentTooltip_.startHideTimer();
  465. }
  466. goog.events.unlisten(
  467. element, goog.events.EventType.MOUSEOVER, this.handleTooltipMouseOver,
  468. false, this);
  469. goog.events.unlisten(
  470. element, goog.events.EventType.MOUSEOUT, this.handleTooltipMouseOut,
  471. false, this);
  472. this.anchor = undefined;
  473. // If we are still waiting to show a different hovercard, don't abort it
  474. // because you think you haven't seen a mouse move:
  475. if (this.getState() == goog.ui.Tooltip.State.INACTIVE) {
  476. this.seenInteraction_ = false;
  477. }
  478. goog.ui.PopupBase.prototype.onHide.call(this);
  479. };
  480. /**
  481. * Called by timer from mouse over handler. Shows tooltip if cursor is still
  482. * over the same element.
  483. *
  484. * @param {Element} el Element to show tooltip for.
  485. * @param {goog.positioning.AbstractPosition=} opt_pos Position to display popup
  486. * at.
  487. */
  488. goog.ui.Tooltip.prototype.maybeShow = function(el, opt_pos) {
  489. // Assert that the mouse is still over the same element, and that we have not
  490. // detached from the anchor in the meantime.
  491. if (this.anchor == el && this.elements_.contains(this.anchor)) {
  492. if (this.seenInteraction_ || !this.requireInteraction_) {
  493. // If it is currently showing, then hide it, and abort if it doesn't hide.
  494. this.setVisible(false);
  495. if (!this.isVisible()) {
  496. this.positionAndShow_(el, opt_pos);
  497. }
  498. } else {
  499. this.anchor = undefined;
  500. }
  501. }
  502. this.showTimer = undefined;
  503. };
  504. /**
  505. * @return {goog.structs.Set} Elements this widget is attached to.
  506. * @protected
  507. */
  508. goog.ui.Tooltip.prototype.getElements = function() {
  509. return this.elements_;
  510. };
  511. /**
  512. * @return {Element} Active element reference.
  513. */
  514. goog.ui.Tooltip.prototype.getActiveElement = function() {
  515. return this.activeEl_;
  516. };
  517. /**
  518. * @param {Element} activeEl Active element reference.
  519. * @protected
  520. */
  521. goog.ui.Tooltip.prototype.setActiveElement = function(activeEl) {
  522. this.activeEl_ = activeEl;
  523. };
  524. /**
  525. * Shows tooltip for a specific element.
  526. *
  527. * @param {Element} el Element to show tooltip for.
  528. * @param {goog.positioning.AbstractPosition=} opt_pos Position to display popup
  529. * at.
  530. */
  531. goog.ui.Tooltip.prototype.showForElement = function(el, opt_pos) {
  532. this.attach(el);
  533. this.activeEl_ = el;
  534. this.positionAndShow_(el, opt_pos);
  535. };
  536. /**
  537. * Sets tooltip position and shows it.
  538. *
  539. * @param {Element} el Element to show tooltip for.
  540. * @param {goog.positioning.AbstractPosition=} opt_pos Position to display popup
  541. * at.
  542. * @private
  543. */
  544. goog.ui.Tooltip.prototype.positionAndShow_ = function(el, opt_pos) {
  545. this.anchor = el;
  546. this.setPosition(
  547. opt_pos ||
  548. this.getPositioningStrategy(goog.ui.Tooltip.Activation.CURSOR));
  549. this.setVisible(true);
  550. };
  551. /**
  552. * Called by timer from mouse out handler. Hides tooltip if cursor is still
  553. * outside element and tooltip, or if a child of tooltip has the focus.
  554. * @param {?Element|undefined} el Tooltip's anchor when hide timer was started.
  555. */
  556. goog.ui.Tooltip.prototype.maybeHide = function(el) {
  557. this.hideTimer = undefined;
  558. if (el == this.anchor) {
  559. var dom = this.getDomHelper();
  560. var focusedEl = dom.getActiveElement();
  561. // If the tooltip content is focused, then don't hide the tooltip.
  562. var tooltipContentFocused = focusedEl && this.getElement() &&
  563. dom.contains(this.getElement(), focusedEl);
  564. if ((this.activeEl_ == null ||
  565. (this.activeEl_ != this.getElement() &&
  566. !this.elements_.contains(this.activeEl_))) &&
  567. !tooltipContentFocused && !this.hasActiveChild()) {
  568. this.setVisible(false);
  569. }
  570. }
  571. };
  572. /**
  573. * @return {boolean} Whether tooltip element contains an active child tooltip,
  574. * and should thus not be hidden. When the child tooltip is hidden, it
  575. * will check if the parent should be hidden, too.
  576. * @protected
  577. */
  578. goog.ui.Tooltip.prototype.hasActiveChild = function() {
  579. return !!(this.childTooltip_ && this.childTooltip_.activeEl_);
  580. };
  581. /**
  582. * Saves the current mouse cursor position to {@code this.cursorPosition}.
  583. * @param {goog.events.BrowserEvent} event MOUSEOVER or MOUSEMOVE event.
  584. * @private
  585. */
  586. goog.ui.Tooltip.prototype.saveCursorPosition_ = function(event) {
  587. var scroll = this.dom_.getDocumentScroll();
  588. this.cursorPosition.x = event.clientX + scroll.x;
  589. this.cursorPosition.y = event.clientY + scroll.y;
  590. };
  591. /**
  592. * Handler for mouse over events.
  593. *
  594. * @param {goog.events.BrowserEvent} event Event object.
  595. * @protected
  596. */
  597. goog.ui.Tooltip.prototype.handleMouseOver = function(event) {
  598. var el = this.getAnchorFromElement(/** @type {Element} */ (event.target));
  599. this.activeEl_ = el;
  600. this.clearHideTimer();
  601. if (el != this.anchor) {
  602. this.anchor = el;
  603. this.startShowTimer(el);
  604. this.checkForParentTooltip_();
  605. this.saveCursorPosition_(event);
  606. }
  607. };
  608. /**
  609. * Find anchor containing the given element, if any.
  610. *
  611. * @param {Element} el Element that triggered event.
  612. * @return {Element} Element in elements_ array that contains given element,
  613. * or null if not found.
  614. * @protected
  615. */
  616. goog.ui.Tooltip.prototype.getAnchorFromElement = function(el) {
  617. // FireFox has a bug where mouse events relating to <input> elements are
  618. // sometimes duplicated (often in FF2, rarely in FF3): once for the
  619. // <input> element and once for a magic hidden <div> element. Javascript
  620. // code does not have sufficient permissions to read properties on that
  621. // magic element and thus will throw an error in this call to
  622. // getAnchorFromElement_(). In that case we swallow the error.
  623. // See https://bugzilla.mozilla.org/show_bug.cgi?id=330961
  624. try {
  625. while (el && !this.elements_.contains(el)) {
  626. el = /** @type {Element} */ (el.parentNode);
  627. }
  628. return el;
  629. } catch (e) {
  630. return null;
  631. }
  632. };
  633. /**
  634. * Handler for mouse move events.
  635. *
  636. * @param {goog.events.BrowserEvent} event MOUSEMOVE event.
  637. * @protected
  638. */
  639. goog.ui.Tooltip.prototype.handleMouseMove = function(event) {
  640. this.saveCursorPosition_(event);
  641. this.seenInteraction_ = true;
  642. };
  643. /**
  644. * Handler for focus events.
  645. *
  646. * @param {goog.events.BrowserEvent} event Event object.
  647. * @protected
  648. */
  649. goog.ui.Tooltip.prototype.handleFocus = function(event) {
  650. var el = this.getAnchorFromElement(/** @type {Element} */ (event.target));
  651. this.activeEl_ = el;
  652. this.seenInteraction_ = true;
  653. if (this.anchor != el) {
  654. this.anchor = el;
  655. var pos = this.getPositioningStrategy(goog.ui.Tooltip.Activation.FOCUS);
  656. this.clearHideTimer();
  657. this.startShowTimer(el, pos);
  658. this.checkForParentTooltip_();
  659. }
  660. };
  661. /**
  662. * Return a Position instance for repositioning the tooltip. Override in
  663. * subclasses to customize the way repositioning is done.
  664. *
  665. * @param {goog.ui.Tooltip.Activation} activationType Information about what
  666. * kind of event caused the popup to be shown.
  667. * @return {!goog.positioning.AbstractPosition} The position object used
  668. * to position the tooltip.
  669. * @protected
  670. */
  671. goog.ui.Tooltip.prototype.getPositioningStrategy = function(activationType) {
  672. if (activationType == goog.ui.Tooltip.Activation.CURSOR) {
  673. var coord = this.cursorPosition.clone();
  674. return new goog.ui.Tooltip.CursorTooltipPosition(coord);
  675. }
  676. return new goog.ui.Tooltip.ElementTooltipPosition(this.activeEl_);
  677. };
  678. /**
  679. * Looks for an active tooltip whose element contains this tooltip's anchor.
  680. * This allows us to prevent hides until they are really necessary.
  681. *
  682. * @private
  683. */
  684. goog.ui.Tooltip.prototype.checkForParentTooltip_ = function() {
  685. if (this.anchor) {
  686. for (var tt, i = 0; tt = goog.ui.Tooltip.activeInstances_[i]; i++) {
  687. if (goog.dom.contains(tt.getElement(), this.anchor)) {
  688. tt.childTooltip_ = this;
  689. this.parentTooltip_ = tt;
  690. }
  691. }
  692. }
  693. };
  694. /**
  695. * Handler for mouse out and blur events.
  696. *
  697. * @param {goog.events.BrowserEvent} event Event object.
  698. * @protected
  699. */
  700. goog.ui.Tooltip.prototype.handleMouseOutAndBlur = function(event) {
  701. var el = this.getAnchorFromElement(/** @type {Element} */ (event.target));
  702. var elTo = this.getAnchorFromElement(
  703. /** @type {Element} */ (event.relatedTarget));
  704. if (el == elTo) {
  705. // We haven't really left the anchor, just moved from one child to
  706. // another.
  707. return;
  708. }
  709. if (el == this.activeEl_) {
  710. this.activeEl_ = null;
  711. }
  712. this.clearShowTimer();
  713. this.seenInteraction_ = false;
  714. if (this.isVisible() &&
  715. (!event.relatedTarget ||
  716. !goog.dom.contains(this.getElement(), event.relatedTarget))) {
  717. this.startHideTimer();
  718. } else {
  719. this.anchor = undefined;
  720. }
  721. };
  722. /**
  723. * Handler for mouse over events for the tooltip element.
  724. *
  725. * @param {goog.events.BrowserEvent} event Event object.
  726. * @protected
  727. */
  728. goog.ui.Tooltip.prototype.handleTooltipMouseOver = function(event) {
  729. var element = this.getElement();
  730. if (this.activeEl_ != element) {
  731. this.clearHideTimer();
  732. this.activeEl_ = element;
  733. }
  734. };
  735. /**
  736. * Handler for mouse out events for the tooltip element.
  737. *
  738. * @param {goog.events.BrowserEvent} event Event object.
  739. * @protected
  740. */
  741. goog.ui.Tooltip.prototype.handleTooltipMouseOut = function(event) {
  742. var element = this.getElement();
  743. if (this.activeEl_ == element &&
  744. (!event.relatedTarget ||
  745. !goog.dom.contains(element, event.relatedTarget))) {
  746. this.activeEl_ = null;
  747. this.startHideTimer();
  748. }
  749. };
  750. /**
  751. * Helper method, starts timer that calls maybeShow. Parameters are passed to
  752. * the maybeShow method.
  753. *
  754. * @param {Element} el Element to show tooltip for.
  755. * @param {goog.positioning.AbstractPosition=} opt_pos Position to display popup
  756. * at.
  757. * @protected
  758. */
  759. goog.ui.Tooltip.prototype.startShowTimer = function(el, opt_pos) {
  760. if (!this.showTimer) {
  761. this.showTimer = goog.Timer.callOnce(
  762. goog.bind(this.maybeShow, this, el, opt_pos), this.showDelayMs_);
  763. }
  764. };
  765. /**
  766. * Helper method called to clear the show timer.
  767. *
  768. * @protected
  769. */
  770. goog.ui.Tooltip.prototype.clearShowTimer = function() {
  771. if (this.showTimer) {
  772. goog.Timer.clear(this.showTimer);
  773. this.showTimer = undefined;
  774. }
  775. };
  776. /**
  777. * Helper method called to start the close timer.
  778. * @protected
  779. */
  780. goog.ui.Tooltip.prototype.startHideTimer = function() {
  781. if (this.getState() == goog.ui.Tooltip.State.SHOWING) {
  782. this.hideTimer = goog.Timer.callOnce(
  783. goog.bind(this.maybeHide, this, this.anchor), this.getHideDelayMs());
  784. }
  785. };
  786. /**
  787. * Helper method called to clear the close timer.
  788. * @protected
  789. */
  790. goog.ui.Tooltip.prototype.clearHideTimer = function() {
  791. if (this.hideTimer) {
  792. goog.Timer.clear(this.hideTimer);
  793. this.hideTimer = undefined;
  794. }
  795. };
  796. /** @override */
  797. goog.ui.Tooltip.prototype.disposeInternal = function() {
  798. this.setVisible(false);
  799. this.clearShowTimer();
  800. this.detach();
  801. if (this.getElement()) {
  802. goog.dom.removeNode(this.getElement());
  803. }
  804. this.activeEl_ = null;
  805. delete this.dom_;
  806. goog.ui.Tooltip.superClass_.disposeInternal.call(this);
  807. };
  808. /**
  809. * Popup position implementation that positions the popup (the tooltip in this
  810. * case) based on the cursor position. It's positioned below the cursor to the
  811. * right if there's enough room to fit all of it inside the Viewport. Otherwise
  812. * it's displayed as far right as possible either above or below the element.
  813. *
  814. * Used to position tooltips triggered by the cursor.
  815. *
  816. * @param {number|!goog.math.Coordinate} arg1 Left position or coordinate.
  817. * @param {number=} opt_arg2 Top position.
  818. * @constructor
  819. * @extends {goog.positioning.ViewportPosition}
  820. * @final
  821. */
  822. goog.ui.Tooltip.CursorTooltipPosition = function(arg1, opt_arg2) {
  823. goog.positioning.ViewportPosition.call(this, arg1, opt_arg2);
  824. };
  825. goog.inherits(
  826. goog.ui.Tooltip.CursorTooltipPosition, goog.positioning.ViewportPosition);
  827. /**
  828. * Repositions the popup based on cursor position.
  829. *
  830. * @param {Element} element The DOM element of the popup.
  831. * @param {goog.positioning.Corner} popupCorner The corner of the popup element
  832. * that that should be positioned adjacent to the anchorElement.
  833. * @param {goog.math.Box=} opt_margin A margin specified in pixels.
  834. * @override
  835. */
  836. goog.ui.Tooltip.CursorTooltipPosition.prototype.reposition = function(
  837. element, popupCorner, opt_margin) {
  838. var viewportElt = goog.style.getClientViewportElement(element);
  839. var viewport = goog.style.getVisibleRectForElement(viewportElt);
  840. var margin = opt_margin ?
  841. new goog.math.Box(
  842. opt_margin.top + 10, opt_margin.right, opt_margin.bottom,
  843. opt_margin.left + 10) :
  844. new goog.math.Box(10, 0, 0, 10);
  845. if (goog.positioning.positionAtCoordinate(
  846. this.coordinate, element, goog.positioning.Corner.TOP_START, margin,
  847. viewport, goog.positioning.Overflow.ADJUST_X |
  848. goog.positioning.Overflow.FAIL_Y) &
  849. goog.positioning.OverflowStatus.FAILED) {
  850. goog.positioning.positionAtCoordinate(
  851. this.coordinate, element, goog.positioning.Corner.TOP_START, margin,
  852. viewport, goog.positioning.Overflow.ADJUST_X |
  853. goog.positioning.Overflow.ADJUST_Y);
  854. }
  855. };
  856. /**
  857. * Popup position implementation that positions the popup (the tooltip in this
  858. * case) based on the element position. It's positioned below the element to the
  859. * right if there's enough room to fit all of it inside the Viewport. Otherwise
  860. * it's displayed as far right as possible either above or below the element.
  861. *
  862. * Used to position tooltips triggered by focus changes.
  863. *
  864. * @param {Element} element The element to anchor the popup at.
  865. * @constructor
  866. * @extends {goog.positioning.AnchoredPosition}
  867. */
  868. goog.ui.Tooltip.ElementTooltipPosition = function(element) {
  869. goog.positioning.AnchoredPosition.call(
  870. this, element, goog.positioning.Corner.BOTTOM_RIGHT);
  871. };
  872. goog.inherits(
  873. goog.ui.Tooltip.ElementTooltipPosition, goog.positioning.AnchoredPosition);
  874. /**
  875. * Repositions the popup based on element position.
  876. *
  877. * @param {Element} element The DOM element of the popup.
  878. * @param {goog.positioning.Corner} popupCorner The corner of the popup element
  879. * that should be positioned adjacent to the anchorElement.
  880. * @param {goog.math.Box=} opt_margin A margin specified in pixels.
  881. * @override
  882. */
  883. goog.ui.Tooltip.ElementTooltipPosition.prototype.reposition = function(
  884. element, popupCorner, opt_margin) {
  885. var offset = new goog.math.Coordinate(10, 0);
  886. if (goog.positioning.positionAtAnchor(
  887. this.element, this.corner, element, popupCorner, offset, opt_margin,
  888. goog.positioning.Overflow.ADJUST_X |
  889. goog.positioning.Overflow.FAIL_Y) &
  890. goog.positioning.OverflowStatus.FAILED) {
  891. goog.positioning.positionAtAnchor(
  892. this.element, goog.positioning.Corner.TOP_RIGHT, element,
  893. goog.positioning.Corner.BOTTOM_LEFT, offset, opt_margin,
  894. goog.positioning.Overflow.ADJUST_X |
  895. goog.positioning.Overflow.ADJUST_Y);
  896. }
  897. };