Ignore non-active fullscreen windows for shelf state.
[chromium-blink-merge.git] / remoting / webapp / ui_mode.js
blob2f3d178c75c0fb47c98d44f0fa3f37bf67ae5156
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.
5 /**
6 * @fileoverview
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.
14 'use strict';
16 /** @suppress {duplicate} */
17 var remoting = remoting || {};
19 /** @enum {string} */
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.
22 remoting.AppMode = {
23 HOME: 'home',
24 TOKEN_REFRESH_FAILED: 'home.token-refresh-failed',
25 HOST: 'home.host',
26 HOST_WAITING_FOR_CODE: 'home.host.waiting-for-code',
27 HOST_WAITING_FOR_CONNECTION: 'home.host.waiting-for-connection',
28 HOST_SHARED: 'home.host.shared',
29 HOST_SHARE_FAILED: 'home.host.share-failed',
30 HOST_SHARE_FINISHED: 'home.host.share-finished',
31 CLIENT: 'home.client',
32 CLIENT_UNCONNECTED: 'home.client.unconnected',
33 CLIENT_PIN_PROMPT: 'home.client.pin-prompt',
34 CLIENT_THIRD_PARTY_AUTH: 'home.client.third-party-auth',
35 CLIENT_CONNECTING: 'home.client.connecting',
36 CLIENT_CONNECT_FAILED_IT2ME: 'home.client.connect-failed.it2me',
37 CLIENT_CONNECT_FAILED_ME2ME: 'home.client.connect-failed.me2me',
38 CLIENT_SESSION_FINISHED_IT2ME: 'home.client.session-finished.it2me',
39 CLIENT_SESSION_FINISHED_ME2ME: 'home.client.session-finished.me2me',
40 CLIENT_HOST_NEEDS_UPGRADE: 'home.client.host-needs-upgrade',
41 HISTORY: 'home.history',
42 CONFIRM_HOST_DELETE: 'home.confirm-host-delete',
43 HOST_SETUP: 'home.host-setup',
44 HOST_SETUP_INSTALL: 'home.host-setup.install',
45 HOST_SETUP_INSTALL_PENDING: 'home.host-setup.install-pending',
46 HOST_SETUP_ASK_PIN: 'home.host-setup.ask-pin',
47 HOST_SETUP_PROCESSING: 'home.host-setup.processing',
48 HOST_SETUP_DONE: 'home.host-setup.done',
49 HOST_SETUP_ERROR: 'home.host-setup.error',
50 HOME_MANAGE_PAIRINGS: 'home.manage-pairings',
51 IN_SESSION: 'in-session'
54 /** @const */
55 remoting.kIT2MeVisitedStorageKey = 'it2me-visited';
56 /** @const */
57 remoting.kMe2MeVisitedStorageKey = 'me2me-visited';
59 /**
60 * @param {Element} element The element to check.
61 * @param {string} attrName The attribute on the element to check.
62 * @param {Array.<string>} modes The modes to check for.
63 * @return {boolean} True if any mode in |modes| is found within the attribute.
65 remoting.hasModeAttribute = function(element, attrName, modes) {
66 var attr = element.getAttribute(attrName);
67 for (var i = 0; i < modes.length; ++i) {
68 if (attr.match(new RegExp('(\\s|^)' + modes[i] + '(\\s|$)')) != null) {
69 return true;
72 return false;
75 /**
76 * Update the DOM by showing or hiding elements based on whether or not they
77 * have an attribute matching the specified name.
78 * @param {string} mode The value against which to match the attribute.
79 * @param {string} attr The attribute name to match.
80 * @return {void} Nothing.
82 remoting.updateModalUi = function(mode, attr) {
83 var modes = mode.split('.');
84 for (var i = 1; i < modes.length; ++i)
85 modes[i] = modes[i - 1] + '.' + modes[i];
86 var elements = document.querySelectorAll('[' + attr + ']');
87 // Hide elements first so that we don't end up trying to show two modal
88 // dialogs at once (which would break keyboard-navigation confinement).
89 for (var i = 0; i < elements.length; ++i) {
90 var element = /** @type {Element} */ elements[i];
91 if (!remoting.hasModeAttribute(element, attr, modes)) {
92 element.hidden = true;
95 for (var i = 0; i < elements.length; ++i) {
96 var element = /** @type {Element} */ elements[i];
97 if (remoting.hasModeAttribute(element, attr, modes)) {
98 element.hidden = false;
99 var autofocusNode = element.querySelector('[autofocus]');
100 if (autofocusNode) {
101 autofocusNode.focus();
108 * @type {remoting.AppMode} The current app mode
110 remoting.currentMode = remoting.AppMode.HOME;
113 * Change the app's modal state to |mode|, determined by the data-ui-mode
114 * attribute.
116 * @param {remoting.AppMode} mode The new modal state.
118 remoting.setMode = function(mode) {
119 remoting.updateModalUi(mode, 'data-ui-mode');
120 console.log('App mode: ' + mode);
121 remoting.currentMode = mode;
122 if (mode == remoting.AppMode.IN_SESSION) {
123 document.removeEventListener('keydown', remoting.ConnectionStats.onKeydown,
124 false);
125 if ('hidden' in document) {
126 document.addEventListener('visibilitychange',
127 remoting.onVisibilityChanged, false);
128 } else {
129 document.addEventListener('webkitvisibilitychange',
130 remoting.onVisibilityChanged, false);
132 } else {
133 document.addEventListener('keydown', remoting.ConnectionStats.onKeydown,
134 false);
135 document.removeEventListener('visibilitychange',
136 remoting.onVisibilityChanged, false);
137 document.removeEventListener('webkitvisibilitychange',
138 remoting.onVisibilityChanged, false);
139 // TODO(jamiewalch): crbug.com/252796: Remove this once crbug.com/240772
140 // is fixed.
141 var htmlNode = /** @type {HTMLElement} */ (document.body.parentNode);
142 htmlNode.classList.remove('no-horizontal-scroll');
143 htmlNode.classList.remove('no-vertical-scroll');
148 * Get the major mode that the app is running in.
149 * @return {string} The app's current major mode.
151 remoting.getMajorMode = function() {
152 return remoting.currentMode.split('.')[0];
156 * Helper function for showing or hiding the infographic UI based on
157 * whether or not the user has already dismissed it.
159 * @param {string} mode
160 * @param {!Object} items
162 remoting.showOrHideCallback = function(mode, items) {
163 // Get the first element of a dictionary or array, without needing to know
164 // the key.
165 /** @type {string} */
166 var key = Object.keys(items)[0];
167 var visited = !!items[key];
168 document.getElementById(mode + '-first-run').hidden = visited;
169 document.getElementById(mode + '-content').hidden = !visited;
172 remoting.showOrHideIT2MeUi = function() {
173 chrome.storage.local.get(remoting.kIT2MeVisitedStorageKey,
174 remoting.showOrHideCallback.bind(null, 'it2me'));
177 remoting.showOrHideMe2MeUi = function() {
178 chrome.storage.local.get(remoting.kMe2MeVisitedStorageKey,
179 remoting.showOrHideCallback.bind(null, 'me2me'));
182 remoting.showIT2MeUiAndSave = function() {
183 var items = {};
184 items[remoting.kIT2MeVisitedStorageKey] = true;
185 chrome.storage.local.set(items);
186 remoting.showOrHideCallback('it2me', [true]);
189 remoting.showMe2MeUiAndSave = function() {
190 var items = {};
191 items[remoting.kMe2MeVisitedStorageKey] = true;
192 chrome.storage.local.set(items);
193 remoting.showOrHideCallback('me2me', [true]);
196 remoting.resetInfographics = function() {
197 chrome.storage.local.remove(remoting.kIT2MeVisitedStorageKey);
198 chrome.storage.local.remove(remoting.kMe2MeVisitedStorageKey);
199 remoting.showOrHideCallback('it2me', [false]);
200 remoting.showOrHideCallback('me2me', [false]);
205 * Initialize all modal dialogs (class kd-modaldialog), adding event handlers
206 * to confine keyboard navigation to child controls of the dialog when it is
207 * shown and restore keyboard navigation when it is hidden.
209 remoting.initModalDialogs = function() {
210 var dialogs = document.querySelectorAll('.kd-modaldialog');
211 var observer = new MutationObserver(confineOrRestoreFocus_);
212 var options = {
213 subtree: false,
214 attributes: true
216 for (var i = 0; i < dialogs.length; ++i) {
217 observer.observe(dialogs[i], options);
222 * @param {Array.<MutationRecord>} mutations The set of mutations affecting
223 * an observed node.
225 function confineOrRestoreFocus_(mutations) {
226 // The list of mutations can include duplicates, so reduce it to a canonical
227 // show/hide list.
228 /** @type {Array.<Element>} */
229 var shown = [];
230 /** @type {Array.<Element>} */
231 var hidden = [];
232 for (var i = 0; i < mutations.length; ++i) {
233 var mutation = mutations[i];
234 if (mutation.type == 'attributes' &&
235 mutation.attributeName == 'hidden') {
236 var node = mutation.target;
237 if (node.hidden && hidden.indexOf(node) == -1) {
238 hidden.push(node);
239 } else if (!node.hidden && shown.indexOf(node) == -1) {
240 shown.push(node);
244 var kSavedAttributeName = 'data-saved-tab-index';
245 // If any dialogs have been dismissed, restore all the tabIndex attributes.
246 if (hidden.length != 0) {
247 var elements = document.querySelectorAll('[' + kSavedAttributeName + ']');
248 for (var i = 0 ; i < elements.length; ++i) {
249 var element = /** @type {Element} */ elements[i];
250 element.tabIndex = element.getAttribute(kSavedAttributeName);
251 element.removeAttribute(kSavedAttributeName);
254 // If any dialogs have been shown, confine keyboard navigation to the first
255 // one. We don't have nested modal dialogs, so this will suffice for now.
256 if (shown.length != 0) {
257 var selector = '[tabIndex],a,area,button,input,select,textarea';
258 var disable = document.querySelectorAll(selector);
259 var except = shown[0].querySelectorAll(selector);
260 for (var i = 0; i < disable.length; ++i) {
261 var element = /** @type {Element} */ disable[i];
262 var removeFromKeyboardNavigation = true;
263 for (var j = 0; j < except.length; ++j) { // No indexOf on NodeList
264 if (element == except[j]) {
265 removeFromKeyboardNavigation = false;
266 break;
269 if (removeFromKeyboardNavigation) {
270 element.setAttribute(kSavedAttributeName, element.tabIndex);
271 element.tabIndex = -1;