| OLD | NEW |
| (Empty) | |
| 1 <!-- |
| 2 @license |
| 3 Copyright (c) 2017 The Polymer Project Authors. All rights reserved. |
| 4 This code may only be used under the BSD style license found at http://polymer.g
ithub.io/LICENSE.txt |
| 5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt |
| 6 The complete set of contributors may be found at http://polymer.github.io/CONTRI
BUTORS.txt |
| 7 Code distributed by Google as part of the polymer project is also |
| 8 subject to an additional IP rights grant found at http://polymer.github.io/PATEN
TS.txt |
| 9 --> |
| 10 |
| 11 <link rel="import" href="../../polymer-element.html"> |
| 12 <link rel="import" href="../utils/mixin.html"> |
| 13 <link rel="import" href="../utils/array-splice.html"> |
| 14 |
| 15 <script> |
| 16 (function() { |
| 17 'use strict'; |
| 18 |
| 19 /** |
| 20 * Element mixin for recording dynamic associations between item paths in a |
| 21 * master `items` array and a `selected` array such that path changes to the |
| 22 * master array (at the host) element or elsewhere via data-binding) are |
| 23 * correctly propagated to items in the selected array and vice-versa. |
| 24 * |
| 25 * The `items` property accepts an array of user data, and via the |
| 26 * `select(item)` and `deselect(item)` API, updates the `selected` property |
| 27 * which may be bound to other parts of the application, and any changes to |
| 28 * sub-fields of `selected` item(s) will be kept in sync with items in the |
| 29 * `items` array. When `multi` is false, `selected` is a property |
| 30 * representing the last selected item. When `multi` is true, `selected` |
| 31 * is an array of multiply selected items. |
| 32 * |
| 33 * @polymerMixin |
| 34 * @memberof Polymer |
| 35 */ |
| 36 let ArraySelectorMixin = Polymer.dedupingMixin(superClass => { |
| 37 |
| 38 /** |
| 39 * @polymerMixinClass |
| 40 * @implements {Polymer_ArraySelectorMixin} |
| 41 */ |
| 42 class ArraySelectorMixin extends superClass { |
| 43 |
| 44 static get properties() { |
| 45 |
| 46 return { |
| 47 |
| 48 /** |
| 49 * An array containing items from which selection will be made. |
| 50 */ |
| 51 items: { |
| 52 type: Array, |
| 53 }, |
| 54 |
| 55 /** |
| 56 * When `true`, multiple items may be selected at once (in this case, |
| 57 * `selected` is an array of currently selected items). When `false`, |
| 58 * only one item may be selected at a time. |
| 59 */ |
| 60 multi: { |
| 61 type: Boolean, |
| 62 value: false, |
| 63 }, |
| 64 |
| 65 /** |
| 66 * When `multi` is true, this is an array that contains any selected. |
| 67 * When `multi` is false, this is the currently selected item, or `nul
l` |
| 68 * if no item is selected. |
| 69 */ |
| 70 selected: { |
| 71 type: Object, |
| 72 notify: true |
| 73 }, |
| 74 |
| 75 /** |
| 76 * When `multi` is false, this is the currently selected item, or `nul
l` |
| 77 * if no item is selected. |
| 78 */ |
| 79 selectedItem: { |
| 80 type: Object, |
| 81 notify: true |
| 82 }, |
| 83 |
| 84 /** |
| 85 * When `true`, calling `select` on an item that is already selected |
| 86 * will deselect the item. |
| 87 */ |
| 88 toggle: { |
| 89 type: Boolean, |
| 90 value: false |
| 91 } |
| 92 |
| 93 } |
| 94 } |
| 95 |
| 96 static get observers() { |
| 97 return ['__updateSelection(multi, items.*)'] |
| 98 } |
| 99 |
| 100 constructor() { |
| 101 super(); |
| 102 this.__lastItems = null; |
| 103 this.__lastMulti = null; |
| 104 this.__selectedMap = null; |
| 105 } |
| 106 |
| 107 __updateSelection(multi, itemsInfo) { |
| 108 let path = itemsInfo.path; |
| 109 if (path == 'items') { |
| 110 // Case 1 - items array changed, so diff against previous array and |
| 111 // deselect any removed items and adjust selected indices |
| 112 let newItems = itemsInfo.base || []; |
| 113 let lastItems = this.__lastItems; |
| 114 let lastMulti = this.__lastMulti; |
| 115 if (multi !== lastMulti) { |
| 116 this.clearSelection(); |
| 117 } |
| 118 if (lastItems) { |
| 119 let splices = Polymer.ArraySplice.calculateSplices(newItems, lastIte
ms); |
| 120 this.__applySplices(splices); |
| 121 } |
| 122 this.__lastItems = newItems; |
| 123 this.__lastMulti = multi; |
| 124 } else if (itemsInfo.path == 'items.splices') { |
| 125 // Case 2 - got specific splice information describing the array mutat
ion: |
| 126 // deselect any removed items and adjust selected indices |
| 127 this.__applySplices(itemsInfo.value.indexSplices); |
| 128 } else { |
| 129 // Case 3 - an array element was changed, so deselect the previous |
| 130 // item for that index if it was previously selected |
| 131 let part = path.slice('items.'.length); |
| 132 let idx = parseInt(part, 10); |
| 133 if ((part.indexOf('.') < 0) && part == idx) { |
| 134 this.__deselectChangedIdx(idx); |
| 135 } |
| 136 } |
| 137 } |
| 138 |
| 139 __applySplices(splices) { |
| 140 let selected = this.__selectedMap; |
| 141 // Adjust selected indices and mark removals |
| 142 for (let i=0; i<splices.length; i++) { |
| 143 let s = splices[i]; |
| 144 selected.forEach((idx, item) => { |
| 145 if (idx < s.index) { |
| 146 // no change |
| 147 } else if (idx >= s.index + s.removed.length) { |
| 148 // adjust index |
| 149 selected.set(item, idx + s.addedCount - s.removed.length); |
| 150 } else { |
| 151 // remove index |
| 152 selected.set(item, -1); |
| 153 } |
| 154 }); |
| 155 for (let j=0; j<s.addedCount; j++) { |
| 156 let idx = s.index + j; |
| 157 if (selected.has(this.items[idx])) { |
| 158 selected.set(this.items[idx], idx); |
| 159 } |
| 160 } |
| 161 } |
| 162 // Update linked paths |
| 163 this.__updateLinks(); |
| 164 // Remove selected items that were removed from the items array |
| 165 let sidx = 0; |
| 166 selected.forEach((idx, item) => { |
| 167 if (idx < 0) { |
| 168 if (this.multi) { |
| 169 this.splice('selected', sidx, 1); |
| 170 } else { |
| 171 this.selected = this.selectedItem = null; |
| 172 } |
| 173 selected.delete(item); |
| 174 } else { |
| 175 sidx++; |
| 176 } |
| 177 }); |
| 178 } |
| 179 |
| 180 __updateLinks() { |
| 181 this.__dataLinkedPaths = {}; |
| 182 if (this.multi) { |
| 183 let sidx = 0; |
| 184 this.__selectedMap.forEach(idx => { |
| 185 if (idx >= 0) { |
| 186 this.linkPaths('items.' + idx, 'selected.' + sidx++); |
| 187 } |
| 188 }); |
| 189 } else { |
| 190 this.__selectedMap.forEach(idx => { |
| 191 this.linkPaths('selected', 'items.' + idx); |
| 192 this.linkPaths('selectedItem', 'items.' + idx); |
| 193 }); |
| 194 } |
| 195 } |
| 196 |
| 197 /** |
| 198 * Clears the selection state. |
| 199 * |
| 200 */ |
| 201 clearSelection() { |
| 202 // Unbind previous selection |
| 203 this.__dataLinkedPaths = {}; |
| 204 // The selected map stores 3 pieces of information: |
| 205 // key: items array object |
| 206 // value: items array index |
| 207 // order: selected array index |
| 208 this.__selectedMap = new Map(); |
| 209 // Initialize selection |
| 210 this.selected = this.multi ? [] : null |
| 211 this.selectedItem = null; |
| 212 } |
| 213 |
| 214 /** |
| 215 * Returns whether the item is currently selected. |
| 216 * |
| 217 * @param {*} item Item from `items` array to test |
| 218 * @return {boolean} Whether the item is selected |
| 219 */ |
| 220 isSelected(item) { |
| 221 return this.__selectedMap.has(item); |
| 222 } |
| 223 |
| 224 /** |
| 225 * Returns whether the item is currently selected. |
| 226 * |
| 227 * @param {*} idx Index from `items` array to test |
| 228 * @return {boolean} Whether the item is selected |
| 229 */ |
| 230 isIndexSelected(idx) { |
| 231 return this.isSelected(this.items[idx]); |
| 232 } |
| 233 |
| 234 __deselectChangedIdx(idx) { |
| 235 let sidx = this.__selectedIndexForItemIndex(idx); |
| 236 if (sidx >= 0) { |
| 237 let i = 0; |
| 238 this.__selectedMap.forEach((idx, item) => { |
| 239 if (sidx == i++) { |
| 240 this.deselect(item); |
| 241 } |
| 242 }); |
| 243 } |
| 244 } |
| 245 |
| 246 __selectedIndexForItemIndex(idx) { |
| 247 let selected = this.__dataLinkedPaths['items.' + idx]; |
| 248 if (selected) { |
| 249 return parseInt(selected.slice('selected.'.length), 10); |
| 250 } |
| 251 } |
| 252 |
| 253 /** |
| 254 * Deselects the given item if it is already selected. |
| 255 * |
| 256 * @param {*} item Item from `items` array to deselect |
| 257 */ |
| 258 deselect(item) { |
| 259 let idx = this.__selectedMap.get(item); |
| 260 if (idx >= 0) { |
| 261 this.__selectedMap.delete(item); |
| 262 let sidx; |
| 263 if (this.multi) { |
| 264 sidx = this.__selectedIndexForItemIndex(idx); |
| 265 } |
| 266 this.__updateLinks(); |
| 267 if (this.multi) { |
| 268 this.splice('selected', sidx, 1); |
| 269 } else { |
| 270 this.selected = this.selectedItem = null; |
| 271 } |
| 272 } |
| 273 } |
| 274 |
| 275 /** |
| 276 * Deselects the given index if it is already selected. |
| 277 * |
| 278 * @param {number} idx Index from `items` array to deselect |
| 279 */ |
| 280 deselectIndex(idx) { |
| 281 this.deselect(this.items[idx]); |
| 282 } |
| 283 |
| 284 /** |
| 285 * Selects the given item. When `toggle` is true, this will automatically |
| 286 * deselect the item if already selected. |
| 287 * |
| 288 * @param {*} item Item from `items` array to select |
| 289 */ |
| 290 select(item) { |
| 291 this.selectIndex(this.items.indexOf(item)); |
| 292 } |
| 293 |
| 294 /** |
| 295 * Selects the given index. When `toggle` is true, this will automaticall
y |
| 296 * deselect the item if already selected. |
| 297 * |
| 298 * @param {number} idx Index from `items` array to select |
| 299 */ |
| 300 selectIndex(idx) { |
| 301 let item = this.items[idx]; |
| 302 if (!this.isSelected(item)) { |
| 303 if (!this.multi) { |
| 304 this.__selectedMap.clear(); |
| 305 } |
| 306 this.__selectedMap.set(item, idx); |
| 307 this.__updateLinks(); |
| 308 if (this.multi) { |
| 309 this.push('selected', item); |
| 310 } else { |
| 311 this.selected = this.selectedItem = item; |
| 312 } |
| 313 } else if (this.toggle) { |
| 314 this.deselectIndex(idx); |
| 315 } |
| 316 } |
| 317 |
| 318 } |
| 319 |
| 320 return ArraySelectorMixin; |
| 321 |
| 322 }); |
| 323 |
| 324 // export mixin |
| 325 Polymer.ArraySelectorMixin = ArraySelectorMixin; |
| 326 |
| 327 /** |
| 328 * @constructor |
| 329 * @extends {Polymer.Element} |
| 330 * @implements {Polymer_ArraySelectorMixin} |
| 331 */ |
| 332 let baseArraySelector = ArraySelectorMixin(Polymer.Element); |
| 333 |
| 334 /** |
| 335 * Element implementing the `Polymer.ArraySelector` mixin, which records |
| 336 * dynamic associations between item paths in a master `items` array and a |
| 337 * `selected` array such that path changes to the master array (at the host) |
| 338 * element or elsewhere via data-binding) are correctly propagated to items |
| 339 * in the selected array and vice-versa. |
| 340 * |
| 341 * The `items` property accepts an array of user data, and via the |
| 342 * `select(item)` and `deselect(item)` API, updates the `selected` property |
| 343 * which may be bound to other parts of the application, and any changes to |
| 344 * sub-fields of `selected` item(s) will be kept in sync with items in the |
| 345 * `items` array. When `multi` is false, `selected` is a property |
| 346 * representing the last selected item. When `multi` is true, `selected` |
| 347 * is an array of multiply selected items. |
| 348 * |
| 349 * Example: |
| 350 * |
| 351 * ```html |
| 352 * <dom-module id="employee-list"> |
| 353 * |
| 354 * <template> |
| 355 * |
| 356 * <div> Employee list: </div> |
| 357 * <template is="dom-repeat" id="employeeList" items="{{employees}}"> |
| 358 * <div>First name: <span>{{item.first}}</span></div> |
| 359 * <div>Last name: <span>{{item.last}}</span></div> |
| 360 * <button on-click="toggleSelection">Select</button> |
| 361 * </template> |
| 362 * |
| 363 * <array-selector id="selector" items="{{employees}}" selected="{{selecte
d}}" multi toggle></array-selector> |
| 364 * |
| 365 * <div> Selected employees: </div> |
| 366 * <template is="dom-repeat" items="{{selected}}"> |
| 367 * <div>First name: <span>{{item.first}}</span></div> |
| 368 * <div>Last name: <span>{{item.last}}</span></div> |
| 369 * </template> |
| 370 * |
| 371 * </template> |
| 372 * |
| 373 * </dom-module> |
| 374 * ``` |
| 375 * |
| 376 * ```js |
| 377 * Polymer({ |
| 378 * is: 'employee-list', |
| 379 * ready() { |
| 380 * this.employees = [ |
| 381 * {first: 'Bob', last: 'Smith'}, |
| 382 * {first: 'Sally', last: 'Johnson'}, |
| 383 * ... |
| 384 * ]; |
| 385 * }, |
| 386 * toggleSelection(e) { |
| 387 * let item = this.$.employeeList.itemForElement(e.target); |
| 388 * this.$.selector.select(item); |
| 389 * } |
| 390 * }); |
| 391 * ``` |
| 392 * |
| 393 * @polymerElement |
| 394 * @extends Polymer.Element |
| 395 * @mixes Polymer.ArraySelectorMixin |
| 396 * @memberof Polymer |
| 397 * @summary Custom element that links paths between an input `items` array and |
| 398 * an output `selected` item or array based on calls to its selection API. |
| 399 */ |
| 400 class ArraySelector extends baseArraySelector { |
| 401 // Not needed to find template; can be removed once the analyzer |
| 402 // can find the tag name from customElements.define call |
| 403 static get is() { return 'array-selector' } |
| 404 } |
| 405 customElements.define(ArraySelector.is, ArraySelector); |
| 406 Polymer.ArraySelector = ArraySelector; |
| 407 |
| 408 })(); |
| 409 |
| 410 </script> |
| OLD | NEW |