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