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: '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' 52}; 53 54/** @const */ 55remoting.kIT2MeVisitedStorageKey = 'it2me-visited'; 56/** @const */ 57remoting.kMe2MeVisitedStorageKey = 'me2me-visited'; 58 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. 64 */ 65remoting.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; 70 } 71 } 72 return false; 73}; 74 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. 81 */ 82remoting.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; 93 } 94 } 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(); 102 } 103 } 104 } 105}; 106 107/** 108 * @type {remoting.AppMode} The current app mode 109 */ 110remoting.currentMode = remoting.AppMode.HOME; 111 112/** 113 * Change the app's modal state to |mode|, determined by the data-ui-mode 114 * attribute. 115 * 116 * @param {remoting.AppMode} mode The new modal state. 117 */ 118remoting.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); 131 } 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'); 144 } 145}; 146 147/** 148 * Get the major mode that the app is running in. 149 * @return {string} The app's current major mode. 150 */ 151remoting.getMajorMode = function() { 152 return remoting.currentMode.split('.')[0]; 153}; 154 155/** 156 * Helper function for showing or hiding the infographic UI based on 157 * whether or not the user has already dismissed it. 158 * 159 * @param {string} mode 160 * @param {!Object} items 161 */ 162remoting.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; 170}; 171 172remoting.showOrHideIT2MeUi = function() { 173 chrome.storage.local.get(remoting.kIT2MeVisitedStorageKey, 174 remoting.showOrHideCallback.bind(null, 'it2me')); 175}; 176 177remoting.showOrHideMe2MeUi = function() { 178 chrome.storage.local.get(remoting.kMe2MeVisitedStorageKey, 179 remoting.showOrHideCallback.bind(null, 'me2me')); 180}; 181 182remoting.showIT2MeUiAndSave = function() { 183 var items = {}; 184 items[remoting.kIT2MeVisitedStorageKey] = true; 185 chrome.storage.local.set(items); 186 remoting.showOrHideCallback('it2me', [true]); 187}; 188 189remoting.showMe2MeUiAndSave = function() { 190 var items = {}; 191 items[remoting.kMe2MeVisitedStorageKey] = true; 192 chrome.storage.local.set(items); 193 remoting.showOrHideCallback('me2me', [true]); 194}; 195 196remoting.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]); 201} 202 203 204/** 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. 208 */ 209remoting.initModalDialogs = function() { 210 var dialogs = document.querySelectorAll('.kd-modaldialog'); 211 var observer = new MutationObserver(confineOrRestoreFocus_); 212 var options = { 213 subtree: false, 214 attributes: true 215 }; 216 for (var i = 0; i < dialogs.length; ++i) { 217 observer.observe(dialogs[i], options); 218 } 219}; 220 221/** 222 * @param {Array.<MutationRecord>} mutations The set of mutations affecting 223 * an observed node. 224 */ 225function 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); 241 } 242 } 243 } 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); 252 } 253 } 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; 267 } 268 } 269 if (removeFromKeyboardNavigation) { 270 element.setAttribute(kSavedAttributeName, element.tabIndex); 271 element.tabIndex = -1; 272 } 273 } 274 } 275} 276