Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 /** | 4 /** |
| 5 * @unrestricted | 5 * @unrestricted |
| 6 */ | 6 */ |
| 7 UI.ViewportControl = class { | 7 UI.ViewportControl = class { |
| 8 /** | 8 /** |
| 9 * @param {!UI.ViewportControl.Provider} provider | 9 * @param {!UI.ViewportProvider} provider |
| 10 */ | 10 */ |
| 11 constructor(provider) { | 11 constructor(provider) { |
| 12 this.element = createElement('div'); | 12 this.element = createElement('div'); |
| 13 this.element.style.overflow = 'auto'; | 13 this.element.style.overflow = 'auto'; |
| 14 this._innerElement = this.element.createChild('div'); | 14 this._topElement = this.element.createChild('div'); |
| 15 this._innerElement.style.height = '0px'; | 15 this._bottomElement = this.element.createChild('div'); |
| 16 this._innerElement.style.position = 'relative'; | |
| 17 this._innerElement.style.overflow = 'hidden'; | |
| 18 | |
| 19 this._provider = provider; | 16 this._provider = provider; |
| 20 this.element.addEventListener('scroll', this._update.bind(this), false); | 17 this._firstIndex = 0; |
| 21 this._itemCount = 0; | 18 this._lastIndex = 0; |
| 22 this._indexSymbol = Symbol('UI.ViewportControl._indexSymbol'); | 19 this.element.addEventListener('scroll', this._onScroll.bind(this), false); |
| 20 this._update(0); | |
| 21 } | |
| 22 | |
| 23 /** | |
| 24 * @param {number} from | |
| 25 * @param {number} to | |
| 26 */ | |
| 27 spliced(from, to) { | |
| 28 var scrollTop = this.element.scrollTop; | |
| 29 var totalHeight = this._provider.totalHeight(); | |
| 30 if (this._totalHeight < this.element.offsetHeight) { | |
| 31 this.refresh(); | |
| 32 return; | |
| 33 } | |
| 34 if (to <= this._firstIndex) { | |
| 35 var topHeight = this._topHeight + totalHeight - this._totalHeight; | |
| 36 this._topElement.style.height = topHeight + 'px'; | |
| 37 this.element.scrollTop = scrollTop + totalHeight - this._totalHeight; | |
| 38 this._topHeight = topHeight; | |
| 39 this._totalHeight = totalHeight; | |
| 40 return; | |
| 41 } | |
| 42 if (from >= this._lastIndex) { | |
| 43 var bottomHeight = this._bottomHeight + totalHeight - this._totalHeight; | |
| 44 this._bottomElement.style.height = bottomHeight + 'px'; | |
| 45 this.element.scrollTop = scrollTop + totalHeight - this._totalHeight; | |
| 46 this._bottomHeight = bottomHeight; | |
| 47 this._totalHeight = totalHeight; | |
| 48 return; | |
| 49 } | |
| 50 this.refresh(); | |
| 23 } | 51 } |
| 24 | 52 |
| 25 refresh() { | 53 refresh() { |
| 26 this._itemCount = this._provider.itemCount(); | 54 this._firstIndex = 0; |
| 27 this._innerElement.removeChildren(); | 55 this._lastIndex = 0; |
| 28 | 56 this.element.removeChildren(); |
| 29 var height = 0; | 57 this.element.appendChild(this._topElement); |
| 30 this._cumulativeHeights = new Int32Array(this._itemCount); | 58 this.element.appendChild(this._bottomElement); |
| 31 for (var i = 0; i < this._itemCount; ++i) { | 59 this._update(0); |
| 32 height += this._provider.fastItemHeight(i); | 60 } |
| 33 this._cumulativeHeights[i] = height; | 61 |
| 34 } | 62 _onScroll() { |
| 35 this._innerElement.style.height = height + 'px'; | 63 this._update(this.element.scrollTop); |
| 36 | 64 } |
| 37 this._update(); | 65 |
| 38 } | 66 /** |
| 39 | 67 * @param {number} scrollTop |
| 40 _update() { | 68 */ |
| 41 if (!this._cumulativeHeights) { | 69 _update(scrollTop) { |
| 42 this.refresh(); | 70 var totalHeight = this._provider.totalHeight(); |
| 43 return; | 71 if (!totalHeight) { |
| 44 } | 72 this._firstIndex = 0; |
| 45 | 73 this._lastIndex = 0; |
| 46 var visibleHeight = this._visibleHeight(); | 74 this._topHeight = 0; |
| 47 var visibleFrom = this.element.scrollTop; | 75 this._bottomHeight = 0; |
| 48 var activeHeight = visibleHeight * 2; | 76 this._totalHeight = 0; |
| 49 var firstActiveIndex = Math.max( | 77 this._topElement.style.height = '0px'; |
| 50 Array.prototype.lowerBound.call(this._cumulativeHeights, visibleFrom + 1 - (activeHeight - visibleHeight) / 2), | 78 this._bottomElement.style.height = '0px'; |
| 51 0); | 79 return; |
| 52 var lastActiveIndex = Math.min( | 80 } |
| 53 Array.prototype.lowerBound.call( | 81 |
| 54 this._cumulativeHeights, visibleFrom + visibleHeight + (activeHeight - visibleHeight) / 2), | 82 var height = this.element.offsetHeight; |
| 55 this._itemCount - 1); | 83 var firstIndex = this._provider.indexAtOffset(Math.max(0, scrollTop - height )); |
| 56 | 84 var lastIndex = this._provider.indexAtOffset(Math.min(totalHeight, scrollTop + 2 * height)) + 1; |
| 57 var children = this._innerElement.children; | 85 |
| 58 for (var i = children.length - 1; i >= 0; --i) { | 86 for (var index = this._firstIndex; index < firstIndex; index++) { |
| 59 var element = children[i]; | 87 var element = this._provider.elementAtIndex(index); |
| 60 if (element[this._indexSymbol] < firstActiveIndex || element[this._indexSy mbol] > lastActiveIndex) | 88 element.remove(); |
| 61 element.remove(); | 89 this._firstIndex++; |
| 62 } | 90 } |
| 63 | 91 for (var index = this._lastIndex - 1; index >= lastIndex; index--) { |
| 64 for (var i = firstActiveIndex; i <= lastActiveIndex; ++i) | 92 var element = this._provider.elementAtIndex(index); |
| 65 this._insertElement(i); | 93 element.remove(); |
| 66 } | 94 this._lastIndex--; |
| 67 | 95 } |
| 68 /** | 96 this._firstIndex = Math.min(this._firstIndex, lastIndex); |
| 69 * @param {number} index | 97 this._lastIndex = Math.max(this._lastIndex, firstIndex); |
| 70 */ | 98 for (var index = this._firstIndex - 1; index >= firstIndex; index--) { |
| 71 _insertElement(index) { | 99 var element = this._provider.elementAtIndex(index); |
| 72 var element = this._provider.itemElement(index); | 100 this.element.insertBefore(element, this._topElement.nextSibling); |
| 73 if (!element || element.parentElement === this._innerElement) | 101 } |
| 74 return; | 102 for (var index = this._lastIndex; index < lastIndex; index++) { |
| 75 | 103 var element = this._provider.elementAtIndex(index); |
| 76 element.style.position = 'absolute'; | 104 this.element.insertBefore(element, this._bottomElement); |
| 77 element.style.top = (this._cumulativeHeights[index - 1] || 0) + 'px'; | 105 } |
| 78 element.style.left = '0'; | 106 |
| 79 element.style.right = '0'; | 107 this._firstIndex = firstIndex; |
| 80 element[this._indexSymbol] = index; | 108 this._lastIndex = lastIndex; |
| 81 this._innerElement.appendChild(element); | 109 this._totalHeight = totalHeight; |
| 82 } | 110 this._topHeight = this._provider.offsetAtIndex(firstIndex); |
| 83 | 111 this._topElement.style.height = this._topHeight + 'px'; |
| 84 /** | 112 this._bottomHeight = (totalHeight - this._provider.offsetAtIndex(lastIndex)) ; |
| 85 * @return {number} | 113 this._bottomElement.style.height = this._bottomHeight + 'px'; |
| 86 */ | 114 this.element.scrollTop = scrollTop; |
| 87 firstVisibleIndex() { | 115 } |
| 88 return Math.max(Array.prototype.lowerBound.call(this._cumulativeHeights, thi s.element.scrollTop + 1), 0); | 116 |
| 89 } | 117 /** |
| 90 | 118 * @param {number} index |
| 91 /** | 119 */ |
| 92 * @return {number} | 120 scrollItemIntoView(index) { |
| 93 */ | 121 var top = this._provider.offsetAtIndex(index); |
| 94 lastVisibleIndex() { | 122 var bottom = this._provider.offsetAtIndex(index + 1); |
| 95 return Math.min( | 123 var scrollTop = this.element.scrollTop; |
| 96 Array.prototype.lowerBound.call(this._cumulativeHeights, this.element.sc rollTop + this._visibleHeight()), | 124 var height = this.element.offsetHeight; |
| 97 this._itemCount); | 125 if (top < scrollTop) |
| 98 } | 126 this._update(top); |
| 99 | 127 else if (bottom > scrollTop + height) |
| 100 /** | 128 this._update(bottom - height); |
| 101 * @param {number} index | |
| 102 * @param {boolean=} makeLast | |
| 103 */ | |
| 104 scrollItemIntoView(index, makeLast) { | |
| 105 var firstVisibleIndex = this.firstVisibleIndex(); | |
| 106 var lastVisibleIndex = this.lastVisibleIndex(); | |
| 107 if (index > firstVisibleIndex && index < lastVisibleIndex) | |
| 108 return; | |
| 109 if (makeLast) | |
| 110 this.forceScrollItemToBeLast(index); | |
| 111 else if (index <= firstVisibleIndex) | |
| 112 this.forceScrollItemToBeFirst(index); | |
| 113 else if (index >= lastVisibleIndex) | |
| 114 this.forceScrollItemToBeLast(index); | |
| 115 } | |
| 116 | |
| 117 /** | |
| 118 * @param {number} index | |
| 119 */ | |
| 120 forceScrollItemToBeFirst(index) { | |
| 121 this.element.scrollTop = index > 0 ? this._cumulativeHeights[index - 1] : 0; | |
| 122 this._update(); | |
| 123 } | |
| 124 | |
| 125 /** | |
| 126 * @param {number} index | |
| 127 */ | |
| 128 forceScrollItemToBeLast(index) { | |
| 129 this.element.scrollTop = this._cumulativeHeights[index] - this._visibleHeigh t(); | |
| 130 this._update(); | |
| 131 } | |
| 132 | |
| 133 /** | |
| 134 * @return {number} | |
| 135 */ | |
| 136 _visibleHeight() { | |
| 137 return this.element.offsetHeight; | |
| 138 } | 129 } |
| 139 }; | 130 }; |
| 140 | 131 |
| 141 /** | 132 /** |
| 142 * @interface | 133 * @interface |
| 143 */ | 134 */ |
| 144 UI.ViewportControl.Provider = function() {}; | 135 UI.ViewportProvider = function() {}; |
| 145 | 136 |
| 146 UI.ViewportControl.Provider.prototype = { | 137 UI.ViewportProvider.prototype = { |
| 147 /** | 138 /** |
| 148 * @param {number} index | 139 * @param {number} height |
| 149 * @return {number} | 140 * @return {number} |
| 150 */ | 141 */ |
| 151 fastItemHeight(index) { | 142 indexAtOffset(height) {}, |
|
pfeldman
2016/12/20 01:23:51
I understand the former API, but don't get the new
| |
| 152 return 0; | 143 |
| 153 }, | 144 /** |
| 154 | 145 * @return {number} |
| 155 /** | 146 */ |
| 156 * @return {number} | 147 totalHeight() {}, |
| 157 */ | 148 |
| 158 itemCount() { | 149 /** |
| 159 return 0; | 150 * @param {number} index |
| 160 }, | 151 * @return {!Element} |
| 161 | 152 */ |
| 162 /** | 153 elementAtIndex(index) {}, |
| 163 * @param {number} index | 154 |
| 164 * @return {?Element} | 155 /** |
| 165 */ | 156 * @param {number} index |
| 166 itemElement(index) { | 157 * @return {number} |
| 167 return null; | 158 */ |
| 168 } | 159 offsetAtIndex(index) {}, |
| 169 }; | 160 }; |
| 161 | |
| 162 /** | |
| 163 * @implements {UI.ViewportProvider} | |
| 164 */ | |
| 165 UI.SimpleViewport = class { | |
| 166 /** | |
| 167 * @param {function(number):!Element} elementsFactory | |
| 168 */ | |
| 169 constructor(elementsFactory) { | |
| 170 this._elementsFactory = elementsFactory; | |
| 171 /** @type {!Array<?Element>} */ | |
| 172 this._elements = []; | |
| 173 /** @type {number} */ | |
| 174 this._height = 0; | |
| 175 this._length = 0; | |
| 176 this._viewport = new UI.ViewportControl(this); | |
| 177 this.element = this._viewport.element; | |
| 178 } | |
| 179 | |
| 180 /** | |
| 181 * @param {number} length | |
| 182 */ | |
| 183 refresh(length) { | |
| 184 this._elements = []; | |
| 185 var oldLength = this._length; | |
| 186 this._length = length; | |
| 187 this._viewport.spliced(0, oldLength); | |
| 188 } | |
| 189 | |
| 190 /** | |
| 191 * @return {number} | |
| 192 */ | |
| 193 elementHeight() { | |
| 194 if (!this._height) | |
| 195 this._height = UI.measurePreferredSize(this.elementAtIndex(0), this._viewp ort.element).height; | |
| 196 return this._height; | |
| 197 } | |
| 198 | |
| 199 resetElementHeight() { | |
| 200 this._height = 0; | |
| 201 } | |
| 202 | |
| 203 /** | |
| 204 * @return {number} | |
| 205 */ | |
| 206 elementsPerViewport() { | |
| 207 if (!this._length) | |
| 208 return 0; | |
| 209 return Math.floor(this._viewport.element.offsetHeight / this.elementHeight() ); | |
| 210 } | |
| 211 | |
| 212 /** | |
| 213 * @param {number} index | |
| 214 */ | |
| 215 scrollItemIntoView(index) { | |
| 216 this._viewport.scrollItemIntoView(index); | |
| 217 } | |
| 218 | |
| 219 /** | |
| 220 * @override | |
| 221 * @param {number} height | |
| 222 * @return {number} | |
| 223 */ | |
| 224 indexAtOffset(height) { | |
| 225 if (!this._length) | |
| 226 return 0; | |
| 227 var index = Math.floor(height / this.elementHeight()); | |
| 228 if (index >= this._length) | |
| 229 return this._length - 1; | |
| 230 return index; | |
| 231 } | |
| 232 | |
| 233 /** | |
| 234 * @override | |
| 235 * @return {number} | |
| 236 */ | |
| 237 totalHeight() { | |
| 238 if (!this._length) | |
| 239 return 0; | |
| 240 return this._length * this.elementHeight(); | |
| 241 } | |
| 242 | |
| 243 /** | |
| 244 * @override | |
| 245 * @param {number} index | |
| 246 * @return {!Element} | |
| 247 */ | |
| 248 elementAtIndex(index) { | |
| 249 var element = this._elements[index]; | |
| 250 if (!element) { | |
| 251 element = this._elementsFactory.call(null, index); | |
| 252 this._elements[index] = element; | |
| 253 } | |
| 254 return element; | |
| 255 } | |
| 256 | |
| 257 /** | |
| 258 * @override | |
| 259 * @param {number} index | |
| 260 * @return {number} | |
| 261 */ | |
| 262 offsetAtIndex(index) { | |
| 263 if (!this._length) | |
| 264 return 0; | |
| 265 return index * this.elementHeight(); | |
| 266 } | |
| 267 }; | |
| OLD | NEW |