- add sources.
[platform/framework/web/crosswalk.git] / src / ui / webui / resources / js / cr / ui / focus_manager.js
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 cr.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       // If |this.focusDirBackwards_| is true, the user has pressed "Shift+Tab"
116       // and has caused the focus to be transferred backward, outside of the
117       // current dialog. In this case, loop around and try to focus the last
118       // element of the dialog; otherwise, try to focus the first element of the
119       // dialog.
120       var focusableElements = this.getFocusableElements_();
121       var element = this.focusDirBackwards_ ? focusableElements.pop() :
122                                               focusableElements.shift();
123       if (element) {
124         element.focus();
125         this.dispatchFocusEvent_(element);
126       }
127     },
128
129     /**
130      * Attempts to focus the first element in the current dialog.
131      */
132     focusFirstElement: function() {
133       this.focusFirstElement_();
134     },
135
136     /**
137      * Handler for focus events on the page.
138      * @param {Event} event The focus event.
139      * @private
140      */
141     onDocumentFocus_: function(event) {
142       // If the element being focused is a descendant of the currently visible
143       // page, focus is valid.
144       if (this.isDescendantOf_(this.getFocusParent(), event.target)) {
145         this.dispatchFocusEvent_(event.target);
146         return;
147       }
148
149       // The target of the focus event is not in the topmost visible page and
150       // should not be focused.
151       event.target.blur();
152
153       // Attempt to wrap around focus within the current page.
154       this.setFocus_();
155     },
156
157     /**
158      * Handler for keydown events on the page.
159      * @param {Event} event The keydown event.
160      * @private
161      */
162     onDocumentKeyDown_: function(event) {
163       /** @const */ var tabKeyCode = 9;
164
165       if (event.keyCode == tabKeyCode) {
166         // If the "Shift" key is held, focus is being transferred backward in
167         // the page.
168         this.focusDirBackwards_ = event.shiftKey ? true : false;
169       }
170     },
171
172     /**
173      * Initializes the FocusManager by listening for events in the document.
174      */
175     initialize: function() {
176       document.addEventListener('focus', this.onDocumentFocus_.bind(this),
177           true);
178       document.addEventListener('keydown', this.onDocumentKeyDown_.bind(this),
179           true);
180     },
181   };
182
183   /**
184    * Disable mouse-focus for button controls.
185    * Button form controls are mouse-focusable since Chromium 30.  We want the
186    * old behavior in some WebUI pages.
187    */
188   FocusManager.disableMouseFocusOnButtons = function() {
189     document.addEventListener('mousedown', function(event) {
190       var node = event.target;
191       var tagName = node.tagName;
192       if (tagName != 'BUTTON' && tagName != 'INPUT') {
193         do {
194           node = node.parentNode;
195           if (!node || node.nodeType != Node.ELEMENT_NODE)
196             return;
197         } while (node.tagName != 'BUTTON');
198       }
199       var type = node.type;
200       if (type == 'button' || type == 'reset' || type == 'submit' ||
201           type == 'radio' || type == 'checkbox') {
202         if (document.activeElement != node)
203           document.activeElement.blur();
204
205         // Focus the current window so that if the active element is in another
206         // window, it is deactivated.
207         window.focus();
208         event.preventDefault();
209       }
210     }, false);
211   };
212
213   return {
214     FocusManager: FocusManager,
215   };
216 });