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