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

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: moar cleanup 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 {
ojan 2014/10/31 19:40:21 Is this needed? We've been talking about removing
rafaelw 2014/11/04 19:01:55 Probably not, since it doesn't do anything. The d
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 if (loadedPre >= LOAD_BUFFER_PRE && loadedPost >= LOAD_BUFFER_POST) {
202 if (window.startLoad) {
203 var loadTime = (new Date().getTime()) - window.startLoad);
204 console.log('Load: ' + loadTime + 'ms');
205 window.startLoad = undefined;
206 }
207 return;
208 }
209
210 this.loadingData = true;
211
212 var loadIndex;
213
214 if (!this.data) {
215 // Initial batch
216 loadIndex = 0;
217 } else if (loadedPost < LOAD_BUFFER_POST) {
218 // Load forward first
219 loadIndex = this.data.items.length;
220 } else {
221 // Then load backward
222 loadIndex = -LOAD_LENGTH;
223 }
224
225 var self = this;
226 var externalIndex = this.externalIndex(loadIndex);
227
228 try {
229 CityDataService.service.then(function(cityService) {
230 return cityService.get(externalIndex, LOAD_LENGTH)
231 .then(function(cities) {
232 var indexOffset = 0;
233 var newData = new CitySequence(cities);
234 if (!self.data) {
235 self.data = newData;
236 } else if (loadIndex > 0) {
237 self.data.append(newData);
238 } else {
239 self.zeroIndex += LOAD_LENGTH;
240 indexOffset = LOAD_LENGTH;
241 newData.append(self.data);
242 self.data = newData;
243 }
244
245 self.loadingData = false;
246 dataloadedCallback(self.data, indexOffset);
247 });
248 }).catch(function(ex) {
249 console.log(ex.stack);
ojan 2014/10/31 19:40:21 Did we fork before promises correctly logged uncau
rafaelw 2014/11/04 19:01:55 The do not (log uncaught exceptions) to the consol
250 });
251 } catch (ex) {
252 console.log(ex.stack);
253 }
254 }
255
256 function Scroller() {
257 this.contentarea = null;
258 this.scroller = null;
259 this.contentTop = 0; // #contentarea's current top
260 this.scrollTop = 0; // #scrollarea's current top
261 this.scrollHeight = -1; // height of #scroller (the viewport)
262 this.lastScrollTop = 0; // last known scrollTop to compute deltas
263 }
264
265 Scroller.prototype.setup = function(scroller, scrollarea, contentarea) {
266 this.contentarea = contentarea;
267 this.scroller = scroller;
268
269 this.scrollHeight = scroller.offsetHeight;
270 scrollarea.style.height = (this.scrollHeight) * 4 + 'px';
271
272 this.reset();
273 }
274
275 Scroller.prototype.captureNewFrame = function(event) {
276 var scrollTop = event.target.scrollTop;
277
278 // Protect from re-entry.
279 if (this.lastScrollTop == scrollTop)
280 return false;
281
282 var scrollDown = scrollTop > this.lastScrollTop;
283 if (scrollDown) {
284 while (scrollTop > this.scrollHeight * 1.5) {
285 scrollTop -= this.scrollHeight;
286 this.contentTop -= this.scrollHeight;
287 }
288 } else {
289 while(scrollTop < this.scrollHeight * 1.5) {
290 scrollTop += this.scrollHeight;
291 this.contentTop += this.scrollHeight;
292 }
293 }
294
295 this.lastScrollTop = scrollTop;
296 event.target.scrollTop = scrollTop;
297 this.contentarea.style.top = this.contentTop + 'px';
298
299 this.scrollTop = scrollTop - this.contentTop;
300
301 return true;
302 }
303
304 Scroller.prototype.reset = function() {
305 if (!this.contentarea)
306 return;
307
308 this.scroller.scrollTop = this.scrollHeight;
309 this.lastScrollTop = this.scrollHeight;
310
311 this.contentarea.style.top = this.scrollHeight + 'px';
312 this.contentTop = this.scrollHeight;
313 this.scrollTop = 0;
314 }
315
316 // Current position and height of the scroller, that could
317 // be used (by Tiler, for example) to reason about where to
318 // place visible things.
319 Scroller.prototype.getCurrentFrame = function() {
320 return { top: this.scrollTop, height: this.scrollHeight };
321 }
322
323 Scroller.prototype.hasFrame = function() {
324 return this.scrollHeight != -1;
325 }
326
327 function Tile(datum, element, viewType, index) {
328 this.datum = datum;
329 this.element = element;
330 this.viewType = viewType;
331 this.index = index;
332 }
333
334 function Tiler(contentArea, views, viewHeights) {
335 this.contentArea = contentArea;
336 this.drawTop = 0;
337 this.drawBottom = 0;
338 this.firstItem = -1;
339 this.tiles = [];
340 this.viewHeights = viewHeights;
341 this.views = views;
342 }
343
344 Tiler.prototype.setupViews = function(scrollFrame) {
345 for (var type in this.viewHeights) {
346 this.initializeViewType(scrollFrame, type, this.viewHeights[type]);
347 }
348 }
349
350 Tiler.prototype.initializeViewType = function(scrollFrame, viewType,
351 height) {
352 var count = Math.ceil(scrollFrame.height / height) * 2;
353 var viewCache = this.views[viewType] = {
354 indices: [],
355 elements: []
356 };
357
358 var protoElement;
359 switch (viewType) {
360 case 'stateHeader':
361 protoElement = document.createElement('state-header');
362 break;
363 case 'letterHeader':
364 protoElement = document.createElement('letter-header');
365 break;
366 case 'cityItem':
367 protoElement = document.createElement('city-item');
368 break;
369 default:
370 console.warn('Unknown viewType: ' + viewType);
371 }
372 protoElement.style.display = 'none';
373 protoElement.style.height = height;
374 protoElement.classList.add('position');
375
376 for (var i = 0; i < count; i++) {
377 var clone = protoElement.cloneNode(false);
378 this.contentArea.appendChild(clone);
379 viewCache.elements.push(clone);
380 viewCache.indices.push(i);
381 }
382 }
383
384 Tiler.prototype.checkoutTile = function(viewType, datum, top) {
385 var viewCache = this.views[viewType];
386 var index = viewCache.indices.pop();
387 var element = viewCache.elements[index];
388 element.datum = datum;
389 element.style.display = '';
390 element.style.top = top + 'px';
391
392 return new Tile(datum, element, viewType, index);
393 }
394
395 Tiler.prototype.checkinTile = function(tile) {
396 if (!tile.element)
397 return;
398
399 tile.element.style.display = 'none';
400 this.views[tile.viewType].indices.push(tile.index);
401 }
402
403 Tiler.prototype.getFirstVisibleDatum = function(scrollFrame) {
404 var tiles = this.tiles;
405 var viewHeights = this.viewHeights;
406
407 var itemTop = this.drawTop - scrollFrame.top;
408 if (itemTop >= 0 && tiles.length)
409 return tiles[0].datum;
410
411 var tile;
412 for (var i = 0; i < tiles.length && itemTop < 0; i++) {
413 tile = tiles[i];
414 var height = viewHeights[tile.viewType];
415 itemTop += height;
416 }
417
418 return tile ? tile.datum : null;
419 }
420
421 Tiler.prototype.viewType = function(datum) {
422 switch (datum.headerOrder) {
423 case 1: return 'stateHeader';
424 case 2: return 'letterHeader';
425 default: return 'cityItem';
426 }
427 }
428
429 Tiler.prototype.drawTiles = function(scrollFrame, data) {
430 var tiles = this.tiles;
431 var viewHeights = this.viewHeights;
432
433 var buffer = Math.round(scrollFrame.height / 2);
434 var targetTop = scrollFrame.top - buffer;
435 var targetBottom = scrollFrame.top + scrollFrame.height + buffer;
436
437 // Collect down to targetTop
438 var first = tiles[0];
439 while (tiles.length &&
440 targetTop > this.drawTop + viewHeights[first.viewType]) {
441
442 var height = viewHeights[first.viewType];
443 this.drawTop += height;
444
445 this.firstItem++;
446 this.checkinTile(tiles.shift());
447
448 first = tiles[0];
449 }
450
451 // Collect up to targetBottom
452 var last = tiles[tiles.length - 1];
453 while(tiles.length &&
454 targetBottom < this.drawBottom - viewHeights[last.viewType]) {
455
456 var height = viewHeights[last.viewType];
457 this.drawBottom -= height;
458
459 this.checkinTile(tiles.pop());
460
461 last = tiles[tiles.length - 1];
462 }
463
464 // Layout up to targetTop
465 while (this.firstItem > 0 &&
466 targetTop < this.drawTop) {
467
468 var datum = data[this.firstItem - 1];
469 var type = this.viewType(datum);
470 var height = viewHeights[type];
471
472 this.drawTop -= height;
473
474 var tile = targetBottom < this.drawTop ?
475 new Tile(datum, null, viewType, -1) : // off-screen
476 this.checkoutTile(type, datum, this.drawTop);
477
478 this.firstItem--;
479 tiles.unshift(tile);
480 }
481
482 // Layout down to targetBottom
483 while (this.firstItem + tiles.length < data.length - 1 &&
484 targetBottom > this.drawBottom) {
485
486 var datum = data[this.firstItem + tiles.length];
487 var type = this.viewType(datum);
488 var height = viewHeights[type];
489
490 this.drawBottom += height;
491
492 var tile = targetTop > this.drawBottom ?
493 new Tile(datum, null, viewType, -1) : // off-screen
494 this.checkoutTile(type, datum, this.drawBottom - height);
495
496 tiles.push(tile);
497 }
498
499 // Debug validate:
500 // for (var i = 0; i < tiles.length; i++) {
501 // if (tiles[i].datum !== data[this.firstItem + i])
502 // throw Error('Invalid')
503 // }
504 }
505
506 // FIXME: Needs better name.
507 Tiler.prototype.updateFirstItem = function(offset) {
508 var tiles = this.tiles;
509
510 if (!tiles.length) {
511 this.firstItem = 0;
512 } else {
513 this.firstItem += offset;
514 }
515 }
516
517 Tiler.prototype.checkinAllTiles = function() {
518 var tiles = this.tiles;
519 while (tiles.length) {
520 this.checkinTile(tiles.pop());
521 }
522 }
523
524 Tiler.prototype.reset = function() {
525 this.checkinAllTiles();
526 this.drawTop = 0;
527 this.drawBottom = 0;
528 }
529
530 SkyElement({
531 name: 'city-list',
532 loader: null,
533 scroller: null,
534 tiler: null,
535 date: null,
536 month: null,
537 views: null,
538
539 attached: function() {
540 this.views = {};
541 this.loader = new Loader();
542 this.scroller = new Scroller();
543 this.tiler = new Tiler(
544 this.shadowRoot.getElementById('contentarea'), this.views, {
545 stateHeader: 27,
546 letterHeader: 18,
547 cityItem: 30
548 });
549
550 this.dataLoaded = this.dataLoaded.bind(this);
551 this.shadowRoot.getElementById('scroller')
552 .addEventListener('scroll', this.handleScroll.bind(this));
553
554 var self = this;
555 requestAnimationFrame(function() {
556 self.domReady();
557 self.loader.maybeLoadMoreData(self.dataLoaded);
558 });
559 },
560
561 domReady: function() {
562 this.scroller.setup(this.shadowRoot.getElementById('scroller'),
563 this.shadowRoot.getElementById('scrollarea'),
564 this.shadowRoot.getElementById('contentarea'));
565 var scrollFrame = this.scroller.getCurrentFrame();
566 this.tiler.setupViews(scrollFrame);
567 },
568
569 updateView: function(data, scrollChanged) {
570 var scrollFrame = this.scroller.getCurrentFrame();
571 this.tiler.drawTiles(scrollFrame, data);
572 var datum = scrollChanged ?
573 this.tiler.getFirstVisibleDatum(scrollFrame) : null;
574 this.loader.maybeLoadMoreData(this.dataLoaded, datum);
575 },
576
577 dataLoaded: function(data, indexOffset) {
578 var scrollFrame = this.scroller.getCurrentFrame();
579 this.tiler.updateFirstItem(indexOffset);
580 this.updateView(data.items, false);
581 },
582
583 handleScroll: function(event) {
584 if (!this.scroller.captureNewFrame(event))
585 return;
586
587 this.updateView(this.loader.getItems(), true);
588 }
589 });
590
591 })(this);
592
593 </script>
594
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