list_selection_controller.js revision 21d179b334e59e9a3bfcaed4c4430bef1bc5759d
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 :
141                                       (e.ctrlKey && !e.shiftKey))) {
142          // Selection is handled at mouseUp on windows/linux, mouseDown on mac.
143          if (cr.isMac? isDown : !isDown) {
144            // toggle the current one and make it anchor index
145            sm.setIndexSelected(index, !sm.getIndexSelected(index));
146            sm.leadIndex = index;
147            sm.anchorIndex = index;
148          }
149        } else if (e.shiftKey && anchorIndex != -1 && anchorIndex != index) {
150          // Shift is done in mousedown
151          if (isDown) {
152            sm.unselectAll();
153            sm.leadIndex = index;
154            if (sm.multiple)
155              sm.selectRange(anchorIndex, index);
156            else
157              sm.setIndexSelected(index, true);
158          }
159        } else {
160          // Right click for a context menu need to not clear the selection.
161          var isRightClick = e.button == 2;
162
163          // If the index is selected this is handled in mouseup.
164          var indexSelected = sm.getIndexSelected(index);
165          if ((indexSelected && !isDown || !indexSelected && isDown) &&
166              !(indexSelected && isRightClick)) {
167            sm.unselectAll();
168            sm.setIndexSelected(index, true);
169            sm.leadIndex = index;
170            sm.anchorIndex = index;
171          }
172        }
173      }
174
175      sm.endChange();
176    },
177
178    /**
179     * Called by the view when it recieves a keydown event.
180     * @param {Event} e The keydown event.
181     */
182    handleKeyDown: function(e) {
183      var sm = this.selectionModel;
184      var newIndex = -1;
185      var leadIndex = sm.leadIndex;
186      var prevent = true;
187
188      // Ctrl/Meta+A
189      if (sm.multiple && e.keyCode == 65 &&
190          (cr.isMac && e.metaKey || !cr.isMac && e.ctrlKey)) {
191        sm.selectAll();
192        e.preventDefault();
193        return;
194      }
195
196      // Space
197      if (e.keyCode == 32) {
198        if (leadIndex != -1) {
199          var selected = sm.getIndexSelected(leadIndex);
200          if (e.ctrlKey || !selected) {
201            sm.setIndexSelected(leadIndex, !selected || !sm.multiple);
202            return;
203          }
204        }
205      }
206
207      switch (e.keyIdentifier) {
208        case 'Home':
209          newIndex = this.getFirstIndex();
210          break;
211        case 'End':
212          newIndex = this.getLastIndex();
213          break;
214        case 'Up':
215          newIndex = leadIndex == -1 ?
216              this.getLastIndex() : this.getIndexAbove(leadIndex);
217          break;
218        case 'Down':
219          newIndex = leadIndex == -1 ?
220              this.getFirstIndex() : this.getIndexBelow(leadIndex);
221          break;
222        case 'Left':
223          newIndex = leadIndex == -1 ?
224              this.getLastIndex() : this.getIndexBefore(leadIndex);
225          break;
226        case 'Right':
227          newIndex = leadIndex == -1 ?
228              this.getFirstIndex() : this.getIndexAfter(leadIndex);
229          break;
230        default:
231          prevent = false;
232      }
233
234      if (newIndex != -1) {
235        sm.beginChange();
236
237        sm.leadIndex = newIndex;
238        if (e.shiftKey) {
239          var anchorIndex = sm.anchorIndex;
240          if (sm.multiple)
241            sm.unselectAll();
242          if (anchorIndex == -1) {
243            sm.setIndexSelected(newIndex, true);
244            sm.anchorIndex = newIndex;
245          } else {
246            sm.selectRange(anchorIndex, newIndex);
247          }
248        } else if (e.ctrlKey && !cr.isMac) {
249          // Setting the lead index is done above
250          // Mac does not allow you to change the lead.
251        } else {
252          if (sm.multiple)
253            sm.unselectAll();
254          sm.setIndexSelected(newIndex, true);
255          sm.anchorIndex = newIndex;
256        }
257
258        sm.endChange();
259
260        if (prevent)
261          e.preventDefault();
262      }
263    }
264  };
265
266  return {
267    ListSelectionController: ListSelectionController
268  };
269});
270