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 5 /** 6 * @fileoverview Network drop-down implementation. 7 */ 8 9 cr.define('cr.ui', function() { 10 /** 11 * Whether keyboard flow is in use. When setting to true, up/down arrow key 12 * will be used to move focus instead of opening the drop down. 13 */ 14 var useKeyboardFlow = false; 15 16 /** 17 * Creates a new container for the drop down menu items. 18 * @constructor 19 * @extends {HTMLDivElement} 20 */ 21 var DropDownContainer = cr.ui.define('div'); 22 23 DropDownContainer.prototype = { 24 __proto__: HTMLDivElement.prototype, 25 26 /** @override */ 27 decorate: function() { 28 this.classList.add('dropdown-container'); 29 // Selected item in the menu list. 30 this.selectedItem = null; 31 // First item which could be selected. 32 this.firstItem = null; 33 this.setAttribute('role', 'menu'); 34 // Whether scroll has just happened. 35 this.scrollJustHappened = false; 36 }, 37 38 /** 39 * Gets scroll action to be done for the item. 40 * @param {!Object} item Menu item. 41 * @return {integer} -1 for scroll up; 0 for no action; 1 for scroll down. 42 */ 43 scrollAction: function(item) { 44 var thisTop = this.scrollTop; 45 var thisBottom = thisTop + this.offsetHeight; 46 var itemTop = item.offsetTop; 47 var itemBottom = itemTop + item.offsetHeight; 48 if (itemTop <= thisTop) return -1; 49 if (itemBottom >= thisBottom) return 1; 50 return 0; 51 }, 52 53 /** 54 * Selects new item. 55 * @param {!Object} selectedItem Item to be selected. 56 * @param {boolean} mouseOver Is mouseover event triggered? 57 */ 58 selectItem: function(selectedItem, mouseOver) { 59 if (mouseOver && this.scrollJustHappened) { 60 this.scrollJustHappened = false; 61 return; 62 } 63 if (this.selectedItem) 64 this.selectedItem.classList.remove('hover'); 65 selectedItem.classList.add('hover'); 66 this.selectedItem = selectedItem; 67 if (!this.hidden) { 68 this.previousSibling.setAttribute( 69 'aria-activedescendant', selectedItem.id); 70 } 71 var action = this.scrollAction(selectedItem); 72 if (action != 0) { 73 selectedItem.scrollIntoView(action < 0); 74 this.scrollJustHappened = true; 75 } 76 } 77 }; 78 79 /** 80 * Creates a new DropDown div. 81 * @constructor 82 * @extends {HTMLDivElement} 83 */ 84 var DropDown = cr.ui.define('div'); 85 86 DropDown.ITEM_DIVIDER_ID = -2; 87 88 DropDown.KEYCODE_DOWN = 40; 89 DropDown.KEYCODE_ENTER = 13; 90 DropDown.KEYCODE_ESC = 27; 91 DropDown.KEYCODE_SPACE = 32; 92 DropDown.KEYCODE_TAB = 9; 93 DropDown.KEYCODE_UP = 38; 94 95 DropDown.prototype = { 96 __proto__: HTMLDivElement.prototype, 97 98 /** @override */ 99 decorate: function() { 100 this.appendChild(this.createOverlay_()); 101 this.appendChild(this.title_ = this.createTitle_()); 102 var container = new DropDownContainer(); 103 container.id = this.id + '-dropdown-container'; 104 this.appendChild(container); 105 106 this.addEventListener('keydown', this.keyDownHandler_); 107 108 this.title_.id = this.id + '-dropdown'; 109 this.title_.setAttribute('role', 'button'); 110 this.title_.setAttribute('aria-haspopup', 'true'); 111 this.title_.setAttribute('aria-owns', container.id); 112 }, 113 114 /** 115 * Returns true if dropdown menu is shown. 116 * @type {bool} Whether menu element is shown. 117 */ 118 get isShown() { 119 return !this.container.hidden; 120 }, 121 122 /** 123 * Sets dropdown menu visibility. 124 * @param {bool} show New visibility state for dropdown menu. 125 */ 126 set isShown(show) { 127 this.firstElementChild.hidden = !show; 128 this.container.hidden = !show; 129 if (show) { 130 this.container.selectItem(this.container.firstItem, false); 131 } else { 132 this.title_.removeAttribute('aria-activedescendant'); 133 } 134 135 // Flag for keyboard flow util to forward the up/down keys. 136 this.title_.classList.toggle('needs-up-down-keys', show); 137 }, 138 139 /** 140 * Returns container of the menu items. 141 */ 142 get container() { 143 return this.lastElementChild; 144 }, 145 146 /** 147 * Sets title and icon. 148 * @param {string} title Text on dropdown. 149 * @param {string} icon Icon in dataURL format. 150 */ 151 setTitle: function(title, icon) { 152 this.title_.firstElementChild.src = icon; 153 this.title_.lastElementChild.textContent = title; 154 }, 155 156 /** 157 * Sets dropdown items. 158 * @param {Array} items Dropdown items array. 159 */ 160 setItems: function(items) { 161 this.container.innerHTML = ''; 162 this.container.firstItem = null; 163 this.container.selectedItem = null; 164 for (var i = 0; i < items.length; ++i) { 165 var item = items[i]; 166 if ('sub' in item) { 167 // Workaround for submenus, add items on top level. 168 // TODO(altimofeev): support submenus. 169 for (var j = 0; j < item.sub.length; ++j) 170 this.createItem_(this.container, item.sub[j]); 171 continue; 172 } 173 this.createItem_(this.container, item); 174 } 175 this.container.selectItem(this.container.firstItem, false); 176 177 var maxHeight = cr.ui.LoginUITools.getMaxHeightBeforeShelfOverlapping( 178 this.container); 179 if (maxHeight < this.container.offsetHeight) 180 this.container.style.maxHeight = maxHeight + 'px'; 181 }, 182 183 /** 184 * Id of the active drop-down element. 185 * @private 186 */ 187 activeElementId_: '', 188 189 /** 190 * Creates dropdown item element and adds into container. 191 * @param {HTMLElement} container Container where item is added. 192 * @param {!Object} item Item to be added. 193 * @private 194 */ 195 createItem_: function(container, item) { 196 var itemContentElement; 197 var className = 'dropdown-item'; 198 if (item.id == DropDown.ITEM_DIVIDER_ID) { 199 className = 'dropdown-divider'; 200 itemContentElement = this.ownerDocument.createElement('hr'); 201 } else { 202 var span = this.ownerDocument.createElement('span'); 203 itemContentElement = span; 204 span.textContent = item.label; 205 if ('bold' in item && item.bold) 206 span.classList.add('bold'); 207 var image = this.ownerDocument.createElement('img'); 208 image.alt = ''; 209 image.classList.add('dropdown-image'); 210 if (item.icon) 211 image.src = item.icon; 212 } 213 214 var itemElement = this.ownerDocument.createElement('div'); 215 itemElement.classList.add(className); 216 itemElement.appendChild(itemContentElement); 217 itemElement.iid = item.id; 218 itemElement.controller = this; 219 var enabled = 'enabled' in item && item.enabled; 220 if (!enabled) 221 itemElement.classList.add('disabled-item'); 222 223 if (item.id > 0) { 224 var wrapperDiv = this.ownerDocument.createElement('div'); 225 wrapperDiv.setAttribute('role', 'menuitem'); 226 wrapperDiv.id = this.id + item.id; 227 if (!enabled) 228 wrapperDiv.setAttribute('aria-disabled', 'true'); 229 wrapperDiv.classList.add('dropdown-item-container'); 230 var imageDiv = this.ownerDocument.createElement('div'); 231 imageDiv.appendChild(image); 232 wrapperDiv.appendChild(imageDiv); 233 wrapperDiv.appendChild(itemElement); 234 wrapperDiv.addEventListener('click', function f(e) { 235 var item = this.lastElementChild; 236 if (item.iid < -1 || item.classList.contains('disabled-item')) 237 return; 238 item.controller.isShown = false; 239 if (item.iid >= 0) 240 chrome.send('networkItemChosen', [item.iid]); 241 this.parentNode.parentNode.title_.focus(); 242 }); 243 wrapperDiv.addEventListener('mouseover', function f(e) { 244 this.parentNode.selectItem(this, true); 245 }); 246 itemElement = wrapperDiv; 247 } 248 container.appendChild(itemElement); 249 if (!container.firstItem && item.id >= 0) { 250 container.firstItem = itemElement; 251 } 252 }, 253 254 /** 255 * Creates dropdown overlay element, which catches outside clicks. 256 * @type {HTMLElement} 257 * @private 258 */ 259 createOverlay_: function() { 260 var overlay = this.ownerDocument.createElement('div'); 261 overlay.classList.add('dropdown-overlay'); 262 overlay.addEventListener('click', function() { 263 this.parentNode.title_.focus(); 264 this.parentNode.isShown = false; 265 }); 266 return overlay; 267 }, 268 269 /** 270 * Creates dropdown title element. 271 * @type {HTMLElement} 272 * @private 273 */ 274 createTitle_: function() { 275 var image = this.ownerDocument.createElement('img'); 276 image.alt = ''; 277 image.classList.add('dropdown-image'); 278 var text = this.ownerDocument.createElement('div'); 279 280 var el = this.ownerDocument.createElement('div'); 281 el.appendChild(image); 282 el.appendChild(text); 283 284 el.tabIndex = 0; 285 el.classList.add('dropdown-title'); 286 el.iid = -1; 287 el.controller = this; 288 el.inFocus = false; 289 el.opening = false; 290 291 el.addEventListener('click', function f(e) { 292 this.controller.isShown = !this.controller.isShown; 293 }); 294 295 el.addEventListener('focus', function(e) { 296 this.inFocus = true; 297 }); 298 299 el.addEventListener('blur', function(e) { 300 this.inFocus = false; 301 }); 302 303 el.addEventListener('keydown', function f(e) { 304 if (this.inFocus && !this.controller.isShown && 305 (e.keyCode == DropDown.KEYCODE_ENTER || 306 e.keyCode == DropDown.KEYCODE_SPACE || 307 (!useKeyboardFlow && (e.keyCode == DropDown.KEYCODE_UP || 308 e.keyCode == DropDown.KEYCODE_DOWN)))) { 309 this.opening = true; 310 this.controller.isShown = true; 311 e.stopPropagation(); 312 e.preventDefault(); 313 } 314 }); 315 return el; 316 }, 317 318 /** 319 * Handles keydown event from the keyboard. 320 * @private 321 * @param {!Event} e Keydown event. 322 */ 323 keyDownHandler_: function(e) { 324 if (!this.isShown) 325 return; 326 var selected = this.container.selectedItem; 327 var handled = false; 328 switch (e.keyCode) { 329 case DropDown.KEYCODE_UP: { 330 do { 331 selected = selected.previousSibling; 332 if (!selected) 333 selected = this.container.lastElementChild; 334 } while (selected.iid < 0); 335 this.container.selectItem(selected, false); 336 handled = true; 337 break; 338 } 339 case DropDown.KEYCODE_DOWN: { 340 do { 341 selected = selected.nextSibling; 342 if (!selected) 343 selected = this.container.firstItem; 344 } while (selected.iid < 0); 345 this.container.selectItem(selected, false); 346 handled = true; 347 break; 348 } 349 case DropDown.KEYCODE_ESC: { 350 this.isShown = false; 351 handled = true; 352 break; 353 } 354 case DropDown.KEYCODE_TAB: { 355 this.isShown = false; 356 handled = true; 357 break; 358 } 359 case DropDown.KEYCODE_ENTER: { 360 if (!this.title_.opening) { 361 this.title_.focus(); 362 this.isShown = false; 363 var item = 364 this.title_.controller.container.selectedItem.lastElementChild; 365 if (item.iid >= 0 && !item.classList.contains('disabled-item')) 366 chrome.send('networkItemChosen', [item.iid]); 367 } 368 handled = true; 369 break; 370 } 371 } 372 if (handled) { 373 e.stopPropagation(); 374 e.preventDefault(); 375 } 376 this.title_.opening = false; 377 } 378 }; 379 380 /** 381 * Updates networks list with the new data. 382 * @param {!Object} data Networks list. 383 */ 384 DropDown.updateNetworks = function(data) { 385 if (DropDown.activeElementId_) 386 $(DropDown.activeElementId_).setItems(data); 387 }; 388 389 /** 390 * Updates network title, which is shown by the drop-down. 391 * @param {string} title Title to be displayed. 392 * @param {!Object} icon Icon to be displayed. 393 */ 394 DropDown.updateNetworkTitle = function(title, icon) { 395 if (DropDown.activeElementId_) 396 $(DropDown.activeElementId_).setTitle(title, icon); 397 }; 398 399 /** 400 * Activates network drop-down. Only one network drop-down 401 * can be active at the same time. So activating new drop-down deactivates 402 * the previous one. 403 * @param {string} elementId Id of network drop-down element. 404 * @param {boolean} isOobe Whether drop-down is used by an Oobe screen. 405 */ 406 DropDown.show = function(elementId, isOobe) { 407 $(elementId).isShown = false; 408 if (DropDown.activeElementId_ != elementId) { 409 DropDown.activeElementId_ = elementId; 410 chrome.send('networkDropdownShow', [elementId, isOobe]); 411 } 412 }; 413 414 /** 415 * Deactivates network drop-down. Deactivating inactive drop-down does 416 * nothing. 417 * @param {string} elementId Id of network drop-down element. 418 */ 419 DropDown.hide = function(elementId) { 420 if (DropDown.activeElementId_ == elementId) { 421 DropDown.activeElementId_ = ''; 422 chrome.send('networkDropdownHide'); 423 } 424 }; 425 426 /** 427 * Refreshes network drop-down. Should be called on language change. 428 */ 429 DropDown.refresh = function() { 430 chrome.send('networkDropdownRefresh'); 431 }; 432 433 /** 434 * Sets the keyboard flow flag. 435 */ 436 DropDown.enableKeyboardFlow = function() { 437 useKeyboardFlow = true; 438 }; 439 440 return { 441 DropDown: DropDown 442 }; 443 }); 444