| 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
 | 
| deleted file mode 100644
 | 
| index 4d8e55012f491f0ae6c8b6d31d1a1aeb30937c06..0000000000000000000000000000000000000000
 | 
| --- a/sky/examples/city-list/city-list.sky
 | 
| +++ /dev/null
 | 
| @@ -1,532 +0,0 @@
 | 
| -<!--
 | 
| -// 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>
 | 
| 
 |