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

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