| 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 |