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 9cr.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 178 /** 179 * Id of the active drop-down element. 180 * @private 181 */ 182 activeElementId_: '', 183 184 /** 185 * Creates dropdown item element and adds into container. 186 * @param {HTMLElement} container Container where item is added. 187 * @param {!Object} item Item to be added. 188 * @private 189 */ 190 createItem_: function(container, item) { 191 var itemContentElement; 192 var className = 'dropdown-item'; 193 if (item.id == DropDown.ITEM_DIVIDER_ID) { 194 className = 'dropdown-divider'; 195 itemContentElement = this.ownerDocument.createElement('hr'); 196 } else { 197 var span = this.ownerDocument.createElement('span'); 198 itemContentElement = span; 199 span.textContent = item.label; 200 if ('bold' in item && item.bold) 201 span.classList.add('bold'); 202 var image = this.ownerDocument.createElement('img'); 203 image.alt = ''; 204 image.classList.add('dropdown-image'); 205 if (item.icon) 206 image.src = item.icon; 207 } 208 209 var itemElement = this.ownerDocument.createElement('div'); 210 itemElement.classList.add(className); 211 itemElement.appendChild(itemContentElement); 212 itemElement.iid = item.id; 213 itemElement.controller = this; 214 var enabled = 'enabled' in item && item.enabled; 215 if (!enabled) 216 itemElement.classList.add('disabled-item'); 217 218 if (item.id > 0) { 219 var wrapperDiv = this.ownerDocument.createElement('div'); 220 wrapperDiv.setAttribute('role', 'menuitem'); 221 wrapperDiv.id = this.id + item.id; 222 if (!enabled) 223 wrapperDiv.setAttribute('aria-disabled', 'true'); 224 wrapperDiv.classList.add('dropdown-item-container'); 225 var imageDiv = this.ownerDocument.createElement('div'); 226 imageDiv.appendChild(image); 227 wrapperDiv.appendChild(imageDiv); 228 wrapperDiv.appendChild(itemElement); 229 wrapperDiv.addEventListener('click', function f(e) { 230 var item = this.lastElementChild; 231 if (item.iid < -1 || item.classList.contains('disabled-item')) 232 return; 233 item.controller.isShown = false; 234 if (item.iid >= 0) 235 chrome.send('networkItemChosen', [item.iid]); 236 this.parentNode.parentNode.title_.focus(); 237 }); 238 wrapperDiv.addEventListener('mouseover', function f(e) { 239 this.parentNode.selectItem(this, true); 240 }); 241 itemElement = wrapperDiv; 242 } 243 container.appendChild(itemElement); 244 if (!container.firstItem && item.id >= 0) { 245 container.firstItem = itemElement; 246 } 247 }, 248 249 /** 250 * Creates dropdown overlay element, which catches outside clicks. 251 * @type {HTMLElement} 252 * @private 253 */ 254 createOverlay_: function() { 255 var overlay = this.ownerDocument.createElement('div'); 256 overlay.classList.add('dropdown-overlay'); 257 overlay.addEventListener('click', function() { 258 this.parentNode.title_.focus(); 259 this.parentNode.isShown = false; 260 }); 261 return overlay; 262 }, 263 264 /** 265 * Creates dropdown title element. 266 * @type {HTMLElement} 267 * @private 268 */ 269 createTitle_: function() { 270 var image = this.ownerDocument.createElement('img'); 271 image.alt = ''; 272 image.classList.add('dropdown-image'); 273 var text = this.ownerDocument.createElement('div'); 274 275 var el = this.ownerDocument.createElement('div'); 276 el.appendChild(image); 277 el.appendChild(text); 278 279 el.tabIndex = 0; 280 el.classList.add('dropdown-title'); 281 el.iid = -1; 282 el.controller = this; 283 el.inFocus = false; 284 el.opening = false; 285 286 el.addEventListener('click', function f(e) { 287 this.controller.isShown = !this.controller.isShown; 288 }); 289 290 el.addEventListener('focus', function(e) { 291 this.inFocus = true; 292 }); 293 294 el.addEventListener('blur', function(e) { 295 this.inFocus = false; 296 }); 297 298 el.addEventListener('keydown', function f(e) { 299 if (this.inFocus && !this.controller.isShown && 300 (e.keyCode == DropDown.KEYCODE_ENTER || 301 e.keyCode == DropDown.KEYCODE_SPACE || 302 (!useKeyboardFlow && (e.keyCode == DropDown.KEYCODE_UP || 303 e.keyCode == DropDown.KEYCODE_DOWN)))) { 304 this.opening = true; 305 this.controller.isShown = true; 306 e.stopPropagation(); 307 e.preventDefault(); 308 } 309 }); 310 return el; 311 }, 312 313 /** 314 * Handles keydown event from the keyboard. 315 * @private 316 * @param {!Event} e Keydown event. 317 */ 318 keyDownHandler_: function(e) { 319 if (!this.isShown) 320 return; 321 var selected = this.container.selectedItem; 322 var handled = false; 323 switch (e.keyCode) { 324 case DropDown.KEYCODE_UP: { 325 do { 326 selected = selected.previousSibling; 327 if (!selected) 328 selected = this.container.lastElementChild; 329 } while (selected.iid < 0); 330 this.container.selectItem(selected, false); 331 handled = true; 332 break; 333 } 334 case DropDown.KEYCODE_DOWN: { 335 do { 336 selected = selected.nextSibling; 337 if (!selected) 338 selected = this.container.firstItem; 339 } while (selected.iid < 0); 340 this.container.selectItem(selected, false); 341 handled = true; 342 break; 343 } 344 case DropDown.KEYCODE_ESC: { 345 this.isShown = false; 346 handled = true; 347 break; 348 } 349 case DropDown.KEYCODE_TAB: { 350 this.isShown = false; 351 handled = true; 352 break; 353 } 354 case DropDown.KEYCODE_ENTER: { 355 if (!this.title_.opening) { 356 this.title_.focus(); 357 this.isShown = false; 358 var item = 359 this.title_.controller.container.selectedItem.lastElementChild; 360 if (item.iid >= 0 && !item.classList.contains('disabled-item')) 361 chrome.send('networkItemChosen', [item.iid]); 362 } 363 handled = true; 364 break; 365 } 366 } 367 if (handled) { 368 e.stopPropagation(); 369 e.preventDefault(); 370 } 371 this.title_.opening = false; 372 } 373 }; 374 375 /** 376 * Updates networks list with the new data. 377 * @param {!Object} data Networks list. 378 */ 379 DropDown.updateNetworks = function(data) { 380 if (DropDown.activeElementId_) 381 $(DropDown.activeElementId_).setItems(data); 382 }; 383 384 /** 385 * Updates network title, which is shown by the drop-down. 386 * @param {string} title Title to be displayed. 387 * @param {!Object} icon Icon to be displayed. 388 */ 389 DropDown.updateNetworkTitle = function(title, icon) { 390 if (DropDown.activeElementId_) 391 $(DropDown.activeElementId_).setTitle(title, icon); 392 }; 393 394 /** 395 * Activates network drop-down. Only one network drop-down 396 * can be active at the same time. So activating new drop-down deactivates 397 * the previous one. 398 * @param {string} elementId Id of network drop-down element. 399 * @param {boolean} isOobe Whether drop-down is used by an Oobe screen. 400 */ 401 DropDown.show = function(elementId, isOobe) { 402 $(elementId).isShown = false; 403 if (DropDown.activeElementId_ != elementId) { 404 DropDown.activeElementId_ = elementId; 405 chrome.send('networkDropdownShow', [elementId, isOobe]); 406 } 407 }; 408 409 /** 410 * Deactivates network drop-down. Deactivating inactive drop-down does 411 * nothing. 412 * @param {string} elementId Id of network drop-down element. 413 */ 414 DropDown.hide = function(elementId) { 415 if (DropDown.activeElementId_ == elementId) { 416 DropDown.activeElementId_ = ''; 417 chrome.send('networkDropdownHide'); 418 } 419 }; 420 421 /** 422 * Refreshes network drop-down. Should be called on language change. 423 */ 424 DropDown.refresh = function() { 425 chrome.send('networkDropdownRefresh'); 426 }; 427 428 /** 429 * Sets the keyboard flow flag. 430 */ 431 DropDown.enableKeyboardFlow = function() { 432 useKeyboardFlow = true; 433 }; 434 435 return { 436 DropDown: DropDown 437 }; 438}); 439