Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(16)

Side by Side Diff: third_party/WebKit/Source/devtools/front_end/ui/ViewportControl.js

Issue 2466123002: DevTools: reformat front-end code to match chromium style. (Closed)
Patch Set: all done Created 4 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 /* 1 /*
2 * Copyright (C) 2013 Google Inc. All rights reserved. 2 * Copyright (C) 2013 Google Inc. All rights reserved.
3 * 3 *
4 * Redistribution and use in source and binary forms, with or without 4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are 5 * modification, are permitted provided that the following conditions are
6 * met: 6 * met:
7 * 7 *
8 * * Redistributions of source code must retain the above copyright 8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer. 9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above 10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer 11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the 12 * in the documentation and/or other materials provided with the
13 * distribution. 13 * distribution.
14 * * Neither the name of Google Inc. nor the names of its 14 * * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from 15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission. 16 * this software without specific prior written permission.
17 * 17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 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. 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */ 29 */
30
31 /** 30 /**
32 * @constructor 31 * @unrestricted
33 * @param {!WebInspector.ViewportControl.Provider} provider
34 */ 32 */
35 WebInspector.ViewportControl = function(provider) 33 WebInspector.ViewportControl = class {
36 { 34 /**
37 this.element = createElement("div"); 35 * @param {!WebInspector.ViewportControl.Provider} provider
38 this.element.style.overflow = "auto"; 36 */
39 this._topGapElement = this.element.createChild("div"); 37 constructor(provider) {
40 this._topGapElement.style.height = "0px"; 38 this.element = createElement('div');
41 this._topGapElement.style.color = "transparent"; 39 this.element.style.overflow = 'auto';
42 this._contentElement = this.element.createChild("div"); 40 this._topGapElement = this.element.createChild('div');
43 this._bottomGapElement = this.element.createChild("div"); 41 this._topGapElement.style.height = '0px';
44 this._bottomGapElement.style.height = "0px"; 42 this._topGapElement.style.color = 'transparent';
45 this._bottomGapElement.style.color = "transparent"; 43 this._contentElement = this.element.createChild('div');
44 this._bottomGapElement = this.element.createChild('div');
45 this._bottomGapElement.style.height = '0px';
46 this._bottomGapElement.style.color = 'transparent';
46 47
47 // Text content needed for range intersection checks in _updateSelectionMode l. 48 // Text content needed for range intersection checks in _updateSelectionMode l.
48 // Use Unicode ZERO WIDTH NO-BREAK SPACE, which avoids contributing any heig ht to the element's layout overflow. 49 // Use Unicode ZERO WIDTH NO-BREAK SPACE, which avoids contributing any heig ht to the element's layout overflow.
49 this._topGapElement.textContent = "\uFEFF"; 50 this._topGapElement.textContent = '\uFEFF';
50 this._bottomGapElement.textContent = "\uFEFF"; 51 this._bottomGapElement.textContent = '\uFEFF';
51 52
52 this._provider = provider; 53 this._provider = provider;
53 this.element.addEventListener("scroll", this._onScroll.bind(this), false); 54 this.element.addEventListener('scroll', this._onScroll.bind(this), false);
54 this.element.addEventListener("copy", this._onCopy.bind(this), false); 55 this.element.addEventListener('copy', this._onCopy.bind(this), false);
55 this.element.addEventListener("dragstart", this._onDragStart.bind(this), fal se); 56 this.element.addEventListener('dragstart', this._onDragStart.bind(this), fal se);
56 57
57 this._firstActiveIndex = 0; 58 this._firstActiveIndex = 0;
58 this._lastActiveIndex = -1; 59 this._lastActiveIndex = -1;
59 this._renderedItems = []; 60 this._renderedItems = [];
60 this._anchorSelection = null; 61 this._anchorSelection = null;
61 this._headSelection = null; 62 this._headSelection = null;
62 this._itemCount = 0; 63 this._itemCount = 0;
63 64
64 // Listen for any changes to descendants and trigger a refresh. This ensures 65 // Listen for any changes to descendants and trigger a refresh. This ensures
65 // that items updated asynchronously will not break stick-to-bottom behavior 66 // that items updated asynchronously will not break stick-to-bottom behavior
66 // if they change the scroll height. 67 // if they change the scroll height.
67 this._observer = new MutationObserver(this.refresh.bind(this)); 68 this._observer = new MutationObserver(this.refresh.bind(this));
68 this._observerConfig = { childList: true, subtree: true }; 69 this._observerConfig = {childList: true, subtree: true};
70 }
71
72 /**
73 * @return {boolean}
74 */
75 stickToBottom() {
76 return this._stickToBottom;
77 }
78
79 /**
80 * @param {boolean} value
81 */
82 setStickToBottom(value) {
83 this._stickToBottom = value;
84 if (this._stickToBottom)
85 this._observer.observe(this._contentElement, this._observerConfig);
86 else
87 this._observer.disconnect();
88 }
89
90 /**
91 * @param {!Event} event
92 */
93 _onCopy(event) {
94 var text = this._selectedText();
95 if (!text)
96 return;
97 event.preventDefault();
98 event.clipboardData.setData('text/plain', text);
99 }
100
101 /**
102 * @param {!Event} event
103 */
104 _onDragStart(event) {
105 var text = this._selectedText();
106 if (!text)
107 return false;
108 event.dataTransfer.clearData();
109 event.dataTransfer.setData('text/plain', text);
110 event.dataTransfer.effectAllowed = 'copy';
111 return true;
112 }
113
114 /**
115 * @return {!Element}
116 */
117 contentElement() {
118 return this._contentElement;
119 }
120
121 invalidate() {
122 delete this._cumulativeHeights;
123 delete this._cachedProviderElements;
124 this._itemCount = this._provider.itemCount();
125 this.refresh();
126 }
127
128 /**
129 * @param {number} index
130 * @return {?WebInspector.ViewportElement}
131 */
132 _providerElement(index) {
133 if (!this._cachedProviderElements)
134 this._cachedProviderElements = new Array(this._itemCount);
135 var element = this._cachedProviderElements[index];
136 if (!element) {
137 element = this._provider.itemElement(index);
138 this._cachedProviderElements[index] = element;
139 }
140 return element;
141 }
142
143 _rebuildCumulativeHeightsIfNeeded() {
144 if (this._cumulativeHeights)
145 return;
146 if (!this._itemCount)
147 return;
148 var firstActiveIndex = this._firstActiveIndex;
149 var lastActiveIndex = this._lastActiveIndex;
150 var height = 0;
151 this._cumulativeHeights = new Int32Array(this._itemCount);
152 for (var i = 0; i < this._itemCount; ++i) {
153 if (firstActiveIndex <= i && i <= lastActiveIndex)
154 height += this._renderedItems[i - firstActiveIndex].element().offsetHeig ht;
155 else
156 height += this._provider.fastHeight(i);
157 this._cumulativeHeights[i] = height;
158 }
159 }
160
161 /**
162 * @param {number} index
163 * @return {number}
164 */
165 _cachedItemHeight(index) {
166 return index === 0 ? this._cumulativeHeights[0] :
167 this._cumulativeHeights[index] - this._cumulativeHeight s[index - 1];
168 }
169
170 /**
171 * @param {?Selection} selection
172 * @suppressGlobalPropertiesCheck
173 */
174 _isSelectionBackwards(selection) {
175 if (!selection || !selection.rangeCount)
176 return false;
177 var range = document.createRange();
178 range.setStart(selection.anchorNode, selection.anchorOffset);
179 range.setEnd(selection.focusNode, selection.focusOffset);
180 return range.collapsed;
181 }
182
183 /**
184 * @param {number} itemIndex
185 * @param {!Node} node
186 * @param {number} offset
187 * @return {!{item: number, node: !Node, offset: number}}
188 */
189 _createSelectionModel(itemIndex, node, offset) {
190 return {item: itemIndex, node: node, offset: offset};
191 }
192
193 /**
194 * @param {?Selection} selection
195 */
196 _updateSelectionModel(selection) {
197 var range = selection && selection.rangeCount ? selection.getRangeAt(0) : nu ll;
198 if (!range || selection.isCollapsed || !this.element.hasSelection()) {
199 this._headSelection = null;
200 this._anchorSelection = null;
201 return false;
202 }
203
204 var firstSelected = Number.MAX_VALUE;
205 var lastSelected = -1;
206
207 var hasVisibleSelection = false;
208 for (var i = 0; i < this._renderedItems.length; ++i) {
209 if (range.intersectsNode(this._renderedItems[i].element())) {
210 var index = i + this._firstActiveIndex;
211 firstSelected = Math.min(firstSelected, index);
212 lastSelected = Math.max(lastSelected, index);
213 hasVisibleSelection = true;
214 }
215 }
216 if (hasVisibleSelection) {
217 firstSelected =
218 this._createSelectionModel(firstSelected, /** @type {!Node} */ (range. startContainer), range.startOffset);
219 lastSelected =
220 this._createSelectionModel(lastSelected, /** @type {!Node} */ (range.e ndContainer), range.endOffset);
221 }
222 var topOverlap = range.intersectsNode(this._topGapElement) && this._topGapEl ement._active;
223 var bottomOverlap = range.intersectsNode(this._bottomGapElement) && this._bo ttomGapElement._active;
224 if (!topOverlap && !bottomOverlap && !hasVisibleSelection) {
225 this._headSelection = null;
226 this._anchorSelection = null;
227 return false;
228 }
229
230 if (!this._anchorSelection || !this._headSelection) {
231 this._anchorSelection = this._createSelectionModel(0, this.element, 0);
232 this._headSelection = this._createSelectionModel(this._itemCount - 1, this .element, this.element.children.length);
233 this._selectionIsBackward = false;
234 }
235
236 var isBackward = this._isSelectionBackwards(selection);
237 var startSelection = this._selectionIsBackward ? this._headSelection : this. _anchorSelection;
238 var endSelection = this._selectionIsBackward ? this._anchorSelection : this. _headSelection;
239 if (topOverlap && bottomOverlap && hasVisibleSelection) {
240 firstSelected = firstSelected.item < startSelection.item ? firstSelected : startSelection;
241 lastSelected = lastSelected.item > endSelection.item ? lastSelected : endS election;
242 } else if (!hasVisibleSelection) {
243 firstSelected = startSelection;
244 lastSelected = endSelection;
245 } else if (topOverlap)
246 firstSelected = isBackward ? this._headSelection : this._anchorSelection;
247 else if (bottomOverlap)
248 lastSelected = isBackward ? this._anchorSelection : this._headSelection;
249
250 if (isBackward) {
251 this._anchorSelection = lastSelected;
252 this._headSelection = firstSelected;
253 } else {
254 this._anchorSelection = firstSelected;
255 this._headSelection = lastSelected;
256 }
257 this._selectionIsBackward = isBackward;
258 return true;
259 }
260
261 /**
262 * @param {?Selection} selection
263 */
264 _restoreSelection(selection) {
265 var anchorElement = null;
266 var anchorOffset;
267 if (this._firstActiveIndex <= this._anchorSelection.item && this._anchorSele ction.item <= this._lastActiveIndex) {
268 anchorElement = this._anchorSelection.node;
269 anchorOffset = this._anchorSelection.offset;
270 } else {
271 if (this._anchorSelection.item < this._firstActiveIndex)
272 anchorElement = this._topGapElement;
273 else if (this._anchorSelection.item > this._lastActiveIndex)
274 anchorElement = this._bottomGapElement;
275 anchorOffset = this._selectionIsBackward ? 1 : 0;
276 }
277
278 var headElement = null;
279 var headOffset;
280 if (this._firstActiveIndex <= this._headSelection.item && this._headSelectio n.item <= this._lastActiveIndex) {
281 headElement = this._headSelection.node;
282 headOffset = this._headSelection.offset;
283 } else {
284 if (this._headSelection.item < this._firstActiveIndex)
285 headElement = this._topGapElement;
286 else if (this._headSelection.item > this._lastActiveIndex)
287 headElement = this._bottomGapElement;
288 headOffset = this._selectionIsBackward ? 0 : 1;
289 }
290
291 selection.setBaseAndExtent(anchorElement, anchorOffset, headElement, headOff set);
292 }
293
294 refresh() {
295 this._observer.disconnect();
296 this._innerRefresh();
297 if (this._stickToBottom)
298 this._observer.observe(this._contentElement, this._observerConfig);
299 }
300
301 _innerRefresh() {
302 if (!this._visibleHeight())
303 return; // Do nothing for invisible controls.
304
305 if (!this._itemCount) {
306 for (var i = 0; i < this._renderedItems.length; ++i)
307 this._renderedItems[i].willHide();
308 this._renderedItems = [];
309 this._contentElement.removeChildren();
310 this._topGapElement.style.height = '0px';
311 this._bottomGapElement.style.height = '0px';
312 this._firstActiveIndex = -1;
313 this._lastActiveIndex = -1;
314 return;
315 }
316
317 var selection = this.element.getComponentSelection();
318 var shouldRestoreSelection = this._updateSelectionModel(selection);
319
320 var visibleFrom = this.element.scrollTop;
321 var visibleHeight = this._visibleHeight();
322 var isInvalidating = !this._cumulativeHeights;
323
324 for (var i = 0; i < this._renderedItems.length; ++i) {
325 // Tolerate 1-pixel error due to double-to-integer rounding errors.
326 if (this._cumulativeHeights &&
327 Math.abs(this._cachedItemHeight(this._firstActiveIndex + i) - this._re nderedItems[i].element().offsetHeight) >
328 1)
329 delete this._cumulativeHeights;
330 }
331 this._rebuildCumulativeHeightsIfNeeded();
332 var oldFirstActiveIndex = this._firstActiveIndex;
333 var oldLastActiveIndex = this._lastActiveIndex;
334 var activeHeight = visibleHeight * 2;
335 // When the viewport is scrolled to the bottom, using the cumulative heights estimate is not
336 // precise enough to determine next visible indices. This stickToBottom chec k avoids extra
337 // calls to refresh in those cases.
338 if (this._stickToBottom) {
339 this._firstActiveIndex =
340 Math.max(this._itemCount - Math.ceil(activeHeight / this._provider.min imumRowHeight()), 0);
341 this._lastActiveIndex = this._itemCount - 1;
342 } else {
343 this._firstActiveIndex = Math.max(
344 Array.prototype.lowerBound.call(
345 this._cumulativeHeights, visibleFrom + 1 - (activeHeight - visible Height) / 2),
346 0);
347 // Proactively render more rows in case some of them will be collapsed wit hout triggering refresh. @see crbug.com/390169
348 this._lastActiveIndex = this._firstActiveIndex + Math.ceil(activeHeight / this._provider.minimumRowHeight()) - 1;
349 this._lastActiveIndex = Math.min(this._lastActiveIndex, this._itemCount - 1);
350 }
351
352 var topGapHeight = this._cumulativeHeights[this._firstActiveIndex - 1] || 0;
353 var bottomGapHeight =
354 this._cumulativeHeights[this._cumulativeHeights.length - 1] - this._cumu lativeHeights[this._lastActiveIndex];
355
356 /**
357 * @this {WebInspector.ViewportControl}
358 */
359 function prepare() {
360 this._topGapElement.style.height = topGapHeight + 'px';
361 this._bottomGapElement.style.height = bottomGapHeight + 'px';
362 this._topGapElement._active = !!topGapHeight;
363 this._bottomGapElement._active = !!bottomGapHeight;
364 this._contentElement.style.setProperty('height', '10000000px');
365 }
366
367 if (isInvalidating)
368 this._fullViewportUpdate(prepare.bind(this));
369 else
370 this._partialViewportUpdate(oldFirstActiveIndex, oldLastActiveIndex, prepa re.bind(this));
371 this._contentElement.style.removeProperty('height');
372 // Should be the last call in the method as it might force layout.
373 if (shouldRestoreSelection)
374 this._restoreSelection(selection);
375 if (this._stickToBottom)
376 this.element.scrollTop = 10000000;
377 }
378
379 /**
380 * @param {function()} prepare
381 */
382 _fullViewportUpdate(prepare) {
383 for (var i = 0; i < this._renderedItems.length; ++i)
384 this._renderedItems[i].willHide();
385 prepare();
386 this._renderedItems = [];
387 this._contentElement.removeChildren();
388 for (var i = this._firstActiveIndex; i <= this._lastActiveIndex; ++i) {
389 var viewportElement = this._providerElement(i);
390 this._contentElement.appendChild(viewportElement.element());
391 this._renderedItems.push(viewportElement);
392 }
393 for (var i = 0; i < this._renderedItems.length; ++i)
394 this._renderedItems[i].wasShown();
395 }
396
397 /**
398 * @param {number} oldFirstActiveIndex
399 * @param {number} oldLastActiveIndex
400 * @param {function()} prepare
401 */
402 _partialViewportUpdate(oldFirstActiveIndex, oldLastActiveIndex, prepare) {
403 var willBeHidden = [];
404 for (var i = 0; i < this._renderedItems.length; ++i) {
405 var index = oldFirstActiveIndex + i;
406 if (index < this._firstActiveIndex || this._lastActiveIndex < index)
407 willBeHidden.push(this._renderedItems[i]);
408 }
409 for (var i = 0; i < willBeHidden.length; ++i)
410 willBeHidden[i].willHide();
411 prepare();
412 for (var i = 0; i < willBeHidden.length; ++i)
413 willBeHidden[i].element().remove();
414
415 this._renderedItems = [];
416 var anchor = this._contentElement.firstChild;
417 var wasShown = [];
418 for (var i = this._firstActiveIndex; i <= this._lastActiveIndex; ++i) {
419 var viewportElement = this._providerElement(i);
420 var element = viewportElement.element();
421 if (element !== anchor) {
422 this._contentElement.insertBefore(element, anchor);
423 wasShown.push(viewportElement);
424 } else {
425 anchor = anchor.nextSibling;
426 }
427 this._renderedItems.push(viewportElement);
428 }
429 for (var i = 0; i < wasShown.length; ++i)
430 wasShown[i].wasShown();
431 }
432
433 /**
434 * @return {?string}
435 */
436 _selectedText() {
437 this._updateSelectionModel(this.element.getComponentSelection());
438 if (!this._headSelection || !this._anchorSelection)
439 return null;
440
441 var startSelection = null;
442 var endSelection = null;
443 if (this._selectionIsBackward) {
444 startSelection = this._headSelection;
445 endSelection = this._anchorSelection;
446 } else {
447 startSelection = this._anchorSelection;
448 endSelection = this._headSelection;
449 }
450
451 var textLines = [];
452 for (var i = startSelection.item; i <= endSelection.item; ++i)
453 textLines.push(this._providerElement(i).element().deepTextContent());
454
455 var endSelectionElement = this._providerElement(endSelection.item).element() ;
456 if (endSelection.node && endSelection.node.isSelfOrDescendant(endSelectionEl ement)) {
457 var itemTextOffset = this._textOffsetInNode(endSelectionElement, endSelect ion.node, endSelection.offset);
458 textLines[textLines.length - 1] = textLines.peekLast().substring(0, itemTe xtOffset);
459 }
460
461 var startSelectionElement = this._providerElement(startSelection.item).eleme nt();
462 if (startSelection.node && startSelection.node.isSelfOrDescendant(startSelec tionElement)) {
463 var itemTextOffset = this._textOffsetInNode(startSelectionElement, startSe lection.node, startSelection.offset);
464 textLines[0] = textLines[0].substring(itemTextOffset);
465 }
466
467 return textLines.join('\n');
468 }
469
470 /**
471 * @param {!Element} itemElement
472 * @param {!Node} container
473 * @param {number} offset
474 * @return {number}
475 */
476 _textOffsetInNode(itemElement, container, offset) {
477 var chars = 0;
478 var node = itemElement;
479 while ((node = node.traverseNextTextNode()) && !node.isSelfOrDescendant(cont ainer))
480 chars += node.textContent.length;
481 return chars + offset;
482 }
483
484 /**
485 * @param {!Event} event
486 */
487 _onScroll(event) {
488 this.refresh();
489 }
490
491 /**
492 * @return {number}
493 */
494 firstVisibleIndex() {
495 var firstVisibleIndex =
496 Math.max(Array.prototype.lowerBound.call(this._cumulativeHeights, this.e lement.scrollTop + 1), 0);
497 return Math.max(firstVisibleIndex, this._firstActiveIndex);
498 }
499
500 /**
501 * @return {number}
502 */
503 lastVisibleIndex() {
504 var lastVisibleIndex;
505 if (this._stickToBottom)
506 lastVisibleIndex = this._itemCount - 1;
507 else
508 lastVisibleIndex =
509 this.firstVisibleIndex() + Math.ceil(this._visibleHeight() / this._pro vider.minimumRowHeight()) - 1;
510 return Math.min(lastVisibleIndex, this._lastActiveIndex);
511 }
512
513 /**
514 * @return {?Element}
515 */
516 renderedElementAt(index) {
517 if (index < this._firstActiveIndex)
518 return null;
519 if (index > this._lastActiveIndex)
520 return null;
521 return this._renderedItems[index - this._firstActiveIndex].element();
522 }
523
524 /**
525 * @param {number} index
526 * @param {boolean=} makeLast
527 */
528 scrollItemIntoView(index, makeLast) {
529 var firstVisibleIndex = this.firstVisibleIndex();
530 var lastVisibleIndex = this.lastVisibleIndex();
531 if (index > firstVisibleIndex && index < lastVisibleIndex)
532 return;
533 if (makeLast)
534 this.forceScrollItemToBeLast(index);
535 else if (index <= firstVisibleIndex)
536 this.forceScrollItemToBeFirst(index);
537 else if (index >= lastVisibleIndex)
538 this.forceScrollItemToBeLast(index);
539 }
540
541 /**
542 * @param {number} index
543 */
544 forceScrollItemToBeFirst(index) {
545 this.setStickToBottom(false);
546 this._rebuildCumulativeHeightsIfNeeded();
547 this.element.scrollTop = index > 0 ? this._cumulativeHeights[index - 1] : 0;
548 if (this.element.isScrolledToBottom())
549 this.setStickToBottom(true);
550 this.refresh();
551 }
552
553 /**
554 * @param {number} index
555 */
556 forceScrollItemToBeLast(index) {
557 this.setStickToBottom(false);
558 this._rebuildCumulativeHeightsIfNeeded();
559 this.element.scrollTop = this._cumulativeHeights[index] - this._visibleHeigh t();
560 if (this.element.isScrolledToBottom())
561 this.setStickToBottom(true);
562 this.refresh();
563 }
564
565 /**
566 * @return {number}
567 */
568 _visibleHeight() {
569 // Use offsetHeight instead of clientHeight to avoid being affected by horiz ontal scroll.
570 return this.element.offsetHeight;
571 }
69 }; 572 };
70 573
71 /** 574 /**
72 * @interface 575 * @interface
73 */ 576 */
74 WebInspector.ViewportControl.Provider = function() 577 WebInspector.ViewportControl.Provider = function() {};
75 {
76 };
77 578
78 WebInspector.ViewportControl.Provider.prototype = { 579 WebInspector.ViewportControl.Provider.prototype = {
79 /** 580 /**
80 * @param {number} index 581 * @param {number} index
81 * @return {number} 582 * @return {number}
82 */ 583 */
83 fastHeight: function(index) { return 0; }, 584 fastHeight: function(index) {
84 585 return 0;
85 /** 586 },
86 * @return {number} 587
87 */ 588 /**
88 itemCount: function() { return 0; }, 589 * @return {number}
89 590 */
90 /** 591 itemCount: function() {
91 * @return {number} 592 return 0;
92 */ 593 },
93 minimumRowHeight: function() { return 0; }, 594
94 595 /**
95 /** 596 * @return {number}
96 * @param {number} index 597 */
97 * @return {?WebInspector.ViewportElement} 598 minimumRowHeight: function() {
98 */ 599 return 0;
99 itemElement: function(index) { return null; } 600 },
601
602 /**
603 * @param {number} index
604 * @return {?WebInspector.ViewportElement}
605 */
606 itemElement: function(index) {
607 return null;
608 }
100 }; 609 };
101 610
102 /** 611 /**
103 * @interface 612 * @interface
104 */ 613 */
105 WebInspector.ViewportElement = function() { }; 614 WebInspector.ViewportElement = function() {};
106 WebInspector.ViewportElement.prototype = { 615 WebInspector.ViewportElement.prototype = {
107 willHide: function() { }, 616 willHide: function() {},
108 617
109 wasShown: function() { }, 618 wasShown: function() {},
110 619
111 /** 620 /**
112 * @return {!Element} 621 * @return {!Element}
113 */ 622 */
114 element: function() { }, 623 element: function() {},
115 }; 624 };
116 625
117 /** 626 /**
118 * @constructor
119 * @implements {WebInspector.ViewportElement} 627 * @implements {WebInspector.ViewportElement}
120 * @param {!Element} element 628 * @unrestricted
121 */ 629 */
122 WebInspector.StaticViewportElement = function(element) 630 WebInspector.StaticViewportElement = class {
123 { 631 /**
632 * @param {!Element} element
633 */
634 constructor(element) {
124 this._element = element; 635 this._element = element;
636 }
637
638 /**
639 * @override
640 */
641 willHide() {
642 }
643
644 /**
645 * @override
646 */
647 wasShown() {
648 }
649
650 /**
651 * @override
652 * @return {!Element}
653 */
654 element() {
655 return this._element;
656 }
125 }; 657 };
126
127 WebInspector.StaticViewportElement.prototype = {
128 /**
129 * @override
130 */
131 willHide: function() { },
132
133 /**
134 * @override
135 */
136 wasShown: function() { },
137
138 /**
139 * @override
140 * @return {!Element}
141 */
142 element: function()
143 {
144 return this._element;
145 },
146 };
147
148 WebInspector.ViewportControl.prototype = {
149 /**
150 * @return {boolean}
151 */
152 stickToBottom: function()
153 {
154 return this._stickToBottom;
155 },
156
157 /**
158 * @param {boolean} value
159 */
160 setStickToBottom: function(value)
161 {
162 this._stickToBottom = value;
163 if (this._stickToBottom)
164 this._observer.observe(this._contentElement, this._observerConfig);
165 else
166 this._observer.disconnect();
167 },
168
169 /**
170 * @param {!Event} event
171 */
172 _onCopy: function(event)
173 {
174 var text = this._selectedText();
175 if (!text)
176 return;
177 event.preventDefault();
178 event.clipboardData.setData("text/plain", text);
179 },
180
181 /**
182 * @param {!Event} event
183 */
184 _onDragStart: function(event)
185 {
186 var text = this._selectedText();
187 if (!text)
188 return false;
189 event.dataTransfer.clearData();
190 event.dataTransfer.setData("text/plain", text);
191 event.dataTransfer.effectAllowed = "copy";
192 return true;
193 },
194
195 /**
196 * @return {!Element}
197 */
198 contentElement: function()
199 {
200 return this._contentElement;
201 },
202
203 invalidate: function()
204 {
205 delete this._cumulativeHeights;
206 delete this._cachedProviderElements;
207 this._itemCount = this._provider.itemCount();
208 this.refresh();
209 },
210
211 /**
212 * @param {number} index
213 * @return {?WebInspector.ViewportElement}
214 */
215 _providerElement: function(index)
216 {
217 if (!this._cachedProviderElements)
218 this._cachedProviderElements = new Array(this._itemCount);
219 var element = this._cachedProviderElements[index];
220 if (!element) {
221 element = this._provider.itemElement(index);
222 this._cachedProviderElements[index] = element;
223 }
224 return element;
225 },
226
227 _rebuildCumulativeHeightsIfNeeded: function()
228 {
229 if (this._cumulativeHeights)
230 return;
231 if (!this._itemCount)
232 return;
233 var firstActiveIndex = this._firstActiveIndex;
234 var lastActiveIndex = this._lastActiveIndex;
235 var height = 0;
236 this._cumulativeHeights = new Int32Array(this._itemCount);
237 for (var i = 0; i < this._itemCount; ++i) {
238 if (firstActiveIndex <= i && i <= lastActiveIndex)
239 height += this._renderedItems[i - firstActiveIndex].element().of fsetHeight;
240 else
241 height += this._provider.fastHeight(i);
242 this._cumulativeHeights[i] = height;
243 }
244 },
245
246 /**
247 * @param {number} index
248 * @return {number}
249 */
250 _cachedItemHeight: function(index)
251 {
252 return index === 0 ? this._cumulativeHeights[0] : this._cumulativeHeight s[index] - this._cumulativeHeights[index - 1];
253 },
254
255 /**
256 * @param {?Selection} selection
257 * @suppressGlobalPropertiesCheck
258 */
259 _isSelectionBackwards: function(selection)
260 {
261 if (!selection || !selection.rangeCount)
262 return false;
263 var range = document.createRange();
264 range.setStart(selection.anchorNode, selection.anchorOffset);
265 range.setEnd(selection.focusNode, selection.focusOffset);
266 return range.collapsed;
267 },
268
269 /**
270 * @param {number} itemIndex
271 * @param {!Node} node
272 * @param {number} offset
273 * @return {!{item: number, node: !Node, offset: number}}
274 */
275 _createSelectionModel: function(itemIndex, node, offset)
276 {
277 return {
278 item: itemIndex,
279 node: node,
280 offset: offset
281 };
282 },
283
284 /**
285 * @param {?Selection} selection
286 */
287 _updateSelectionModel: function(selection)
288 {
289 var range = selection && selection.rangeCount ? selection.getRangeAt(0) : null;
290 if (!range || selection.isCollapsed || !this.element.hasSelection()) {
291 this._headSelection = null;
292 this._anchorSelection = null;
293 return false;
294 }
295
296 var firstSelected = Number.MAX_VALUE;
297 var lastSelected = -1;
298
299 var hasVisibleSelection = false;
300 for (var i = 0; i < this._renderedItems.length; ++i) {
301 if (range.intersectsNode(this._renderedItems[i].element())) {
302 var index = i + this._firstActiveIndex;
303 firstSelected = Math.min(firstSelected, index);
304 lastSelected = Math.max(lastSelected, index);
305 hasVisibleSelection = true;
306 }
307 }
308 if (hasVisibleSelection) {
309 firstSelected = this._createSelectionModel(firstSelected, /** @type {!Node} */(range.startContainer), range.startOffset);
310 lastSelected = this._createSelectionModel(lastSelected, /** @type {! Node} */(range.endContainer), range.endOffset);
311 }
312 var topOverlap = range.intersectsNode(this._topGapElement) && this._topG apElement._active;
313 var bottomOverlap = range.intersectsNode(this._bottomGapElement) && this ._bottomGapElement._active;
314 if (!topOverlap && !bottomOverlap && !hasVisibleSelection) {
315 this._headSelection = null;
316 this._anchorSelection = null;
317 return false;
318 }
319
320 if (!this._anchorSelection || !this._headSelection) {
321 this._anchorSelection = this._createSelectionModel(0, this.element, 0);
322 this._headSelection = this._createSelectionModel(this._itemCount - 1 , this.element, this.element.children.length);
323 this._selectionIsBackward = false;
324 }
325
326 var isBackward = this._isSelectionBackwards(selection);
327 var startSelection = this._selectionIsBackward ? this._headSelection : t his._anchorSelection;
328 var endSelection = this._selectionIsBackward ? this._anchorSelection : t his._headSelection;
329 if (topOverlap && bottomOverlap && hasVisibleSelection) {
330 firstSelected = firstSelected.item < startSelection.item ? firstSele cted : startSelection;
331 lastSelected = lastSelected.item > endSelection.item ? lastSelected : endSelection;
332 } else if (!hasVisibleSelection) {
333 firstSelected = startSelection;
334 lastSelected = endSelection;
335 } else if (topOverlap)
336 firstSelected = isBackward ? this._headSelection : this._anchorSelec tion;
337 else if (bottomOverlap)
338 lastSelected = isBackward ? this._anchorSelection : this._headSelect ion;
339
340 if (isBackward) {
341 this._anchorSelection = lastSelected;
342 this._headSelection = firstSelected;
343 } else {
344 this._anchorSelection = firstSelected;
345 this._headSelection = lastSelected;
346 }
347 this._selectionIsBackward = isBackward;
348 return true;
349 },
350
351 /**
352 * @param {?Selection} selection
353 */
354 _restoreSelection: function(selection)
355 {
356 var anchorElement = null;
357 var anchorOffset;
358 if (this._firstActiveIndex <= this._anchorSelection.item && this._anchor Selection.item <= this._lastActiveIndex) {
359 anchorElement = this._anchorSelection.node;
360 anchorOffset = this._anchorSelection.offset;
361 } else {
362 if (this._anchorSelection.item < this._firstActiveIndex)
363 anchorElement = this._topGapElement;
364 else if (this._anchorSelection.item > this._lastActiveIndex)
365 anchorElement = this._bottomGapElement;
366 anchorOffset = this._selectionIsBackward ? 1 : 0;
367 }
368
369 var headElement = null;
370 var headOffset;
371 if (this._firstActiveIndex <= this._headSelection.item && this._headSele ction.item <= this._lastActiveIndex) {
372 headElement = this._headSelection.node;
373 headOffset = this._headSelection.offset;
374 } else {
375 if (this._headSelection.item < this._firstActiveIndex)
376 headElement = this._topGapElement;
377 else if (this._headSelection.item > this._lastActiveIndex)
378 headElement = this._bottomGapElement;
379 headOffset = this._selectionIsBackward ? 0 : 1;
380 }
381
382 selection.setBaseAndExtent(anchorElement, anchorOffset, headElement, hea dOffset);
383 },
384
385 refresh: function()
386 {
387 this._observer.disconnect();
388 this._innerRefresh();
389 if (this._stickToBottom)
390 this._observer.observe(this._contentElement, this._observerConfig);
391 },
392
393 _innerRefresh: function()
394 {
395 if (!this._visibleHeight())
396 return; // Do nothing for invisible controls.
397
398 if (!this._itemCount) {
399 for (var i = 0; i < this._renderedItems.length; ++i)
400 this._renderedItems[i].willHide();
401 this._renderedItems = [];
402 this._contentElement.removeChildren();
403 this._topGapElement.style.height = "0px";
404 this._bottomGapElement.style.height = "0px";
405 this._firstActiveIndex = -1;
406 this._lastActiveIndex = -1;
407 return;
408 }
409
410 var selection = this.element.getComponentSelection();
411 var shouldRestoreSelection = this._updateSelectionModel(selection);
412
413 var visibleFrom = this.element.scrollTop;
414 var visibleHeight = this._visibleHeight();
415 var isInvalidating = !this._cumulativeHeights;
416
417 for (var i = 0; i < this._renderedItems.length; ++i) {
418 // Tolerate 1-pixel error due to double-to-integer rounding errors.
419 if (this._cumulativeHeights && Math.abs(this._cachedItemHeight(this. _firstActiveIndex + i) - this._renderedItems[i].element().offsetHeight) > 1)
420 delete this._cumulativeHeights;
421 }
422 this._rebuildCumulativeHeightsIfNeeded();
423 var oldFirstActiveIndex = this._firstActiveIndex;
424 var oldLastActiveIndex = this._lastActiveIndex;
425 var activeHeight = visibleHeight * 2;
426 // When the viewport is scrolled to the bottom, using the cumulative hei ghts estimate is not
427 // precise enough to determine next visible indices. This stickToBottom check avoids extra
428 // calls to refresh in those cases.
429 if (this._stickToBottom) {
430 this._firstActiveIndex = Math.max(this._itemCount - Math.ceil(active Height / this._provider.minimumRowHeight()), 0);
431 this._lastActiveIndex = this._itemCount - 1;
432 } else {
433 this._firstActiveIndex = Math.max(Array.prototype.lowerBound.call(th is._cumulativeHeights, visibleFrom + 1 - (activeHeight - visibleHeight) / 2), 0) ;
434 // Proactively render more rows in case some of them will be collaps ed without triggering refresh. @see crbug.com/390169
435 this._lastActiveIndex = this._firstActiveIndex + Math.ceil(activeHei ght / this._provider.minimumRowHeight()) - 1;
436 this._lastActiveIndex = Math.min(this._lastActiveIndex, this._itemCo unt - 1);
437 }
438
439 var topGapHeight = this._cumulativeHeights[this._firstActiveIndex - 1] | | 0;
440 var bottomGapHeight = this._cumulativeHeights[this._cumulativeHeights.le ngth - 1] - this._cumulativeHeights[this._lastActiveIndex];
441
442 /**
443 * @this {WebInspector.ViewportControl}
444 */
445 function prepare()
446 {
447 this._topGapElement.style.height = topGapHeight + "px";
448 this._bottomGapElement.style.height = bottomGapHeight + "px";
449 this._topGapElement._active = !!topGapHeight;
450 this._bottomGapElement._active = !!bottomGapHeight;
451 this._contentElement.style.setProperty("height", "10000000px");
452 }
453
454 if (isInvalidating)
455 this._fullViewportUpdate(prepare.bind(this));
456 else
457 this._partialViewportUpdate(oldFirstActiveIndex, oldLastActiveIndex, prepare.bind(this));
458 this._contentElement.style.removeProperty("height");
459 // Should be the last call in the method as it might force layout.
460 if (shouldRestoreSelection)
461 this._restoreSelection(selection);
462 if (this._stickToBottom)
463 this.element.scrollTop = 10000000;
464 },
465
466 /**
467 * @param {function()} prepare
468 */
469 _fullViewportUpdate: function(prepare)
470 {
471 for (var i = 0; i < this._renderedItems.length; ++i)
472 this._renderedItems[i].willHide();
473 prepare();
474 this._renderedItems = [];
475 this._contentElement.removeChildren();
476 for (var i = this._firstActiveIndex; i <= this._lastActiveIndex; ++i) {
477 var viewportElement = this._providerElement(i);
478 this._contentElement.appendChild(viewportElement.element());
479 this._renderedItems.push(viewportElement);
480 }
481 for (var i = 0; i < this._renderedItems.length; ++i)
482 this._renderedItems[i].wasShown();
483 },
484
485 /**
486 * @param {number} oldFirstActiveIndex
487 * @param {number} oldLastActiveIndex
488 * @param {function()} prepare
489 */
490 _partialViewportUpdate: function(oldFirstActiveIndex, oldLastActiveIndex, pr epare)
491 {
492 var willBeHidden = [];
493 for (var i = 0; i < this._renderedItems.length; ++i) {
494 var index = oldFirstActiveIndex + i;
495 if (index < this._firstActiveIndex || this._lastActiveIndex < index)
496 willBeHidden.push(this._renderedItems[i]);
497 }
498 for (var i = 0; i < willBeHidden.length; ++i)
499 willBeHidden[i].willHide();
500 prepare();
501 for (var i = 0; i < willBeHidden.length; ++i)
502 willBeHidden[i].element().remove();
503
504 this._renderedItems = [];
505 var anchor = this._contentElement.firstChild;
506 var wasShown = [];
507 for (var i = this._firstActiveIndex; i <= this._lastActiveIndex; ++i) {
508 var viewportElement = this._providerElement(i);
509 var element = viewportElement.element();
510 if (element !== anchor) {
511 this._contentElement.insertBefore(element, anchor);
512 wasShown.push(viewportElement);
513 } else {
514 anchor = anchor.nextSibling;
515 }
516 this._renderedItems.push(viewportElement);
517 }
518 for (var i = 0; i < wasShown.length; ++i)
519 wasShown[i].wasShown();
520 },
521
522 /**
523 * @return {?string}
524 */
525 _selectedText: function()
526 {
527 this._updateSelectionModel(this.element.getComponentSelection());
528 if (!this._headSelection || !this._anchorSelection)
529 return null;
530
531 var startSelection = null;
532 var endSelection = null;
533 if (this._selectionIsBackward) {
534 startSelection = this._headSelection;
535 endSelection = this._anchorSelection;
536 } else {
537 startSelection = this._anchorSelection;
538 endSelection = this._headSelection;
539 }
540
541 var textLines = [];
542 for (var i = startSelection.item; i <= endSelection.item; ++i)
543 textLines.push(this._providerElement(i).element().deepTextContent()) ;
544
545 var endSelectionElement = this._providerElement(endSelection.item).eleme nt();
546 if (endSelection.node && endSelection.node.isSelfOrDescendant(endSelecti onElement)) {
547 var itemTextOffset = this._textOffsetInNode(endSelectionElement, end Selection.node, endSelection.offset);
548 textLines[textLines.length - 1] = textLines.peekLast().substring(0, itemTextOffset);
549 }
550
551 var startSelectionElement = this._providerElement(startSelection.item).e lement();
552 if (startSelection.node && startSelection.node.isSelfOrDescendant(startS electionElement)) {
553 var itemTextOffset = this._textOffsetInNode(startSelectionElement, s tartSelection.node, startSelection.offset);
554 textLines[0] = textLines[0].substring(itemTextOffset);
555 }
556
557 return textLines.join("\n");
558 },
559
560 /**
561 * @param {!Element} itemElement
562 * @param {!Node} container
563 * @param {number} offset
564 * @return {number}
565 */
566 _textOffsetInNode: function(itemElement, container, offset)
567 {
568 var chars = 0;
569 var node = itemElement;
570 while ((node = node.traverseNextTextNode()) && !node.isSelfOrDescendant( container))
571 chars += node.textContent.length;
572 return chars + offset;
573 },
574
575 /**
576 * @param {!Event} event
577 */
578 _onScroll: function(event)
579 {
580 this.refresh();
581 },
582
583 /**
584 * @return {number}
585 */
586 firstVisibleIndex: function()
587 {
588 var firstVisibleIndex = Math.max(Array.prototype.lowerBound.call(this._c umulativeHeights, this.element.scrollTop + 1), 0);
589 return Math.max(firstVisibleIndex, this._firstActiveIndex);
590 },
591
592 /**
593 * @return {number}
594 */
595 lastVisibleIndex: function()
596 {
597 var lastVisibleIndex;
598 if (this._stickToBottom)
599 lastVisibleIndex = this._itemCount - 1;
600 else
601 lastVisibleIndex = this.firstVisibleIndex() + Math.ceil(this._visibl eHeight() / this._provider.minimumRowHeight()) - 1;
602 return Math.min(lastVisibleIndex, this._lastActiveIndex);
603 },
604
605 /**
606 * @return {?Element}
607 */
608 renderedElementAt: function(index)
609 {
610 if (index < this._firstActiveIndex)
611 return null;
612 if (index > this._lastActiveIndex)
613 return null;
614 return this._renderedItems[index - this._firstActiveIndex].element();
615 },
616
617 /**
618 * @param {number} index
619 * @param {boolean=} makeLast
620 */
621 scrollItemIntoView: function(index, makeLast)
622 {
623 var firstVisibleIndex = this.firstVisibleIndex();
624 var lastVisibleIndex = this.lastVisibleIndex();
625 if (index > firstVisibleIndex && index < lastVisibleIndex)
626 return;
627 if (makeLast)
628 this.forceScrollItemToBeLast(index);
629 else if (index <= firstVisibleIndex)
630 this.forceScrollItemToBeFirst(index);
631 else if (index >= lastVisibleIndex)
632 this.forceScrollItemToBeLast(index);
633 },
634
635 /**
636 * @param {number} index
637 */
638 forceScrollItemToBeFirst: function(index)
639 {
640 this.setStickToBottom(false);
641 this._rebuildCumulativeHeightsIfNeeded();
642 this.element.scrollTop = index > 0 ? this._cumulativeHeights[index - 1] : 0;
643 if (this.element.isScrolledToBottom())
644 this.setStickToBottom(true);
645 this.refresh();
646 },
647
648 /**
649 * @param {number} index
650 */
651 forceScrollItemToBeLast: function(index)
652 {
653 this.setStickToBottom(false);
654 this._rebuildCumulativeHeightsIfNeeded();
655 this.element.scrollTop = this._cumulativeHeights[index] - this._visibleH eight();
656 if (this.element.isScrolledToBottom())
657 this.setStickToBottom(true);
658 this.refresh();
659 },
660
661 /**
662 * @return {number}
663 */
664 _visibleHeight: function()
665 {
666 // Use offsetHeight instead of clientHeight to avoid being affected by h orizontal scroll.
667 return this.element.offsetHeight;
668 }
669 };
OLDNEW
« no previous file with comments | « third_party/WebKit/Source/devtools/front_end/ui/View.js ('k') | third_party/WebKit/Source/devtools/front_end/ui/Widget.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698