2 * Copyright (C) 2009 Google Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
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
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.
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.
33 * @extends {WebInspector.Widget}
34 * @param {!WebInspector.PopoverHelper=} popoverHelper
36 WebInspector
.Popover = function(popoverHelper
)
38 WebInspector
.Widget
.call(this);
40 this.element
.className
= WebInspector
.Popover
._classNamePrefix
; // Override
41 this._containerElement
= createElementWithClass("div", "fill popover-container");
43 this._popupArrowElement
= this.element
.createChild("div", "arrow");
44 this._contentDiv
= this.element
.createChild("div", "content");
46 this._popoverHelper
= popoverHelper
;
47 this._hideBound
= this.hide
.bind(this);
50 WebInspector
.Popover
._classNamePrefix
= "popover custom-popup-vertical-scroll custom-popup-horizontal-scroll";
52 WebInspector
.Popover
.prototype = {
54 * @param {!Element} element
55 * @param {!Element|!AnchorBox} anchor
56 * @param {?number=} preferredWidth
57 * @param {?number=} preferredHeight
58 * @param {?WebInspector.Popover.Orientation=} arrowDirection
60 showForAnchor: function(element
, anchor
, preferredWidth
, preferredHeight
, arrowDirection
)
62 this._innerShow(null, element
, anchor
, preferredWidth
, preferredHeight
, arrowDirection
);
66 * @param {!WebInspector.Widget} view
67 * @param {!Element|!AnchorBox} anchor
68 * @param {?number=} preferredWidth
69 * @param {?number=} preferredHeight
71 showView: function(view
, anchor
, preferredWidth
, preferredHeight
)
73 this._innerShow(view
, view
.element
, anchor
, preferredWidth
, preferredHeight
);
77 * @param {?WebInspector.Widget} view
78 * @param {!Element} contentElement
79 * @param {!Element|!AnchorBox} anchor
80 * @param {?number=} preferredWidth
81 * @param {?number=} preferredHeight
82 * @param {?WebInspector.Popover.Orientation=} arrowDirection
84 _innerShow: function(view
, contentElement
, anchor
, preferredWidth
, preferredHeight
, arrowDirection
)
88 this._contentElement
= contentElement
;
90 // This should not happen, but we hide previous popup to be on the safe side.
91 if (WebInspector
.Popover
._popover
)
92 WebInspector
.Popover
._popover
.hide();
93 WebInspector
.Popover
._popover
= this;
95 var document
= anchor
instanceof Element
? anchor
.ownerDocument
: contentElement
.ownerDocument
;
96 var window
= document
.defaultView
;
98 // Temporarily attach in order to measure preferred dimensions.
99 var preferredSize
= view
? view
.measurePreferredSize() : WebInspector
.measurePreferredSize(this._contentElement
);
100 this._preferredWidth
= preferredWidth
|| preferredSize
.width
;
101 this._preferredHeight
= preferredHeight
|| preferredSize
.height
;
103 window
.addEventListener("resize", this._hideBound
, false);
104 document
.body
.appendChild(this._containerElement
);
105 WebInspector
.Widget
.prototype.show
.call(this, this._containerElement
);
108 view
.show(this._contentDiv
);
110 this._contentDiv
.appendChild(this._contentElement
);
112 this.positionElement(anchor
, this._preferredWidth
, this._preferredHeight
, arrowDirection
);
114 if (this._popoverHelper
) {
115 this._contentDiv
.addEventListener("mousemove", this._popoverHelper
._killHidePopoverTimer
.bind(this._popoverHelper
), true);
116 this.element
.addEventListener("mouseout", this._popoverHelper
._popoverMouseOut
.bind(this._popoverHelper
), true);
122 this._containerElement
.ownerDocument
.defaultView
.removeEventListener("resize", this._hideBound
, false);
124 this._containerElement
.remove();
125 delete WebInspector
.Popover
._popover
;
130 return this._disposed
;
135 if (this.isShowing())
137 this._disposed
= true;
141 * @param {boolean} canShrink
143 setCanShrink: function(canShrink
)
145 this._hasFixedHeight
= !canShrink
;
146 this._contentDiv
.classList
.toggle("fixed-height", this._hasFixedHeight
);
150 * @param {boolean} noMargins
152 setNoMargins: function(noMargins
)
154 this._hasNoMargins
= noMargins
;
155 this._contentDiv
.classList
.toggle("no-margin", this._hasNoMargins
);
159 * @param {!Element|!AnchorBox} anchorElement
160 * @param {number=} preferredWidth
161 * @param {number=} preferredHeight
162 * @param {?WebInspector.Popover.Orientation=} arrowDirection
164 positionElement: function(anchorElement
, preferredWidth
, preferredHeight
, arrowDirection
)
166 const borderWidth
= this._hasNoMargins
? 0 : 8;
167 const scrollerWidth
= this._hasFixedHeight
? 0 : 11;
168 const arrowHeight
= this._hasNoMargins
? 8 : 15;
169 const arrowOffset
= 10;
170 const borderRadius
= 4;
171 const arrowRadius
= 6;
172 preferredWidth
= preferredWidth
|| this._preferredWidth
;
173 preferredHeight
= preferredHeight
|| this._preferredHeight
;
175 // Skinny tooltips are not pretty, their arrow location is not nice.
176 preferredWidth
= Math
.max(preferredWidth
, 50);
177 // Position relative to main DevTools element.
178 const container
= WebInspector
.Dialog
.modalHostView().element
;
179 const totalWidth
= container
.offsetWidth
;
180 const totalHeight
= container
.offsetHeight
;
182 var anchorBox
= anchorElement
instanceof AnchorBox
? anchorElement
: anchorElement
.boxInWindow(window
);
183 anchorBox
= anchorBox
.relativeToElement(container
);
184 var newElementPosition
= { x
: 0, y
: 0, width
: preferredWidth
+ scrollerWidth
, height
: preferredHeight
};
186 var verticalAlignment
;
187 var roomAbove
= anchorBox
.y
;
188 var roomBelow
= totalHeight
- anchorBox
.y
- anchorBox
.height
;
190 if ((roomAbove
> roomBelow
) || (arrowDirection
=== WebInspector
.Popover
.Orientation
.Bottom
)) {
191 // Positioning above the anchor.
192 if ((anchorBox
.y
> newElementPosition
.height
+ arrowHeight
+ borderRadius
) || (arrowDirection
=== WebInspector
.Popover
.Orientation
.Bottom
))
193 newElementPosition
.y
= anchorBox
.y
- newElementPosition
.height
- arrowHeight
;
195 newElementPosition
.y
= borderRadius
;
196 newElementPosition
.height
= anchorBox
.y
- borderRadius
* 2 - arrowHeight
;
197 if (this._hasFixedHeight
&& newElementPosition
.height
< preferredHeight
) {
198 newElementPosition
.y
= borderRadius
;
199 newElementPosition
.height
= preferredHeight
;
202 verticalAlignment
= WebInspector
.Popover
.Orientation
.Bottom
;
204 // Positioning below the anchor.
205 newElementPosition
.y
= anchorBox
.y
+ anchorBox
.height
+ arrowHeight
;
206 if ((newElementPosition
.y
+ newElementPosition
.height
+ borderRadius
>= totalHeight
) && (arrowDirection
!== WebInspector
.Popover
.Orientation
.Top
)) {
207 newElementPosition
.height
= totalHeight
- borderRadius
- newElementPosition
.y
;
208 if (this._hasFixedHeight
&& newElementPosition
.height
< preferredHeight
) {
209 newElementPosition
.y
= totalHeight
- preferredHeight
- borderRadius
;
210 newElementPosition
.height
= preferredHeight
;
214 verticalAlignment
= WebInspector
.Popover
.Orientation
.Top
;
217 var horizontalAlignment
;
218 this._popupArrowElement
.removeAttribute("style");
219 if (anchorBox
.x
+ newElementPosition
.width
< totalWidth
) {
220 newElementPosition
.x
= Math
.max(borderRadius
, anchorBox
.x
- borderRadius
- arrowOffset
);
221 horizontalAlignment
= "left";
222 this._popupArrowElement
.style
.left
= arrowOffset
+ "px";
223 } else if (newElementPosition
.width
+ borderRadius
* 2 < totalWidth
) {
224 newElementPosition
.x
= totalWidth
- newElementPosition
.width
- borderRadius
- 2 * borderWidth
;
225 horizontalAlignment
= "right";
226 // Position arrow accurately.
227 var arrowRightPosition
= Math
.max(0, totalWidth
- anchorBox
.x
- anchorBox
.width
- borderRadius
- arrowOffset
);
228 arrowRightPosition
+= anchorBox
.width
/ 2;
229 arrowRightPosition
= Math
.min(arrowRightPosition
, newElementPosition
.width
- borderRadius
- arrowOffset
);
230 this._popupArrowElement
.style
.right
= arrowRightPosition
+ "px";
232 newElementPosition
.x
= borderRadius
;
233 newElementPosition
.width
= totalWidth
- borderRadius
* 2;
234 newElementPosition
.height
+= scrollerWidth
;
235 horizontalAlignment
= "left";
236 if (verticalAlignment
=== WebInspector
.Popover
.Orientation
.Bottom
)
237 newElementPosition
.y
-= scrollerWidth
;
238 // Position arrow accurately.
239 this._popupArrowElement
.style
.left
= Math
.max(0, anchorBox
.x
- newElementPosition
.x
- borderRadius
- arrowRadius
+ anchorBox
.width
/ 2) + "px";
242 this.element
.className
= WebInspector
.Popover
._classNamePrefix
+ " " + verticalAlignment
+ "-" + horizontalAlignment
+ "-arrow";
243 this.element
.positionAt(newElementPosition
.x
, newElementPosition
.y
- borderWidth
, container
);
244 this.element
.style
.width
= newElementPosition
.width
+ borderWidth
* 2 + "px";
245 this.element
.style
.height
= newElementPosition
.height
+ borderWidth
* 2 + "px";
248 __proto__
: WebInspector
.Widget
.prototype
253 * @param {!Element} panelElement
254 * @param {function(!Element, !Event):(!Element|!AnchorBox|undefined)} getAnchor
255 * @param {function(!Element, !WebInspector.Popover):undefined} showPopover
256 * @param {function()=} onHide
257 * @param {boolean=} disableOnClick
259 WebInspector
.PopoverHelper = function(panelElement
, getAnchor
, showPopover
, onHide
, disableOnClick
)
261 this._getAnchor
= getAnchor
;
262 this._showPopover
= showPopover
;
263 this._onHide
= onHide
;
264 this._disableOnClick
= !!disableOnClick
;
265 panelElement
.addEventListener("mousedown", this._mouseDown
.bind(this), false);
266 panelElement
.addEventListener("mousemove", this._mouseMove
.bind(this), false);
267 panelElement
.addEventListener("mouseout", this._mouseOut
.bind(this), false);
268 this.setTimeout(1000, 500);
271 WebInspector
.PopoverHelper
.prototype = {
273 * @param {number} timeout
274 * @param {number=} hideTimeout
276 setTimeout: function(timeout
, hideTimeout
)
278 this._timeout
= timeout
;
279 if (typeof hideTimeout
=== "number")
280 this._hideTimeout
= hideTimeout
;
282 this._hideTimeout
= timeout
/ 2;
286 * @param {!MouseEvent} event
289 _eventInHoverElement: function(event
)
291 if (!this._hoverElement
)
293 var box
= this._hoverElement
instanceof AnchorBox
? this._hoverElement
: this._hoverElement
.boxInWindow();
294 return (box
.x
<= event
.clientX
&& event
.clientX
<= box
.x
+ box
.width
&&
295 box
.y
<= event
.clientY
&& event
.clientY
<= box
.y
+ box
.height
);
298 _mouseDown: function(event
)
300 if (this._disableOnClick
|| !this._eventInHoverElement(event
))
303 this._killHidePopoverTimer();
304 this._handleMouseAction(event
, true);
308 _mouseMove: function(event
)
310 // Pretend that nothing has happened.
311 if (this._eventInHoverElement(event
))
314 this._startHidePopoverTimer();
315 this._handleMouseAction(event
, false);
318 _popoverMouseOut: function(event
)
320 if (!this.isPopoverVisible())
322 if (event
.relatedTarget
&& !event
.relatedTarget
.isSelfOrDescendant(this._popover
._contentDiv
))
323 this._startHidePopoverTimer();
326 _mouseOut: function(event
)
328 if (!this.isPopoverVisible())
330 if (!this._eventInHoverElement(event
))
331 this._startHidePopoverTimer();
334 _startHidePopoverTimer: function()
336 // User has 500ms (this._hideTimeout) to reach the popup.
337 if (!this._popover
|| this._hidePopoverTimer
)
341 * @this {WebInspector.PopoverHelper}
346 delete this._hidePopoverTimer
;
348 this._hidePopoverTimer
= setTimeout(doHide
.bind(this), this._hideTimeout
);
351 _handleMouseAction: function(event
, isMouseDown
)
353 this._resetHoverTimer();
354 if (event
.which
&& this._disableOnClick
)
356 this._hoverElement
= this._getAnchor(event
.target
, event
);
357 if (!this._hoverElement
)
359 const toolTipDelay
= isMouseDown
? 0 : (this._popup
? this._timeout
* 0.6 : this._timeout
);
360 this._hoverTimer
= setTimeout(this._mouseHover
.bind(this, this._hoverElement
), toolTipDelay
);
363 _resetHoverTimer: function()
365 if (this._hoverTimer
) {
366 clearTimeout(this._hoverTimer
);
367 delete this._hoverTimer
;
374 isPopoverVisible: function()
376 return !!this._popover
;
379 hidePopover: function()
381 this._resetHoverTimer();
385 _hidePopover: function()
393 this._popover
.dispose();
394 delete this._popover
;
395 this._hoverElement
= null;
398 _mouseHover: function(element
)
400 delete this._hoverTimer
;
403 this._popover
= new WebInspector
.Popover(this);
404 this._showPopover(element
, this._popover
);
407 _killHidePopoverTimer: function()
409 if (this._hidePopoverTimer
) {
410 clearTimeout(this._hidePopoverTimer
);
411 delete this._hidePopoverTimer
;
413 // We know that we reached the popup, but we might have moved over other elements.
414 // Discard pending command.
415 this._resetHoverTimer();
420 /** @enum {string} */
421 WebInspector
.Popover
.Orientation
= {