list_selection_model.js revision 3345a6884c488ff3a535c2c9acdd33d74b37e311
1// Copyright (c) 2010 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5cr.define('cr.ui', function() {
6  const Event = cr.Event;
7  const EventTarget = cr.EventTarget;
8
9  /**
10   * Creates a new selection model that is to be used with lists.
11   *
12   * @param {number=} opt_length The number items in the selection.
13   *
14   * @constructor
15   * @extends {!cr.EventTarget}
16   */
17  function ListSelectionModel(opt_length) {
18    this.length_ = opt_length || 0;
19    // Even though selectedIndexes_ is really a map we use an array here to get
20    // iteration in the order of the indexes.
21    this.selectedIndexes_ = [];
22  }
23
24  ListSelectionModel.prototype = {
25    __proto__: EventTarget.prototype,
26
27    /**
28     * The number of items in the model.
29     * @type {number}
30     */
31    get length() {
32      return this.length_;
33    },
34
35    /**
36     * @type {!Array} The selected indexes.
37     */
38    get selectedIndexes() {
39      return Object.keys(this.selectedIndexes_).map(Number);
40    },
41    set selectedIndexes(selectedIndexes) {
42      this.beginChange();
43      this.unselectAll();
44      for (var i = 0; i < selectedIndexes.length; i++) {
45        this.setIndexSelected(selectedIndexes[i], true);
46      }
47      if (selectedIndexes.length) {
48        this.leadIndex = this.anchorIndex = selectedIndexes[0];
49      } else {
50        this.leadIndex = this.anchorIndex = -1;
51      }
52      this.endChange();
53    },
54
55    /**
56     * Convenience getter which returns the first selected index.
57     * @type {number}
58     */
59    get selectedIndex() {
60      for (var i in this.selectedIndexes_) {
61        return Number(i);
62      }
63      return -1;
64    },
65    set selectedIndex(selectedIndex) {
66      this.beginChange();
67      this.unselectAll();
68      if (selectedIndex != -1) {
69        this.selectedIndexes = [selectedIndex];
70      } else {
71        this.leadIndex = this.anchorIndex = -1;
72      }
73      this.endChange();
74    },
75
76    /**
77     * Selects a range of indexes, starting with {@code start} and ends with
78     * {@code end}.
79     * @param {number} start The first index to select.
80     * @param {number} end The last index to select.
81     */
82    selectRange: function(start, end) {
83      // Swap if starts comes after end.
84      if (start > end) {
85        var tmp = start;
86        start = end;
87        end = tmp;
88      }
89
90      this.beginChange();
91
92      for (var index = start; index != end; index++) {
93        this.setIndexSelected(index, true);
94      }
95      this.setIndexSelected(end, true);
96
97      this.endChange();
98    },
99
100    /**
101     * Selects all indexes.
102     */
103    selectAll: function() {
104      this.selectRange(0, this.length - 1);
105    },
106
107    /**
108     * Clears the selection
109     */
110    clear: function() {
111      this.beginChange();
112      this.length_ = 0;
113      this.anchorIndex = this.leadIndex = -1;
114      this.unselectAll();
115      this.endChange();
116    },
117
118    /**
119     * Unselects all selected items.
120     */
121    unselectAll: function() {
122      this.beginChange();
123      for (var i in this.selectedIndexes_) {
124        this.setIndexSelected(i, false);
125      }
126      this.endChange();
127    },
128
129    /**
130     * Sets the selected state for an index.
131     * @param {number} index The index to set the selected state for.
132     * @param {boolean} b Whether to select the index or not.
133     */
134    setIndexSelected: function(index, b) {
135      var oldSelected = index in this.selectedIndexes_;
136      if (oldSelected == b)
137        return;
138
139      if (b)
140        this.selectedIndexes_[index] = true;
141      else
142        delete this.selectedIndexes_[index];
143
144      this.beginChange();
145
146      // Changing back?
147      if (index in this.changedIndexes_ && this.changedIndexes_[index] == !b) {
148        delete this.changedIndexes_[index];
149      } else {
150        this.changedIndexes_[index] = b;
151      }
152
153      // End change dispatches an event which in turn may update the view.
154      this.endChange();
155    },
156
157    /**
158     * Whether a given index is selected or not.
159     * @param {number} index The index to check.
160     * @return {boolean} Whether an index is selected.
161     */
162    getIndexSelected: function(index) {
163      return index in this.selectedIndexes_;
164    },
165
166    /**
167     * This is used to begin batching changes. Call {@code endChange} when you
168     * are done making changes.
169     */
170    beginChange: function() {
171      if (!this.changeCount_) {
172        this.changeCount_ = 0;
173        this.changedIndexes_ = {};
174      }
175      this.changeCount_++;
176    },
177
178    /**
179     * Call this after changes are done and it will dispatch a change event if
180     * any changes were actually done.
181     */
182    endChange: function() {
183      this.changeCount_--;
184      if (!this.changeCount_) {
185        var indexes = Object.keys(this.changedIndexes_);
186        if (indexes.length) {
187          var e = new Event('change');
188          e.changes = indexes.map(function(index) {
189            return {
190              index: index,
191              selected: this.changedIndexes_[index]
192            };
193          }, this);
194          this.dispatchEvent(e);
195        }
196        this.changedIndexes_ = {};
197      }
198    },
199
200    leadIndex_: -1,
201
202    /**
203     * The leadIndex is used with multiple selection and it is the index that
204     * the user is moving using the arrow keys.
205     * @type {number}
206     */
207    get leadIndex() {
208      return this.leadIndex_;
209    },
210    set leadIndex(leadIndex) {
211      var li = Math.max(-1, Math.min(this.length_ - 1, leadIndex));
212      if (li != this.leadIndex_) {
213        var oldLeadIndex = this.leadIndex_;
214        this.leadIndex_ = li;
215        cr.dispatchPropertyChange(this, 'leadIndex', li, oldLeadIndex);
216      }
217    },
218
219    anchorIndex_: -1,
220
221    /**
222     * The anchorIndex is used with multiple selection.
223     * @type {number}
224     */
225    get anchorIndex() {
226      return this.anchorIndex_;
227    },
228    set anchorIndex(anchorIndex) {
229      var ai = Math.max(-1, Math.min(this.length_ - 1, anchorIndex));
230      if (ai != this.anchorIndex_) {
231        var oldAnchorIndex = this.anchorIndex_;
232        this.anchorIndex_ = ai;
233        cr.dispatchPropertyChange(this, 'anchorIndex', ai, oldAnchorIndex);
234      }
235    },
236
237    /**
238     * Whether the selection model supports multiple selected items.
239     * @type {boolean}
240     */
241    get multiple() {
242      return true;
243    },
244
245    /**
246     * Adjust the selection by adding or removing a certain numbers of items.
247     * This should be called by the owner of the selection model as items are
248     * added and removed from the underlying data model.
249     * @param {number} index The index of the first change.
250     * @param {number} itemsRemoved Number of items removed.
251     * @param {number} itemsAdded Number of items added.
252     */
253    adjust: function(index, itemsRemoved, itemsAdded) {
254      function getNewAdjustedIndex(i) {
255        if (i >= index && i < index + itemsRemoved) {
256          return index
257        } else if (i >= index) {
258          return i - itemsRemoved + itemsAdded;
259        }
260        return i;
261      }
262
263      this.length_ += itemsAdded - itemsRemoved;
264
265      var newMap = [];
266      for (var i in this.selectedIndexes_) {
267        i = Number(i);
268        if (i < index) {
269          newMap[i] = true;
270        } else if (i < index + itemsRemoved) {
271          // noop
272        } else {
273          newMap[i + itemsAdded - itemsRemoved] = true;
274        }
275      }
276      this.selectedIndexes_ = newMap;
277
278      this.leadIndex = getNewAdjustedIndex(this.leadIndex);
279      this.anchorIndex = getNewAdjustedIndex(this.anchorIndex);
280    }
281  };
282
283  return {
284    ListSelectionModel: ListSelectionModel
285  };
286});
287