Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / WebKit / Source / devtools / front_end / ui / Popover.js
blob5822b9b30adba4327f3b5f7205b1fd4c815a920c
1 /*
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
6 * met:
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.
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.
31 /**
32 * @constructor
33 * @extends {WebInspector.Widget}
34 * @param {!WebInspector.PopoverHelper=} popoverHelper
36 WebInspector.Popover = function(popoverHelper)
38 WebInspector.Widget.call(this);
39 this.markAsRoot();
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 = {
53 /**
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);
65 /**
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);
76 /**
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)
86 if (this._disposed)
87 return;
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);
107 if (view)
108 view.show(this._contentDiv);
109 else
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);
120 hide: function()
122 this._containerElement.ownerDocument.defaultView.removeEventListener("resize", this._hideBound, false);
123 this.detach();
124 this._containerElement.remove();
125 delete WebInspector.Popover._popover;
128 get disposed()
130 return this._disposed;
133 dispose: function()
135 if (this.isShowing())
136 this.hide();
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;
194 else {
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;
203 } else {
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;
213 // Align arrow.
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";
231 } else {
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
252 * @constructor
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;
281 else
282 this._hideTimeout = timeout / 2;
286 * @param {!MouseEvent} event
287 * @return {boolean}
289 _eventInHoverElement: function(event)
291 if (!this._hoverElement)
292 return false;
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))
301 this.hidePopover();
302 else {
303 this._killHidePopoverTimer();
304 this._handleMouseAction(event, true);
308 _mouseMove: function(event)
310 // Pretend that nothing has happened.
311 if (this._eventInHoverElement(event))
312 return;
314 this._startHidePopoverTimer();
315 this._handleMouseAction(event, false);
318 _popoverMouseOut: function(event)
320 if (!this.isPopoverVisible())
321 return;
322 if (event.relatedTarget && !event.relatedTarget.isSelfOrDescendant(this._popover._contentDiv))
323 this._startHidePopoverTimer();
326 _mouseOut: function(event)
328 if (!this.isPopoverVisible())
329 return;
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)
338 return;
341 * @this {WebInspector.PopoverHelper}
343 function doHide()
345 this._hidePopover();
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)
355 return;
356 this._hoverElement = this._getAnchor(event.target, event);
357 if (!this._hoverElement)
358 return;
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;
372 * @return {boolean}
374 isPopoverVisible: function()
376 return !!this._popover;
379 hidePopover: function()
381 this._resetHoverTimer();
382 this._hidePopover();
385 _hidePopover: function()
387 if (!this._popover)
388 return;
390 if (this._onHide)
391 this._onHide();
393 this._popover.dispose();
394 delete this._popover;
395 this._hoverElement = null;
398 _mouseHover: function(element)
400 delete this._hoverTimer;
402 this._hidePopover();
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 = {
422 Top: "top",
423 Bottom: "bottom"