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 */ 36WebInspector.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"; // Override 41 this._containerElement = document.createElementWithClass("div", "fill popover-container"); 42 43 this._popupArrowElement = this.element.createChild("div", "arrow"); 44 this._contentDiv = this.element.createChild("div", "content"); 45 46 this._popoverHelper = popoverHelper; 47 this._hideBound = this.hide.bind(this); 48} 49 50WebInspector.Popover.prototype = { 51 /** 52 * @param {!Element} element 53 * @param {!Element|!AnchorBox} anchor 54 * @param {?number=} preferredWidth 55 * @param {?number=} preferredHeight 56 * @param {?WebInspector.Popover.Orientation=} arrowDirection 57 */ 58 show: function(element, anchor, preferredWidth, preferredHeight, arrowDirection) 59 { 60 this._innerShow(null, element, anchor, preferredWidth, preferredHeight, arrowDirection); 61 }, 62 63 /** 64 * @param {!WebInspector.View} view 65 * @param {!Element|!AnchorBox} anchor 66 * @param {?number=} preferredWidth 67 * @param {?number=} preferredHeight 68 */ 69 showView: function(view, anchor, preferredWidth, preferredHeight) 70 { 71 this._innerShow(view, view.element, anchor, preferredWidth, preferredHeight); 72 }, 73 74 /** 75 * @param {?WebInspector.View} view 76 * @param {!Element} contentElement 77 * @param {!Element|!AnchorBox} anchor 78 * @param {?number=} preferredWidth 79 * @param {?number=} preferredHeight 80 * @param {?WebInspector.Popover.Orientation=} arrowDirection 81 */ 82 _innerShow: function(view, contentElement, anchor, preferredWidth, preferredHeight, arrowDirection) 83 { 84 if (this._disposed) 85 return; 86 this.contentElement = contentElement; 87 88 // This should not happen, but we hide previous popup to be on the safe side. 89 if (WebInspector.Popover._popover) 90 WebInspector.Popover._popover.hide(); 91 WebInspector.Popover._popover = this; 92 93 // Temporarily attach in order to measure preferred dimensions. 94 var preferredSize = view ? view.measurePreferredSize() : this.contentElement.measurePreferredSize(); 95 preferredWidth = preferredWidth || preferredSize.width; 96 preferredHeight = preferredHeight || preferredSize.height; 97 98 window.addEventListener("resize", this._hideBound, false); 99 document.body.appendChild(this._containerElement); 100 WebInspector.View.prototype.show.call(this, this._containerElement); 101 102 if (view) 103 view.show(this._contentDiv); 104 else 105 this._contentDiv.appendChild(this.contentElement); 106 107 this._positionElement(anchor, preferredWidth, preferredHeight, arrowDirection); 108 109 if (this._popoverHelper) { 110 this._contentDiv.addEventListener("mousemove", this._popoverHelper._killHidePopoverTimer.bind(this._popoverHelper), true); 111 this.element.addEventListener("mouseout", this._popoverHelper._popoverMouseOut.bind(this._popoverHelper), true); 112 } 113 }, 114 115 hide: function() 116 { 117 window.removeEventListener("resize", this._hideBound, false); 118 this.detach(); 119 this._containerElement.remove(); 120 delete WebInspector.Popover._popover; 121 }, 122 123 get disposed() 124 { 125 return this._disposed; 126 }, 127 128 dispose: function() 129 { 130 if (this.isShowing()) 131 this.hide(); 132 this._disposed = true; 133 }, 134 135 setCanShrink: function(canShrink) 136 { 137 this._hasFixedHeight = !canShrink; 138 this._contentDiv.classList.add("fixed-height"); 139 }, 140 141 /** 142 * @param {!Element|!AnchorBox} anchorElement 143 * @param {number} preferredWidth 144 * @param {number} preferredHeight 145 * @param {?WebInspector.Popover.Orientation=} arrowDirection 146 */ 147 _positionElement: function(anchorElement, preferredWidth, preferredHeight, arrowDirection) 148 { 149 const borderWidth = 25; 150 const scrollerWidth = this._hasFixedHeight ? 0 : 11; 151 const arrowHeight = 15; 152 const arrowOffset = 10; 153 const borderRadius = 10; 154 155 // Skinny tooltips are not pretty, their arrow location is not nice. 156 preferredWidth = Math.max(preferredWidth, 50); 157 // Position relative to main DevTools element. 158 const container = WebInspector.Dialog.modalHostView().element; 159 const totalWidth = container.offsetWidth; 160 const totalHeight = container.offsetHeight; 161 162 var anchorBox = anchorElement instanceof AnchorBox ? anchorElement : anchorElement.boxInWindow(window); 163 anchorBox = anchorBox.relativeToElement(container); 164 var newElementPosition = { x: 0, y: 0, width: preferredWidth + scrollerWidth, height: preferredHeight }; 165 166 var verticalAlignment; 167 var roomAbove = anchorBox.y; 168 var roomBelow = totalHeight - anchorBox.y - anchorBox.height; 169 170 if ((roomAbove > roomBelow) || (arrowDirection === WebInspector.Popover.Orientation.Bottom)) { 171 // Positioning above the anchor. 172 if ((anchorBox.y > newElementPosition.height + arrowHeight + borderRadius) || (arrowDirection === WebInspector.Popover.Orientation.Bottom)) 173 newElementPosition.y = anchorBox.y - newElementPosition.height - arrowHeight; 174 else { 175 newElementPosition.y = borderRadius; 176 newElementPosition.height = anchorBox.y - borderRadius * 2 - arrowHeight; 177 if (this._hasFixedHeight && newElementPosition.height < preferredHeight) { 178 newElementPosition.y = borderRadius; 179 newElementPosition.height = preferredHeight; 180 } 181 } 182 verticalAlignment = WebInspector.Popover.Orientation.Bottom; 183 } else { 184 // Positioning below the anchor. 185 newElementPosition.y = anchorBox.y + anchorBox.height + arrowHeight; 186 if ((newElementPosition.y + newElementPosition.height + borderRadius >= totalHeight) && (arrowDirection !== WebInspector.Popover.Orientation.Top)) { 187 newElementPosition.height = totalHeight - borderRadius - newElementPosition.y; 188 if (this._hasFixedHeight && newElementPosition.height < preferredHeight) { 189 newElementPosition.y = totalHeight - preferredHeight - borderRadius; 190 newElementPosition.height = preferredHeight; 191 } 192 } 193 // Align arrow. 194 verticalAlignment = WebInspector.Popover.Orientation.Top; 195 } 196 197 var horizontalAlignment; 198 if (anchorBox.x + newElementPosition.width < totalWidth) { 199 newElementPosition.x = Math.max(borderRadius, anchorBox.x - borderRadius - arrowOffset); 200 horizontalAlignment = "left"; 201 } else if (newElementPosition.width + borderRadius * 2 < totalWidth) { 202 newElementPosition.x = totalWidth - newElementPosition.width - borderRadius; 203 horizontalAlignment = "right"; 204 // Position arrow accurately. 205 var arrowRightPosition = Math.max(0, totalWidth - anchorBox.x - anchorBox.width - borderRadius - arrowOffset); 206 arrowRightPosition += anchorBox.width / 2; 207 arrowRightPosition = Math.min(arrowRightPosition, newElementPosition.width - borderRadius - arrowOffset); 208 this._popupArrowElement.style.right = arrowRightPosition + "px"; 209 } else { 210 newElementPosition.x = borderRadius; 211 newElementPosition.width = totalWidth - borderRadius * 2; 212 newElementPosition.height += scrollerWidth; 213 horizontalAlignment = "left"; 214 if (verticalAlignment === WebInspector.Popover.Orientation.Bottom) 215 newElementPosition.y -= scrollerWidth; 216 // Position arrow accurately. 217 this._popupArrowElement.style.left = Math.max(0, anchorBox.x - borderRadius * 2 - arrowOffset) + "px"; 218 this._popupArrowElement.style.left += anchorBox.width / 2; 219 } 220 221 this.element.className = "popover custom-popup-vertical-scroll custom-popup-horizontal-scroll " + verticalAlignment + "-" + horizontalAlignment + "-arrow"; 222 this.element.positionAt(newElementPosition.x - borderWidth, newElementPosition.y - borderWidth, container); 223 this.element.style.width = newElementPosition.width + borderWidth * 2 + "px"; 224 this.element.style.height = newElementPosition.height + borderWidth * 2 + "px"; 225 }, 226 227 __proto__: WebInspector.View.prototype 228} 229 230/** 231 * @constructor 232 * @param {!Element} panelElement 233 * @param {function(!Element, !Event):(!Element|!AnchorBox|undefined)} getAnchor 234 * @param {function(!Element, !WebInspector.Popover):undefined} showPopover 235 * @param {function()=} onHide 236 * @param {boolean=} disableOnClick 237 */ 238WebInspector.PopoverHelper = function(panelElement, getAnchor, showPopover, onHide, disableOnClick) 239{ 240 this._panelElement = panelElement; 241 this._getAnchor = getAnchor; 242 this._showPopover = showPopover; 243 this._onHide = onHide; 244 this._disableOnClick = !!disableOnClick; 245 panelElement.addEventListener("mousedown", this._mouseDown.bind(this), false); 246 panelElement.addEventListener("mousemove", this._mouseMove.bind(this), false); 247 panelElement.addEventListener("mouseout", this._mouseOut.bind(this), false); 248 this.setTimeout(1000, 500); 249} 250 251WebInspector.PopoverHelper.prototype = { 252 /** 253 * @param {number} timeout 254 * @param {number=} hideTimeout 255 */ 256 setTimeout: function(timeout, hideTimeout) 257 { 258 this._timeout = timeout; 259 if (typeof hideTimeout === "number") 260 this._hideTimeout = hideTimeout; 261 else 262 this._hideTimeout = timeout / 2; 263 }, 264 265 /** 266 * @param {!MouseEvent} event 267 * @return {boolean} 268 */ 269 _eventInHoverElement: function(event) 270 { 271 if (!this._hoverElement) 272 return false; 273 var box = this._hoverElement instanceof AnchorBox ? this._hoverElement : this._hoverElement.boxInWindow(); 274 return (box.x <= event.clientX && event.clientX <= box.x + box.width && 275 box.y <= event.clientY && event.clientY <= box.y + box.height); 276 }, 277 278 _mouseDown: function(event) 279 { 280 if (this._disableOnClick || !this._eventInHoverElement(event)) 281 this.hidePopover(); 282 else { 283 this._killHidePopoverTimer(); 284 this._handleMouseAction(event, true); 285 } 286 }, 287 288 _mouseMove: function(event) 289 { 290 // Pretend that nothing has happened. 291 if (this._eventInHoverElement(event)) 292 return; 293 294 this._startHidePopoverTimer(); 295 this._handleMouseAction(event, false); 296 }, 297 298 _popoverMouseOut: function(event) 299 { 300 if (!this.isPopoverVisible()) 301 return; 302 if (event.relatedTarget && !event.relatedTarget.isSelfOrDescendant(this._popover._contentDiv)) 303 this._startHidePopoverTimer(); 304 }, 305 306 _mouseOut: function(event) 307 { 308 if (!this.isPopoverVisible()) 309 return; 310 if (!this._eventInHoverElement(event)) 311 this._startHidePopoverTimer(); 312 }, 313 314 _startHidePopoverTimer: function() 315 { 316 // User has 500ms (this._hideTimeout) to reach the popup. 317 if (!this._popover || this._hidePopoverTimer) 318 return; 319 320 /** 321 * @this {WebInspector.PopoverHelper} 322 */ 323 function doHide() 324 { 325 this._hidePopover(); 326 delete this._hidePopoverTimer; 327 } 328 this._hidePopoverTimer = setTimeout(doHide.bind(this), this._hideTimeout); 329 }, 330 331 _handleMouseAction: function(event, isMouseDown) 332 { 333 this._resetHoverTimer(); 334 if (event.which && this._disableOnClick) 335 return; 336 this._hoverElement = this._getAnchor(event.target, event); 337 if (!this._hoverElement) 338 return; 339 const toolTipDelay = isMouseDown ? 0 : (this._popup ? this._timeout * 0.6 : this._timeout); 340 this._hoverTimer = setTimeout(this._mouseHover.bind(this, this._hoverElement), toolTipDelay); 341 }, 342 343 _resetHoverTimer: function() 344 { 345 if (this._hoverTimer) { 346 clearTimeout(this._hoverTimer); 347 delete this._hoverTimer; 348 } 349 }, 350 351 /** 352 * @return {boolean} 353 */ 354 isPopoverVisible: function() 355 { 356 return !!this._popover; 357 }, 358 359 hidePopover: function() 360 { 361 this._resetHoverTimer(); 362 this._hidePopover(); 363 }, 364 365 _hidePopover: function() 366 { 367 if (!this._popover) 368 return; 369 370 if (this._onHide) 371 this._onHide(); 372 373 this._popover.dispose(); 374 delete this._popover; 375 this._hoverElement = null; 376 }, 377 378 _mouseHover: function(element) 379 { 380 delete this._hoverTimer; 381 382 this._hidePopover(); 383 this._popover = new WebInspector.Popover(this); 384 this._showPopover(element, this._popover); 385 }, 386 387 _killHidePopoverTimer: function() 388 { 389 if (this._hidePopoverTimer) { 390 clearTimeout(this._hidePopoverTimer); 391 delete this._hidePopoverTimer; 392 393 // We know that we reached the popup, but we might have moved over other elements. 394 // Discard pending command. 395 this._resetHoverTimer(); 396 } 397 } 398} 399 400/** @enum {string} */ 401WebInspector.Popover.Orientation = { 402 Top: "top", 403 Bottom: "bottom" 404} 405