// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview Network drop-down implementation. */ cr.define('cr.ui', function() { /** * Whether keyboard flow is in use. When setting to true, up/down arrow key * will be used to move focus instead of opening the drop down. */ var useKeyboardFlow = false; /** * Creates a new container for the drop down menu items. * @constructor * @extends {HTMLDivElement} */ var DropDownContainer = cr.ui.define('div'); DropDownContainer.prototype = { __proto__: HTMLDivElement.prototype, /** @override */ decorate: function() { this.classList.add('dropdown-container'); // Selected item in the menu list. this.selectedItem = null; // First item which could be selected. this.firstItem = null; this.setAttribute('role', 'menu'); // Whether scroll has just happened. this.scrollJustHappened = false; }, /** * Gets scroll action to be done for the item. * @param {!Object} item Menu item. * @return {integer} -1 for scroll up; 0 for no action; 1 for scroll down. */ scrollAction: function(item) { var thisTop = this.scrollTop; var thisBottom = thisTop + this.offsetHeight; var itemTop = item.offsetTop; var itemBottom = itemTop + item.offsetHeight; if (itemTop <= thisTop) return -1; if (itemBottom >= thisBottom) return 1; return 0; }, /** * Selects new item. * @param {!Object} selectedItem Item to be selected. * @param {boolean} mouseOver Is mouseover event triggered? */ selectItem: function(selectedItem, mouseOver) { if (mouseOver && this.scrollJustHappened) { this.scrollJustHappened = false; return; } if (this.selectedItem) this.selectedItem.classList.remove('hover'); selectedItem.classList.add('hover'); this.selectedItem = selectedItem; if (!this.hidden) { this.previousSibling.setAttribute( 'aria-activedescendant', selectedItem.id); } var action = this.scrollAction(selectedItem); if (action != 0) { selectedItem.scrollIntoView(action < 0); this.scrollJustHappened = true; } } }; /** * Creates a new DropDown div. * @constructor * @extends {HTMLDivElement} */ var DropDown = cr.ui.define('div'); DropDown.ITEM_DIVIDER_ID = -2; DropDown.KEYCODE_DOWN = 40; DropDown.KEYCODE_ENTER = 13; DropDown.KEYCODE_ESC = 27; DropDown.KEYCODE_SPACE = 32; DropDown.KEYCODE_TAB = 9; DropDown.KEYCODE_UP = 38; DropDown.prototype = { __proto__: HTMLDivElement.prototype, /** @override */ decorate: function() { this.appendChild(this.createOverlay_()); this.appendChild(this.title_ = this.createTitle_()); var container = new DropDownContainer(); container.id = this.id + '-dropdown-container'; this.appendChild(container); this.addEventListener('keydown', this.keyDownHandler_); this.title_.id = this.id + '-dropdown'; this.title_.setAttribute('role', 'button'); this.title_.setAttribute('aria-haspopup', 'true'); this.title_.setAttribute('aria-owns', container.id); }, /** * Returns true if dropdown menu is shown. * @type {bool} Whether menu element is shown. */ get isShown() { return !this.container.hidden; }, /** * Sets dropdown menu visibility. * @param {bool} show New visibility state for dropdown menu. */ set isShown(show) { this.firstElementChild.hidden = !show; this.container.hidden = !show; if (show) { this.container.selectItem(this.container.firstItem, false); } else { this.title_.removeAttribute('aria-activedescendant'); } // Flag for keyboard flow util to forward the up/down keys. this.title_.classList.toggle('needs-up-down-keys', show); }, /** * Returns container of the menu items. */ get container() { return this.lastElementChild; }, /** * Sets title and icon. * @param {string} title Text on dropdown. * @param {string} icon Icon in dataURL format. */ setTitle: function(title, icon) { this.title_.firstElementChild.src = icon; this.title_.lastElementChild.textContent = title; }, /** * Sets dropdown items. * @param {Array} items Dropdown items array. */ setItems: function(items) { this.container.innerHTML = ''; this.container.firstItem = null; this.container.selectedItem = null; for (var i = 0; i < items.length; ++i) { var item = items[i]; if ('sub' in item) { // Workaround for submenus, add items on top level. // TODO(altimofeev): support submenus. for (var j = 0; j < item.sub.length; ++j) this.createItem_(this.container, item.sub[j]); continue; } this.createItem_(this.container, item); } this.container.selectItem(this.container.firstItem, false); }, /** * Id of the active drop-down element. * @private */ activeElementId_: '', /** * Creates dropdown item element and adds into container. * @param {HTMLElement} container Container where item is added. * @param {!Object} item Item to be added. * @private */ createItem_: function(container, item) { var itemContentElement; var className = 'dropdown-item'; if (item.id == DropDown.ITEM_DIVIDER_ID) { className = 'dropdown-divider'; itemContentElement = this.ownerDocument.createElement('hr'); } else { var span = this.ownerDocument.createElement('span'); itemContentElement = span; span.textContent = item.label; if ('bold' in item && item.bold) span.classList.add('bold'); var image = this.ownerDocument.createElement('img'); image.alt = ''; image.classList.add('dropdown-image'); if (item.icon) image.src = item.icon; } var itemElement = this.ownerDocument.createElement('div'); itemElement.classList.add(className); itemElement.appendChild(itemContentElement); itemElement.iid = item.id; itemElement.controller = this; var enabled = 'enabled' in item && item.enabled; if (!enabled) itemElement.classList.add('disabled-item'); if (item.id > 0) { var wrapperDiv = this.ownerDocument.createElement('div'); wrapperDiv.setAttribute('role', 'menuitem'); wrapperDiv.id = this.id + item.id; if (!enabled) wrapperDiv.setAttribute('aria-disabled', 'true'); wrapperDiv.classList.add('dropdown-item-container'); var imageDiv = this.ownerDocument.createElement('div'); imageDiv.appendChild(image); wrapperDiv.appendChild(imageDiv); wrapperDiv.appendChild(itemElement); wrapperDiv.addEventListener('click', function f(e) { var item = this.lastElementChild; if (item.iid < -1 || item.classList.contains('disabled-item')) return; item.controller.isShown = false; if (item.iid >= 0) chrome.send('networkItemChosen', [item.iid]); this.parentNode.parentNode.title_.focus(); }); wrapperDiv.addEventListener('mouseover', function f(e) { this.parentNode.selectItem(this, true); }); itemElement = wrapperDiv; } container.appendChild(itemElement); if (!container.firstItem && item.id >= 0) { container.firstItem = itemElement; } }, /** * Creates dropdown overlay element, which catches outside clicks. * @type {HTMLElement} * @private */ createOverlay_: function() { var overlay = this.ownerDocument.createElement('div'); overlay.classList.add('dropdown-overlay'); overlay.addEventListener('click', function() { this.parentNode.title_.focus(); this.parentNode.isShown = false; }); return overlay; }, /** * Creates dropdown title element. * @type {HTMLElement} * @private */ createTitle_: function() { var image = this.ownerDocument.createElement('img'); image.alt = ''; image.classList.add('dropdown-image'); var text = this.ownerDocument.createElement('div'); var el = this.ownerDocument.createElement('div'); el.appendChild(image); el.appendChild(text); el.tabIndex = 0; el.classList.add('dropdown-title'); el.iid = -1; el.controller = this; el.inFocus = false; el.opening = false; el.addEventListener('click', function f(e) { this.controller.isShown = !this.controller.isShown; }); el.addEventListener('focus', function(e) { this.inFocus = true; }); el.addEventListener('blur', function(e) { this.inFocus = false; }); el.addEventListener('keydown', function f(e) { if (this.inFocus && !this.controller.isShown && (e.keyCode == DropDown.KEYCODE_ENTER || e.keyCode == DropDown.KEYCODE_SPACE || (!useKeyboardFlow && (e.keyCode == DropDown.KEYCODE_UP || e.keyCode == DropDown.KEYCODE_DOWN)))) { this.opening = true; this.controller.isShown = true; e.stopPropagation(); e.preventDefault(); } }); return el; }, /** * Handles keydown event from the keyboard. * @private * @param {!Event} e Keydown event. */ keyDownHandler_: function(e) { if (!this.isShown) return; var selected = this.container.selectedItem; var handled = false; switch (e.keyCode) { case DropDown.KEYCODE_UP: { do { selected = selected.previousSibling; if (!selected) selected = this.container.lastElementChild; } while (selected.iid < 0); this.container.selectItem(selected, false); handled = true; break; } case DropDown.KEYCODE_DOWN: { do { selected = selected.nextSibling; if (!selected) selected = this.container.firstItem; } while (selected.iid < 0); this.container.selectItem(selected, false); handled = true; break; } case DropDown.KEYCODE_ESC: { this.isShown = false; handled = true; break; } case DropDown.KEYCODE_TAB: { this.isShown = false; handled = true; break; } case DropDown.KEYCODE_ENTER: { if (!this.title_.opening) { this.title_.focus(); this.isShown = false; var item = this.title_.controller.container.selectedItem.lastElementChild; if (item.iid >= 0 && !item.classList.contains('disabled-item')) chrome.send('networkItemChosen', [item.iid]); } handled = true; break; } } if (handled) { e.stopPropagation(); e.preventDefault(); } this.title_.opening = false; } }; /** * Updates networks list with the new data. * @param {!Object} data Networks list. */ DropDown.updateNetworks = function(data) { if (DropDown.activeElementId_) $(DropDown.activeElementId_).setItems(data); }; /** * Updates network title, which is shown by the drop-down. * @param {string} title Title to be displayed. * @param {!Object} icon Icon to be displayed. */ DropDown.updateNetworkTitle = function(title, icon) { if (DropDown.activeElementId_) $(DropDown.activeElementId_).setTitle(title, icon); }; /** * Activates network drop-down. Only one network drop-down * can be active at the same time. So activating new drop-down deactivates * the previous one. * @param {string} elementId Id of network drop-down element. * @param {boolean} isOobe Whether drop-down is used by an Oobe screen. */ DropDown.show = function(elementId, isOobe) { $(elementId).isShown = false; if (DropDown.activeElementId_ != elementId) { DropDown.activeElementId_ = elementId; chrome.send('networkDropdownShow', [elementId, isOobe]); } }; /** * Deactivates network drop-down. Deactivating inactive drop-down does * nothing. * @param {string} elementId Id of network drop-down element. */ DropDown.hide = function(elementId) { if (DropDown.activeElementId_ == elementId) { DropDown.activeElementId_ = ''; chrome.send('networkDropdownHide'); } }; /** * Refreshes network drop-down. Should be called on language change. */ DropDown.refresh = function() { chrome.send('networkDropdownRefresh'); }; /** * Sets the keyboard flow flag. */ DropDown.enableKeyboardFlow = function() { useKeyboardFlow = true; }; return { DropDown: DropDown }; });