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