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 WebInspector.Popover = function(contentElement) 32 { 33 this.element = document.createElement("div"); 34 this.element.className = "popover"; 35 36 this._popupArrowElement = document.createElement("div"); 37 this._popupArrowElement.className = "arrow"; 38 this.element.appendChild(this._popupArrowElement); 39 40 this.contentElement = contentElement; 41 this._contentDiv = document.createElement("div"); 42 this._contentDiv.className = "content"; 43 this._visible = false; 44 } 45 46 WebInspector.Popover.prototype = { 47 show: function(anchor, preferredWidth, preferredHeight) 48 { 49 // This should not happen, but we hide previous popup to be on the safe side. 50 if (WebInspector.Popover._popoverElement) 51 document.body.removeChild(WebInspector.Popover._popoverElement); 52 WebInspector.Popover._popoverElement = this.element; 53 54 // Temporarily attach in order to measure preferred dimensions. 55 this.contentElement.positionAt(0, 0); 56 document.body.appendChild(this.contentElement); 57 var preferredWidth = preferredWidth || this.contentElement.offsetWidth; 58 var preferredHeight = preferredHeight || this.contentElement.offsetHeight; 59 60 this._contentDiv.appendChild(this.contentElement); 61 this.element.appendChild(this._contentDiv); 62 document.body.appendChild(this.element); 63 this._positionElement(anchor, preferredWidth, preferredHeight); 64 this._visible = true; 65 }, 66 67 hide: function() 68 { 69 if (WebInspector.Popover._popoverElement) { 70 delete WebInspector.Popover._popoverElement; 71 document.body.removeChild(this.element); 72 } 73 this._visible = false; 74 }, 75 76 get visible() 77 { 78 return this._visible; 79 }, 80 81 _positionElement: function(anchorElement, preferredWidth, preferredHeight) 82 { 83 const borderWidth = 25; 84 const scrollerWidth = 11; 85 const arrowHeight = 15; 86 const arrowOffset = 10; 87 const borderRadius = 10; 88 89 // Skinny tooltips are not pretty, their arrow location is not nice. 90 preferredWidth = Math.max(preferredWidth, 50); 91 const totalWidth = window.innerWidth; 92 const totalHeight = window.innerHeight; 93 94 var anchorBox = {x: anchorElement.totalOffsetLeft, y: anchorElement.totalOffsetTop, width: anchorElement.offsetWidth, height: anchorElement.offsetHeight}; 95 while (anchorElement !== document.body) { 96 if (anchorElement.scrollLeft) 97 anchorBox.x -= anchorElement.scrollLeft; 98 if (anchorElement.scrollTop) 99 anchorBox.y -= anchorElement.scrollTop; 100 anchorElement = anchorElement.parentElement; 101 } 102 103 var newElementPosition = { x: 0, y: 0, width: preferredWidth + scrollerWidth, height: preferredHeight }; 104 105 var verticalAlignment; 106 var roomAbove = anchorBox.y; 107 var roomBelow = totalHeight - anchorBox.y - anchorBox.height; 108 109 if (roomAbove > roomBelow) { 110 // Positioning above the anchor. 111 if (anchorBox.y > newElementPosition.height + arrowHeight + borderRadius) 112 newElementPosition.y = anchorBox.y - newElementPosition.height - arrowHeight; 113 else { 114 newElementPosition.y = borderRadius * 2; 115 newElementPosition.height = anchorBox.y - borderRadius * 2 - arrowHeight; 116 } 117 verticalAlignment = "bottom"; 118 } else { 119 // Positioning below the anchor. 120 newElementPosition.y = anchorBox.y + anchorBox.height + arrowHeight; 121 if (newElementPosition.y + newElementPosition.height + arrowHeight - borderWidth >= totalHeight) 122 newElementPosition.height = totalHeight - anchorBox.y - anchorBox.height - borderRadius * 2 - arrowHeight; 123 // Align arrow. 124 verticalAlignment = "top"; 125 } 126 127 var horizontalAlignment; 128 if (anchorBox.x + newElementPosition.width < totalWidth) { 129 newElementPosition.x = Math.max(borderRadius, anchorBox.x - borderRadius - arrowOffset); 130 horizontalAlignment = "left"; 131 } else if (newElementPosition.width + borderRadius * 2 < totalWidth) { 132 newElementPosition.x = totalWidth - newElementPosition.width - borderRadius; 133 horizontalAlignment = "right"; 134 // Position arrow accurately. 135 var arrowRightPosition = Math.max(0, totalWidth - anchorBox.x - anchorBox.width - borderRadius - arrowOffset); 136 arrowRightPosition += anchorBox.width / 2; 137 this._popupArrowElement.style.right = arrowRightPosition + "px"; 138 } else { 139 newElementPosition.x = borderRadius; 140 newElementPosition.width = totalWidth - borderRadius * 2; 141 newElementPosition.height += scrollerWidth; 142 horizontalAlignment = "left"; 143 if (verticalAlignment === "bottom") 144 newElementPosition.y -= scrollerWidth; 145 // Position arrow accurately. 146 this._popupArrowElement.style.left = Math.max(0, anchorBox.x - borderRadius * 2 - arrowOffset) + "px"; 147 this._popupArrowElement.style.left += anchorBox.width / 2; 148 } 149 150 this.element.className = "popover " + verticalAlignment + "-" + horizontalAlignment + "-arrow"; 151 this.element.positionAt(newElementPosition.x - borderWidth, newElementPosition.y - borderWidth); 152 this.element.style.width = newElementPosition.width + borderWidth * 2 + "px"; 153 this.element.style.height = newElementPosition.height + borderWidth * 2 + "px"; 154 } 155 } 156 157 WebInspector.PopoverHelper = function(panelElement, getAnchor, showPopup, showOnClick, onHide) 158 { 159 this._panelElement = panelElement; 160 this._getAnchor = getAnchor; 161 this._showPopup = showPopup; 162 this._showOnClick = showOnClick; 163 this._onHide = onHide; 164 panelElement.addEventListener("mousedown", this._mouseDown.bind(this), false); 165 panelElement.addEventListener("mousemove", this._mouseMove.bind(this), false); 166 this.setTimeout(1000); 167 } 168 169 WebInspector.PopoverHelper.prototype = { 170 setTimeout: function(timeout) 171 { 172 this._timeout = timeout; 173 }, 174 175 _mouseDown: function(event) 176 { 177 this._killHidePopupTimer(); 178 this._handleMouseAction(event, true); 179 }, 180 181 _mouseMove: function(event) 182 { 183 // Pretend that nothing has happened. 184 if (this._hoverElement === event.target || (this._hoverElement && this._hoverElement.isAncestor(event.target))) 185 return; 186 187 // User has 500ms (this._timeout / 2) to reach the popup. 188 if (this._popup && !this._hidePopupTimer) { 189 var self = this; 190 function doHide() 191 { 192 self._hidePopup(); 193 delete self._hidePopupTimer; 194 } 195 this._hidePopupTimer = setTimeout(doHide, this._timeout / 2); 196 } 197 198 this._handleMouseAction(event); 199 }, 200 201 _handleMouseAction: function(event, isMouseDown) 202 { 203 this._resetHoverTimer(); 204 205 this._hoverElement = this._getAnchor(event.target); 206 if (!this._hoverElement) 207 return; 208 209 const toolTipDelay = isMouseDown ? 0 : (this._popup ? this._timeout * 0.6 : this._timeout); 210 this._hoverTimer = setTimeout(this._mouseHover.bind(this, this._hoverElement), toolTipDelay); 211 }, 212 213 _resetHoverTimer: function() 214 { 215 if (this._hoverTimer) { 216 clearTimeout(this._hoverTimer); 217 delete this._hoverTimer; 218 } 219 }, 220 221 hidePopup: function() 222 { 223 this._resetHoverTimer(); 224 this._hidePopup(); 225 }, 226 227 _hidePopup: function() 228 { 229 if (!this._popup) 230 return; 231 232 if (this._onHide) 233 this._onHide(); 234 235 this._popup.hide(); 236 delete this._popup; 237 }, 238 239 _mouseHover: function(element) 240 { 241 delete this._hoverTimer; 242 243 this._popup = this._showPopup(element); 244 if (this._popup) 245 this._popup.contentElement.addEventListener("mousemove", this._killHidePopupTimer.bind(this), true); 246 }, 247 248 _killHidePopupTimer: function() 249 { 250 if (this._hidePopupTimer) { 251 clearTimeout(this._hidePopupTimer); 252 delete this._hidePopupTimer; 253 254 // We know that we reached the popup, but we might have moved over other elements. 255 // Discard pending command. 256 this._resetHoverTimer(); 257 } 258 } 259 } 260