1 /* 2 * Copyright (C) 2009 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 /** 32 * @constructor 33 * @extends {WebInspector.View} 34 * @param {!WebInspector.PopoverHelper=} popoverHelper 35 */ 36 WebInspector.Popover = function(popoverHelper) 37 { 38 WebInspector.View.call(this); 39 this.markAsRoot(); 40 this.element.className = "popover custom-popup-vertical-scroll custom-popup-horizontal-scroll"; 41 42 this._popupArrowElement = document.createElement("div"); 43 this._popupArrowElement.className = "arrow"; 44 this.element.appendChild(this._popupArrowElement); 45 46 this._contentDiv = document.createElement("div"); 47 this._contentDiv.className = "content"; 48 this.element.appendChild(this._contentDiv); 49 50 this._popoverHelper = popoverHelper; 51 } 52 53 WebInspector.Popover.prototype = { 54 /** 55 * @param {!Element} element 56 * @param {!Element|!AnchorBox} anchor 57 * @param {?number=} preferredWidth 58 * @param {?number=} preferredHeight 59 * @param {?WebInspector.Popover.Orientation=} arrowDirection 60 */ 61 show: function(element, anchor, preferredWidth, preferredHeight, arrowDirection) 62 { 63 this._innerShow(null, element, anchor, preferredWidth, preferredHeight, arrowDirection); 64 }, 65 66 /** 67 * @param {!WebInspector.View} view 68 * @param {!Element|!AnchorBox} anchor 69 * @param {?number=} preferredWidth 70 * @param {?number=} preferredHeight 71 */ 72 showView: function(view, anchor, preferredWidth, preferredHeight) 73 { 74 this._innerShow(view, view.element, anchor, preferredWidth, preferredHeight); 75 }, 76 77 /** 78 * @param {?WebInspector.View} view 79 * @param {!Element} contentElement 80 * @param {!Element|!AnchorBox} anchor 81 * @param {?number=} preferredWidth 82 * @param {?number=} preferredHeight 83 * @param {?WebInspector.Popover.Orientation=} arrowDirection 84 */ 85 _innerShow: function(view, contentElement, anchor, preferredWidth, preferredHeight, arrowDirection) 86 { 87 if (this._disposed) 88 return; 89 this.contentElement = contentElement; 90 91 // This should not happen, but we hide previous popup to be on the safe side. 92 if (WebInspector.Popover._popover) 93 WebInspector.Popover._popover.detach(); 94 WebInspector.Popover._popover = this; 95 96 // Temporarily attach in order to measure preferred dimensions. 97 var preferredSize = view ? view.measurePreferredSize() : this.contentElement.measurePreferredSize(); 98 preferredWidth = preferredWidth || preferredSize.width; 99 preferredHeight = preferredHeight || preferredSize.height; 100 101 WebInspector.View.prototype.show.call(this, document.body); 102 103 if (view) 104 view.show(this._contentDiv); 105 else 106 this._contentDiv.appendChild(this.contentElement); 107 108 this._positionElement(anchor, preferredWidth, preferredHeight, arrowDirection); 109 110 if (this._popoverHelper) { 111 this._contentDiv.addEventListener("mousemove", this._popoverHelper._killHidePopoverTimer.bind(this._popoverHelper), true); 112 this.element.addEventListener("mouseout", this._popoverHelper._popoverMouseOut.bind(this._popoverHelper), true); 113 } 114 }, 115 116 hide: function() 117 { 118 this.detach(); 119 delete WebInspector.Popover._popover; 120 }, 121 122 get disposed() 123 { 124 return this._disposed; 125 }, 126 127 dispose: function() 128 { 129 if (this.isShowing()) 130 this.hide(); 131 this._disposed = true; 132 }, 133 134 setCanShrink: function(canShrink) 135 { 136 this._hasFixedHeight = !canShrink; 137 this._contentDiv.classList.add("fixed-height"); 138 }, 139 140 /** 141 * @param {!Element|!AnchorBox} anchorElement 142 * @param {number} preferredWidth 143 * @param {number} preferredHeight 144 * @param {?WebInspector.Popover.Orientation=} arrowDirection 145 */ 146 _positionElement: function(anchorElement, preferredWidth, preferredHeight, arrowDirection) 147 { 148 const borderWidth = 25; 149 const scrollerWidth = this._hasFixedHeight ? 0 : 11; 150 const arrowHeight = 15; 151 const arrowOffset = 10; 152 const borderRadius = 10; 153 154 // Skinny tooltips are not pretty, their arrow location is not nice. 155 preferredWidth = Math.max(preferredWidth, 50); 156 const totalWidth = window.innerWidth; 157 const totalHeight = window.innerHeight; 158 159 var anchorBox = anchorElement instanceof AnchorBox ? anchorElement : anchorElement.boxInWindow(window); 160 var newElementPosition = { x: 0, y: 0, width: preferredWidth + scrollerWidth, height: preferredHeight }; 161 162 var verticalAlignment; 163 var roomAbove = anchorBox.y; 164 var roomBelow = totalHeight - anchorBox.y - anchorBox.height; 165 166 if ((roomAbove > roomBelow) || (arrowDirection === WebInspector.Popover.Orientation.Bottom)) { 167 // Positioning above the anchor. 168 if ((anchorBox.y > newElementPosition.height + arrowHeight + borderRadius) || (arrowDirection === WebInspector.Popover.Orientation.Bottom)) 169 newElementPosition.y = anchorBox.y - newElementPosition.height - arrowHeight; 170 else { 171 newElementPosition.y = borderRadius; 172 newElementPosition.height = anchorBox.y - borderRadius * 2 - arrowHeight; 173 if (this._hasFixedHeight && newElementPosition.height < preferredHeight) { 174 newElementPosition.y = borderRadius; 175 newElementPosition.height = preferredHeight; 176 } 177 } 178 verticalAlignment = WebInspector.Popover.Orientation.Bottom; 179 } else { 180 // Positioning below the anchor. 181 newElementPosition.y = anchorBox.y + anchorBox.height + arrowHeight; 182 if ((newElementPosition.y + newElementPosition.height + arrowHeight - borderWidth >= totalHeight) && (arrowDirection !== WebInspector.Popover.Orientation.Top)) { 183 newElementPosition.height = totalHeight - anchorBox.y - anchorBox.height - borderRadius * 2 - arrowHeight; 184 if (this._hasFixedHeight && newElementPosition.height < preferredHeight) { 185 newElementPosition.y = totalHeight - preferredHeight - borderRadius; 186 newElementPosition.height = preferredHeight; 187 } 188 } 189 // Align arrow. 190 verticalAlignment = WebInspector.Popover.Orientation.Top; 191 } 192 193 var horizontalAlignment; 194 if (anchorBox.x + newElementPosition.width < totalWidth) { 195 newElementPosition.x = Math.max(borderRadius, anchorBox.x - borderRadius - arrowOffset); 196 horizontalAlignment = "left"; 197 } else if (newElementPosition.width + borderRadius * 2 < totalWidth) { 198 newElementPosition.x = totalWidth - newElementPosition.width - borderRadius; 199 horizontalAlignment = "right"; 200 // Position arrow accurately. 201 var arrowRightPosition = Math.max(0, totalWidth - anchorBox.x - anchorBox.width - borderRadius - arrowOffset); 202 arrowRightPosition += anchorBox.width / 2; 203 arrowRightPosition = Math.min(arrowRightPosition, newElementPosition.width - borderRadius - arrowOffset); 204 this._popupArrowElement.style.right = arrowRightPosition + "px"; 205 } else { 206 newElementPosition.x = borderRadius; 207 newElementPosition.width = totalWidth - borderRadius * 2; 208 newElementPosition.height += scrollerWidth; 209 horizontalAlignment = "left"; 210 if (verticalAlignment === WebInspector.Popover.Orientation.Bottom) 211 newElementPosition.y -= scrollerWidth; 212 // Position arrow accurately. 213 this._popupArrowElement.style.left = Math.max(0, anchorBox.x - borderRadius * 2 - arrowOffset) + "px"; 214 this._popupArrowElement.style.left += anchorBox.width / 2; 215 } 216 217 this.element.className = "popover custom-popup-vertical-scroll custom-popup-horizontal-scroll " + verticalAlignment + "-" + horizontalAlignment + "-arrow"; 218 this.element.positionAt(newElementPosition.x - borderWidth, newElementPosition.y - borderWidth); 219 this.element.style.width = newElementPosition.width + borderWidth * 2 + "px"; 220 this.element.style.height = newElementPosition.height + borderWidth * 2 + "px"; 221 }, 222 223 __proto__: WebInspector.View.prototype 224 } 225 226 /** 227 * @constructor 228 * @param {!Element} panelElement 229 * @param {function(!Element, !Event):(!Element|!AnchorBox)|undefined} getAnchor 230 * @param {function(!Element, !WebInspector.Popover):undefined} showPopover 231 * @param {function()=} onHide 232 * @param {boolean=} disableOnClick 233 */ 234 WebInspector.PopoverHelper = function(panelElement, getAnchor, showPopover, onHide, disableOnClick) 235 { 236 this._panelElement = panelElement; 237 this._getAnchor = getAnchor; 238 this._showPopover = showPopover; 239 this._onHide = onHide; 240 this._disableOnClick = !!disableOnClick; 241 panelElement.addEventListener("mousedown", this._mouseDown.bind(this), false); 242 panelElement.addEventListener("mousemove", this._mouseMove.bind(this), false); 243 panelElement.addEventListener("mouseout", this._mouseOut.bind(this), false); 244 this.setTimeout(1000); 245 } 246 247 WebInspector.PopoverHelper.prototype = { 248 setTimeout: function(timeout) 249 { 250 this._timeout = timeout; 251 }, 252 253 /** 254 * @param {!MouseEvent} event 255 * @return {boolean} 256 */ 257 _eventInHoverElement: function(event) 258 { 259 if (!this._hoverElement) 260 return false; 261 var box = this._hoverElement instanceof AnchorBox ? this._hoverElement : this._hoverElement.boxInWindow(); 262 return (box.x <= event.clientX && event.clientX <= box.x + box.width && 263 box.y <= event.clientY && event.clientY <= box.y + box.height); 264 }, 265 266 _mouseDown: function(event) 267 { 268 if (this._disableOnClick || !this._eventInHoverElement(event)) 269 this.hidePopover(); 270 else { 271 this._killHidePopoverTimer(); 272 this._handleMouseAction(event, true); 273 } 274 }, 275 276 _mouseMove: function(event) 277 { 278 // Pretend that nothing has happened. 279 if (this._eventInHoverElement(event)) 280 return; 281 282 this._startHidePopoverTimer(); 283 this._handleMouseAction(event, false); 284 }, 285 286 _popoverMouseOut: function(event) 287 { 288 if (!this.isPopoverVisible()) 289 return; 290 if (event.relatedTarget && !event.relatedTarget.isSelfOrDescendant(this._popover._contentDiv)) 291 this._startHidePopoverTimer(); 292 }, 293 294 _mouseOut: function(event) 295 { 296 if (!this.isPopoverVisible()) 297 return; 298 if (!this._eventInHoverElement(event)) 299 this._startHidePopoverTimer(); 300 }, 301 302 _startHidePopoverTimer: function() 303 { 304 // User has 500ms (this._timeout / 2) to reach the popup. 305 if (!this._popover || this._hidePopoverTimer) 306 return; 307 308 /** 309 * @this {WebInspector.PopoverHelper} 310 */ 311 function doHide() 312 { 313 this._hidePopover(); 314 delete this._hidePopoverTimer; 315 } 316 this._hidePopoverTimer = setTimeout(doHide.bind(this), this._timeout / 2); 317 }, 318 319 _handleMouseAction: function(event, isMouseDown) 320 { 321 this._resetHoverTimer(); 322 if (event.which && this._disableOnClick) 323 return; 324 this._hoverElement = this._getAnchor(event.target, event); 325 if (!this._hoverElement) 326 return; 327 const toolTipDelay = isMouseDown ? 0 : (this._popup ? this._timeout * 0.6 : this._timeout); 328 this._hoverTimer = setTimeout(this._mouseHover.bind(this, this._hoverElement), toolTipDelay); 329 }, 330 331 _resetHoverTimer: function() 332 { 333 if (this._hoverTimer) { 334 clearTimeout(this._hoverTimer); 335 delete this._hoverTimer; 336 } 337 }, 338 339 isPopoverVisible: function() 340 { 341 return !!this._popover; 342 }, 343 344 hidePopover: function() 345 { 346 this._resetHoverTimer(); 347 this._hidePopover(); 348 }, 349 350 _hidePopover: function() 351 { 352 if (!this._popover) 353 return; 354 355 if (this._onHide) 356 this._onHide(); 357 358 this._popover.dispose(); 359 delete this._popover; 360 this._hoverElement = null; 361 }, 362 363 _mouseHover: function(element) 364 { 365 delete this._hoverTimer; 366 367 this._hidePopover(); 368 this._popover = new WebInspector.Popover(this); 369 this._showPopover(element, this._popover); 370 }, 371 372 _killHidePopoverTimer: function() 373 { 374 if (this._hidePopoverTimer) { 375 clearTimeout(this._hidePopoverTimer); 376 delete this._hidePopoverTimer; 377 378 // We know that we reached the popup, but we might have moved over other elements. 379 // Discard pending command. 380 this._resetHoverTimer(); 381 } 382 } 383 } 384 385 /** @enum {string} */ 386 WebInspector.Popover.Orientation = { 387 Top: "top", 388 Bottom: "bottom" 389 } 390