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