| Index: sky/examples/city-list/city-list.sky
|
| diff --git a/sky/examples/city-list/city-list.sky b/sky/examples/city-list/city-list.sky
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..0162f6c08db21545088bbb498e877a60f956ad63
|
| --- /dev/null
|
| +++ b/sky/examples/city-list/city-list.sky
|
| @@ -0,0 +1,595 @@
|
| +<!--
|
| +// 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.
|
| +-->
|
| +<link rel="import"
|
| + href="../../framework/sky-element/sky-element.sky"
|
| + as="SkyElement" />
|
| +<link rel="import" href="city-data-service.sky" as="CityDataService" />
|
| +<link rel="import" href="city-sequence.sky" as="CitySequence" />
|
| +
|
| +<template>
|
| + <style>
|
| + div {
|
| + font-size: 16px;
|
| + color: #FFF;
|
| + background-color: #333;
|
| + padding: 4px 4px 4px 12px;
|
| + }
|
| + </style>
|
| + <div>{{ state }}</div>
|
| +</template>
|
| +<script>
|
| +SkyElement({
|
| + name: 'state-header',
|
| +
|
| + set datum(datum) {
|
| + this.state = datum.state;
|
| + }
|
| +});
|
| +</script>
|
| +
|
| +<template>
|
| + <style>
|
| + div {
|
| + font-size: 12px;
|
| + font-weight: bold;
|
| + padding: 2px 4px 4px 12px;
|
| + background-color: #DDD;
|
| + }
|
| + </style>
|
| + <div>{{ letter }}</div>
|
| +</template>
|
| +<script>
|
| +SkyElement({
|
| + name: 'letter-header',
|
| +
|
| + set datum(datum) {
|
| + this.letter = datum.letter;
|
| + }
|
| +});
|
| +</script>
|
| +
|
| +<template>
|
| + <style>
|
| + :host {
|
| + display: flex;
|
| + font-size: 13px;
|
| + padding: 8px 4px 4px 12px;
|
| + border-bottom: 1px solid #EEE;
|
| + line-height: 15px;
|
| + overflow: hidden;
|
| + }
|
| +
|
| + span {
|
| + display: inline;
|
| + }
|
| +
|
| + #name {
|
| + font-weight: bold
|
| + }
|
| +
|
| + #population {
|
| + color: #AAA;
|
| + }
|
| +
|
| + </style>
|
| + <div>
|
| + <span id="name">{{ name }}</span>,
|
| + <span id="population">population {{ population }}</span>
|
| + </div>
|
| +</template>
|
| +<script>
|
| + SkyElement({
|
| + name: 'city-item',
|
| +
|
| + set datum(datum) {
|
| + this.name = datum.name;
|
| + this.population = datum.population;
|
| + }
|
| + });
|
| +</script>
|
| +
|
| +<template>
|
| + <style>
|
| +
|
| + :host {
|
| + overflow: hidden;
|
| + position: absolute;
|
| + top: 0;
|
| + right: 0;
|
| + bottom: 0;
|
| + left: 0;
|
| + display: block;
|
| + background-color: #fff;
|
| + }
|
| +
|
| + #scroller {
|
| + overflow-x: hidden;
|
| + overflow-y: auto;
|
| + perspective: 5px;
|
| + position: absolute;
|
| + left: 0;
|
| + right: 0;
|
| + top: 0;
|
| + bottom: 0;
|
| + }
|
| +
|
| + #scroller::-webkit-scrollbar {
|
| + display:none;
|
| + }
|
| +
|
| + #scrollarea {
|
| + will-change: transform;
|
| + transform-style: preserve-3d;
|
| + }
|
| +
|
| + #contentarea {
|
| + position: absolute;
|
| + will-change: contents;
|
| + width: 100%;
|
| + }
|
| +
|
| + .void {
|
| + display: none;
|
| + }
|
| +
|
| + .position {
|
| + position: absolute;
|
| + left: 0;
|
| + right: 0;
|
| + }
|
| +
|
| + </style>
|
| +
|
| + <div id="scroller" fit>
|
| + <div id="scrollarea">
|
| + <div id="contentarea">
|
| + </div>
|
| + </div>
|
| + </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() {
|
| + 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) {
|
| + 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.scroller = null;
|
| + this.contentTop = 0; // #contentarea's current top
|
| + this.scrollTop = 0; // #scrollarea's current top
|
| + this.scrollHeight = -1; // height of #scroller (the viewport)
|
| + this.lastScrollTop = 0; // last known scrollTop to compute deltas
|
| + }
|
| +
|
| + Scroller.prototype.setup = function(scroller, scrollarea, contentarea) {
|
| + this.contentarea = contentarea;
|
| + this.scroller = scroller;
|
| +
|
| + this.scrollHeight = scroller.offsetHeight;
|
| + scrollarea.style.height = (this.scrollHeight) * 4 + 'px';
|
| +
|
| + this.reset();
|
| + }
|
| +
|
| + Scroller.prototype.captureNewFrame = function(event) {
|
| + var scrollTop = event.target.scrollTop;
|
| +
|
| + // Protect from re-entry.
|
| + if (this.lastScrollTop == scrollTop)
|
| + return false;
|
| +
|
| + var scrollDown = scrollTop > this.lastScrollTop;
|
| + if (scrollDown) {
|
| + while (scrollTop > this.scrollHeight * 1.5) {
|
| + scrollTop -= this.scrollHeight;
|
| + this.contentTop -= this.scrollHeight;
|
| + }
|
| + } else {
|
| + while(scrollTop < this.scrollHeight * 1.5) {
|
| + scrollTop += this.scrollHeight;
|
| + this.contentTop += this.scrollHeight;
|
| + }
|
| + }
|
| +
|
| + this.lastScrollTop = scrollTop;
|
| + event.target.scrollTop = scrollTop;
|
| + this.contentarea.style.top = this.contentTop + 'px';
|
| +
|
| + this.scrollTop = scrollTop - this.contentTop;
|
| +
|
| + return true;
|
| + }
|
| +
|
| + Scroller.prototype.reset = function() {
|
| + if (!this.contentarea)
|
| + return;
|
| +
|
| + this.scroller.scrollTop = this.scrollHeight;
|
| + this.lastScrollTop = this.scrollHeight;
|
| +
|
| + this.contentarea.style.top = this.scrollHeight + 'px';
|
| + this.contentTop = this.scrollHeight;
|
| + this.scrollTop = 0;
|
| + }
|
| +
|
| + // 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, 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, 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;
|
| + }
|
| + }
|
| +
|
| + Tiler.prototype.checkinAllTiles = function() {
|
| + var tiles = this.tiles;
|
| + while (tiles.length) {
|
| + this.checkinTile(tiles.pop());
|
| + }
|
| + }
|
| +
|
| + Tiler.prototype.reset = function() {
|
| + this.checkinAllTiles();
|
| + this.drawTop = 0;
|
| + this.drawBottom = 0;
|
| + }
|
| +
|
| + SkyElement({
|
| + name: 'city-list',
|
| + loader: null,
|
| + scroller: null,
|
| + tiler: null,
|
| + date: null,
|
| + month: null,
|
| + views: null,
|
| +
|
| + attached: function() {
|
| + this.views = {};
|
| + this.loader = new Loader();
|
| + this.scroller = new Scroller();
|
| + this.tiler = new Tiler(
|
| + this.shadowRoot.getElementById('contentarea'), this.views, {
|
| + stateHeader: 27,
|
| + letterHeader: 18,
|
| + cityItem: 30
|
| + });
|
| +
|
| + this.dataLoaded = this.dataLoaded.bind(this);
|
| + this.shadowRoot.getElementById('scroller')
|
| + .addEventListener('scroll', this.handleScroll.bind(this));
|
| +
|
| + var self = this;
|
| + requestAnimationFrame(function() {
|
| + self.domReady();
|
| + self.loader.maybeLoadMoreData(self.dataLoaded);
|
| + });
|
| + },
|
| +
|
| + domReady: function() {
|
| + this.scroller.setup(this.shadowRoot.getElementById('scroller'),
|
| + this.shadowRoot.getElementById('scrollarea'),
|
| + this.shadowRoot.getElementById('contentarea'));
|
| + var scrollFrame = this.scroller.getCurrentFrame();
|
| + this.tiler.setupViews(scrollFrame);
|
| + },
|
| +
|
| + updateView: function(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, datum);
|
| + },
|
| +
|
| + dataLoaded: function(data, indexOffset) {
|
| + var scrollFrame = this.scroller.getCurrentFrame();
|
| + this.tiler.updateFirstItem(indexOffset);
|
| + this.updateView(data.items, false);
|
| + },
|
| +
|
| + handleScroll: function(event) {
|
| + if (!this.scroller.captureNewFrame(event))
|
| + return;
|
| +
|
| + this.updateView(this.loader.getItems(), true);
|
| + }
|
| + });
|
| +
|
| +})(this);
|
| +
|
| +</script>
|
| +
|
|
|