Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 <!-- | |
| 2 // Copyright 2014 The Chromium Authors. All rights reserved. | |
| 3 // Use of this source code is governed by a BSD-style license that can be | |
| 4 // found in the LICENSE file. | |
| 5 --> | |
| 6 <link rel="import" | |
| 7 href="../../framework/sky-element/sky-element.sky" | |
| 8 as="SkyElement" /> | |
| 9 <link rel="import" href="city-data-service.sky" as="CityDataService" /> | |
| 10 <link rel="import" href="city-sequence.sky" as="CitySequence" /> | |
| 11 | |
| 12 <template> | |
| 13 <style> | |
| 14 div { | |
| 15 font-size: 16px; | |
| 16 color: #FFF; | |
| 17 background-color: #333; | |
| 18 padding: 4px 4px 4px 12px; | |
| 19 } | |
| 20 </style> | |
| 21 <div>{{ state }}</div> | |
| 22 </template> | |
| 23 <script> | |
| 24 SkyElement({ | |
| 25 name: 'state-header', | |
| 26 | |
| 27 set datum(datum) { | |
| 28 this.state = datum.state; | |
| 29 } | |
| 30 }); | |
| 31 </script> | |
| 32 | |
| 33 <template> | |
| 34 <style> | |
| 35 div { | |
| 36 font-size: 12px; | |
| 37 font-weight: bold; | |
| 38 padding: 2px 4px 4px 12px; | |
| 39 background-color: #DDD; | |
| 40 } | |
| 41 </style> | |
| 42 <div>{{ letter }}</div> | |
| 43 </template> | |
| 44 <script> | |
| 45 SkyElement({ | |
| 46 name: 'letter-header', | |
| 47 | |
| 48 set datum(datum) { | |
| 49 this.letter = datum.letter; | |
| 50 } | |
| 51 }); | |
| 52 </script> | |
| 53 | |
| 54 <template> | |
| 55 <style> | |
| 56 :host { | |
| 57 display: flex; | |
| 58 font-size: 13px; | |
| 59 padding: 8px 4px 4px 12px; | |
| 60 border-bottom: 1px solid #EEE; | |
| 61 line-height: 15px; | |
| 62 overflow: hidden; | |
| 63 } | |
| 64 | |
| 65 span { | |
| 66 display: inline; | |
| 67 } | |
| 68 | |
| 69 #name { | |
| 70 font-weight: bold | |
| 71 } | |
| 72 | |
| 73 #population { | |
| 74 color: #AAA; | |
| 75 } | |
| 76 | |
| 77 </style> | |
| 78 <div> | |
| 79 <span id="name">{{ name }}</span>, | |
| 80 <span id="population">population {{ population }}</span> | |
| 81 </div> | |
| 82 </template> | |
| 83 <script> | |
| 84 SkyElement({ | |
| 85 name: 'city-item', | |
| 86 | |
| 87 set datum(datum) { | |
| 88 this.name = datum.name; | |
| 89 this.population = datum.population; | |
| 90 } | |
| 91 }); | |
| 92 </script> | |
| 93 | |
| 94 <template> | |
| 95 <style> | |
| 96 | |
| 97 :host { | |
| 98 overflow: hidden; | |
| 99 position: absolute; | |
| 100 top: 0; | |
| 101 right: 0; | |
| 102 bottom: 0; | |
| 103 left: 0; | |
| 104 display: block; | |
| 105 background-color: #fff; | |
| 106 } | |
| 107 | |
| 108 #scroller { | |
| 109 overflow-x: hidden; | |
| 110 overflow-y: auto; | |
| 111 perspective: 5px; | |
| 112 position: absolute; | |
| 113 left: 0; | |
| 114 right: 0; | |
| 115 top: 0; | |
| 116 bottom: 0; | |
| 117 } | |
| 118 | |
| 119 #scroller::-webkit-scrollbar { | |
|
ojan
2014/10/31 19:40:21
Is this needed? We've been talking about removing
rafaelw
2014/11/04 19:01:55
Probably not, since it doesn't do anything.
The d
| |
| 120 display:none; | |
| 121 } | |
| 122 | |
| 123 #scrollarea { | |
| 124 will-change: transform; | |
| 125 transform-style: preserve-3d; | |
| 126 } | |
| 127 | |
| 128 #contentarea { | |
| 129 position: absolute; | |
| 130 will-change: contents; | |
| 131 width: 100%; | |
| 132 } | |
| 133 | |
| 134 .void { | |
| 135 display: none; | |
| 136 } | |
| 137 | |
| 138 .position { | |
| 139 position: absolute; | |
| 140 left: 0; | |
| 141 right: 0; | |
| 142 } | |
| 143 | |
| 144 </style> | |
| 145 | |
| 146 <div id="scroller" fit> | |
| 147 <div id="scrollarea"> | |
| 148 <div id="contentarea"> | |
| 149 </div> | |
| 150 </div> | |
| 151 </div> | |
| 152 | |
| 153 </template> | |
| 154 <script> | |
| 155 | |
| 156 (function(global) { | |
| 157 "use strict"; | |
| 158 | |
| 159 var LOAD_LENGTH = 20; | |
| 160 var LOAD_BUFFER_PRE = LOAD_LENGTH * 4; | |
| 161 var LOAD_BUFFER_POST = LOAD_LENGTH * 4; | |
| 162 | |
| 163 function Loader() { | |
| 164 this.loadingData = false; | |
| 165 this.data = null; | |
| 166 this.zeroIndex = 0; | |
| 167 this.loadIndex = 0; | |
| 168 } | |
| 169 | |
| 170 Loader.prototype.localIndex = function(externalIndex) { | |
| 171 return externalIndex + this.zeroIndex; | |
| 172 } | |
| 173 | |
| 174 Loader.prototype.externalIndex = function(localIndex) { | |
| 175 return localIndex - this.zeroIndex; | |
| 176 } | |
| 177 | |
| 178 Loader.prototype.getItems = function() { | |
| 179 return this.data ? this.data.items : []; | |
| 180 } | |
| 181 | |
| 182 Loader.prototype.maybeLoadMoreData = function(dataloadedCallback, | |
| 183 firstVisible) { | |
| 184 if (this.loadingData) | |
| 185 return; | |
| 186 | |
| 187 if (firstVisible) { | |
| 188 this.loadIndex = this.externalIndex( | |
| 189 this.data.items.indexOf(firstVisible)); | |
| 190 } | |
| 191 | |
| 192 var localIndex = this.localIndex(this.loadIndex); | |
| 193 var loadedPre = 0; | |
| 194 var loadedPost = 0; | |
| 195 | |
| 196 if (this.data) { | |
| 197 loadedPre = localIndex; | |
| 198 loadedPost = this.data.items.length - loadedPre; | |
| 199 } | |
| 200 | |
| 201 if (loadedPre >= LOAD_BUFFER_PRE && loadedPost >= LOAD_BUFFER_POST) { | |
| 202 if (window.startLoad) { | |
| 203 var loadTime = (new Date().getTime()) - window.startLoad); | |
| 204 console.log('Load: ' + loadTime + 'ms'); | |
| 205 window.startLoad = undefined; | |
| 206 } | |
| 207 return; | |
| 208 } | |
| 209 | |
| 210 this.loadingData = true; | |
| 211 | |
| 212 var loadIndex; | |
| 213 | |
| 214 if (!this.data) { | |
| 215 // Initial batch | |
| 216 loadIndex = 0; | |
| 217 } else if (loadedPost < LOAD_BUFFER_POST) { | |
| 218 // Load forward first | |
| 219 loadIndex = this.data.items.length; | |
| 220 } else { | |
| 221 // Then load backward | |
| 222 loadIndex = -LOAD_LENGTH; | |
| 223 } | |
| 224 | |
| 225 var self = this; | |
| 226 var externalIndex = this.externalIndex(loadIndex); | |
| 227 | |
| 228 try { | |
| 229 CityDataService.service.then(function(cityService) { | |
| 230 return cityService.get(externalIndex, LOAD_LENGTH) | |
| 231 .then(function(cities) { | |
| 232 var indexOffset = 0; | |
| 233 var newData = new CitySequence(cities); | |
| 234 if (!self.data) { | |
| 235 self.data = newData; | |
| 236 } else if (loadIndex > 0) { | |
| 237 self.data.append(newData); | |
| 238 } else { | |
| 239 self.zeroIndex += LOAD_LENGTH; | |
| 240 indexOffset = LOAD_LENGTH; | |
| 241 newData.append(self.data); | |
| 242 self.data = newData; | |
| 243 } | |
| 244 | |
| 245 self.loadingData = false; | |
| 246 dataloadedCallback(self.data, indexOffset); | |
| 247 }); | |
| 248 }).catch(function(ex) { | |
| 249 console.log(ex.stack); | |
|
ojan
2014/10/31 19:40:21
Did we fork before promises correctly logged uncau
rafaelw
2014/11/04 19:01:55
The do not (log uncaught exceptions) to the consol
| |
| 250 }); | |
| 251 } catch (ex) { | |
| 252 console.log(ex.stack); | |
| 253 } | |
| 254 } | |
| 255 | |
| 256 function Scroller() { | |
| 257 this.contentarea = null; | |
| 258 this.scroller = null; | |
| 259 this.contentTop = 0; // #contentarea's current top | |
| 260 this.scrollTop = 0; // #scrollarea's current top | |
| 261 this.scrollHeight = -1; // height of #scroller (the viewport) | |
| 262 this.lastScrollTop = 0; // last known scrollTop to compute deltas | |
| 263 } | |
| 264 | |
| 265 Scroller.prototype.setup = function(scroller, scrollarea, contentarea) { | |
| 266 this.contentarea = contentarea; | |
| 267 this.scroller = scroller; | |
| 268 | |
| 269 this.scrollHeight = scroller.offsetHeight; | |
| 270 scrollarea.style.height = (this.scrollHeight) * 4 + 'px'; | |
| 271 | |
| 272 this.reset(); | |
| 273 } | |
| 274 | |
| 275 Scroller.prototype.captureNewFrame = function(event) { | |
| 276 var scrollTop = event.target.scrollTop; | |
| 277 | |
| 278 // Protect from re-entry. | |
| 279 if (this.lastScrollTop == scrollTop) | |
| 280 return false; | |
| 281 | |
| 282 var scrollDown = scrollTop > this.lastScrollTop; | |
| 283 if (scrollDown) { | |
| 284 while (scrollTop > this.scrollHeight * 1.5) { | |
| 285 scrollTop -= this.scrollHeight; | |
| 286 this.contentTop -= this.scrollHeight; | |
| 287 } | |
| 288 } else { | |
| 289 while(scrollTop < this.scrollHeight * 1.5) { | |
| 290 scrollTop += this.scrollHeight; | |
| 291 this.contentTop += this.scrollHeight; | |
| 292 } | |
| 293 } | |
| 294 | |
| 295 this.lastScrollTop = scrollTop; | |
| 296 event.target.scrollTop = scrollTop; | |
| 297 this.contentarea.style.top = this.contentTop + 'px'; | |
| 298 | |
| 299 this.scrollTop = scrollTop - this.contentTop; | |
| 300 | |
| 301 return true; | |
| 302 } | |
| 303 | |
| 304 Scroller.prototype.reset = function() { | |
| 305 if (!this.contentarea) | |
| 306 return; | |
| 307 | |
| 308 this.scroller.scrollTop = this.scrollHeight; | |
| 309 this.lastScrollTop = this.scrollHeight; | |
| 310 | |
| 311 this.contentarea.style.top = this.scrollHeight + 'px'; | |
| 312 this.contentTop = this.scrollHeight; | |
| 313 this.scrollTop = 0; | |
| 314 } | |
| 315 | |
| 316 // Current position and height of the scroller, that could | |
| 317 // be used (by Tiler, for example) to reason about where to | |
| 318 // place visible things. | |
| 319 Scroller.prototype.getCurrentFrame = function() { | |
| 320 return { top: this.scrollTop, height: this.scrollHeight }; | |
| 321 } | |
| 322 | |
| 323 Scroller.prototype.hasFrame = function() { | |
| 324 return this.scrollHeight != -1; | |
| 325 } | |
| 326 | |
| 327 function Tile(datum, element, viewType, index) { | |
| 328 this.datum = datum; | |
| 329 this.element = element; | |
| 330 this.viewType = viewType; | |
| 331 this.index = index; | |
| 332 } | |
| 333 | |
| 334 function Tiler(contentArea, views, viewHeights) { | |
| 335 this.contentArea = contentArea; | |
| 336 this.drawTop = 0; | |
| 337 this.drawBottom = 0; | |
| 338 this.firstItem = -1; | |
| 339 this.tiles = []; | |
| 340 this.viewHeights = viewHeights; | |
| 341 this.views = views; | |
| 342 } | |
| 343 | |
| 344 Tiler.prototype.setupViews = function(scrollFrame) { | |
| 345 for (var type in this.viewHeights) { | |
| 346 this.initializeViewType(scrollFrame, type, this.viewHeights[type]); | |
| 347 } | |
| 348 } | |
| 349 | |
| 350 Tiler.prototype.initializeViewType = function(scrollFrame, viewType, | |
| 351 height) { | |
| 352 var count = Math.ceil(scrollFrame.height / height) * 2; | |
| 353 var viewCache = this.views[viewType] = { | |
| 354 indices: [], | |
| 355 elements: [] | |
| 356 }; | |
| 357 | |
| 358 var protoElement; | |
| 359 switch (viewType) { | |
| 360 case 'stateHeader': | |
| 361 protoElement = document.createElement('state-header'); | |
| 362 break; | |
| 363 case 'letterHeader': | |
| 364 protoElement = document.createElement('letter-header'); | |
| 365 break; | |
| 366 case 'cityItem': | |
| 367 protoElement = document.createElement('city-item'); | |
| 368 break; | |
| 369 default: | |
| 370 console.warn('Unknown viewType: ' + viewType); | |
| 371 } | |
| 372 protoElement.style.display = 'none'; | |
| 373 protoElement.style.height = height; | |
| 374 protoElement.classList.add('position'); | |
| 375 | |
| 376 for (var i = 0; i < count; i++) { | |
| 377 var clone = protoElement.cloneNode(false); | |
| 378 this.contentArea.appendChild(clone); | |
| 379 viewCache.elements.push(clone); | |
| 380 viewCache.indices.push(i); | |
| 381 } | |
| 382 } | |
| 383 | |
| 384 Tiler.prototype.checkoutTile = function(viewType, datum, top) { | |
| 385 var viewCache = this.views[viewType]; | |
| 386 var index = viewCache.indices.pop(); | |
| 387 var element = viewCache.elements[index]; | |
| 388 element.datum = datum; | |
| 389 element.style.display = ''; | |
| 390 element.style.top = top + 'px'; | |
| 391 | |
| 392 return new Tile(datum, element, viewType, index); | |
| 393 } | |
| 394 | |
| 395 Tiler.prototype.checkinTile = function(tile) { | |
| 396 if (!tile.element) | |
| 397 return; | |
| 398 | |
| 399 tile.element.style.display = 'none'; | |
| 400 this.views[tile.viewType].indices.push(tile.index); | |
| 401 } | |
| 402 | |
| 403 Tiler.prototype.getFirstVisibleDatum = function(scrollFrame) { | |
| 404 var tiles = this.tiles; | |
| 405 var viewHeights = this.viewHeights; | |
| 406 | |
| 407 var itemTop = this.drawTop - scrollFrame.top; | |
| 408 if (itemTop >= 0 && tiles.length) | |
| 409 return tiles[0].datum; | |
| 410 | |
| 411 var tile; | |
| 412 for (var i = 0; i < tiles.length && itemTop < 0; i++) { | |
| 413 tile = tiles[i]; | |
| 414 var height = viewHeights[tile.viewType]; | |
| 415 itemTop += height; | |
| 416 } | |
| 417 | |
| 418 return tile ? tile.datum : null; | |
| 419 } | |
| 420 | |
| 421 Tiler.prototype.viewType = function(datum) { | |
| 422 switch (datum.headerOrder) { | |
| 423 case 1: return 'stateHeader'; | |
| 424 case 2: return 'letterHeader'; | |
| 425 default: return 'cityItem'; | |
| 426 } | |
| 427 } | |
| 428 | |
| 429 Tiler.prototype.drawTiles = function(scrollFrame, data) { | |
| 430 var tiles = this.tiles; | |
| 431 var viewHeights = this.viewHeights; | |
| 432 | |
| 433 var buffer = Math.round(scrollFrame.height / 2); | |
| 434 var targetTop = scrollFrame.top - buffer; | |
| 435 var targetBottom = scrollFrame.top + scrollFrame.height + buffer; | |
| 436 | |
| 437 // Collect down to targetTop | |
| 438 var first = tiles[0]; | |
| 439 while (tiles.length && | |
| 440 targetTop > this.drawTop + viewHeights[first.viewType]) { | |
| 441 | |
| 442 var height = viewHeights[first.viewType]; | |
| 443 this.drawTop += height; | |
| 444 | |
| 445 this.firstItem++; | |
| 446 this.checkinTile(tiles.shift()); | |
| 447 | |
| 448 first = tiles[0]; | |
| 449 } | |
| 450 | |
| 451 // Collect up to targetBottom | |
| 452 var last = tiles[tiles.length - 1]; | |
| 453 while(tiles.length && | |
| 454 targetBottom < this.drawBottom - viewHeights[last.viewType]) { | |
| 455 | |
| 456 var height = viewHeights[last.viewType]; | |
| 457 this.drawBottom -= height; | |
| 458 | |
| 459 this.checkinTile(tiles.pop()); | |
| 460 | |
| 461 last = tiles[tiles.length - 1]; | |
| 462 } | |
| 463 | |
| 464 // Layout up to targetTop | |
| 465 while (this.firstItem > 0 && | |
| 466 targetTop < this.drawTop) { | |
| 467 | |
| 468 var datum = data[this.firstItem - 1]; | |
| 469 var type = this.viewType(datum); | |
| 470 var height = viewHeights[type]; | |
| 471 | |
| 472 this.drawTop -= height; | |
| 473 | |
| 474 var tile = targetBottom < this.drawTop ? | |
| 475 new Tile(datum, null, viewType, -1) : // off-screen | |
| 476 this.checkoutTile(type, datum, this.drawTop); | |
| 477 | |
| 478 this.firstItem--; | |
| 479 tiles.unshift(tile); | |
| 480 } | |
| 481 | |
| 482 // Layout down to targetBottom | |
| 483 while (this.firstItem + tiles.length < data.length - 1 && | |
| 484 targetBottom > this.drawBottom) { | |
| 485 | |
| 486 var datum = data[this.firstItem + tiles.length]; | |
| 487 var type = this.viewType(datum); | |
| 488 var height = viewHeights[type]; | |
| 489 | |
| 490 this.drawBottom += height; | |
| 491 | |
| 492 var tile = targetTop > this.drawBottom ? | |
| 493 new Tile(datum, null, viewType, -1) : // off-screen | |
| 494 this.checkoutTile(type, datum, this.drawBottom - height); | |
| 495 | |
| 496 tiles.push(tile); | |
| 497 } | |
| 498 | |
| 499 // Debug validate: | |
| 500 // for (var i = 0; i < tiles.length; i++) { | |
| 501 // if (tiles[i].datum !== data[this.firstItem + i]) | |
| 502 // throw Error('Invalid') | |
| 503 // } | |
| 504 } | |
| 505 | |
| 506 // FIXME: Needs better name. | |
| 507 Tiler.prototype.updateFirstItem = function(offset) { | |
| 508 var tiles = this.tiles; | |
| 509 | |
| 510 if (!tiles.length) { | |
| 511 this.firstItem = 0; | |
| 512 } else { | |
| 513 this.firstItem += offset; | |
| 514 } | |
| 515 } | |
| 516 | |
| 517 Tiler.prototype.checkinAllTiles = function() { | |
| 518 var tiles = this.tiles; | |
| 519 while (tiles.length) { | |
| 520 this.checkinTile(tiles.pop()); | |
| 521 } | |
| 522 } | |
| 523 | |
| 524 Tiler.prototype.reset = function() { | |
| 525 this.checkinAllTiles(); | |
| 526 this.drawTop = 0; | |
| 527 this.drawBottom = 0; | |
| 528 } | |
| 529 | |
| 530 SkyElement({ | |
| 531 name: 'city-list', | |
| 532 loader: null, | |
| 533 scroller: null, | |
| 534 tiler: null, | |
| 535 date: null, | |
| 536 month: null, | |
| 537 views: null, | |
| 538 | |
| 539 attached: function() { | |
| 540 this.views = {}; | |
| 541 this.loader = new Loader(); | |
| 542 this.scroller = new Scroller(); | |
| 543 this.tiler = new Tiler( | |
| 544 this.shadowRoot.getElementById('contentarea'), this.views, { | |
| 545 stateHeader: 27, | |
| 546 letterHeader: 18, | |
| 547 cityItem: 30 | |
| 548 }); | |
| 549 | |
| 550 this.dataLoaded = this.dataLoaded.bind(this); | |
| 551 this.shadowRoot.getElementById('scroller') | |
| 552 .addEventListener('scroll', this.handleScroll.bind(this)); | |
| 553 | |
| 554 var self = this; | |
| 555 requestAnimationFrame(function() { | |
| 556 self.domReady(); | |
| 557 self.loader.maybeLoadMoreData(self.dataLoaded); | |
| 558 }); | |
| 559 }, | |
| 560 | |
| 561 domReady: function() { | |
| 562 this.scroller.setup(this.shadowRoot.getElementById('scroller'), | |
| 563 this.shadowRoot.getElementById('scrollarea'), | |
| 564 this.shadowRoot.getElementById('contentarea')); | |
| 565 var scrollFrame = this.scroller.getCurrentFrame(); | |
| 566 this.tiler.setupViews(scrollFrame); | |
| 567 }, | |
| 568 | |
| 569 updateView: function(data, scrollChanged) { | |
| 570 var scrollFrame = this.scroller.getCurrentFrame(); | |
| 571 this.tiler.drawTiles(scrollFrame, data); | |
| 572 var datum = scrollChanged ? | |
| 573 this.tiler.getFirstVisibleDatum(scrollFrame) : null; | |
| 574 this.loader.maybeLoadMoreData(this.dataLoaded, datum); | |
| 575 }, | |
| 576 | |
| 577 dataLoaded: function(data, indexOffset) { | |
| 578 var scrollFrame = this.scroller.getCurrentFrame(); | |
| 579 this.tiler.updateFirstItem(indexOffset); | |
| 580 this.updateView(data.items, false); | |
| 581 }, | |
| 582 | |
| 583 handleScroll: function(event) { | |
| 584 if (!this.scroller.captureNewFrame(event)) | |
| 585 return; | |
| 586 | |
| 587 this.updateView(this.loader.getItems(), true); | |
| 588 } | |
| 589 }); | |
| 590 | |
| 591 })(this); | |
| 592 | |
| 593 </script> | |
| 594 | |
| OLD | NEW |