| <!-- |
| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| --> |
| <import src="../../framework/sky-element/sky-element.sky" as="SkyElement" /> |
| <import src="city-data-service.sky" as="CityDataService" /> |
| <import src="city-sequence.sky" as="CitySequence" /> |
| |
| <sky-element name="state-header"> |
| <template> |
| <style> |
| div { |
| font-size: 16px; |
| color: #FFF; |
| background-color: #333; |
| padding: 4px 4px 4px 12px; |
| display: paragraph; |
| } |
| </style> |
| <div>{{ state }}</div> |
| </template> |
| <script> |
| module.exports.StateHeaderElement = class extends SkyElement { |
| created() { |
| this.state = ""; |
| } |
| set datum(datum) { |
| this.state = datum.state; |
| } |
| }.register(); |
| </script> |
| </sky-element> |
| |
| <sky-element name="letter-header"> |
| <template> |
| <style> |
| div { |
| font-size: 12px; |
| font-weight: bold; |
| padding: 2px 4px 4px 12px; |
| background-color: #DDD; |
| display: paragraph; |
| } |
| </style> |
| <div>{{ letter }}</div> |
| </template> |
| <script> |
| module.exports.LetterHeaderElement = class extends SkyElement { |
| created() { |
| this.letter = ""; |
| } |
| set datum(datum) { |
| this.letter = datum.letter; |
| } |
| }.register(); |
| </script> |
| </sky-element> |
| |
| <sky-element name="city-item"> |
| <template> |
| <style> |
| :host { |
| display: flex; |
| font-size: 13px; |
| padding: 8px 4px 4px 12px; |
| border-bottom: 1px solid #EEE; |
| line-height: 15px; |
| overflow: hidden; |
| } |
| |
| div { |
| display: paragraph; |
| } |
| |
| span { |
| display: inline; |
| } |
| |
| #name { |
| font-weight: bold |
| } |
| |
| #population { |
| color: #AAA; |
| } |
| </style> |
| <div> |
| <span id="name">{{ name }}</span> |
| <t>, </t> |
| <span id="population">population {{ population }}</span> |
| </div> |
| </template> |
| <script> |
| module.exports.CityItemElement = class extends SkyElement { |
| created() { |
| this.name = ""; |
| this.population = ""; |
| } |
| set datum(datum) { |
| this.name = datum.name; |
| this.population = datum.population; |
| } |
| }.register(); |
| </script> |
| </sky-element> |
| |
| <sky-element name="city-list"> |
| <template> |
| <style> |
| |
| :host { |
| overflow: hidden; |
| position: absolute; |
| top: 0; |
| right: 0; |
| bottom: 0; |
| left: 0; |
| display: block; |
| background-color: #fff; |
| } |
| |
| #contentarea { |
| will-change: transform; |
| } |
| |
| .position { |
| position: absolute; |
| left: 0; |
| right: 0; |
| } |
| |
| </style> |
| <div id="contentarea"> |
| </div> |
| </template> |
| <script> |
| |
| (function(global) { |
| "use strict"; |
| |
| var LOAD_LENGTH = 20; |
| var LOAD_BUFFER_PRE = LOAD_LENGTH * 4; |
| var LOAD_BUFFER_POST = LOAD_LENGTH * 4; |
| |
| function Loader(cityList) { |
| this.cityList = cityList; |
| this.loadingData = false; |
| this.data = null; |
| this.zeroIndex = 0; |
| this.loadIndex = 0; |
| } |
| |
| Loader.prototype.localIndex = function(externalIndex) { |
| return externalIndex + this.zeroIndex; |
| } |
| |
| Loader.prototype.externalIndex = function(localIndex) { |
| return localIndex - this.zeroIndex; |
| } |
| |
| Loader.prototype.getItems = function() { |
| return this.data ? this.data.items : []; |
| } |
| |
| Loader.prototype.maybeLoadMoreData = |
| function(dataloadedCallback, firstVisible) { |
| if (this.loadingData) |
| return; |
| |
| if (firstVisible) { |
| this.loadIndex = this.externalIndex( |
| this.data.items.indexOf(firstVisible)); |
| } |
| |
| var localIndex = this.localIndex(this.loadIndex); |
| var loadedPre = 0; |
| var loadedPost = 0; |
| |
| if (this.data) { |
| loadedPre = localIndex; |
| loadedPost = this.data.items.length - loadedPre; |
| } |
| |
| var loadTime; |
| if (loadedPre >= LOAD_BUFFER_PRE && |
| loadedPost >= LOAD_BUFFER_POST) { |
| |
| var cityList = this.cityList; |
| setTimeout(function() { |
| cityList.dispatchEvent(new Event('load')); |
| }); |
| |
| if (window.startLoad) { |
| loadTime = new Date().getTime() - window.startLoad; |
| console.log('Load: ' + loadTime + 'ms'); |
| window.startLoad = undefined; |
| } |
| return; |
| } |
| |
| this.loadingData = true; |
| |
| var loadIndex; |
| |
| if (!this.data) { |
| // Initial batch |
| loadIndex = 0; |
| } else if (loadedPost < LOAD_BUFFER_POST) { |
| // Load forward first |
| loadIndex = this.data.items.length; |
| } else { |
| // Then load backward |
| loadIndex = -LOAD_LENGTH; |
| } |
| |
| var self = this; |
| var externalIndex = this.externalIndex(loadIndex); |
| |
| try { |
| CityDataService.service.then(function(cityService) { |
| return cityService.get(externalIndex, LOAD_LENGTH) |
| .then(function(cities) { |
| var indexOffset = 0; |
| var newData = new CitySequence(cities); |
| if (!self.data) { |
| self.data = newData; |
| } else if (loadIndex > 0) { |
| self.data.append(newData); |
| } else { |
| self.zeroIndex += LOAD_LENGTH; |
| indexOffset = LOAD_LENGTH; |
| newData.append(self.data); |
| self.data = newData; |
| } |
| |
| self.loadingData = false; |
| dataloadedCallback(self.data, indexOffset); |
| }); |
| }).catch(function(ex) { |
| console.log(ex.stack); |
| }); |
| } catch (ex) { |
| console.log(ex.stack); |
| } |
| } |
| |
| function Scroller() { |
| this.contentarea = null; |
| this.scrollTop = 0; |
| this.scrollHeight = -1; |
| } |
| |
| Scroller.prototype.setup = function(scrollHeight, contentarea) { |
| this.scrollHeight = scrollHeight; |
| this.contentarea = contentarea; |
| } |
| |
| Scroller.prototype.scrollBy = function(amount) { |
| this.scrollTop += amount; |
| this.scrollTo(); |
| } |
| |
| Scroller.prototype.scrollTo = function() { |
| var transform = 'translateY(' + -this.scrollTop.toFixed(2) + 'px)'; |
| this.contentarea.style.transform = transform; |
| } |
| |
| // Current position and height of the scroller, that could |
| // be used (by Tiler, for example) to reason about where to |
| // place visible things. |
| Scroller.prototype.getCurrentFrame = function() { |
| return { top: this.scrollTop, height: this.scrollHeight }; |
| } |
| |
| Scroller.prototype.hasFrame = function() { |
| return this.scrollHeight != -1; |
| } |
| |
| function Tile(datum, element, viewType, index) { |
| this.datum = datum; |
| this.element = element; |
| this.viewType = viewType; |
| this.index = index; |
| } |
| |
| function Tiler(contentArea, views, viewHeights) { |
| this.contentArea = contentArea; |
| this.drawTop = 0; |
| this.drawBottom = 0; |
| this.firstItem = -1; |
| this.tiles = []; |
| this.viewHeights = viewHeights; |
| this.views = views; |
| } |
| |
| Tiler.prototype.setupViews = function(scrollFrame) { |
| for (var type in this.viewHeights) { |
| this.initializeViewType(scrollFrame, type, this.viewHeights[type]); |
| } |
| } |
| |
| Tiler.prototype.initializeViewType = function(scrollFrame, viewType, |
| height) { |
| var count = Math.ceil(scrollFrame.height / height) * 2; |
| var viewCache = this.views[viewType] = { |
| indices: [], |
| elements: [] |
| }; |
| |
| var protoElement; |
| switch (viewType) { |
| case 'stateHeader': |
| protoElement = document.createElement('state-header'); |
| break; |
| case 'letterHeader': |
| protoElement = document.createElement('letter-header'); |
| break; |
| case 'cityItem': |
| protoElement = document.createElement('city-item'); |
| break; |
| default: |
| console.warn('Unknown viewType: ' + viewType); |
| } |
| protoElement.style.display = 'none'; |
| protoElement.style.height = height; |
| protoElement.classList.add('position'); |
| |
| for (var i = 0; i < count; i++) { |
| var clone = protoElement.cloneNode(false); |
| this.contentArea.appendChild(clone); |
| viewCache.elements.push(clone); |
| viewCache.indices.push(i); |
| } |
| } |
| |
| Tiler.prototype.checkoutTile = function(viewType, datum, top) { |
| var viewCache = this.views[viewType]; |
| var index = viewCache.indices.pop(); |
| var element = viewCache.elements[index]; |
| element.datum = datum; |
| element.style.display = ''; |
| element.style.top = top + 'px'; |
| |
| return new Tile(datum, element, viewType, index); |
| } |
| |
| Tiler.prototype.checkinTile = function(tile) { |
| if (!tile.element) |
| return; |
| |
| tile.element.style.display = 'none'; |
| this.views[tile.viewType].indices.push(tile.index); |
| } |
| |
| Tiler.prototype.getFirstVisibleDatum = function(scrollFrame) { |
| var tiles = this.tiles; |
| var viewHeights = this.viewHeights; |
| |
| var itemTop = this.drawTop - scrollFrame.top; |
| if (itemTop >= 0 && tiles.length) |
| return tiles[0].datum; |
| |
| var tile; |
| for (var i = 0; i < tiles.length && itemTop < 0; i++) { |
| tile = tiles[i]; |
| var height = viewHeights[tile.viewType]; |
| itemTop += height; |
| } |
| |
| return tile ? tile.datum : null; |
| } |
| |
| Tiler.prototype.viewType = function(datum) { |
| switch (datum.headerOrder) { |
| case 1: return 'stateHeader'; |
| case 2: return 'letterHeader'; |
| default: return 'cityItem'; |
| } |
| } |
| |
| Tiler.prototype.drawTiles = function(scrollFrame, data) { |
| var tiles = this.tiles; |
| var viewHeights = this.viewHeights; |
| |
| var buffer = Math.round(scrollFrame.height / 2); |
| var targetTop = scrollFrame.top - buffer; |
| var targetBottom = scrollFrame.top + scrollFrame.height + buffer; |
| |
| // Collect down to targetTop |
| var first = tiles[0]; |
| while (tiles.length && |
| targetTop > this.drawTop + viewHeights[first.viewType]) { |
| |
| var height = viewHeights[first.viewType]; |
| this.drawTop += height; |
| |
| this.firstItem++; |
| this.checkinTile(tiles.shift()); |
| |
| first = tiles[0]; |
| } |
| |
| // Collect up to targetBottom |
| var last = tiles[tiles.length - 1]; |
| while(tiles.length && |
| targetBottom < this.drawBottom - viewHeights[last.viewType]) { |
| |
| var height = viewHeights[last.viewType]; |
| this.drawBottom -= height; |
| |
| this.checkinTile(tiles.pop()); |
| |
| last = tiles[tiles.length - 1]; |
| } |
| |
| // Layout up to targetTop |
| while (this.firstItem > 0 && |
| targetTop < this.drawTop) { |
| |
| var datum = data[this.firstItem - 1]; |
| var type = this.viewType(datum); |
| var height = viewHeights[type]; |
| |
| this.drawTop -= height; |
| |
| var tile = targetBottom < this.drawTop ? |
| new Tile(datum, null, datum.viewType, -1) : // off-screen |
| this.checkoutTile(type, datum, this.drawTop); |
| |
| this.firstItem--; |
| tiles.unshift(tile); |
| } |
| |
| // Layout down to targetBottom |
| while (this.firstItem + tiles.length < data.length - 1 && |
| targetBottom > this.drawBottom) { |
| |
| var datum = data[this.firstItem + tiles.length]; |
| var type = this.viewType(datum); |
| var height = viewHeights[type]; |
| |
| this.drawBottom += height; |
| |
| var tile = targetTop > this.drawBottom ? |
| new Tile(datum, null, datum.viewType, -1) : // off-screen |
| this.checkoutTile(type, datum, this.drawBottom - height); |
| |
| tiles.push(tile); |
| } |
| |
| // Debug validate: |
| // for (var i = 0; i < tiles.length; i++) { |
| // if (tiles[i].datum !== data[this.firstItem + i]) |
| // throw Error('Invalid') |
| // } |
| } |
| |
| // FIXME: Needs better name. |
| Tiler.prototype.updateFirstItem = function(offset) { |
| var tiles = this.tiles; |
| |
| if (!tiles.length) { |
| this.firstItem = 0; |
| } else { |
| this.firstItem += offset; |
| } |
| } |
| |
| module.exports.CityListElement = class extends SkyElement { |
| |
| created() { |
| this.loader = null; |
| this.scroller = null; |
| this.tiler = null; |
| this.date = null; |
| this.month = null; |
| this.views = null; |
| } |
| |
| attached() { |
| this.views = {}; |
| this.loader = new Loader(this); |
| this.scroller = new Scroller(); |
| this.tiler = new Tiler( |
| this.shadowRoot.getElementById('contentarea'), this.views, { |
| stateHeader: 27, |
| letterHeader: 18, |
| cityItem: 30 |
| }); |
| |
| var self = this; |
| this.addEventListener('wheel', function(event) { |
| self.scrollBy(-event.offsetY) |
| }); |
| |
| setTimeout(function() { |
| self.domReady(); |
| self.loader.maybeLoadMoreData(self.dataLoaded.bind(self)); |
| }); |
| } |
| |
| domReady() { |
| this.scroller.setup(this.clientHeight, |
| this.shadowRoot.getElementById('contentarea')); |
| var scrollFrame = this.scroller.getCurrentFrame(); |
| this.tiler.setupViews(scrollFrame); |
| } |
| |
| updateView(data, scrollChanged) { |
| var scrollFrame = this.scroller.getCurrentFrame(); |
| this.tiler.drawTiles(scrollFrame, data); |
| var datum = scrollChanged ? |
| this.tiler.getFirstVisibleDatum(scrollFrame) : null; |
| this.loader.maybeLoadMoreData(this.dataLoaded.bind(this), datum); |
| } |
| |
| dataLoaded(data, indexOffset) { |
| var scrollFrame = this.scroller.getCurrentFrame(); |
| this.tiler.updateFirstItem(indexOffset); |
| this.updateView(data.items, false); |
| } |
| |
| scrollBy(amount) { |
| this.scroller.scrollBy(amount); |
| this.updateView(this.loader.getItems(), true); |
| } |
| }.register(); |
| |
| })(this); |
| </script> |
| </sky-element> |