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 * Constructor for FocusManager singleton. Checks focus of elements to ensure 8 * that elements in "background" pages (i.e., those in a dialog that is not 9 * the topmost overlay) do not receive focus. 10 * @constructor 11 */ 12 function FocusManager() { 13 } 14 15 FocusManager.prototype = { 16 /** 17 * Whether focus is being transferred backward or forward through the DOM. 18 * @type {boolean} 19 * @private 20 */ 21 focusDirBackwards_: false, 22 23 /** 24 * Determines whether the |child| is a descendant of |parent| in the page's 25 * DOM. 26 * @param {Element} parent The parent element to test. 27 * @param {Element} child The child element to test. 28 * @return {boolean} True if |child| is a descendant of |parent|. 29 * @private 30 */ 31 isDescendantOf_: function(parent, child) { 32 var current = child; 33 34 while (current) { 35 current = current.parentNode; 36 if (typeof(current) == 'undefined' || 37 typeof(current) == 'null' || 38 current === document.body) { 39 return false; 40 } else if (current === parent) { 41 return true; 42 } 43 } 44 45 return false; 46 }, 47 48 /** 49 * Returns the parent element containing all elements which should be 50 * allowed to receive focus. 51 * @return {Element} The element containing focusable elements. 52 */ 53 getFocusParent: function() { 54 return document.body; 55 }, 56 57 /** 58 * Returns the elements on the page capable of receiving focus. 59 * @return {Array.Element} The focusable elements. 60 */ 61 getFocusableElements_: function() { 62 var focusableDiv = this.getFocusParent(); 63 64 // Create a TreeWalker object to traverse the DOM from |focusableDiv|. 65 var treeWalker = document.createTreeWalker( 66 focusableDiv, 67 NodeFilter.SHOW_ELEMENT, 68 { acceptNode: function(node) { 69 var style = window.getComputedStyle(node); 70 // Reject all hidden nodes. FILTER_REJECT also rejects these 71 // nodes' children, so non-hidden elements that are descendants of 72 // hidden <div>s will correctly be rejected. 73 if (node.hidden || style.display == 'none' || 74 style.visibility == 'hidden') { 75 return NodeFilter.FILTER_REJECT; 76 } 77 78 // Skip nodes that cannot receive focus. FILTER_SKIP does not 79 // cause this node's children also to be skipped. 80 if (node.disabled || node.tabIndex < 0) 81 return NodeFilter.FILTER_SKIP; 82 83 // Accept nodes that are non-hidden and focusable. 84 return NodeFilter.FILTER_ACCEPT; 85 } 86 }, 87 false); 88 89 var focusable = []; 90 while (treeWalker.nextNode()) 91 focusable.push(treeWalker.currentNode); 92 93 return focusable; 94 }, 95 96 /** 97 * Dispatches an 'elementFocused' event to notify an element that it has 98 * received focus. When focus wraps around within the a page, only the 99 * element that has focus after the wrapping receives an 'elementFocused' 100 * event. This differs from the native 'focus' event which is received by 101 * an element outside the page first, followed by a 'focus' on an element 102 * within the page after the FocusManager has intervened. 103 * @param {Element} element The element that has received focus. 104 * @private 105 */ 106 dispatchFocusEvent_: function(element) { 107 cr.dispatchSimpleEvent(element, 'elementFocused', true, false); 108 }, 109 110 /** 111 * Attempts to focus the appropriate element in the current dialog. 112 * @private 113 */ 114 setFocus_: function() { 115 var element = this.selectFocusableElement_(); 116 if (element) { 117 element.focus(); 118 this.dispatchFocusEvent_(element); 119 } 120 }, 121 122 /** 123 * Attempts to focus the first element in the current dialog. 124 */ 125 focusFirstElement: function() { 126 this.focusFirstElement_(); 127 }, 128 129 /** 130 * Selects first appropriate focusable element according to the 131 * current focus direction and element type. If it is a radio button, 132 * checked one is selected from the group. 133 * @private 134 */ 135 selectFocusableElement_: function() { 136 // If |this.focusDirBackwards_| is true, the user has pressed "Shift+Tab" 137 // and has caused the focus to be transferred backward, outside of the 138 // current dialog. In this case, loop around and try to focus the last 139 // element of the dialog; otherwise, try to focus the first element of the 140 // dialog. 141 var focusableElements = this.getFocusableElements_(); 142 var element = this.focusDirBackwards_ ? focusableElements.pop() : 143 focusableElements.shift(); 144 if (!element) 145 return null; 146 if (element.tagName != 'INPUT' || element.type != 'radio' || 147 element.name == '') { 148 return element; 149 } 150 if (!element.checked) { 151 for (var i = 0; i < focusableElements.length; i++) { 152 var e = focusableElements[i]; 153 if (e && e.tagName == 'INPUT' && e.type == 'radio' && 154 e.name == element.name && e.checked) { 155 element = e; 156 break; 157 } 158 } 159 } 160 return element; 161 }, 162 163 /** 164 * Handler for focus events on the page. 165 * @param {Event} event The focus event. 166 * @private 167 */ 168 onDocumentFocus_: function(event) { 169 // If the element being focused is a descendant of the currently visible 170 // page, focus is valid. 171 if (this.isDescendantOf_(this.getFocusParent(), event.target)) { 172 this.dispatchFocusEvent_(event.target); 173 return; 174 } 175 176 // Focus event handlers for descendant elements might dispatch another 177 // focus event. 178 event.stopPropagation(); 179 180 // The target of the focus event is not in the topmost visible page and 181 // should not be focused. 182 event.target.blur(); 183 184 // Attempt to wrap around focus within the current page. 185 this.setFocus_(); 186 }, 187 188 /** 189 * Handler for keydown events on the page. 190 * @param {Event} event The keydown event. 191 * @private 192 */ 193 onDocumentKeyDown_: function(event) { 194 /** @const */ var tabKeyCode = 9; 195 196 if (event.keyCode == tabKeyCode) { 197 // If the "Shift" key is held, focus is being transferred backward in 198 // the page. 199 this.focusDirBackwards_ = event.shiftKey ? true : false; 200 } 201 }, 202 203 /** 204 * Initializes the FocusManager by listening for events in the document. 205 */ 206 initialize: function() { 207 document.addEventListener('focus', this.onDocumentFocus_.bind(this), 208 true); 209 document.addEventListener('keydown', this.onDocumentKeyDown_.bind(this), 210 true); 211 }, 212 }; 213 214 /** 215 * Disable mouse-focus for button controls. 216 * Button form controls are mouse-focusable since Chromium 30. We want the 217 * old behavior in some WebUI pages. 218 */ 219 FocusManager.disableMouseFocusOnButtons = function() { 220 document.addEventListener('mousedown', function(event) { 221 var node = event.target; 222 var tagName = node.tagName; 223 if (tagName != 'BUTTON' && tagName != 'INPUT') { 224 do { 225 node = node.parentNode; 226 if (!node || node.nodeType != Node.ELEMENT_NODE) 227 return; 228 } while (node.tagName != 'BUTTON'); 229 } 230 var type = node.type; 231 if (type == 'button' || type == 'reset' || type == 'submit' || 232 type == 'radio' || type == 'checkbox') { 233 if (document.activeElement != node) 234 document.activeElement.blur(); 235 236 // Focus the current window so that if the active element is in another 237 // window, it is deactivated. 238 window.focus(); 239 event.preventDefault(); 240 } 241 }, false); 242 }; 243 244 return { 245 FocusManager: FocusManager, 246 }; 247}); 248