Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / ui / webui / resources / js / cr / ui / focus_row.js
1 // Copyright 2014 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    * A class to manage focus between given horizontally arranged elements.
8    * For example, given the page:
9    *
10    *   <input type="checkbox"> <label>Check me!</label> <button>X</button>
11    *
12    * One could create a FocusRow by doing:
13    *
14    *   new cr.ui.FocusRow([checkboxEl, labelEl, buttonEl])
15    *
16    * if there are references to each node or querying them from the DOM like so:
17    *
18    *   new cr.ui.FocusRow(dialog.querySelectorAll('list input[type=checkbox]'))
19    *
20    * Pressing left cycles backward and pressing right cycles forward in item
21    * order. Pressing Home goes to the beginning of the list and End goes to the
22    * end of the list.
23    *
24    * If an item in this row is focused, it'll stay active (accessible via tab).
25    * If no items in this row are focused, the row can stay active until focus
26    * changes to a node inside |this.boundary_|. If opt_boundary isn't
27    * specified, any focus change deactivates the row.
28    *
29    * @param {!Array.<!Element>|!NodeList} items Elements to track focus of.
30    * @param {Node=} opt_boundary Focus events are ignored outside of this node.
31    * @param {FocusRow.Delegate=} opt_delegate A delegate to handle key events.
32    * @param {FocusRow.Observer=} opt_observer An observer that's notified if
33    *     this focus row is added to or removed from the focus order.
34    * @constructor
35    */
36   function FocusRow(items, opt_boundary, opt_delegate, opt_observer) {
37     /** @type {!Array.<!Element>} */
38     this.items = Array.prototype.slice.call(items);
39     assert(this.items.length > 0);
40
41     /** @type {!Node} */
42     this.boundary_ = opt_boundary || document;
43
44     /** @private {cr.ui.FocusRow.Delegate|undefined} */
45     this.delegate_ = opt_delegate;
46
47     /** @private {cr.ui.FocusRow.Observer|undefined} */
48     this.observer_ = opt_observer;
49
50     /** @private {!EventTracker} */
51     this.eventTracker_ = new EventTracker;
52     this.eventTracker_.add(cr.doc, 'focusin', this.onFocusin_.bind(this));
53     this.eventTracker_.add(cr.doc, 'keydown', this.onKeydown_.bind(this));
54
55     this.items.forEach(function(item) {
56       if (item != document.activeElement)
57         item.tabIndex = -1;
58
59       this.eventTracker_.add(item, 'mousedown', this.onMousedown_.bind(this));
60     }, this);
61
62     /**
63      * The index that should be actively participating in the page tab order.
64      * @type {number}
65      * @private
66      */
67     this.activeIndex_ = this.items.indexOf(document.activeElement);
68   }
69
70   /** @interface */
71   FocusRow.Delegate = function() {};
72
73   FocusRow.Delegate.prototype = {
74     /**
75      * Called when a key is pressed while an item in |this.items| is focused. If
76      * |e|'s default is prevented, further processing is skipped.
77      * @param {cr.ui.FocusRow} row The row that detected a keydown.
78      * @param {Event} e
79      * @return {boolean} Whether the event was handled.
80      */
81     onKeydown: assertNotReached,
82
83     /**
84      * @param {cr.ui.FocusRow} row The row that detected the mouse going down.
85      * @param {Event} e
86      * @return {boolean} Whether the event was handled.
87      */
88     onMousedown: assertNotReached,
89   };
90
91   /** @interface */
92   FocusRow.Observer = function() {};
93
94   FocusRow.Observer.prototype = {
95     /**
96      * Called when the row is activated (added to the focus order).
97      * @param {cr.ui.FocusRow} row The row added to the focus order.
98      */
99     onActivate: assertNotReached,
100
101     /**
102      * Called when the row is deactivated (removed from the focus order).
103      * @param {cr.ui.FocusRow} row The row removed from the focus order.
104      */
105     onDeactivate: assertNotReached,
106   };
107
108   FocusRow.prototype = {
109     get activeIndex() {
110       return this.activeIndex_;
111     },
112     set activeIndex(index) {
113       var wasActive = this.items[this.activeIndex_];
114       if (wasActive)
115         wasActive.tabIndex = -1;
116
117       this.items.forEach(function(item) { assert(item.tabIndex == -1); });
118       this.activeIndex_ = index;
119
120       if (this.items[index])
121         this.items[index].tabIndex = 0;
122
123       if (!this.observer_)
124         return;
125
126       var isActive = index >= 0 && index < this.items.length;
127       if (isActive == !!wasActive)
128         return;
129
130       if (isActive)
131         this.observer_.onActivate(this);
132       else
133         this.observer_.onDeactivate(this);
134     },
135
136     /**
137      * Focuses the item at |index|.
138      * @param {number} index An index to focus. Must be between 0 and
139      *     this.items.length - 1.
140      */
141     focusIndex: function(index) {
142       this.items[index].focus();
143     },
144
145     /** Call this to clean up event handling before dereferencing. */
146     destroy: function() {
147       this.eventTracker_.removeAll();
148     },
149
150     /**
151      * @param {Event} e The focusin event.
152      * @private
153      */
154     onFocusin_: function(e) {
155       if (this.boundary_.contains(assertInstanceof(e.target, Node)))
156         this.activeIndex = this.items.indexOf(e.target);
157     },
158
159     /**
160      * @param {Event} e A focus event.
161      * @private
162      */
163     onKeydown_: function(e) {
164       var item = this.items.indexOf(e.target);
165       if (item < 0)
166         return;
167
168       if (this.delegate_ && this.delegate_.onKeydown(this, e))
169         return;
170
171       var index = -1;
172
173       if (e.keyIdentifier == 'Left')
174         index = item + (isRTL() ? 1 : -1);
175       else if (e.keyIdentifier == 'Right')
176         index = item + (isRTL() ? -1 : 1);
177       else if (e.keyIdentifier == 'Home')
178         index = 0;
179       else if (e.keyIdentifier == 'End')
180         index = this.items.length - 1;
181
182       if (!this.items[index])
183         return;
184
185       this.focusIndex(index);
186       e.preventDefault();
187     },
188
189     /**
190      * @param {Event} e A click event.
191      * @private
192      */
193     onMousedown_: function(e) {
194       if (this.delegate_ && this.delegate_.onMousedown(this, e))
195         return;
196
197       if (!e.button)
198         this.activeIndex = this.items.indexOf(e.currentTarget);
199     },
200   };
201
202   return {
203     FocusRow: FocusRow,
204   };
205 });