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/**
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.
12 */
13
14'use strict';
15
16/** @suppress {duplicate} */
17var remoting = remoting || {};
18
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.
22remoting.AppMode = {
23  HOME: 'home',
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',
28    HOST: 'home.host',
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'
53};
54
55/** @const */
56remoting.kIT2MeVisitedStorageKey = 'it2me-visited';
57/** @const */
58remoting.kMe2MeVisitedStorageKey = 'me2me-visited';
59
60/**
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.
65 */
66remoting.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) {
70      return true;
71    }
72  }
73  return false;
74};
75
76/**
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.
82 */
83remoting.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;
94    }
95  }
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]');
101      if (autofocusNode) {
102        autofocusNode.focus();
103      }
104    }
105  }
106};
107
108/**
109 * @type {remoting.AppMode} The current app mode
110 */
111remoting.currentMode = remoting.AppMode.HOME;
112
113/**
114 * Change the app's modal state to |mode|, determined by the data-ui-mode
115 * attribute.
116 *
117 * @param {remoting.AppMode} mode The new modal state.
118 */
119remoting.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,
125                                 false);
126    if ('hidden' in document) {
127      document.addEventListener('visibilitychange',
128                                remoting.onVisibilityChanged, false);
129    } else {
130      document.addEventListener('webkitvisibilitychange',
131                                remoting.onVisibilityChanged, false);
132    }
133  } else {
134    document.addEventListener('keydown', remoting.ConnectionStats.onKeydown,
135                              false);
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
141    // is fixed.
142    var scroller = document.getElementById('scroller');
143    scroller.classList.remove('no-horizontal-scroll');
144    scroller.classList.remove('no-vertical-scroll');
145  }
146
147  remoting.testEvents.raiseEvent(remoting.testEvents.Names.uiModeChanged, mode);
148};
149
150/**
151 * Get the major mode that the app is running in.
152 * @return {string} The app's current major mode.
153 */
154remoting.getMajorMode = function() {
155  return remoting.currentMode.split('.')[0];
156};
157
158/**
159 * Helper function for showing or hiding the infographic UI based on
160 * whether or not the user has already dismissed it.
161 *
162 * @param {string} mode
163 * @param {!Object} items
164 */
165remoting.showOrHideCallback = function(mode, items) {
166  // Get the first element of a dictionary or array, without needing to know
167  // the key.
168  /** @type {string} */
169  var key = Object.keys(items)[0];
170  var visited = !!items[key];
171  document.getElementById(mode + '-first-run').hidden = visited;
172  document.getElementById(mode + '-content').hidden = !visited;
173};
174
175remoting.showOrHideIT2MeUi = function() {
176  chrome.storage.local.get(remoting.kIT2MeVisitedStorageKey,
177                           remoting.showOrHideCallback.bind(null, 'it2me'));
178};
179
180remoting.showOrHideMe2MeUi = function() {
181  chrome.storage.local.get(remoting.kMe2MeVisitedStorageKey,
182                           remoting.showOrHideCallback.bind(null, 'me2me'));
183};
184
185remoting.showIT2MeUiAndSave = function() {
186  var items = {};
187  items[remoting.kIT2MeVisitedStorageKey] = true;
188  chrome.storage.local.set(items);
189  remoting.showOrHideCallback('it2me', [true]);
190};
191
192remoting.showMe2MeUiAndSave = function() {
193  var items = {};
194  items[remoting.kMe2MeVisitedStorageKey] = true;
195  chrome.storage.local.set(items);
196  remoting.showOrHideCallback('me2me', [true]);
197};
198
199remoting.resetInfographics = function() {
200  chrome.storage.local.remove(remoting.kIT2MeVisitedStorageKey);
201  chrome.storage.local.remove(remoting.kMe2MeVisitedStorageKey);
202  remoting.showOrHideCallback('it2me', [false]);
203  remoting.showOrHideCallback('me2me', [false]);
204}
205
206
207/**
208 * Initialize all modal dialogs (class kd-modaldialog), adding event handlers
209 * to confine keyboard navigation to child controls of the dialog when it is
210 * shown and restore keyboard navigation when it is hidden.
211 */
212remoting.initModalDialogs = function() {
213  var dialogs = document.querySelectorAll('.kd-modaldialog');
214  var observer = new MutationObserver(confineOrRestoreFocus_);
215  var options = {
216    subtree: false,
217    attributes: true
218  };
219  for (var i = 0; i < dialogs.length; ++i) {
220    observer.observe(dialogs[i], options);
221  }
222};
223
224/**
225 * @param {Array.<MutationRecord>} mutations The set of mutations affecting
226 *     an observed node.
227 */
228function confineOrRestoreFocus_(mutations) {
229  // The list of mutations can include duplicates, so reduce it to a canonical
230  // show/hide list.
231  /** @type {Array.<Element>} */
232  var shown = [];
233  /** @type {Array.<Element>} */
234  var hidden = [];
235  for (var i = 0; i < mutations.length; ++i) {
236    var mutation = mutations[i];
237    if (mutation.type == 'attributes' &&
238        mutation.attributeName == 'hidden') {
239      var node = mutation.target;
240      if (node.hidden && hidden.indexOf(node) == -1) {
241        hidden.push(node);
242      } else if (!node.hidden && shown.indexOf(node) == -1) {
243        shown.push(node);
244      }
245    }
246  }
247  var kSavedAttributeName = 'data-saved-tab-index';
248  // If any dialogs have been dismissed, restore all the tabIndex attributes.
249  if (hidden.length != 0) {
250    var elements = document.querySelectorAll('[' + kSavedAttributeName + ']');
251    for (var i = 0 ; i < elements.length; ++i) {
252      var element = /** @type {Element} */ elements[i];
253      element.tabIndex = element.getAttribute(kSavedAttributeName);
254      element.removeAttribute(kSavedAttributeName);
255    }
256  }
257  // If any dialogs have been shown, confine keyboard navigation to the first
258  // one. We don't have nested modal dialogs, so this will suffice for now.
259  if (shown.length != 0) {
260    var selector = '[tabIndex],a,area,button,input,select,textarea';
261    var disable = document.querySelectorAll(selector);
262    var except = shown[0].querySelectorAll(selector);
263    for (var i = 0; i < disable.length; ++i) {
264      var element = /** @type {Element} */ disable[i];
265      var removeFromKeyboardNavigation = true;
266      for (var j = 0; j < except.length; ++j) {  // No indexOf on NodeList
267        if (element == except[j]) {
268          removeFromKeyboardNavigation = false;
269          break;
270        }
271      }
272      if (removeFromKeyboardNavigation) {
273        element.setAttribute(kSavedAttributeName, element.tabIndex);
274        element.tabIndex = -1;
275      }
276    }
277  }
278}
279
280/**
281 * @param {string} tag
282 */
283remoting.showSetupProcessingMessage = function(tag) {
284  var messageDiv = document.getElementById('host-setup-processing-message');
285  l10n.localizeElementFromTag(messageDiv, tag);
286  remoting.setMode(remoting.AppMode.HOST_SETUP_PROCESSING);
287}
288