Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / devtools / front_end / ui / ViewportControl.js
1 /*
2  * Copyright (C) 2013 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  * @param {!WebInspector.ViewportControl.Provider} provider
34  */
35 WebInspector.ViewportControl = function(provider)
36 {
37     this.element = document.createElement("div");
38     this.element.style.overflow = "auto";
39     this._topGapElement = this.element.createChild("div", "viewport-control-gap-element");
40     this._topGapElement.textContent = ".";
41     this._topGapElement.style.height = "0px";
42     this._contentElement = this.element.createChild("div");
43     this._bottomGapElement = this.element.createChild("div", "viewport-control-gap-element");
44     this._bottomGapElement.textContent = ".";
45     this._bottomGapElement.style.height = "0px";
46
47     this._provider = provider;
48     this.element.addEventListener("scroll", this._onScroll.bind(this), false);
49     this.element.addEventListener("copy", this._onCopy.bind(this), false);
50     this.element.addEventListener("dragstart", this._onDragStart.bind(this), false);
51
52     this._firstVisibleIndex = 0;
53     this._lastVisibleIndex = -1;
54     this._renderedItems = [];
55     this._anchorSelection = null;
56     this._headSelection = null;
57     this._stickToBottom = false;
58     this._scrolledToBottom = true;
59 }
60
61 /**
62  * @interface
63  */
64 WebInspector.ViewportControl.Provider = function()
65 {
66 }
67
68 WebInspector.ViewportControl.Provider.prototype = {
69     /**
70      * @param {number} index
71      * @return {number}
72      */
73     fastHeight: function(index) { return 0; },
74
75     /**
76      * @return {number}
77      */
78     itemCount: function() { return 0; },
79
80     /**
81      * @return {number}
82      */
83     minimumRowHeight: function() { return 0; },
84
85     /**
86      * @param {number} index
87      * @return {?WebInspector.ViewportElement}
88      */
89     itemElement: function(index) { return null; }
90 }
91
92 /**
93  * @interface
94  */
95 WebInspector.ViewportElement = function() { }
96 WebInspector.ViewportElement.prototype = {
97     cacheFastHeight: function() { },
98
99     willHide: function() { },
100
101     wasShown: function() { },
102
103     /**
104      * @return {!Element}
105      */
106     element: function() { },
107 }
108
109 /**
110  * @constructor
111  * @implements {WebInspector.ViewportElement}
112  * @param {!Element} element
113  */
114 WebInspector.StaticViewportElement = function(element)
115 {
116     this._element = element;
117 }
118
119 WebInspector.StaticViewportElement.prototype = {
120     cacheFastHeight: function() { },
121
122     willHide: function() { },
123
124     wasShown: function() { },
125
126     /**
127      * @return {!Element}
128      */
129     element: function()
130     {
131         return this._element;
132     },
133 }
134
135 WebInspector.ViewportControl.prototype = {
136     /**
137      * @return {boolean}
138      */
139     scrolledToBottom: function()
140     {
141         return this._scrolledToBottom;
142     },
143
144     /**
145      * @param {boolean} value
146      */
147     setStickToBottom: function(value)
148     {
149         this._stickToBottom = value;
150     },
151
152     /**
153      * @param {!Event} event
154      */
155     _onCopy: function(event)
156     {
157         var text = this._selectedText();
158         if (!text)
159             return;
160         event.preventDefault();
161         event.clipboardData.setData("text/plain", text);
162     },
163
164     /**
165      * @param {!Event} event
166      */
167     _onDragStart: function(event)
168     {
169         var text = this._selectedText();
170         if (!text)
171             return false;
172         event.dataTransfer.clearData();
173         event.dataTransfer.setData("text/plain", text);
174         event.dataTransfer.effectAllowed = "copy";
175         return true;
176     },
177
178     /**
179      * @return {!Element}
180      */
181     contentElement: function()
182     {
183         return this._contentElement;
184     },
185
186     invalidate: function()
187     {
188         delete this._cumulativeHeights;
189         delete this._cachedProviderElements;
190         this.refresh();
191     },
192
193     /**
194      * @param {number} index
195      * @return {?WebInspector.ViewportElement}
196      */
197     _providerElement: function(index)
198     {
199         if (!this._cachedProviderElements)
200             this._cachedProviderElements = new Array(this._provider.itemCount());
201         var element = this._cachedProviderElements[index];
202         if (!element) {
203             element = this._provider.itemElement(index);
204             this._cachedProviderElements[index] = element;
205         }
206         return element;
207     },
208
209     _rebuildCumulativeHeightsIfNeeded: function()
210     {
211         if (this._cumulativeHeights)
212             return;
213         var itemCount = this._provider.itemCount();
214         if (!itemCount)
215             return;
216         this._cumulativeHeights = new Int32Array(itemCount);
217         this._cumulativeHeights[0] = this._provider.fastHeight(0);
218         for (var i = 1; i < itemCount; ++i)
219             this._cumulativeHeights[i] = this._cumulativeHeights[i - 1] + this._provider.fastHeight(i);
220     },
221
222     /**
223      * @param {number} index
224      * @return {number}
225      */
226     _cachedItemHeight: function(index)
227     {
228         return index === 0 ? this._cumulativeHeights[0] : this._cumulativeHeights[index] - this._cumulativeHeights[index - 1];
229     },
230
231     /**
232      * @param {?Selection} selection
233      */
234     _isSelectionBackwards: function(selection)
235     {
236         if (!selection || !selection.rangeCount)
237             return false;
238         var range = document.createRange();
239         range.setStart(selection.anchorNode, selection.anchorOffset);
240         range.setEnd(selection.focusNode, selection.focusOffset);
241         return range.collapsed;
242     },
243
244     /**
245      * @param {number} itemIndex
246      * @param {!Node} node
247      * @param {number} offset
248      * @return {!{item: number, node: !Node, offset: number}}
249      */
250     _createSelectionModel: function(itemIndex, node, offset)
251     {
252         return {
253             item: itemIndex,
254             node: node,
255             offset: offset
256         };
257     },
258
259     /**
260      * @param {?Selection} selection
261      */
262     _updateSelectionModel: function(selection)
263     {
264         if (!selection || !selection.rangeCount) {
265             this._headSelection = null;
266             this._anchorSelection = null;
267             return false;
268         }
269
270         var firstSelected = Number.MAX_VALUE;
271         var lastSelected = -1;
272
273         var range = selection.getRangeAt(0);
274         var hasVisibleSelection = false;
275         for (var i = 0; i < this._renderedItems.length; ++i) {
276             if (range.intersectsNode(this._renderedItems[i].element())) {
277                 var index = i + this._firstVisibleIndex;
278                 firstSelected = Math.min(firstSelected, index);
279                 lastSelected = Math.max(lastSelected, index);
280                 hasVisibleSelection = true;
281             }
282         }
283         if (hasVisibleSelection) {
284             firstSelected = this._createSelectionModel(firstSelected, /** @type {!Node} */(range.startContainer), range.startOffset);
285             lastSelected = this._createSelectionModel(lastSelected, /** @type {!Node} */(range.endContainer), range.endOffset);
286         }
287         var topOverlap = range.intersectsNode(this._topGapElement) && this._topGapElement._active;
288         var bottomOverlap = range.intersectsNode(this._bottomGapElement) && this._bottomGapElement._active;
289         if (!topOverlap && !bottomOverlap && !hasVisibleSelection) {
290             this._headSelection = null;
291             this._anchorSelection = null;
292             return false;
293         }
294
295         if (!this._anchorSelection || !this._headSelection) {
296             this._anchorSelection = this._createSelectionModel(0, this.element, 0);
297             this._headSelection = this._createSelectionModel(this._provider.itemCount() - 1, this.element, this.element.children.length);
298             this._selectionIsBackward = false;
299         }
300
301         var isBackward = this._isSelectionBackwards(selection);
302         var startSelection = this._selectionIsBackward ? this._headSelection : this._anchorSelection;
303         var endSelection = this._selectionIsBackward ? this._anchorSelection : this._headSelection;
304         if (topOverlap && bottomOverlap && hasVisibleSelection) {
305             firstSelected = firstSelected.item < startSelection.item ? firstSelected : startSelection;
306             lastSelected = lastSelected.item > endSelection.item ? lastSelected : endSelection;
307         } else if (!hasVisibleSelection) {
308             firstSelected = startSelection;
309             lastSelected = endSelection;
310         } else if (topOverlap)
311             firstSelected = isBackward ? this._headSelection : this._anchorSelection;
312         else if (bottomOverlap)
313             lastSelected = isBackward ? this._anchorSelection : this._headSelection;
314
315         if (isBackward) {
316             this._anchorSelection = lastSelected;
317             this._headSelection = firstSelected;
318         } else {
319             this._anchorSelection = firstSelected;
320             this._headSelection = lastSelected;
321         }
322         this._selectionIsBackward = isBackward;
323         return true;
324     },
325
326     /**
327      * @param {?Selection} selection
328      */
329     _restoreSelection: function(selection)
330     {
331         var anchorElement = null;
332         var anchorOffset;
333         if (this._firstVisibleIndex <= this._anchorSelection.item && this._anchorSelection.item <= this._lastVisibleIndex) {
334             anchorElement = this._anchorSelection.node;
335             anchorOffset = this._anchorSelection.offset;
336         } else {
337             if (this._anchorSelection.item < this._firstVisibleIndex)
338                 anchorElement = this._topGapElement;
339             else if (this._anchorSelection.item > this._lastVisibleIndex)
340                 anchorElement = this._bottomGapElement;
341             anchorOffset = this._selectionIsBackward ? 1 : 0;
342         }
343
344         var headElement = null;
345         var headOffset;
346         if (this._firstVisibleIndex <= this._headSelection.item && this._headSelection.item <= this._lastVisibleIndex) {
347             headElement = this._headSelection.node;
348             headOffset = this._headSelection.offset;
349         } else {
350             if (this._headSelection.item < this._firstVisibleIndex)
351                 headElement = this._topGapElement;
352             else if (this._headSelection.item > this._lastVisibleIndex)
353                 headElement = this._bottomGapElement;
354             headOffset = this._selectionIsBackward ? 0 : 1;
355         }
356
357         selection.setBaseAndExtent(anchorElement, anchorOffset, headElement, headOffset);
358     },
359
360     refresh: function()
361     {
362         if (!this._visibleHeight())
363             return;  // Do nothing for invisible controls.
364
365         var itemCount = this._provider.itemCount();
366         if (!itemCount) {
367             for (var i = 0; i < this._renderedItems.length; ++i)
368                 this._renderedItems[i].cacheFastHeight();
369             for (var i = 0; i < this._renderedItems.length; ++i)
370                 this._renderedItems[i].willHide();
371             this._renderedItems = [];
372             this._contentElement.removeChildren();
373             this._topGapElement.style.height = "0px";
374             this._bottomGapElement.style.height = "0px";
375             this._firstVisibleIndex = -1;
376             this._lastVisibleIndex = -1;
377             return;
378         }
379
380         var selection = window.getSelection();
381         var shouldRestoreSelection = this._updateSelectionModel(selection);
382
383         var visibleFrom = this.element.scrollTop;
384         var visibleHeight = this._visibleHeight();
385         this._scrolledToBottom = this.element.isScrolledToBottom();
386         var isInvalidating = !this._cumulativeHeights;
387
388         if (this._cumulativeHeights && itemCount !== this._cumulativeHeights.length)
389             delete this._cumulativeHeights;
390         for (var i = 0; i < this._renderedItems.length; ++i) {
391             this._renderedItems[i].cacheFastHeight();
392             // Tolerate 1-pixel error due to double-to-integer rounding errors.
393             if (this._cumulativeHeights && Math.abs(this._cachedItemHeight(this._firstVisibleIndex + i) - this._provider.fastHeight(i + this._firstVisibleIndex)) > 1)
394                 delete this._cumulativeHeights;
395         }
396         this._rebuildCumulativeHeightsIfNeeded();
397         var oldFirstVisibleIndex = this._firstVisibleIndex;
398         var oldLastVisibleIndex = this._lastVisibleIndex;
399
400         var shouldStickToBottom = this._stickToBottom && this._scrolledToBottom;
401         if (shouldStickToBottom) {
402             this._lastVisibleIndex = itemCount - 1;
403             this._firstVisibleIndex = Math.max(itemCount - Math.ceil(visibleHeight / this._provider.minimumRowHeight()), 0);
404         } else {
405             this._firstVisibleIndex = Math.max(Array.prototype.lowerBound.call(this._cumulativeHeights, visibleFrom + 1), 0);
406             // Proactively render more rows in case some of them will be collapsed without triggering refresh. @see crbug.com/390169
407             this._lastVisibleIndex = this._firstVisibleIndex + Math.ceil(visibleHeight / this._provider.minimumRowHeight()) - 1;
408             this._lastVisibleIndex = Math.min(this._lastVisibleIndex, itemCount - 1);
409         }
410         var topGapHeight = this._cumulativeHeights[this._firstVisibleIndex - 1] || 0;
411         var bottomGapHeight = this._cumulativeHeights[this._cumulativeHeights.length - 1] - this._cumulativeHeights[this._lastVisibleIndex];
412
413         this._topGapElement.style.height = topGapHeight + "px";
414         this._bottomGapElement.style.height = bottomGapHeight + "px";
415         this._topGapElement._active = !!topGapHeight;
416         this._bottomGapElement._active = !!bottomGapHeight;
417
418         this._contentElement.style.setProperty("height", "10000000px");
419         if (isInvalidating)
420             this._fullViewportUpdate();
421         else
422             this._partialViewportUpdate(oldFirstVisibleIndex, oldLastVisibleIndex);
423         this._contentElement.style.removeProperty("height");
424         // Should be the last call in the method as it might force layout.
425         if (shouldRestoreSelection)
426             this._restoreSelection(selection);
427         if (shouldStickToBottom)
428             this.element.scrollTop = this.element.scrollHeight;
429     },
430
431     _fullViewportUpdate: function()
432     {
433         for (var i = 0; i < this._renderedItems.length; ++i)
434             this._renderedItems[i].willHide();
435         this._renderedItems = [];
436         this._contentElement.removeChildren();
437         for (var i = this._firstVisibleIndex; i <= this._lastVisibleIndex; ++i) {
438             var viewportElement = this._providerElement(i);
439             this._contentElement.appendChild(viewportElement.element());
440             this._renderedItems.push(viewportElement);
441             viewportElement.wasShown();
442         }
443     },
444
445     /**
446      * @param {number} oldFirstVisibleIndex
447      * @param {number} oldLastVisibleIndex
448      */
449     _partialViewportUpdate: function(oldFirstVisibleIndex, oldLastVisibleIndex)
450     {
451         var willBeHidden = [];
452         for (var i = 0; i < this._renderedItems.length; ++i) {
453             var index = oldFirstVisibleIndex + i;
454             if (index < this._firstVisibleIndex || this._lastVisibleIndex < index)
455                 willBeHidden.push(this._renderedItems[i]);
456         }
457         for (var i = 0; i < willBeHidden.length; ++i)
458             willBeHidden[i].willHide();
459         for (var i = 0; i < willBeHidden.length; ++i)
460             willBeHidden[i].element().remove();
461
462         this._renderedItems = [];
463         var anchor = this._contentElement.firstChild;
464         for (var i = this._firstVisibleIndex; i <= this._lastVisibleIndex; ++i) {
465             var viewportElement = this._providerElement(i);
466             var element = viewportElement.element();
467             if (element !== anchor) {
468                 this._contentElement.insertBefore(element, anchor);
469                 viewportElement.wasShown();
470             } else {
471                 anchor = anchor.nextSibling;
472             }
473             this._renderedItems.push(viewportElement);
474         }
475     },
476
477     /**
478      * @return {?string}
479      */
480     _selectedText: function()
481     {
482         this._updateSelectionModel(window.getSelection());
483         if (!this._headSelection || !this._anchorSelection)
484             return null;
485
486         var startSelection = null;
487         var endSelection = null;
488         if (this._selectionIsBackward) {
489             startSelection = this._headSelection;
490             endSelection = this._anchorSelection;
491         } else {
492             startSelection = this._anchorSelection;
493             endSelection = this._headSelection;
494         }
495
496         var textLines = [];
497         for (var i = startSelection.item; i <= endSelection.item; ++i)
498             textLines.push(this._providerElement(i).element().textContent);
499
500         var endSelectionElement = this._providerElement(endSelection.item).element();
501         if (endSelection.node && endSelection.node.isSelfOrDescendant(endSelectionElement)) {
502             var itemTextOffset = this._textOffsetInNode(endSelectionElement, endSelection.node, endSelection.offset);
503             textLines[textLines.length - 1] = textLines.peekLast().substring(0, itemTextOffset);
504         }
505
506         var startSelectionElement = this._providerElement(startSelection.item).element();
507         if (startSelection.node && startSelection.node.isSelfOrDescendant(startSelectionElement)) {
508             var itemTextOffset = this._textOffsetInNode(startSelectionElement, startSelection.node, startSelection.offset);
509             textLines[0] = textLines[0].substring(itemTextOffset);
510         }
511
512         return textLines.join("\n");
513     },
514
515     /**
516      * @param {!Element} itemElement
517      * @param {!Node} container
518      * @param {number} offset
519      * @return {number}
520      */
521     _textOffsetInNode: function(itemElement, container, offset)
522     {
523         var chars = 0;
524         var node = itemElement;
525         while ((node = node.traverseNextTextNode()) && node !== container)
526             chars += node.textContent.length;
527         return chars + offset;
528     },
529
530     /**
531      * @param {!Event} event
532      */
533     _onScroll: function(event)
534     {
535         this.refresh();
536     },
537
538     /**
539      * @return {number}
540      */
541     firstVisibleIndex: function()
542     {
543         return this._firstVisibleIndex;
544     },
545
546     /**
547      * @return {number}
548      */
549     lastVisibleIndex: function()
550     {
551         return this._lastVisibleIndex;
552     },
553
554     /**
555      * @return {?Element}
556      */
557     renderedElementAt: function(index)
558     {
559         if (index < this._firstVisibleIndex)
560             return null;
561         if (index > this._lastVisibleIndex)
562             return null;
563         return this._renderedItems[index - this._firstVisibleIndex].element();
564     },
565
566     /**
567      * @param {number} index
568      * @param {boolean=} makeLast
569      */
570     scrollItemIntoView: function(index, makeLast)
571     {
572         if (index > this._firstVisibleIndex && index < this._lastVisibleIndex)
573             return;
574         if (makeLast)
575             this.forceScrollItemToBeLast(index);
576         else if (index <= this._firstVisibleIndex)
577             this.forceScrollItemToBeFirst(index);
578         else if (index >= this._lastVisibleIndex)
579             this.forceScrollItemToBeLast(index);
580     },
581
582     /**
583      * @param {number} index
584      */
585     forceScrollItemToBeFirst: function(index)
586     {
587         this._rebuildCumulativeHeightsIfNeeded();
588         this.element.scrollTop = index > 0 ? this._cumulativeHeights[index - 1] : 0;
589         this.refresh();
590     },
591
592     /**
593      * @param {number} index
594      */
595     forceScrollItemToBeLast: function(index)
596     {
597         this._rebuildCumulativeHeightsIfNeeded();
598         this.element.scrollTop = this._cumulativeHeights[index] - this._visibleHeight();
599         this.refresh();
600     },
601
602     /**
603      * @return {number}
604      */
605     _visibleHeight: function()
606     {
607         // Use offsetHeight instead of clientHeight to avoid being affected by horizontal scroll.
608         return this.element.offsetHeight;
609     }
610 }