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