Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(132)

Unified Diff: sky/examples/city-list/city-list.sky

Issue 698653002: Add initial SkyElement & city-list example (Closed) Base URL: https://github.com/domokit/mojo.git@master
Patch Set: ws Created 6 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « sky/examples/city-list/city-data-service.sky ('k') | sky/examples/city-list/city-sequence.sky » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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>
+
« no previous file with comments | « sky/examples/city-list/city-data-service.sky ('k') | sky/examples/city-list/city-sequence.sky » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698