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.
7 * Functions related to controlling the modal UI state of the app. UI states
8 * are expressed as HTML attributes with a dotted hierarchy. For example, the
9 * string 'host.shared' will match any elements with an associated attribute
10 * of 'host' or 'host.shared', showing those elements and hiding all others.
11 * Elements with no associated attribute are ignored.
16 /** @suppress {duplicate} */
17 var remoting
= remoting
|| {};
20 // TODO(jamiewalch): Move 'in-session' to a separate web-page so that the
21 // 'home' state applies to all elements and can be removed.
24 TOKEN_REFRESH_FAILED
: 'home.token-refresh-failed',
25 HOST_INSTALL
: 'home.host-install',
26 HOST_INSTALL_PROMPT
: 'home.host-install.prompt',
27 HOST_INSTALL_PENDING
: 'home.host-install.pending',
29 HOST_WAITING_FOR_CODE
: 'home.host.waiting-for-code',
30 HOST_WAITING_FOR_CONNECTION
: 'home.host.waiting-for-connection',
31 HOST_SHARED
: 'home.host.shared',
32 HOST_SHARE_FAILED
: 'home.host.share-failed',
33 HOST_SHARE_FINISHED
: 'home.host.share-finished',
34 CLIENT
: 'home.client',
35 CLIENT_UNCONNECTED
: 'home.client.unconnected',
36 CLIENT_PIN_PROMPT
: 'home.client.pin-prompt',
37 CLIENT_THIRD_PARTY_AUTH
: 'home.client.third-party-auth',
38 CLIENT_CONNECTING
: 'home.client.connecting',
39 CLIENT_CONNECT_FAILED_IT2ME
: 'home.client.connect-failed.it2me',
40 CLIENT_CONNECT_FAILED_ME2ME
: 'home.client.connect-failed.me2me',
41 CLIENT_SESSION_FINISHED_IT2ME
: 'home.client.session-finished.it2me',
42 CLIENT_SESSION_FINISHED_ME2ME
: 'home.client.session-finished.me2me',
43 CLIENT_HOST_NEEDS_UPGRADE
: 'home.client.host-needs-upgrade',
44 HISTORY
: 'home.history',
45 CONFIRM_HOST_DELETE
: 'home.confirm-host-delete',
46 HOST_SETUP
: 'home.host-setup',
47 HOST_SETUP_ASK_PIN
: 'home.host-setup.ask-pin',
48 HOST_SETUP_PROCESSING
: 'home.host-setup.processing',
49 HOST_SETUP_DONE
: 'home.host-setup.done',
50 HOST_SETUP_ERROR
: 'home.host-setup.error',
51 HOME_MANAGE_PAIRINGS
: 'home.manage-pairings',
52 IN_SESSION
: 'in-session'
56 remoting
.kIT2MeVisitedStorageKey
= 'it2me-visited';
58 remoting
.kMe2MeVisitedStorageKey
= 'me2me-visited';
61 * @param {Element} element The element to check.
62 * @param {string} attrName The attribute on the element to check.
63 * @param {Array<string>} modes The modes to check for.
64 * @return {boolean} True if any mode in |modes| is found within the attribute.
66 remoting
.hasModeAttribute = function(element
, attrName
, modes
) {
67 var attr
= element
.getAttribute(attrName
);
68 for (var i
= 0; i
< modes
.length
; ++i
) {
69 if (attr
.match(new RegExp('(\\s|^)' + modes
[i
] + '(\\s|$)')) != null) {
77 * Update the DOM by showing or hiding elements based on whether or not they
78 * have an attribute matching the specified name.
79 * @param {string} mode The value against which to match the attribute.
80 * @param {string} attr The attribute name to match.
81 * @return {void} Nothing.
83 remoting
.updateModalUi = function(mode
, attr
) {
84 var modes
= mode
.split('.');
85 for (var i
= 1; i
< modes
.length
; ++i
)
86 modes
[i
] = modes
[i
- 1] + '.' + modes
[i
];
87 var elements
= document
.querySelectorAll('[' + attr
+ ']');
88 // Hide elements first so that we don't end up trying to show two modal
89 // dialogs at once (which would break keyboard-navigation confinement).
90 for (var i
= 0; i
< elements
.length
; ++i
) {
91 var element
= /** @type {Element} */ (elements
[i
]);
92 if (!remoting
.hasModeAttribute(element
, attr
, modes
)) {
93 element
.hidden
= true;
96 for (var i
= 0; i
< elements
.length
; ++i
) {
97 var element
= /** @type {Element} */ (elements
[i
]);
98 if (remoting
.hasModeAttribute(element
, attr
, modes
)) {
99 element
.hidden
= false;
100 var autofocusNode
= element
.querySelector('[autofocus]');
102 autofocusNode
.focus();
109 * @type {remoting.AppMode} The current app mode
111 remoting
.currentMode
= remoting
.AppMode
.HOME
;
114 * Change the app's modal state to |mode|, determined by the data-ui-mode
117 * @param {remoting.AppMode} mode The new modal state.
119 remoting
.setMode = function(mode
) {
120 remoting
.updateModalUi(mode
, 'data-ui-mode');
121 console
.log('App mode: ' + mode
);
122 remoting
.currentMode
= mode
;
123 if (mode
== remoting
.AppMode
.IN_SESSION
) {
124 document
.removeEventListener('keydown', remoting
.ConnectionStats
.onKeydown
,
126 if ('hidden' in document
) {
127 document
.addEventListener('visibilitychange',
128 remoting
.onVisibilityChanged
, false);
130 document
.addEventListener('webkitvisibilitychange',
131 remoting
.onVisibilityChanged
, false);
134 document
.addEventListener('keydown', remoting
.ConnectionStats
.onKeydown
,
136 document
.removeEventListener('visibilitychange',
137 remoting
.onVisibilityChanged
, false);
138 document
.removeEventListener('webkitvisibilitychange',
139 remoting
.onVisibilityChanged
, false);
140 // TODO(jamiewalch): crbug.com/252796: Remove this once crbug.com/240772
142 var scroller
= document
.getElementById('scroller');
144 scroller
.classList
.remove('no-horizontal-scroll');
145 scroller
.classList
.remove('no-vertical-scroll');
149 remoting
.testEvents
.raiseEvent(remoting
.testEvents
.Names
.uiModeChanged
, mode
);
153 * Get the major mode that the app is running in.
154 * @return {string} The app's current major mode.
156 remoting
.getMajorMode = function() {
157 return remoting
.currentMode
.split('.')[0];
161 * Helper function for showing or hiding the infographic UI based on
162 * whether or not the user has already dismissed it.
164 * @param {string} mode
165 * @param {Object<?,string>} items
167 remoting
.showOrHideCallback = function(mode
, items
) {
168 // Get the first element of a dictionary or array, without needing to know
170 var obj
= /** @type {!Object} */(items
);
171 /** @type {string} */
172 var key
= Object
.keys(obj
)[0];
173 var visited
= !!items
[key
];
174 document
.getElementById(mode
+ '-first-run').hidden
= visited
;
175 document
.getElementById(mode
+ '-content').hidden
= !visited
;
179 * @param {Object<?,string>} items
181 remoting
.showOrHideCallbackIT2Me = function(items
) {
182 remoting
.showOrHideCallback('it2me', items
);
186 * @param {Object<?,string>} items
188 remoting
.showOrHideCallbackMe2Me = function(items
) {
189 remoting
.showOrHideCallback('me2me', items
);
192 remoting
.showOrHideIT2MeUi = function() {
193 chrome
.storage
.local
.get(remoting
.kIT2MeVisitedStorageKey
,
194 remoting
.showOrHideCallbackIT2Me
);
197 remoting
.showOrHideMe2MeUi = function() {
198 chrome
.storage
.local
.get(remoting
.kMe2MeVisitedStorageKey
,
199 remoting
.showOrHideCallbackMe2Me
);
202 remoting
.showIT2MeUiAndSave = function() {
204 items
[remoting
.kIT2MeVisitedStorageKey
] = true;
205 chrome
.storage
.local
.set(items
);
206 remoting
.showOrHideCallback('it2me', [true]);
209 remoting
.showMe2MeUiAndSave = function() {
211 items
[remoting
.kMe2MeVisitedStorageKey
] = true;
212 chrome
.storage
.local
.set(items
);
213 remoting
.showOrHideCallback('me2me', [true]);
216 remoting
.resetInfographics = function() {
217 chrome
.storage
.local
.remove(remoting
.kIT2MeVisitedStorageKey
);
218 chrome
.storage
.local
.remove(remoting
.kMe2MeVisitedStorageKey
);
219 remoting
.showOrHideCallback('it2me', [false]);
220 remoting
.showOrHideCallback('me2me', [false]);
225 * Initialize all modal dialogs (class kd-modaldialog), adding event handlers
226 * to confine keyboard navigation to child controls of the dialog when it is
227 * shown and restore keyboard navigation when it is hidden.
229 remoting
.initModalDialogs = function() {
230 var dialogs
= document
.querySelectorAll('.kd-modaldialog');
231 var observer
= new MutationObserver(confineOrRestoreFocus_
);
232 var options
= /** @type {MutationObserverInit} */({
236 for (var i
= 0; i
< dialogs
.length
; ++i
) {
237 observer
.observe(dialogs
[i
], options
);
242 * @param {Array<MutationRecord>} mutations The set of mutations affecting
245 function confineOrRestoreFocus_(mutations
) {
246 // The list of mutations can include duplicates, so reduce it to a canonical
248 /** @type {Array<Node>} */
250 /** @type {Array<Node>} */
252 for (var i
= 0; i
< mutations
.length
; ++i
) {
253 var mutation
= mutations
[i
];
254 if (mutation
.type
== 'attributes' &&
255 mutation
.attributeName
== 'hidden') {
256 var node
= mutation
.target
;
257 if (node
.hidden
&& hidden
.indexOf(node
) == -1) {
259 } else if (!node
.hidden
&& shown
.indexOf(node
) == -1) {
264 var kSavedAttributeName
= 'data-saved-tab-index';
265 // If any dialogs have been dismissed, restore all the tabIndex attributes.
266 if (hidden
.length
!= 0) {
267 var elements
= document
.querySelectorAll('[' + kSavedAttributeName
+ ']');
268 for (var i
= 0 ; i
< elements
.length
; ++i
) {
269 var element
= /** @type {Element} */ (elements
[i
]);
270 element
.tabIndex
= element
.getAttribute(kSavedAttributeName
);
271 element
.removeAttribute(kSavedAttributeName
);
274 // If any dialogs have been shown, confine keyboard navigation to the first
275 // one. We don't have nested modal dialogs, so this will suffice for now.
276 if (shown
.length
!= 0) {
277 var selector
= '[tabIndex],a,area,button,input,select,textarea';
278 var disable
= document
.querySelectorAll(selector
);
279 var except
= shown
[0].querySelectorAll(selector
);
280 for (var i
= 0; i
< disable
.length
; ++i
) {
281 var element
= /** @type {Element} */ (disable
[i
]);
282 var removeFromKeyboardNavigation
= true;
283 for (var j
= 0; j
< except
.length
; ++j
) { // No indexOf on NodeList
284 if (element
== except
[j
]) {
285 removeFromKeyboardNavigation
= false;
289 if (removeFromKeyboardNavigation
) {
290 element
.setAttribute(kSavedAttributeName
, element
.tabIndex
);
291 element
.tabIndex
= -1;
298 * @param {string} tag
300 remoting
.showSetupProcessingMessage = function(tag
) {
301 var messageDiv
= document
.getElementById('host-setup-processing-message');
302 l10n
.localizeElementFromTag(messageDiv
, tag
);
303 remoting
.setMode(remoting
.AppMode
.HOST_SETUP_PROCESSING
);