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} */ 17 var 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. 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' 52 }; 53 54 /** @const */ 55 remoting.kIT2MeVisitedStorageKey = 'it2me-visited'; 56 /** @const */ 57 remoting.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 */ 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; 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 */ 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; 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 */ 110 remoting.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 */ 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 document.addEventListener('webkitvisibilitychange', 126 remoting.onVisibilityChanged, false); 127 } else { 128 document.addEventListener('keydown', remoting.ConnectionStats.onKeydown, 129 false); 130 document.removeEventListener('webkitvisibilitychange', 131 remoting.onVisibilityChanged, false); 132 // TODO(jamiewalch): crbug.com/252796: Remove this once crbug.com/240772 133 // is fixed. 134 var htmlNode = /** @type {HTMLElement} */ (document.body.parentNode); 135 htmlNode.classList.remove('no-scroll'); 136 } 137 }; 138 139 /** 140 * Get the major mode that the app is running in. 141 * @return {string} The app's current major mode. 142 */ 143 remoting.getMajorMode = function() { 144 return remoting.currentMode.split('.')[0]; 145 }; 146 147 /** 148 * Helper function for showing or hiding the infographic UI based on 149 * whether or not the user has already dismissed it. 150 * 151 * @param {string} mode 152 * @param {!Object} items 153 */ 154 remoting.showOrHideCallback = function(mode, items) { 155 // Get the first element of a dictionary or array, without needing to know 156 // the key. 157 /** @type {string} */ 158 var key = Object.keys(items)[0]; 159 var visited = !!items[key]; 160 document.getElementById(mode + '-first-run').hidden = visited; 161 document.getElementById(mode + '-content').hidden = !visited; 162 }; 163 164 remoting.showOrHideIT2MeUi = function() { 165 chrome.storage.local.get(remoting.kIT2MeVisitedStorageKey, 166 remoting.showOrHideCallback.bind(null, 'it2me')); 167 }; 168 169 remoting.showOrHideMe2MeUi = function() { 170 chrome.storage.local.get(remoting.kMe2MeVisitedStorageKey, 171 remoting.showOrHideCallback.bind(null, 'me2me')); 172 }; 173 174 remoting.showIT2MeUiAndSave = function() { 175 var items = {}; 176 items[remoting.kIT2MeVisitedStorageKey] = true; 177 chrome.storage.local.set(items); 178 remoting.showOrHideCallback('it2me', [true]); 179 }; 180 181 remoting.showMe2MeUiAndSave = function() { 182 var items = {}; 183 items[remoting.kMe2MeVisitedStorageKey] = true; 184 chrome.storage.local.set(items); 185 remoting.showOrHideCallback('me2me', [true]); 186 }; 187 188 remoting.resetInfographics = function() { 189 chrome.storage.local.remove(remoting.kIT2MeVisitedStorageKey); 190 chrome.storage.local.remove(remoting.kMe2MeVisitedStorageKey); 191 remoting.showOrHideCallback('it2me', [false]); 192 remoting.showOrHideCallback('me2me', [false]); 193 } 194 195 196 /** 197 * Initialize all modal dialogs (class kd-modaldialog), adding event handlers 198 * to confine keyboard navigation to child controls of the dialog when it is 199 * shown and restore keyboard navigation when it is hidden. 200 */ 201 remoting.initModalDialogs = function() { 202 var dialogs = document.querySelectorAll('.kd-modaldialog'); 203 var observer = new MutationObserver(confineOrRestoreFocus_); 204 var options = { 205 subtree: false, 206 attributes: true 207 }; 208 for (var i = 0; i < dialogs.length; ++i) { 209 observer.observe(dialogs[i], options); 210 } 211 }; 212 213 /** 214 * @param {Array.<MutationRecord>} mutations The set of mutations affecting 215 * an observed node. 216 */ 217 function confineOrRestoreFocus_(mutations) { 218 // The list of mutations can include duplicates, so reduce it to a canonical 219 // show/hide list. 220 /** @type {Array.<Element>} */ 221 var shown = []; 222 /** @type {Array.<Element>} */ 223 var hidden = []; 224 for (var i = 0; i < mutations.length; ++i) { 225 var mutation = mutations[i]; 226 if (mutation.type == 'attributes' && 227 mutation.attributeName == 'hidden') { 228 var node = mutation.target; 229 if (node.hidden && hidden.indexOf(node) == -1) { 230 hidden.push(node); 231 } else if (!node.hidden && shown.indexOf(node) == -1) { 232 shown.push(node); 233 } 234 } 235 } 236 var kSavedAttributeName = 'data-saved-tab-index'; 237 // If any dialogs have been dismissed, restore all the tabIndex attributes. 238 if (hidden.length != 0) { 239 var elements = document.querySelectorAll('[' + kSavedAttributeName + ']'); 240 for (var i = 0 ; i < elements.length; ++i) { 241 var element = /** @type {Element} */ elements[i]; 242 element.tabIndex = element.getAttribute(kSavedAttributeName); 243 element.removeAttribute(kSavedAttributeName); 244 } 245 } 246 // If any dialogs have been shown, confine keyboard navigation to the first 247 // one. We don't have nested modal dialogs, so this will suffice for now. 248 if (shown.length != 0) { 249 var selector = '[tabIndex],a,area,button,input,select,textarea'; 250 var disable = document.querySelectorAll(selector); 251 var except = shown[0].querySelectorAll(selector); 252 for (var i = 0; i < disable.length; ++i) { 253 var element = /** @type {Element} */ disable[i]; 254 var removeFromKeyboardNavigation = true; 255 for (var j = 0; j < except.length; ++j) { // No indexOf on NodeList 256 if (element == except[j]) { 257 removeFromKeyboardNavigation = false; 258 break; 259 } 260 } 261 if (removeFromKeyboardNavigation) { 262 element.setAttribute(kSavedAttributeName, element.tabIndex); 263 element.tabIndex = -1; 264 } 265 } 266 } 267 } 268