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