TooltipToggle.js (6437B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const DEFAULT_TOGGLE_DELAY = 50; 8 9 /** 10 * Tooltip helper designed to show/hide the tooltip when the mouse hovers over 11 * particular nodes. 12 * 13 * This works by tracking mouse movements on a base container node (baseNode) 14 * and showing the tooltip when the mouse stops moving. A callback can be 15 * provided to the start() method to know whether or not the node being 16 * hovered over should indeed receive the tooltip. 17 */ 18 class TooltipToggle { 19 constructor(tooltip) { 20 this.tooltip = tooltip; 21 this.win = tooltip.doc.defaultView; 22 23 this._onMouseMove = this._onMouseMove.bind(this); 24 this._onMouseOut = this._onMouseOut.bind(this); 25 26 this._onTooltipMouseOver = this._onTooltipMouseOver.bind(this); 27 this._onTooltipMouseOut = this._onTooltipMouseOut.bind(this); 28 } 29 /** 30 * Start tracking mouse movements on the provided baseNode to show the 31 * tooltip. 32 * 33 * 2 Ways to make this work: 34 * - Provide a single node to attach the tooltip to, as the baseNode, and 35 * omit the second targetNodeCb argument 36 * - Provide a baseNode that is the container of possibly numerous children 37 * elements that may receive a tooltip. In this case, provide the second 38 * targetNodeCb argument to decide wether or not a child should receive 39 * a tooltip. 40 * 41 * Note that if you call this function a second time, it will itself call 42 * stop() before adding mouse tracking listeners again. 43 * 44 * @param {node} baseNode 45 * The container for all target nodes 46 * @param {Function} targetNodeCb 47 * A function that accepts a node argument and that checks if a tooltip 48 * should be displayed. Possible return values are: 49 * - false (or a falsy value) if the tooltip should not be displayed 50 * - true if the tooltip should be displayed 51 * - a DOM node to display the tooltip on the returned anchor 52 * The function can also return a promise that will resolve to one of 53 * the values listed above. 54 * If omitted, the tooltip will be shown everytime. 55 * @param {object} options 56 Set of optional arguments: 57 * - {Number} toggleDelay 58 * An optional delay (in ms) that will be observed before showing 59 * and before hiding the tooltip. Defaults to DEFAULT_TOGGLE_DELAY. 60 * - {Boolean} interactive 61 * If enabled, the tooltip is not hidden when mouse leaves the 62 * target element and enters the tooltip. Allows the tooltip 63 * content to be interactive. 64 */ 65 start( 66 baseNode, 67 targetNodeCb, 68 { toggleDelay = DEFAULT_TOGGLE_DELAY, interactive = false } = {} 69 ) { 70 this.stop(); 71 72 if (!baseNode) { 73 // Calling tool is in the process of being destroyed. 74 return; 75 } 76 77 this._baseNode = baseNode; 78 this._targetNodeCb = targetNodeCb || (() => true); 79 this._toggleDelay = toggleDelay; 80 this._interactive = interactive; 81 82 baseNode.addEventListener("mousemove", this._onMouseMove); 83 baseNode.addEventListener("mouseout", this._onMouseOut); 84 85 const target = this.tooltip.xulPanelWrapper || this.tooltip.container; 86 if (this._interactive) { 87 target.addEventListener("mouseover", this._onTooltipMouseOver); 88 target.addEventListener("mouseout", this._onTooltipMouseOut); 89 } else { 90 target.classList.add("non-interactive-toggle"); 91 } 92 } 93 94 /** 95 * If the start() function has been used previously, and you want to get rid 96 * of this behavior, then call this function to remove the mouse movement 97 * tracking 98 */ 99 stop() { 100 this.win.clearTimeout(this.toggleTimer); 101 102 if (!this._baseNode) { 103 return; 104 } 105 106 this._baseNode.removeEventListener("mousemove", this._onMouseMove); 107 this._baseNode.removeEventListener("mouseout", this._onMouseOut); 108 109 const target = this.tooltip.xulPanelWrapper || this.tooltip.container; 110 if (this._interactive) { 111 target.removeEventListener("mouseover", this._onTooltipMouseOver); 112 target.removeEventListener("mouseout", this._onTooltipMouseOut); 113 } else { 114 target.classList.remove("non-interactive-toggle"); 115 } 116 117 this._baseNode = null; 118 this._targetNodeCb = null; 119 this._lastHovered = null; 120 } 121 122 _onMouseMove(event) { 123 if (event.target !== this._lastHovered) { 124 this._lastHovered = event.target; 125 126 this.win.clearTimeout(this.toggleTimer); 127 this.toggleTimer = this.win.setTimeout(() => { 128 this.tooltip.hide(); 129 this.isValidHoverTarget(event.target).then( 130 target => { 131 if (target === null || !this._baseNode) { 132 // bail out if no target or if the toggle has been destroyed. 133 return; 134 } 135 this.tooltip.show(target); 136 }, 137 reason => { 138 console.error( 139 "isValidHoverTarget rejected with unexpected reason:" 140 ); 141 console.error(reason); 142 } 143 ); 144 }, this._toggleDelay); 145 } 146 } 147 148 /** 149 * Is the given target DOMNode a valid node for toggling the tooltip on hover. 150 * This delegates to the user-defined _targetNodeCb callback. 151 * 152 * @return {Promise} a promise that will resolve the anchor to use for the 153 * tooltip or null if no valid target was found. 154 */ 155 async isValidHoverTarget(target) { 156 const res = await this._targetNodeCb(target, this.tooltip); 157 if (res) { 158 return res.nodeName ? res : target; 159 } 160 161 return null; 162 } 163 164 _onMouseOut(event) { 165 // Only hide the tooltip if the mouse leaves baseNode. 166 if ( 167 event && 168 this._baseNode && 169 this._baseNode.contains(event.relatedTarget) 170 ) { 171 return; 172 } 173 174 this._lastHovered = null; 175 this.win.clearTimeout(this.toggleTimer); 176 this.toggleTimer = this.win.setTimeout(() => { 177 this.tooltip.hide(); 178 }, this._toggleDelay); 179 } 180 181 _onTooltipMouseOver() { 182 this.win.clearTimeout(this.toggleTimer); 183 } 184 185 _onTooltipMouseOut() { 186 this.win.clearTimeout(this.toggleTimer); 187 this.toggleTimer = this.win.setTimeout(() => { 188 this.tooltip.hide(); 189 }, this._toggleDelay); 190 } 191 192 destroy() { 193 this.stop(); 194 } 195 } 196 197 module.exports.TooltipToggle = TooltipToggle;