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 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.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 receives a keydown event.
180     * @param {Event} e The keydown event.
181     */
182    handleKeyDown: function(e) {
183      const SPACE_KEY_CODE = 32;
184      var tagName = e.target.tagName;
185      // If focus is in an input field of some kind, only handle navigation keys
186      // that aren't likely to conflict with input interaction (e.g., text
187      // editing, or changing the value of a checkbox or select).
188      if (tagName == 'INPUT') {
189        var inputType = e.target.type;
190        // Just protect space (for toggling) for checkbox and radio.
191        if (inputType == 'checkbox' || inputType == 'radio') {
192          if (e.keyCode == SPACE_KEY_CODE)
193            return;
194        // Protect all but the most basic navigation commands in anything else.
195        } else if (e.keyIdentifier != 'Up' && e.keyIdentifier != 'Down') {
196          return;
197        }
198      }
199      // Similarly, don't interfere with select element handling.
200      if (tagName == 'SELECT')
201        return;
202
203      var sm = this.selectionModel;
204      var newIndex = -1;
205      var leadIndex = sm.leadIndex;
206      var prevent = true;
207
208      // Ctrl/Meta+A
209      if (sm.multiple && e.keyCode == 65 &&
210          (cr.isMac && e.metaKey || !cr.isMac && e.ctrlKey)) {
211        sm.selectAll();
212        e.preventDefault();
213        return;
214      }
215
216      // Space
217      if (e.keyCode == SPACE_KEY_CODE) {
218        if (leadIndex != -1) {
219          var selected = sm.getIndexSelected(leadIndex);
220          if (e.ctrlKey || !selected) {
221            sm.setIndexSelected(leadIndex, !selected || !sm.multiple);
222            return;
223          }
224        }
225      }
226
227      switch (e.keyIdentifier) {
228        case 'Home':
229          newIndex = this.getFirstIndex();
230          break;
231        case 'End':
232          newIndex = this.getLastIndex();
233          break;
234        case 'Up':
235          newIndex = leadIndex == -1 ?
236              this.getLastIndex() : this.getIndexAbove(leadIndex);
237          break;
238        case 'Down':
239          newIndex = leadIndex == -1 ?
240              this.getFirstIndex() : this.getIndexBelow(leadIndex);
241          break;
242        case 'Left':
243          newIndex = leadIndex == -1 ?
244              this.getLastIndex() : this.getIndexBefore(leadIndex);
245          break;
246        case 'Right':
247          newIndex = leadIndex == -1 ?
248              this.getFirstIndex() : this.getIndexAfter(leadIndex);
249          break;
250        default:
251          prevent = false;
252      }
253
254      if (newIndex != -1) {
255        sm.beginChange();
256
257        sm.leadIndex = newIndex;
258        if (e.shiftKey) {
259          var anchorIndex = sm.anchorIndex;
260          if (sm.multiple)
261            sm.unselectAll();
262          if (anchorIndex == -1) {
263            sm.setIndexSelected(newIndex, true);
264            sm.anchorIndex = newIndex;
265          } else {
266            sm.selectRange(anchorIndex, newIndex);
267          }
268        } else if (e.ctrlKey && !cr.isMac) {
269          // Setting the lead index is done above.
270          // Mac does not allow you to change the lead.
271        } else {
272          if (sm.multiple)
273            sm.unselectAll();
274          sm.setIndexSelected(newIndex, true);
275          sm.anchorIndex = newIndex;
276        }
277
278        sm.endChange();
279
280        if (prevent)
281          e.preventDefault();
282      }
283    }
284  };
285
286  return {
287    ListSelectionController: ListSelectionController
288  };
289});
290