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