• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2012 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
115     * list.
116     * @param {!Event} e The browser mouse event.
117     * @param {number} index The index that was under the mouse pointer, -1 if
118     *     none.
119     */
120    handlePointerDownUp: function(e, index) {
121      var sm = this.selectionModel;
122      var anchorIndex = sm.anchorIndex;
123      var isDown = (e.type == 'mousedown');
124
125      sm.beginChange();
126
127      if (index == -1) {
128        // On Mac we always clear the selection if the user clicks a blank area.
129        // On Windows, we only clear the selection if neither Shift nor Ctrl are
130        // pressed.
131        if (cr.isMac || cr.isChromeOS) {
132          sm.leadIndex = sm.anchorIndex = -1;
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 needs 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.selectedIndex = index;
168          }
169        }
170      }
171
172      sm.endChange();
173    },
174
175    /**
176     * Called by the view when it receives a keydown event.
177     * @param {Event} e The keydown event.
178     */
179    handleKeyDown: function(e) {
180      var SPACE_KEY_CODE = 32;
181      var tagName = e.target.tagName;
182      // If focus is in an input field of some kind, only handle navigation keys
183      // that aren't likely to conflict with input interaction (e.g., text
184      // editing, or changing the value of a checkbox or select).
185      if (tagName == 'INPUT') {
186        var inputType = e.target.type;
187        // Just protect space (for toggling) for checkbox and radio.
188        if (inputType == 'checkbox' || inputType == 'radio') {
189          if (e.keyCode == SPACE_KEY_CODE)
190            return;
191        // Protect all but the most basic navigation commands in anything else.
192        } else if (e.keyIdentifier != 'Up' && e.keyIdentifier != 'Down') {
193          return;
194        }
195      }
196      // Similarly, don't interfere with select element handling.
197      if (tagName == 'SELECT')
198        return;
199
200      var sm = this.selectionModel;
201      var newIndex = -1;
202      var leadIndex = sm.leadIndex;
203      var prevent = true;
204
205      // Ctrl/Meta+A
206      if (sm.multiple && e.keyCode == 65 &&
207          (cr.isMac && e.metaKey || !cr.isMac && e.ctrlKey)) {
208        sm.selectAll();
209        e.preventDefault();
210        return;
211      }
212
213      // Space
214      if (e.keyCode == SPACE_KEY_CODE) {
215        if (leadIndex != -1) {
216          var selected = sm.getIndexSelected(leadIndex);
217          if (e.ctrlKey || !selected) {
218            sm.setIndexSelected(leadIndex, !selected || !sm.multiple);
219            return;
220          }
221        }
222      }
223
224      switch (e.keyIdentifier) {
225        case 'Home':
226          newIndex = this.getFirstIndex();
227          break;
228        case 'End':
229          newIndex = this.getLastIndex();
230          break;
231        case 'Up':
232          newIndex = leadIndex == -1 ?
233              this.getLastIndex() : this.getIndexAbove(leadIndex);
234          break;
235        case 'Down':
236          newIndex = leadIndex == -1 ?
237              this.getFirstIndex() : this.getIndexBelow(leadIndex);
238          break;
239        case 'Left':
240        case 'MediaPreviousTrack':
241          newIndex = leadIndex == -1 ?
242              this.getLastIndex() : this.getIndexBefore(leadIndex);
243          break;
244        case 'Right':
245        case 'MediaNextTrack':
246          newIndex = leadIndex == -1 ?
247              this.getFirstIndex() : this.getIndexAfter(leadIndex);
248          break;
249        default:
250          prevent = false;
251      }
252
253      if (newIndex != -1) {
254        sm.beginChange();
255
256        sm.leadIndex = newIndex;
257        if (e.shiftKey) {
258          var anchorIndex = sm.anchorIndex;
259          if (sm.multiple)
260            sm.unselectAll();
261          if (anchorIndex == -1) {
262            sm.setIndexSelected(newIndex, true);
263            sm.anchorIndex = newIndex;
264          } else {
265            sm.selectRange(anchorIndex, newIndex);
266          }
267        } else if (e.ctrlKey && !cr.isMac && !cr.isChromeOS) {
268          // Setting the lead index is done above.
269          // Mac does not allow you to change the lead.
270        } else {
271          if (sm.multiple)
272            sm.unselectAll();
273          sm.setIndexSelected(newIndex, true);
274          sm.anchorIndex = newIndex;
275        }
276
277        sm.endChange();
278
279        if (prevent)
280          e.preventDefault();
281      }
282    }
283  };
284
285  return {
286    ListSelectionController: ListSelectionController
287  };
288});
289