Home | History | Annotate | Download | only in webapp
      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