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 |