list_selection_controller.js revision bda42a81ee5f9b20d2bebedcf0bbef1e30e5b293
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  /**
7   * Creates a selection controller that is to be used with lists. This is
8   * implemented for vertical lists but changing the behavior for horizontal
9   * lists or icon views is a matter of overriding {@code getIndexBefore},
10   * {@code getIndexAfter}, {@code getIndexAbove} as well as
11   * {@code getIndexBelow}.
12   *
13   * @param {cr.ui.ListSelectionModel} selectionModel The selection model to
14   *     interact with.
15   *
16   * @constructor
17   * @extends {!cr.EventTarget}
18   */
19  function ListSelectionController(selectionModel) {
20    this.selectionModel_ = selectionModel;
21  }
22
23  ListSelectionController.prototype = {
24
25    /**
26     * The selection model we are interacting with.
27     * @type {cr.ui.ListSelectionModel}
28     */
29    get selectionModel() {
30      return this.selectionModel_;
31    },
32
33    /**
34     * Returns the index below (y axis) the given element.
35     * @param {number} index The index to get the index below.
36     * @return {number} The index below or -1 if not found.
37     */
38    getIndexBelow: function(index) {
39      if (index == this.getLastIndex())
40        return -1;
41      return index + 1;
42    },
43
44    /**
45     * Returns the index above (y axis) the given element.
46     * @param {number} index The index to get the index above.
47     * @return {number} The index below or -1 if not found.
48     */
49    getIndexAbove: function(index) {
50      return index - 1;
51    },
52
53    /**
54     * Returns the index before (x axis) the given element. This returns -1
55     * by default but override this for icon view and horizontal selection
56     * models.
57     *
58     * @param {number} index The index to get the index before.
59     * @return {number} The index before or -1 if not found.
60     */
61    getIndexBefore: function(index) {
62      return -1;
63    },
64
65    /**
66     * Returns the index after (x axis) the given element. This returns -1
67     * by default but override this for icon view and horizontal selection
68     * models.
69     *
70     * @param {number} index The index to get the index after.
71     * @return {number} The index after or -1 if not found.
72     */
73    getIndexAfter: function(index) {
74      return -1;
75    },
76
77    /**
78     * Returns the next list index. This is the next logical and should not
79     * depend on any kind of layout of the list.
80     * @param {number} index The index to get the next index for.
81     * @return {number} The next index or -1 if not found.
82     */
83    getNextIndex: function(index) {
84      if (index == this.getLastIndex())
85        return -1;
86      return index + 1;
87    },
88
89    /**
90     * Returns the prevous list index. This is the previous logical and should
91     * not depend on any kind of layout of the list.
92     * @param {number} index The index to get the previous index for.
93     * @return {number} The previous index or -1 if not found.
94     */
95    getPreviousIndex: function(index) {
96      return index - 1;
97    },
98
99    /**
100     * @return {number} The first index.
101     */
102    getFirstIndex: function() {
103      return 0;
104    },
105
106    /**
107     * @return {number} The last index.
108     */
109    getLastIndex: function() {
110      return this.selectionModel.length - 1;
111    },
112
113    /**
114     * Called by the view when the user does a mousedown or mouseup on the list.
115     * @param {!Event} e The browser mousedown event.
116     * @param {number} index The index that was under the mouse pointer, -1 if
117     *     none.
118     */
119    handleMouseDownUp: function(e, index) {
120      var sm = this.selectionModel;
121      var anchorIndex = sm.anchorIndex;
122      var isDown = e.type == 'mousedown';
123
124      sm.beginChange();
125
126      if (index == -1) {
127        // On Mac we always clear the selection if the user clicks a blank area.
128        // On Windows, we only clear the selection if neither Shift nor Ctrl are
129        // pressed.
130        if (cr.isMac) {
131          sm.leadIndex = sm.anchorIndex = -1;
132          if (sm.multiple)
133            sm.unselectAll();
134        } else if (!isDown && !e.shiftKey && !e.ctrlKey)
135          // Keep anchor and lead indexes. Note that this is intentionally
136          // different than on the Mac.
137          if (sm.multiple)
138            sm.unselectAll();
139      } else {
140        if (sm.multiple && (cr.isMac ? e.metaKey : e.ctrlKey)) {
141          // Selection is handled at mouseUp on windows/linux, mouseDown on mac.
142          if (cr.isMac? isDown : !isDown) {
143            // toggle the current one and make it anchor index
144            sm.setIndexSelected(index, !sm.getIndexSelected(index));
145            sm.leadIndex = index;
146            sm.anchorIndex = index;
147          }
148        } else if (e.shiftKey && anchorIndex != -1 && anchorIndex != index) {
149          // Shift is done in mousedown
150          if (isDown) {
151            sm.unselectAll();
152            sm.leadIndex = index;
153            if (sm.multiple)
154              sm.selectRange(anchorIndex, index);
155            else
156              sm.setIndexSelected(index, true);
157          }
158        } else {
159          // Right click for a context menu need to not clear the selection.
160          var isRightClick = e.button == 2;
161
162          // If the index is selected this is handled in mouseup.
163          var indexSelected = sm.getIndexSelected(index);
164          if ((indexSelected && !isDown || !indexSelected && isDown) &&
165              !(indexSelected && isRightClick)) {
166            sm.unselectAll();
167            sm.setIndexSelected(index, true);
168            sm.leadIndex = index;
169            sm.anchorIndex = index;
170          }
171        }
172      }
173
174      sm.endChange();
175    },
176
177    /**
178     * Called by the view when it recieves a keydown event.
179     * @param {Event} e The keydown event.
180     */
181    handleKeyDown: function(e) {
182      var sm = this.selectionModel;
183      var newIndex = -1;
184      var leadIndex = sm.leadIndex;
185      var prevent = true;
186
187      // Ctrl/Meta+A
188      if (sm.multiple && e.keyCode == 65 &&
189          (cr.isMac && e.metaKey || !cr.isMac && e.ctrlKey)) {
190        sm.selectAll();
191        e.preventDefault();
192        return;
193      }
194
195      // Space
196      if (e.keyCode == 32) {
197        if (leadIndex != -1) {
198          var selected = sm.getIndexSelected(leadIndex);
199          if (e.ctrlKey || !selected) {
200            sm.setIndexSelected(leadIndex, !selected || !sm.multiple);
201            return;
202          }
203        }
204      }
205
206      switch (e.keyIdentifier) {
207        case 'Home':
208          newIndex = this.getFirstIndex();
209          break;
210        case 'End':
211          newIndex = this.getLastIndex();
212          break;
213        case 'Up':
214          newIndex = leadIndex == -1 ?
215              this.getLastIndex() : this.getIndexAbove(leadIndex);
216          break;
217        case 'Down':
218          newIndex = leadIndex == -1 ?
219              this.getFirstIndex() : this.getIndexBelow(leadIndex);
220          break;
221        case 'Left':
222          newIndex = leadIndex == -1 ?
223              this.getLastIndex() : this.getIndexBefore(leadIndex);
224          break;
225        case 'Right':
226          newIndex = leadIndex == -1 ?
227              this.getFirstIndex() : this.getIndexAfter(leadIndex);
228          break;
229        default:
230          prevent = false;
231      }
232
233      if (newIndex != -1) {
234        sm.beginChange();
235
236        sm.leadIndex = newIndex;
237        if (e.shiftKey) {
238          var anchorIndex = sm.anchorIndex;
239          if (sm.multiple)
240            sm.unselectAll();
241          if (anchorIndex == -1) {
242            sm.setIndexSelected(newIndex, true);
243            sm.anchorIndex = newIndex;
244          } else {
245            sm.selectRange(anchorIndex, newIndex);
246          }
247        } else if (e.ctrlKey && !cr.isMac) {
248          // Setting the lead index is done above
249          // Mac does not allow you to change the lead.
250        } else {
251          if (sm.multiple)
252            sm.unselectAll();
253          sm.setIndexSelected(newIndex, true);
254          sm.anchorIndex = newIndex;
255        }
256
257        sm.endChange();
258
259        if (prevent)
260          e.preventDefault();
261      }
262    }
263  };
264
265  return {
266    ListSelectionController: ListSelectionController
267  };
268});
269