| OLD | NEW |
| (Empty) |
| 1 <!-- | |
| 2 Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
| 3 This code may only be used under the BSD style license found at http://polymer.g
ithub.io/LICENSE.txt | |
| 4 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
| 5 The complete set of contributors may be found at http://polymer.github.io/CONTRI
BUTORS.txt | |
| 6 Code distributed by Google as part of the polymer project is also | |
| 7 subject to an additional IP rights grant found at http://polymer.github.io/PATEN
TS.txt | |
| 8 --> | |
| 9 | |
| 10 <!-- | |
| 11 `core-list` displays a virtual, 'infinite' list. The template inside the | |
| 12 `core-list` element represents the dom to create for each list item. The | |
| 13 `data` property specifies an array of list item data. The `height` property | |
| 14 represents the fixed height of a list item (variable height list items are | |
| 15 not yet supported). | |
| 16 | |
| 17 `core-list` manages a viewport of data based on the current scroll position. | |
| 18 For performance reasons, not every item in the list is rendered at once. | |
| 19 | |
| 20 List item templates should bind to template models of the following structure | |
| 21 | |
| 22 { | |
| 23 index: 0, // list index for this item | |
| 24 selected: false, // selection state for this item | |
| 25 model: { // user data corresponding to data[index] | |
| 26 /* user data */ | |
| 27 } | |
| 28 } | |
| 29 | |
| 30 For example, given the following data array: | |
| 31 | |
| 32 [ | |
| 33 {name: 'Bob', checked: true}, | |
| 34 {name: 'Tim', checked: false}, | |
| 35 ... | |
| 36 ] | |
| 37 | |
| 38 The following code would render the list (note the `name` and `checked` | |
| 39 properties are bound from the `model` object provided to the template | |
| 40 scope): | |
| 41 | |
| 42 <core-list data="{{data}}" height="80"> | |
| 43 <template> | |
| 44 <div class="{{ {selected: selected} | tokenList }}"> | |
| 45 List row: {{index}}, User data from model: {{model.name}} | |
| 46 <input type="checkbox" checked="{{model.checked}}"> | |
| 47 </div> | |
| 48 </template> | |
| 49 </core-list> | |
| 50 | |
| 51 By default, the list supports selection via tapping. Styling the selection | |
| 52 should be done via binding to the `selected` property of each model. | |
| 53 | |
| 54 @group Polymer Core Elements | |
| 55 @element core-list | |
| 56 --> | |
| 57 <link rel="import" href="../polymer/polymer.html"> | |
| 58 <link rel="import" href="../core-selection/core-selection.html"> | |
| 59 | |
| 60 <polymer-element name="core-list" on-tap="{{tapHandler}}" tabindex="-1"> | |
| 61 <template> | |
| 62 <core-selection id="selection" multi="{{multi}}" on-core-select="{{selectedHan
dler}}"></core-selection> | |
| 63 <link rel="stylesheet" href="core-list.css"> | |
| 64 <div id="viewport" class="core-list-viewport"><content></content></div> | |
| 65 </template> | |
| 66 <script> | |
| 67 (function() { | |
| 68 | |
| 69 Polymer('core-list', { | |
| 70 | |
| 71 publish: { | |
| 72 /** | |
| 73 * Fired when an item element is tapped. | |
| 74 * | |
| 75 * @event core-activate | |
| 76 * @param {Object} detail | |
| 77 * @param {Object} detail.item the item element | |
| 78 */ | |
| 79 | |
| 80 /** | |
| 81 * | |
| 82 * An array of source data for the list to display. | |
| 83 * | |
| 84 * @attribute data | |
| 85 * @type array | |
| 86 * @default null | |
| 87 */ | |
| 88 data: null, | |
| 89 | |
| 90 /** | |
| 91 * | |
| 92 * An optional element on which to listen for scroll events. | |
| 93 * | |
| 94 * @attribute scrollTarget | |
| 95 * @type Element | |
| 96 * @default core-list | |
| 97 */ | |
| 98 scrollTarget: null, | |
| 99 | |
| 100 /** | |
| 101 * | |
| 102 * The height of a list item. `core-list` currently supports only fixed-he
ight | |
| 103 * list items. This height must be specified via the height property. | |
| 104 * | |
| 105 * @attribute height | |
| 106 * @type number | |
| 107 * @default 80 | |
| 108 */ | |
| 109 height: 80, | |
| 110 | |
| 111 /** | |
| 112 * | |
| 113 * The number of extra items rendered above the minimum set required to | |
| 114 * fill the list's height. | |
| 115 * | |
| 116 * @attribute extraItems | |
| 117 * @type number | |
| 118 * @default 30 | |
| 119 */ | |
| 120 extraItems: 30, | |
| 121 | |
| 122 /** | |
| 123 * | |
| 124 * When true, tapping a row will select the item, placing its data model | |
| 125 * in the set of selected items retrievable via the `selection` property. | |
| 126 * | |
| 127 * Note that tapping focusable elements within the list item will not | |
| 128 * result in selection, since they are presumed to have their own action. | |
| 129 * | |
| 130 * @attribute selectionEnabled | |
| 131 * @type {boolean} | |
| 132 * @default true | |
| 133 */ | |
| 134 selectionEnabled: true, | |
| 135 | |
| 136 /** | |
| 137 * | |
| 138 * Set to true to support multiple selection. Note, existing selection | |
| 139 * state is maintained only when changing `multi` from `false` to `true`; | |
| 140 * it is cleared when changing from `true` to `false`. | |
| 141 * | |
| 142 * @attribute multi | |
| 143 * @type boolean | |
| 144 * @default false | |
| 145 */ | |
| 146 multi: false, | |
| 147 | |
| 148 /** | |
| 149 * | |
| 150 * Data record (or array of records, if `multi: true`) corresponding to | |
| 151 * the currently selected set of items. | |
| 152 * | |
| 153 * @attribute selection | |
| 154 * @type {any} | |
| 155 * @default null | |
| 156 */ | |
| 157 selection: null | |
| 158 }, | |
| 159 | |
| 160 // Local cache of scrollTop | |
| 161 _scrollTop: 0, | |
| 162 | |
| 163 observe: { | |
| 164 'data template scrollTarget': 'initialize', | |
| 165 'multi selectionEnabled': '_resetSelection' | |
| 166 }, | |
| 167 | |
| 168 ready: function() { | |
| 169 this._boundScrollHandler = this.scrollHandler.bind(this); | |
| 170 this._oldMulti = this.multi; | |
| 171 this._oldSelectionEnabled = this.selectionEnabled; | |
| 172 }, | |
| 173 | |
| 174 attached: function() { | |
| 175 this.template = this.querySelector('template'); | |
| 176 if (!this.template.bindingDelegate) { | |
| 177 this.template.bindingDelegate = this.element.syntax; | |
| 178 } | |
| 179 }, | |
| 180 | |
| 181 _resetSelection: function() { | |
| 182 if (((this._oldMulti != this.multi) && !this.multi) || | |
| 183 ((this._oldSelectionEnabled != this.selectionEnabled) && | |
| 184 !this.selectionEnabled)) { | |
| 185 this._clearSelection(); | |
| 186 this.refresh(true); | |
| 187 } else { | |
| 188 this.selection = this.$.selection.getSelection(); | |
| 189 } | |
| 190 this._oldMulti = this.multi; | |
| 191 this._oldSelectionEnabled = this.selectionEnabled; | |
| 192 }, | |
| 193 | |
| 194 // TODO(sorvell): it'd be nice to dispense with 'data' and just use | |
| 195 // template repeat's model. However, we need tighter integration | |
| 196 // with TemplateBinding for this. | |
| 197 initialize: function() { | |
| 198 if (!this.template) { | |
| 199 return; | |
| 200 } | |
| 201 | |
| 202 // TODO(kschaaf): This is currently the only way to know that the array | |
| 203 // was mutated as opposed to newly assigned; to be updated with better API | |
| 204 if (arguments.length == 1) { | |
| 205 var splices = arguments[0]; | |
| 206 for (var i=0; i<splices.length; i++) { | |
| 207 var s = splices[i]; | |
| 208 for (var j=0; j<s.removed.length; j++) { | |
| 209 var d = s.removed[j]; | |
| 210 this.$.selection.setItemSelected(d, false); | |
| 211 } | |
| 212 } | |
| 213 } else { | |
| 214 this._clearSelection(); | |
| 215 } | |
| 216 | |
| 217 var target = this.scrollTarget || this; | |
| 218 if (this._target !== target) { | |
| 219 if (this._target) { | |
| 220 this._target.removeEventListener('scroll', this._boundScrollHandler, f
alse); | |
| 221 } | |
| 222 this._target = target; | |
| 223 this._target.addEventListener('scroll', this._boundScrollHandler, false)
; | |
| 224 } | |
| 225 // Only use -webkit-overflow-touch from iOS8+, where scroll events are fir
ed | |
| 226 var ios = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/)
; | |
| 227 if (ios && ios[1] >= 8) { | |
| 228 target.style.webkitOverflowScrolling = 'touch'; | |
| 229 } | |
| 230 | |
| 231 this.initializeData(); | |
| 232 }, | |
| 233 | |
| 234 initializeData: function() { | |
| 235 var currentCount = this._physicalCount || 0; | |
| 236 var dataLen = this.data && this.data.length || 0; | |
| 237 this._visibleCount = Math.ceil(this._target.offsetHeight / this.height); | |
| 238 this._physicalCount = Math.min(this._visibleCount + this.extraItems, dataL
en); | |
| 239 this._physicalCount = Math.max(currentCount, this._physicalCount); | |
| 240 this._physicalData = this._physicalData || new Array(this._physicalCount); | |
| 241 var needItemInit = false; | |
| 242 while (currentCount < this._physicalCount) { | |
| 243 this._physicalData[currentCount++] = {}; | |
| 244 needItemInit = true; | |
| 245 } | |
| 246 this.template.model = this._physicalData; | |
| 247 this.template.setAttribute('repeat', ''); | |
| 248 if (needItemInit) { | |
| 249 this.onMutation(this, this.initializeItems); | |
| 250 } else { | |
| 251 this.refresh(true); | |
| 252 } | |
| 253 }, | |
| 254 | |
| 255 initializeItems: function() { | |
| 256 var currentCount = this._physicalItems && this._physicalItems.length || 0; | |
| 257 this._physicalItems = this._physicalItems || new Array(this._physicalCount
); | |
| 258 for (var i = 0, item = this.template.nextElementSibling; | |
| 259 item && i < this._physicalCount; | |
| 260 ++i, item = item.nextElementSibling) { | |
| 261 this._physicalItems[i] = item; | |
| 262 item._transformValue = 0; | |
| 263 } | |
| 264 this.refresh(true); | |
| 265 }, | |
| 266 | |
| 267 updateItem: function(virtualIndex, physicalIndex) { | |
| 268 var virtualDatum = this.data && this.data[virtualIndex]; | |
| 269 var physicalDatum = this._physicalData[physicalIndex]; | |
| 270 physicalDatum.model = virtualDatum; | |
| 271 physicalDatum.physicalIndex = physicalIndex; | |
| 272 physicalDatum.index = virtualIndex; | |
| 273 physicalDatum.selected = this.selectionEnabled && virtualDatum ? | |
| 274 this._selectedData.get(virtualDatum) : null; | |
| 275 var physicalItem = this._physicalItems[physicalIndex]; | |
| 276 physicalItem.hidden = !virtualDatum; | |
| 277 }, | |
| 278 | |
| 279 scrollHandler: function(e, detail) { | |
| 280 this._scrollTop = e.detail ? e.detail.target.scrollTop : e.target.scrollTo
p; | |
| 281 this.refresh(false); | |
| 282 }, | |
| 283 | |
| 284 /** | |
| 285 * Refresh the list at the current scroll position. | |
| 286 * | |
| 287 * @method refresh | |
| 288 */ | |
| 289 refresh: function(force) { | |
| 290 // Check that the array hasn't gotten longer since data was initialized | |
| 291 var dataLen = this.data && this.data.length || 0; | |
| 292 if (force) { | |
| 293 if (this._physicalCount < | |
| 294 Math.min(this._visibleCount + this.extraItems, dataLen)) { | |
| 295 // Need to add more items; once new data & items are initialized, | |
| 296 // refresh will be run again | |
| 297 this.initializeData(); | |
| 298 return; | |
| 299 } | |
| 300 this._physicalHeight = this.height * this._physicalCount; | |
| 301 this.$.viewport.style.height = this.height * dataLen + 'px'; | |
| 302 } | |
| 303 | |
| 304 var firstVisibleIndex = Math.floor(this._scrollTop / this.height); | |
| 305 var visibleMidpoint = firstVisibleIndex + this._visibleCount / 2; | |
| 306 | |
| 307 var firstReifiedIndex = Math.max(0, Math.floor(visibleMidpoint - | |
| 308 this._physicalCount / 2)); | |
| 309 firstReifiedIndex = Math.min(firstReifiedIndex, dataLen - | |
| 310 this._physicalCount); | |
| 311 firstReifiedIndex = (firstReifiedIndex < 0) ? 0 : firstReifiedIndex; | |
| 312 | |
| 313 var firstPhysicalIndex = firstReifiedIndex % this._physicalCount; | |
| 314 var baseVirtualIndex = firstReifiedIndex - firstPhysicalIndex; | |
| 315 | |
| 316 var baseTransformValue = Math.floor(this.height * baseVirtualIndex); | |
| 317 var nextTransformValue = Math.floor(baseTransformValue + | |
| 318 this._physicalHeight); | |
| 319 | |
| 320 var baseTransformString = 'translate3d(0,' + baseTransformValue + 'px,0)'; | |
| 321 var nextTransformString = 'translate3d(0,' + nextTransformValue + 'px,0)'; | |
| 322 | |
| 323 this.firstPhysicalIndex = firstPhysicalIndex; | |
| 324 this.baseVirtualIndex = baseVirtualIndex; | |
| 325 | |
| 326 for (var i = 0; i < firstPhysicalIndex; ++i) { | |
| 327 var item = this._physicalItems[i]; | |
| 328 if (force || item._transformValue != nextTransformValue) { | |
| 329 this.updateItem(baseVirtualIndex + this._physicalCount + i, i); | |
| 330 setTransform(item, nextTransformString, nextTransformValue); | |
| 331 } | |
| 332 } | |
| 333 for (var i = firstPhysicalIndex; i < this._physicalCount; ++i) { | |
| 334 var item = this._physicalItems[i]; | |
| 335 if (force || item._transformValue != baseTransformValue) { | |
| 336 this.updateItem(baseVirtualIndex + i, i); | |
| 337 setTransform(item, baseTransformString, baseTransformValue); | |
| 338 } | |
| 339 } | |
| 340 }, | |
| 341 | |
| 342 // list selection | |
| 343 tapHandler: function(e) { | |
| 344 var n = e.target; | |
| 345 var p = e.path; | |
| 346 if (!this.selectionEnabled || (n === this)) { | |
| 347 return; | |
| 348 } | |
| 349 requestAnimationFrame(function() { | |
| 350 // Gambit: only select the item if the tap wasn't on a focusable child | |
| 351 // of the list (since anything with its own action should be focusable | |
| 352 // and not result in result in list selection). To check this, we | |
| 353 // asynchronously check that shadowRoot.activeElement is null, which | |
| 354 // means the tapped item wasn't focusable. On polyfill where | |
| 355 // activeElement doesn't follow the data-hinding part of the spec, we | |
| 356 // can check that document.activeElement is the list itself, which will | |
| 357 // catch focus in lieu of the tapped item being focusable, as we make | |
| 358 // the list focusable (tabindex="-1") for this purpose. Note we also | |
| 359 // allow the list items themselves to be focusable if desired, so those | |
| 360 // are excluded as well. | |
| 361 var active = window.ShadowDOMPolyfill ? | |
| 362 wrap(document.activeElement) : this.shadowRoot.activeElement; | |
| 363 if (active && (active != this) && (active.parentElement != this) && | |
| 364 (document.activeElement != document.body)) { | |
| 365 return; | |
| 366 } | |
| 367 // Unfortunately, Safari does not focus certain form controls via mouse, | |
| 368 // so we also blacklist input, button, & select | |
| 369 // (https://bugs.webkit.org/show_bug.cgi?id=118043) | |
| 370 if ((p[0].localName == 'input') || | |
| 371 (p[0].localName == 'button') || | |
| 372 (p[0].localName == 'select')) { | |
| 373 return; | |
| 374 } | |
| 375 | |
| 376 var model = n.templateInstance && n.templateInstance.model; | |
| 377 if (model) { | |
| 378 var vi = model.index, pi = model.physicalIndex; | |
| 379 var data = this.data[vi], item = this._physicalItems[pi]; | |
| 380 this.$.selection.select(data); | |
| 381 this.asyncFire('core-activate', {data: data, item: item}); | |
| 382 } | |
| 383 }.bind(this)); | |
| 384 }, | |
| 385 | |
| 386 selectedHandler: function(e, detail) { | |
| 387 this.selection = this.$.selection.getSelection(); | |
| 388 var i$ = this.indexesForData(detail.item); | |
| 389 // TODO(sorvell): we should be relying on selection to store the | |
| 390 // selected data but we want to optimize for lookup. | |
| 391 this._selectedData.set(detail.item, detail.isSelected); | |
| 392 if (i$.physical >= 0) { | |
| 393 this.updateItem(i$.virtual, i$.physical); | |
| 394 } | |
| 395 }, | |
| 396 | |
| 397 /** | |
| 398 * Select the list item at the given index. | |
| 399 * | |
| 400 * @method selectItem | |
| 401 * @param {number} index | |
| 402 */ | |
| 403 selectItem: function(index) { | |
| 404 if (!this.selectionEnabled) { | |
| 405 return; | |
| 406 } | |
| 407 var data = this.data[index]; | |
| 408 if (data) { | |
| 409 this.$.selection.select(data); | |
| 410 } | |
| 411 }, | |
| 412 | |
| 413 /** | |
| 414 * Set the selected state of the list item at the given index. | |
| 415 * | |
| 416 * @method setItemSelected | |
| 417 * @param {number} index | |
| 418 * @param {boolean} isSelected | |
| 419 */ | |
| 420 setItemSelected: function(index, isSelected) { | |
| 421 var data = this.data[index]; | |
| 422 if (data) { | |
| 423 this.$.selection.setItemSelected(data, isSelected); | |
| 424 } | |
| 425 }, | |
| 426 | |
| 427 indexesForData: function(data) { | |
| 428 var virtual = this.data.indexOf(data); | |
| 429 var physical = this.virtualToPhysicalIndex(virtual); | |
| 430 return { virtual: virtual, physical: physical }; | |
| 431 }, | |
| 432 | |
| 433 virtualToPhysicalIndex: function(index) { | |
| 434 for (var i=0, l=this._physicalData.length; i<l; i++) { | |
| 435 if (this._physicalData[i].index === index) { | |
| 436 return i; | |
| 437 } | |
| 438 } | |
| 439 return -1; | |
| 440 }, | |
| 441 | |
| 442 /** | |
| 443 * Clears the current selection state of the list. | |
| 444 * | |
| 445 * @method clearSelection | |
| 446 */ | |
| 447 clearSelection: function() { | |
| 448 this._clearSelection(); | |
| 449 this.refresh(true); | |
| 450 }, | |
| 451 | |
| 452 _clearSelection: function() { | |
| 453 this._selectedData = new WeakMap(); | |
| 454 this.$.selection.clear(); | |
| 455 this.selection = this.$.selection.getSelection(); | |
| 456 }, | |
| 457 | |
| 458 scrollToItem: function(index) { | |
| 459 this.scrollTop = index * this.height; | |
| 460 } | |
| 461 | |
| 462 }); | |
| 463 | |
| 464 // determine proper transform mechanizm | |
| 465 if (document.documentElement.style.transform !== undefined) { | |
| 466 var setTransform = function(element, string, value) { | |
| 467 element.style.transform = string; | |
| 468 element._transformValue = value; | |
| 469 } | |
| 470 } else { | |
| 471 var setTransform = function(element, string, value) { | |
| 472 element.style.webkitTransform = string; | |
| 473 element._transformValue = value; | |
| 474 } | |
| 475 } | |
| 476 | |
| 477 })(); | |
| 478 </script> | |
| 479 </polymer-element> | |
| OLD | NEW |