OLD | NEW |
| (Empty) |
1 | |
2 | |
3 Polymer({ | |
4 | |
5 is: 'x-repeat', | |
6 extends: 'template', | |
7 | |
8 properties: { | |
9 | |
10 /** | |
11 * An array containing items determining how many instances of the templat
e | |
12 * to stamp and that that each template instance should bind to. | |
13 */ | |
14 items: { | |
15 type: Array | |
16 }, | |
17 | |
18 /** | |
19 * A function that should determine the sort order of the items. This | |
20 * property should either be provided as a string, indicating a method | |
21 * name on the element's host, or else be an actual function. The | |
22 * function should match the sort function passed to `Array.sort`. | |
23 * Using a sort function has no effect on the underlying `items` array. | |
24 */ | |
25 sort: { | |
26 type: Function, | |
27 observer: '_sortChanged' | |
28 }, | |
29 | |
30 /** | |
31 * A function that can be used to filter items out of the view. This | |
32 * property should either be provided as a string, indicating a method | |
33 * name on the element's host, or else be an actual function. The | |
34 * function should match the sort function passed to `Array.filter`. | |
35 * Using a filter function has no effect on the underlying `items` array. | |
36 */ | |
37 filter: { | |
38 type: Function, | |
39 observer: '_filterChanged' | |
40 }, | |
41 | |
42 /** | |
43 * When using a `filter` or `sort` function, the `observe` property | |
44 * should be set to a space-separated list of the names of item | |
45 * sub-fields that should trigger a re-sort or re-filter when changed. | |
46 * These should generally be fields of `item` that the sort or filter | |
47 * function depends on. | |
48 */ | |
49 observe: { | |
50 type: String, | |
51 observer: '_observeChanged' | |
52 }, | |
53 | |
54 /** | |
55 * When using a `filter` or `sort` function, the `delay` property | |
56 * determines a debounce time after a change to observed item | |
57 * properties that must pass before the filter or sort is re-run. | |
58 * This is useful in rate-limiting shuffing of the view when | |
59 * item changes may be frequent. | |
60 */ | |
61 delay: Number | |
62 }, | |
63 | |
64 behaviors: [ | |
65 Polymer.Templatizer | |
66 ], | |
67 | |
68 observers: [ | |
69 '_itemsChanged(items.*)' | |
70 ], | |
71 | |
72 created: function() { | |
73 this.boundCollectionObserver = this.render.bind(this); | |
74 }, | |
75 | |
76 ready: function() { | |
77 // Templatizing (generating the instance constructor) needs to wait | |
78 // until attached, since it may not have its template content handed | |
79 // back to it until then, following its host template stamping | |
80 if (!this.ctor) { | |
81 this.templatize(this); | |
82 } | |
83 }, | |
84 | |
85 _sortChanged: function() { | |
86 var dataHost = this._getRootDataHost(); | |
87 this._sortFn = this.sort && (typeof this.sort == 'function' ? | |
88 this.sort : dataHost[this.sort].bind(this.host)); | |
89 if (this.items) { | |
90 this.debounce('render', this.render); | |
91 } | |
92 }, | |
93 | |
94 _filterChanged: function() { | |
95 var dataHost = this._getRootDataHost(); | |
96 this._filterFn = this.filter && (typeof this.filter == 'function' ? | |
97 this.filter : dataHost[this.filter].bind(this.host)); | |
98 if (this.items) { | |
99 this.debounce('render', this.render); | |
100 } | |
101 }, | |
102 | |
103 _observeChanged: function() { | |
104 this._observePaths = this.observe && | |
105 this.observe.replace('.*', '.').split(' '); | |
106 }, | |
107 | |
108 _itemsChanged: function(change) { | |
109 if (change.path == 'items') { | |
110 this._unobserveCollection(); | |
111 if (change.value) { | |
112 this._observeCollection(change.value); | |
113 this.debounce('render', this.render); | |
114 } | |
115 } else { | |
116 this._forwardItemPath(change.path, change.value); | |
117 this._checkObservedPaths(change.path); | |
118 } | |
119 }, | |
120 | |
121 _checkObservedPaths: function(path) { | |
122 if (this._observePaths && path.indexOf('items.') === 0) { | |
123 path = path.substring(path.indexOf('.', 6) + 1); | |
124 var paths = this._observePaths; | |
125 for (var i=0; i<paths.length; i++) { | |
126 if (path.indexOf(paths[i]) === 0) { | |
127 this.debounce('render', this.render, this.delay); | |
128 return; | |
129 } | |
130 } | |
131 } | |
132 }, | |
133 | |
134 _observeCollection: function(items) { | |
135 this.collection = Array.isArray(items) ? Polymer.Collection.get(items) : i
tems; | |
136 this.collection.observe(this.boundCollectionObserver); | |
137 }, | |
138 | |
139 _unobserveCollection: function() { | |
140 if (this.collection) { | |
141 this.collection.unobserve(this.boundCollectionObserver); | |
142 } | |
143 }, | |
144 | |
145 render: function(splices) { | |
146 this.flushDebouncer('render'); | |
147 var c = this.collection; | |
148 if (splices) { | |
149 if (this._sortFn || splices[0].index == null) { | |
150 this._applySplicesViewSort(splices); | |
151 } else { | |
152 this._applySplicesArraySort(splices); | |
153 } | |
154 } else { | |
155 this._sortAndFilter(); | |
156 } | |
157 var rowForKey = this._rowForKey = {}; | |
158 var keys = this._orderedKeys; | |
159 // Assign items and keys | |
160 this.rows = this.rows || []; | |
161 for (var i=0; i<keys.length; i++) { | |
162 var key = keys[i]; | |
163 var item = c.getItem(key); | |
164 var row = this.rows[i]; | |
165 rowForKey[key] = i; | |
166 if (!row) { | |
167 this.rows.push(row = this._insertRow(i, null, item)); | |
168 } | |
169 row.item = item; | |
170 row.key = key; | |
171 row.index = i; | |
172 } | |
173 // Remove extra | |
174 for (; i<this.rows.length; i++) { | |
175 this._detachRow(i); | |
176 } | |
177 this.rows.splice(keys.length, this.rows.length-keys.length); | |
178 }, | |
179 | |
180 _sortAndFilter: function() { | |
181 var c = this.collection; | |
182 this._orderedKeys = c.getKeys(); | |
183 // Filter | |
184 if (this._filterFn) { | |
185 this._orderedKeys = this._orderedKeys.filter(function(a) { | |
186 return this._filterFn(c.getItem(a)); | |
187 }, this); | |
188 } | |
189 // Sort | |
190 if (this._sortFn) { | |
191 this._orderedKeys.sort(function(a, b) { | |
192 return this._sortFn(c.getItem(a), c.getItem(b)); | |
193 }.bind(this)); | |
194 } | |
195 }, | |
196 | |
197 _keySort: function(a, b) { | |
198 return this.collection.getKey(a) - this.collection.getKey(b); | |
199 }, | |
200 | |
201 _applySplicesViewSort: function(splices) { | |
202 var c = this.collection; | |
203 var keys = this._orderedKeys; | |
204 var rows = this.rows; | |
205 var removedRows = []; | |
206 var addedKeys = []; | |
207 var pool = []; | |
208 var sortFn = this._sortFn || this._keySort.bind(this); | |
209 splices.forEach(function(s) { | |
210 // Collect all removed row idx's | |
211 for (var i=0; i<s.removed.length; i++) { | |
212 var idx = this._rowForKey[s.removed[i]]; | |
213 if (idx != null) { | |
214 removedRows.push(idx); | |
215 } | |
216 } | |
217 // Collect all added keys | |
218 for (i=0; i<s.added.length; i++) { | |
219 addedKeys.push(s.added[i]); | |
220 } | |
221 }, this); | |
222 if (removedRows.length) { | |
223 // Sort removed rows idx's | |
224 removedRows.sort(); | |
225 // Remove keys and pool rows (backwards, so we don't invalidate rowForKe
y) | |
226 for (i=removedRows.length-1; i>=0 ; i--) { | |
227 var idx = removedRows[i]; | |
228 pool.push(this._detachRow(idx)); | |
229 rows.splice(idx, 1); | |
230 keys.splice(idx, 1); | |
231 } | |
232 } | |
233 if (addedKeys.length) { | |
234 // Filter added keys | |
235 if (this._filterFn) { | |
236 addedKeys = addedKeys.filter(function(a) { | |
237 return this._filterFn(c.getItem(a)); | |
238 }, this); | |
239 } | |
240 // Sort added keys | |
241 addedKeys.sort(function(a, b) { | |
242 return this.sortFn(c.getItem(a), c.getItem(b)); | |
243 }, this); | |
244 // Insert new rows using sort (from pool or newly created) | |
245 var start = 0; | |
246 for (i=0; i<addedKeys.length; i++) { | |
247 start = this._insertRowIntoViewSort(start, addedKeys[i], pool); | |
248 } | |
249 } | |
250 }, | |
251 | |
252 _insertRowIntoViewSort: function(start, key, pool) { | |
253 var c = this.collection; | |
254 var item = c.getItem(key); | |
255 var end = this.rows.length - 1; | |
256 var idx = -1; | |
257 var sortFn = this._sortFn || this._keySort.bind(this); | |
258 // Binary search for insertion point | |
259 while (start <= end) { | |
260 var mid = (start + end) >> 1; | |
261 var midKey = this._orderedKeys[mid]; | |
262 var cmp = sortFn(c.getItem(midKey), item); | |
263 if (cmp < 0) { | |
264 start = mid + 1; | |
265 } else if (cmp > 0) { | |
266 end = mid - 1; | |
267 } else { | |
268 idx = mid; | |
269 break; | |
270 } | |
271 } | |
272 if (idx < 0) { | |
273 idx = end + 1; | |
274 } | |
275 // Insert key & row at insertion point | |
276 this._orderedKeys.splice(idx, 0, key); | |
277 this.rows.splice(idx, 0, this._insertRow(idx, pool)); | |
278 return idx; | |
279 }, | |
280 | |
281 _applySplicesArraySort: function(splices) { | |
282 var keys = this._orderedKeys; | |
283 var pool = []; | |
284 splices.forEach(function(s) { | |
285 // Remove & pool rows first, to ensure we can fully reuse removed rows | |
286 for (var i=0; i<s.removed.length; i++) { | |
287 pool.push(this._detachRow(s.index + i)); | |
288 } | |
289 this.rows.splice(s.index, s.removed.length); | |
290 }, this); | |
291 var c = this.collection; | |
292 var filterDelta = 0; | |
293 splices.forEach(function(s) { | |
294 // Filter added keys | |
295 var addedKeys = s.added; | |
296 if (this._filterFn) { | |
297 addedKeys = addedKeys.filter(function(a) { | |
298 return this._filterFn(c.getItem(a)); | |
299 }, this); | |
300 filterDelta += (s.added.length - addedKeys.length); | |
301 } | |
302 var idx = s.index - filterDelta; | |
303 // Apply splices to keys | |
304 var args = [idx, s.removed.length].concat(addedKeys); | |
305 keys.splice.apply(keys, args); | |
306 // Insert new rows (from pool or newly created) | |
307 var addedRows = []; | |
308 for (i=0; i<s.added.length; i++) { | |
309 addedRows.push(this._insertRow(idx + i, pool)); | |
310 } | |
311 args = [s.index, 0].concat(addedRows); | |
312 this.rows.splice.apply(this.rows, args); | |
313 }, this); | |
314 }, | |
315 | |
316 _detachRow: function(idx) { | |
317 var row = this.rows[idx]; | |
318 var parentNode = Polymer.dom(this).parentNode; | |
319 for (var i=0; i<row._children.length; i++) { | |
320 var el = row._children[i]; | |
321 Polymer.dom(row.root).appendChild(el); | |
322 } | |
323 return row; | |
324 }, | |
325 | |
326 _insertRow: function(idx, pool, item) { | |
327 var row = (pool && pool.pop()) || this._generateRow(idx, item); | |
328 var beforeRow = this.rows[idx]; | |
329 var beforeNode = beforeRow ? beforeRow._children[0] : this; | |
330 var parentNode = Polymer.dom(this).parentNode; | |
331 Polymer.dom(parentNode).insertBefore(row.root, beforeNode); | |
332 return row; | |
333 }, | |
334 | |
335 _generateRow: function(idx, item) { | |
336 var row = this.stamp({ | |
337 index: idx, | |
338 key: this.collection.getKey(item), | |
339 item: item | |
340 }); | |
341 // each row is a document fragment which is lost when we appendChild, | |
342 // so we have to track each child individually | |
343 var children = []; | |
344 for (var n = row.root.firstChild; n; n=n.nextSibling) { | |
345 children.push(n); | |
346 n._templateInstance = row; | |
347 } | |
348 // Since archetype overrides Base/HTMLElement, Safari complains | |
349 // when accessing `children` | |
350 row._children = children; | |
351 return row; | |
352 }, | |
353 | |
354 // Implements extension point from Templatizer mixin | |
355 _getStampedChildren: function() { | |
356 var children = []; | |
357 if (this.rows) { | |
358 for (var i=0; i<this.rows.length; i++) { | |
359 var c = this.rows[i]._children; | |
360 for (var j=0; j<c.length; j++) | |
361 children.push(c[j]); | |
362 } | |
363 } | |
364 return children; | |
365 }, | |
366 | |
367 // Implements extension point from Templatizer | |
368 // Called as a side effect of a template instance path change, responsible | |
369 // for notifying items.<key-for-row>.<path> change up to host | |
370 _forwardInstancePath: function(row, root, subPath, value) { | |
371 if (root == 'item') { | |
372 this.notifyPath('items.' + row.key + '.' + subPath, value); | |
373 } | |
374 }, | |
375 | |
376 // Implements extension point from Templatizer mixin | |
377 // Called as side-effect of a host property change, responsible for | |
378 // notifying parent.<prop> path change on each row | |
379 _forwardParentProp: function(prop, value) { | |
380 if (this.rows) { | |
381 this.rows.forEach(function(row) { | |
382 row.parent[prop] = value; | |
383 row.notifyPath('parent.' + prop, value, true); | |
384 }, this); | |
385 } | |
386 }, | |
387 | |
388 // Implements extension point from Templatizer | |
389 // Called as side-effect of a host path change, responsible for | |
390 // notifying parent.<path> path change on each row | |
391 _forwardParentPath: function(path, value) { | |
392 if (this.rows) { | |
393 this.rows.forEach(function(row) { | |
394 row.notifyPath('parent.' + path, value, true); | |
395 }, this); | |
396 } | |
397 }, | |
398 | |
399 // Called as a side effect of a host items.<key>.<path> path change, | |
400 // responsible for notifying item.<path> changes to row for key | |
401 _forwardItemPath: function(path, value) { | |
402 if (this._rowForKey) { | |
403 // 'items.'.length == 6 | |
404 var dot = path.indexOf('.', 6); | |
405 var key = path.substring(6, dot < 0 ? path.length : dot); | |
406 var idx = this._rowForKey[key]; | |
407 var row = this.rows[idx]; | |
408 if (row) { | |
409 if (dot >= 0) { | |
410 path = 'item.' + path.substring(dot+1); | |
411 row.notifyPath(path, value, true); | |
412 } else { | |
413 row.item = value; | |
414 } | |
415 } | |
416 } | |
417 }, | |
418 | |
419 _instanceForElement: function(el) { | |
420 while (el && !el._templateInstance) { | |
421 el = el.parentNode; | |
422 } | |
423 return el && el._templateInstance; | |
424 }, | |
425 | |
426 /** | |
427 * Returns the item associated with a given element stamped by | |
428 * this `x-repeat`. | |
429 */ | |
430 itemForElement: function(el) { | |
431 var instance = this._instanceForElement(el); | |
432 return instance && instance.item; | |
433 }, | |
434 | |
435 /** | |
436 * Returns the `Polymer.Collection` key associated with a given | |
437 * element stamped by this `x-repeat`. | |
438 */ | |
439 keyForElement: function(el) { | |
440 var instance = this._instanceForElement(el); | |
441 return instance && instance.key; | |
442 }, | |
443 | |
444 /** | |
445 * Returns the index in `items` associated with a given element | |
446 * stamped by this `x-repeat`. | |
447 */ | |
448 indexForElement: function(el) { | |
449 var instance = this._instanceForElement(el); | |
450 return this.rows.indexOf(instance); | |
451 } | |
452 | |
453 }); | |
454 | |
455 | |
OLD | NEW |