Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / devtools / front_end / ui / Popover.js
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"; // 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
50 WebInspector.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  */
238 WebInspector.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
251 WebInspector.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} */
401 WebInspector.Popover.Orientation = {
402     Top: "top",
403     Bottom: "bottom"
404 }