2 * Copyright (C) 2013 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 * @param {!WebInspector.ViewportControl.Provider} provider
35 WebInspector
.ViewportControl = function(provider
)
37 this.element
= 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";
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);
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;
64 WebInspector
.ViewportControl
.Provider = function()
68 WebInspector
.ViewportControl
.Provider
.prototype = {
70 * @param {number} index
73 fastHeight: function(index
) { return 0; },
78 itemCount: function() { return 0; },
83 minimumRowHeight: function() { return 0; },
86 * @param {number} index
87 * @return {?WebInspector.ViewportElement}
89 itemElement: function(index
) { return null; }
95 WebInspector
.ViewportElement = function() { }
96 WebInspector
.ViewportElement
.prototype = {
97 cacheFastHeight: function() { },
99 willHide: function() { },
101 wasShown: function() { },
106 element: function() { },
111 * @implements {WebInspector.ViewportElement}
112 * @param {!Element} element
114 WebInspector
.StaticViewportElement = function(element
)
116 this._element
= element
;
119 WebInspector
.StaticViewportElement
.prototype = {
123 cacheFastHeight: function() { },
128 willHide: function() { },
133 wasShown: function() { },
141 return this._element
;
145 WebInspector
.ViewportControl
.prototype = {
149 scrolledToBottom: function()
151 return this._scrolledToBottom
;
155 * @param {boolean} value
157 setStickToBottom: function(value
)
159 this._stickToBottom
= value
;
163 * @param {!Event} event
165 _onCopy: function(event
)
167 var text
= this._selectedText();
170 event
.preventDefault();
171 event
.clipboardData
.setData("text/plain", text
);
175 * @param {!Event} event
177 _onDragStart: function(event
)
179 var text
= this._selectedText();
182 event
.dataTransfer
.clearData();
183 event
.dataTransfer
.setData("text/plain", text
);
184 event
.dataTransfer
.effectAllowed
= "copy";
191 contentElement: function()
193 return this._contentElement
;
196 invalidate: function()
198 delete this._cumulativeHeights
;
199 delete this._cachedProviderElements
;
204 * @param {number} index
205 * @return {?WebInspector.ViewportElement}
207 _providerElement: function(index
)
209 if (!this._cachedProviderElements
)
210 this._cachedProviderElements
= new Array(this._provider
.itemCount());
211 var element
= this._cachedProviderElements
[index
];
213 element
= this._provider
.itemElement(index
);
214 this._cachedProviderElements
[index
] = element
;
219 _rebuildCumulativeHeightsIfNeeded: function()
221 if (this._cumulativeHeights
)
223 var itemCount
= this._provider
.itemCount();
226 this._cumulativeHeights
= new Int32Array(itemCount
);
227 this._cumulativeHeights
[0] = this._provider
.fastHeight(0);
228 for (var i
= 1; i
< itemCount
; ++i
)
229 this._cumulativeHeights
[i
] = this._cumulativeHeights
[i
- 1] + this._provider
.fastHeight(i
);
233 * @param {number} index
236 _cachedItemHeight: function(index
)
238 return index
=== 0 ? this._cumulativeHeights
[0] : this._cumulativeHeights
[index
] - this._cumulativeHeights
[index
- 1];
242 * @param {?Selection} selection
243 * @suppressGlobalPropertiesCheck
245 _isSelectionBackwards: function(selection
)
247 if (!selection
|| !selection
.rangeCount
)
249 var range
= document
.createRange();
250 range
.setStart(selection
.anchorNode
, selection
.anchorOffset
);
251 range
.setEnd(selection
.focusNode
, selection
.focusOffset
);
252 return range
.collapsed
;
256 * @param {number} itemIndex
257 * @param {!Node} node
258 * @param {number} offset
259 * @return {!{item: number, node: !Node, offset: number}}
261 _createSelectionModel: function(itemIndex
, node
, offset
)
271 * @param {?Selection} selection
273 _updateSelectionModel: function(selection
)
275 if (!selection
|| !selection
.rangeCount
) {
276 this._headSelection
= null;
277 this._anchorSelection
= null;
281 var firstSelected
= Number
.MAX_VALUE
;
282 var lastSelected
= -1;
284 var range
= selection
.getRangeAt(0);
285 var hasVisibleSelection
= false;
286 for (var i
= 0; i
< this._renderedItems
.length
; ++i
) {
287 if (range
.intersectsNode(this._renderedItems
[i
].element())) {
288 var index
= i
+ this._firstVisibleIndex
;
289 firstSelected
= Math
.min(firstSelected
, index
);
290 lastSelected
= Math
.max(lastSelected
, index
);
291 hasVisibleSelection
= true;
294 if (hasVisibleSelection
) {
295 firstSelected
= this._createSelectionModel(firstSelected
, /** @type {!Node} */(range
.startContainer
), range
.startOffset
);
296 lastSelected
= this._createSelectionModel(lastSelected
, /** @type {!Node} */(range
.endContainer
), range
.endOffset
);
298 var topOverlap
= range
.intersectsNode(this._topGapElement
) && this._topGapElement
._active
;
299 var bottomOverlap
= range
.intersectsNode(this._bottomGapElement
) && this._bottomGapElement
._active
;
300 if (!topOverlap
&& !bottomOverlap
&& !hasVisibleSelection
) {
301 this._headSelection
= null;
302 this._anchorSelection
= null;
306 if (!this._anchorSelection
|| !this._headSelection
) {
307 this._anchorSelection
= this._createSelectionModel(0, this.element
, 0);
308 this._headSelection
= this._createSelectionModel(this._provider
.itemCount() - 1, this.element
, this.element
.children
.length
);
309 this._selectionIsBackward
= false;
312 var isBackward
= this._isSelectionBackwards(selection
);
313 var startSelection
= this._selectionIsBackward
? this._headSelection
: this._anchorSelection
;
314 var endSelection
= this._selectionIsBackward
? this._anchorSelection
: this._headSelection
;
315 if (topOverlap
&& bottomOverlap
&& hasVisibleSelection
) {
316 firstSelected
= firstSelected
.item
< startSelection
.item
? firstSelected
: startSelection
;
317 lastSelected
= lastSelected
.item
> endSelection
.item
? lastSelected
: endSelection
;
318 } else if (!hasVisibleSelection
) {
319 firstSelected
= startSelection
;
320 lastSelected
= endSelection
;
321 } else if (topOverlap
)
322 firstSelected
= isBackward
? this._headSelection
: this._anchorSelection
;
323 else if (bottomOverlap
)
324 lastSelected
= isBackward
? this._anchorSelection
: this._headSelection
;
327 this._anchorSelection
= lastSelected
;
328 this._headSelection
= firstSelected
;
330 this._anchorSelection
= firstSelected
;
331 this._headSelection
= lastSelected
;
333 this._selectionIsBackward
= isBackward
;
338 * @param {?Selection} selection
340 _restoreSelection: function(selection
)
342 var anchorElement
= null;
344 if (this._firstVisibleIndex
<= this._anchorSelection
.item
&& this._anchorSelection
.item
<= this._lastVisibleIndex
) {
345 anchorElement
= this._anchorSelection
.node
;
346 anchorOffset
= this._anchorSelection
.offset
;
348 if (this._anchorSelection
.item
< this._firstVisibleIndex
)
349 anchorElement
= this._topGapElement
;
350 else if (this._anchorSelection
.item
> this._lastVisibleIndex
)
351 anchorElement
= this._bottomGapElement
;
352 anchorOffset
= this._selectionIsBackward
? 1 : 0;
355 var headElement
= null;
357 if (this._firstVisibleIndex
<= this._headSelection
.item
&& this._headSelection
.item
<= this._lastVisibleIndex
) {
358 headElement
= this._headSelection
.node
;
359 headOffset
= this._headSelection
.offset
;
361 if (this._headSelection
.item
< this._firstVisibleIndex
)
362 headElement
= this._topGapElement
;
363 else if (this._headSelection
.item
> this._lastVisibleIndex
)
364 headElement
= this._bottomGapElement
;
365 headOffset
= this._selectionIsBackward
? 0 : 1;
368 selection
.setBaseAndExtent(anchorElement
, anchorOffset
, headElement
, headOffset
);
373 if (!this._visibleHeight())
374 return; // Do nothing for invisible controls.
376 var itemCount
= this._provider
.itemCount();
378 for (var i
= 0; i
< this._renderedItems
.length
; ++i
)
379 this._renderedItems
[i
].cacheFastHeight();
380 for (var i
= 0; i
< this._renderedItems
.length
; ++i
)
381 this._renderedItems
[i
].willHide();
382 this._renderedItems
= [];
383 this._contentElement
.removeChildren();
384 this._topGapElement
.style
.height
= "0px";
385 this._bottomGapElement
.style
.height
= "0px";
386 this._firstVisibleIndex
= -1;
387 this._lastVisibleIndex
= -1;
391 var selection
= this.element
.getComponentSelection();
392 var shouldRestoreSelection
= this._updateSelectionModel(selection
);
394 var visibleFrom
= this.element
.scrollTop
;
395 var visibleHeight
= this._visibleHeight();
396 this._scrolledToBottom
= this.element
.isScrolledToBottom();
397 var isInvalidating
= !this._cumulativeHeights
;
399 if (this._cumulativeHeights
&& itemCount
!== this._cumulativeHeights
.length
)
400 delete this._cumulativeHeights
;
401 for (var i
= 0; i
< this._renderedItems
.length
; ++i
) {
402 this._renderedItems
[i
].cacheFastHeight();
403 // Tolerate 1-pixel error due to double-to-integer rounding errors.
404 if (this._cumulativeHeights
&& Math
.abs(this._cachedItemHeight(this._firstVisibleIndex
+ i
) - this._provider
.fastHeight(i
+ this._firstVisibleIndex
)) > 1)
405 delete this._cumulativeHeights
;
407 this._rebuildCumulativeHeightsIfNeeded();
408 var oldFirstVisibleIndex
= this._firstVisibleIndex
;
409 var oldLastVisibleIndex
= this._lastVisibleIndex
;
411 var shouldStickToBottom
= this._stickToBottom
&& this._scrolledToBottom
;
412 if (shouldStickToBottom
) {
413 this._lastVisibleIndex
= itemCount
- 1;
414 this._firstVisibleIndex
= Math
.max(itemCount
- Math
.ceil(visibleHeight
/ this._provider
.minimumRowHeight()), 0);
416 this._firstVisibleIndex
= Math
.max(Array
.prototype.lowerBound
.call(this._cumulativeHeights
, visibleFrom
+ 1), 0);
417 // Proactively render more rows in case some of them will be collapsed without triggering refresh. @see crbug.com/390169
418 this._lastVisibleIndex
= this._firstVisibleIndex
+ Math
.ceil(visibleHeight
/ this._provider
.minimumRowHeight()) - 1;
419 this._lastVisibleIndex
= Math
.min(this._lastVisibleIndex
, itemCount
- 1);
421 var topGapHeight
= this._cumulativeHeights
[this._firstVisibleIndex
- 1] || 0;
422 var bottomGapHeight
= this._cumulativeHeights
[this._cumulativeHeights
.length
- 1] - this._cumulativeHeights
[this._lastVisibleIndex
];
424 this._topGapElement
.style
.height
= topGapHeight
+ "px";
425 this._bottomGapElement
.style
.height
= bottomGapHeight
+ "px";
426 this._topGapElement
._active
= !!topGapHeight
;
427 this._bottomGapElement
._active
= !!bottomGapHeight
;
429 this._contentElement
.style
.setProperty("height", "10000000px");
431 this._fullViewportUpdate();
433 this._partialViewportUpdate(oldFirstVisibleIndex
, oldLastVisibleIndex
);
434 this._contentElement
.style
.removeProperty("height");
435 // Should be the last call in the method as it might force layout.
436 if (shouldRestoreSelection
)
437 this._restoreSelection(selection
);
438 if (shouldStickToBottom
)
439 this.element
.scrollTop
= this.element
.scrollHeight
;
442 _fullViewportUpdate: function()
444 for (var i
= 0; i
< this._renderedItems
.length
; ++i
)
445 this._renderedItems
[i
].willHide();
446 this._renderedItems
= [];
447 this._contentElement
.removeChildren();
448 for (var i
= this._firstVisibleIndex
; i
<= this._lastVisibleIndex
; ++i
) {
449 var viewportElement
= this._providerElement(i
);
450 this._contentElement
.appendChild(viewportElement
.element());
451 this._renderedItems
.push(viewportElement
);
452 viewportElement
.wasShown();
457 * @param {number} oldFirstVisibleIndex
458 * @param {number} oldLastVisibleIndex
460 _partialViewportUpdate: function(oldFirstVisibleIndex
, oldLastVisibleIndex
)
462 var willBeHidden
= [];
463 for (var i
= 0; i
< this._renderedItems
.length
; ++i
) {
464 var index
= oldFirstVisibleIndex
+ i
;
465 if (index
< this._firstVisibleIndex
|| this._lastVisibleIndex
< index
)
466 willBeHidden
.push(this._renderedItems
[i
]);
468 for (var i
= 0; i
< willBeHidden
.length
; ++i
)
469 willBeHidden
[i
].willHide();
470 for (var i
= 0; i
< willBeHidden
.length
; ++i
)
471 willBeHidden
[i
].element().remove();
473 this._renderedItems
= [];
474 var anchor
= this._contentElement
.firstChild
;
475 for (var i
= this._firstVisibleIndex
; i
<= this._lastVisibleIndex
; ++i
) {
476 var viewportElement
= this._providerElement(i
);
477 var element
= viewportElement
.element();
478 if (element
!== anchor
) {
479 this._contentElement
.insertBefore(element
, anchor
);
480 viewportElement
.wasShown();
482 anchor
= anchor
.nextSibling
;
484 this._renderedItems
.push(viewportElement
);
491 _selectedText: function()
493 this._updateSelectionModel(this.element
.getComponentSelection());
494 if (!this._headSelection
|| !this._anchorSelection
)
497 var startSelection
= null;
498 var endSelection
= null;
499 if (this._selectionIsBackward
) {
500 startSelection
= this._headSelection
;
501 endSelection
= this._anchorSelection
;
503 startSelection
= this._anchorSelection
;
504 endSelection
= this._headSelection
;
508 for (var i
= startSelection
.item
; i
<= endSelection
.item
; ++i
)
509 textLines
.push(this._providerElement(i
).element().deepTextContent());
511 var endSelectionElement
= this._providerElement(endSelection
.item
).element();
512 if (endSelection
.node
&& endSelection
.node
.isSelfOrDescendant(endSelectionElement
)) {
513 var itemTextOffset
= this._textOffsetInNode(endSelectionElement
, endSelection
.node
, endSelection
.offset
);
514 textLines
[textLines
.length
- 1] = textLines
.peekLast().substring(0, itemTextOffset
);
517 var startSelectionElement
= this._providerElement(startSelection
.item
).element();
518 if (startSelection
.node
&& startSelection
.node
.isSelfOrDescendant(startSelectionElement
)) {
519 var itemTextOffset
= this._textOffsetInNode(startSelectionElement
, startSelection
.node
, startSelection
.offset
);
520 textLines
[0] = textLines
[0].substring(itemTextOffset
);
523 return textLines
.join("\n");
527 * @param {!Element} itemElement
528 * @param {!Node} container
529 * @param {number} offset
532 _textOffsetInNode: function(itemElement
, container
, offset
)
535 var node
= itemElement
;
536 while ((node
= node
.traverseNextTextNode()) && node
!== container
)
537 chars
+= node
.textContent
.length
;
538 return chars
+ offset
;
542 * @param {!Event} event
544 _onScroll: function(event
)
552 firstVisibleIndex: function()
554 return this._firstVisibleIndex
;
560 lastVisibleIndex: function()
562 return this._lastVisibleIndex
;
568 renderedElementAt: function(index
)
570 if (index
< this._firstVisibleIndex
)
572 if (index
> this._lastVisibleIndex
)
574 return this._renderedItems
[index
- this._firstVisibleIndex
].element();
578 * @param {number} index
579 * @param {boolean=} makeLast
581 scrollItemIntoView: function(index
, makeLast
)
583 if (index
> this._firstVisibleIndex
&& index
< this._lastVisibleIndex
)
586 this.forceScrollItemToBeLast(index
);
587 else if (index
<= this._firstVisibleIndex
)
588 this.forceScrollItemToBeFirst(index
);
589 else if (index
>= this._lastVisibleIndex
)
590 this.forceScrollItemToBeLast(index
);
594 * @param {number} index
596 forceScrollItemToBeFirst: function(index
)
598 this._rebuildCumulativeHeightsIfNeeded();
599 this.element
.scrollTop
= index
> 0 ? this._cumulativeHeights
[index
- 1] : 0;
604 * @param {number} index
606 forceScrollItemToBeLast: function(index
)
608 this._rebuildCumulativeHeightsIfNeeded();
609 this.element
.scrollTop
= this._cumulativeHeights
[index
] - this._visibleHeight();
616 _visibleHeight: function()
618 // Use offsetHeight instead of clientHeight to avoid being affected by horizontal scroll.
619 return this.element
.offsetHeight
;