Update UI and catch executeScript errors now shown in Canary

Update the settings and popup pages. Disable the whitelist box when it's not relevant (the checkbox is not checked). Catch errors from the initial executeScript calls running on prohibited pages, because they're now displayed on the Extensions page otherwise.

BUG=645639
R=rdevlin.cronin@chromium.org

Review URL: https://codereview.chromium.org//2400303003 .
diff --git a/go-back-with-backspace/_locales/en/messages.json b/go-back-with-backspace/_locales/en/messages.json
index 481193a..a403ed0 100644
--- a/go-back-with-backspace/_locales/en/messages.json
+++ b/go-back-with-backspace/_locales/en/messages.json
@@ -11,20 +11,24 @@
     "message": "Go Back With Backspace Options",
     "description": "Used in the page title and top heading of the options page."
   },
+  "optionsAppletGroup": {
+    "message": "Applets",
+    "description": "Label for the group of settings that control this extension's behavior in embedded applets (Flash, etc.)"
+  },
   "optionsBlacklist": {
-    "message": "Backspace should never navigate back on these pages (one per line):",
+    "message": "Backspace should never go back on these pages (one per line):",
     "description": "Heading for the text entry box in which the user enters a list of pages on which the backspace extension should never go back."
   },
   "optionsAppletCheckbox": {
-    "message": "Backspace should also not navigate back in embedded applets (Flash, Java, etc.).",
+    "message": "Backspace should not go back in applets (Flash, Java, PDF, and certain chat tools).",
     "description": "Label for a checkbox indicating whether the extension should go back when focus is in embedded content such as a Java or Flash applet."
   },
   "optionsWhitelist": {
-    "message": "But even in an embedded applet, backspace should navigate back on these pages:",
+    "message": "Even in an applet, backspace should go back on these pages:",
     "description": "Heading for the text entry box in which the user enters a list of pages on which the backspace extension should go back even when focus is in embedded content."
   },
   "optionsSave": {
-    "message": "Done",
+    "message": "OK",
     "description": "Label for the button that saves settings and closes the dialog."
   },
   "optionsCancel": {
@@ -46,7 +50,7 @@
     }
   },
   "popupDisallowedURL": {
-    "message": "The extension is unable to work on this page.",
+    "message": "Extensions prohibited on this page.",
     "description": "Status message shown in the popup when the current page is one of the special pages on which extensions are prohibited."
   },
   "popupFileURL": {
@@ -54,11 +58,11 @@
     "description": "Text of a link shown in the popup when the current page is a file:// URL, whether or not the extension currently has access to file:// URLs. The link leads to chrome://extensions."
   },
   "popupAddBlacklist": {
-    "message": "Never go back on this page",
+    "message": "Disable on this page",
     "description": "Label for the button in the popup that adds the current pageto a blacklist, on which the extension will never navigate back."
   },
   "popupRemoveBlacklist": {
-    "message": "Start going back from this page again",
+    "message": "Enable on this page",
     "description": "Label for the button in the popup that removes the current page from the list that controls when the extension won't navigate back from a page at all."
   },
   "popupStatusSaved": {
@@ -89,7 +93,7 @@
   },
   "reportBody": {
     "message": "Is something not working properly? A feature you'd like to see in this extension? Let us know! (We won't be able to respond to every piece of feedback individually, but we appreciate your time and thoughts.)",
-    "description": "Initial body of the feedback email created by the 'Send feedback' button in places where there is no relevant URL (e.g., the installation notes page)."
+    "description": "Initial body of the feedback email created by the 'Send feedback' button in places where there is no relevant URL (e.g., the options page)."
   },
   "reportBodyWithURL": {
     "message": "The extension doesn't do the right thing on the page at\n$URL$.\n\nAdd any details below. Which part of the page doesn't work properly? (We won't be able to respond to every piece of feedback individually, but we appreciate your time and thoughts.)",
diff --git a/go-back-with-backspace/background.js b/go-back-with-backspace/background.js
index 269b0bd..9c715aa 100644
--- a/go-back-with-backspace/background.js
+++ b/go-back-with-backspace/background.js
@@ -29,14 +29,27 @@
   chrome.tabs.query({}, function(tabs) {
     tabs.forEach(function(tab) {
       scripts.forEach(function(script) {
-        // This will produce an error if extensions are prohibited on the
-        // tab (e.g., chrome:// pages), but we can ignore it.
-        chrome.tabs.executeScript(tab.id,
-                                  {
-                                    file: script,
-                                    allFrames: true,
-                                    runAt: 'document_start'
-                                  });
+        chrome.tabs.executeScript(
+          tab.id,
+          {
+            file: script,
+            allFrames: true,
+            runAt: 'document_start'
+          },
+          function() {
+            if (chrome.runtime.lastError) {
+              // An error will occur if extensions are prohibited on the
+              // tab (e.g., chrome:// pages). We don't need to do anything
+              // about that, but we do need to catch it here to avoid an
+              // error message on the Extensions page.
+              var message = chrome.runtime.lastError.message;
+              if (message != 'Cannot access a chrome:// URL' &&
+                  message != 'The extensions gallery cannot be scripted.' &&
+                  !message.startsWith('Cannot access contents of the page.')) {
+                throw message;
+              }
+            }
+          });
       });
     });
   });
diff --git a/go-back-with-backspace/content_script.js b/go-back-with-backspace/content_script.js
index cc7fca6..c462d5b 100644
--- a/go-back-with-backspace/content_script.js
+++ b/go-back-with-backspace/content_script.js
@@ -44,14 +44,16 @@
     return;
 
   // The whitelist overrides applet focus.
-  // Listening on the Window means the event has no path (see
-  // http://crbug.com/645527), so we'll have to look at the focused (active)
-  // element. This means it will not work properly with shadow DOM.
-  // TODO: Fix behavior with shadow DOM when the above bug is resolved.
-  if (!options.whitelist.includes(url) &&
-      disabledInApplet(document.activeElement))
+  // Before Chrome 55, listening on the Window means the event has no path,
+  // instead pointing to the Window (see http://crbug.com/645527). In that
+  // case, we have to look at the focused (active) element.
+  // TODO: Switch entirely to e.path once Chrome 55 is launched.
+  var target = e.path[0];
+  if (target === window)
+    target = document.activeElement;
+  if (disabledInApplet(url, target))
     return;
-  if (isEditable(document.activeElement))
+  if (isEditable(target))
     return;
 
   // Make sure this extension is still active.
@@ -78,9 +80,10 @@
 }
 
 // Return true if the option to disable the extension in applets is enabled,
-// and focus is in an embedded Flash or Java applet.
-function disabledInApplet(target) {
-  if (!options.disableInApplets)
+// focus is in an embedded Flash or Java applet, and the current page is not
+// in the whitelist of pages for which that should not apply.
+function disabledInApplet(url, target) {
+  if (!options.disableInApplets || options.whitelist.includes(url))
     return false;
 
   var nodeName = target.nodeName.toUpperCase();
diff --git a/go-back-with-backspace/manifest.json b/go-back-with-backspace/manifest.json
index bd8308d..2e7c2ef 100644
--- a/go-back-with-backspace/manifest.json
+++ b/go-back-with-backspace/manifest.json
@@ -37,6 +37,7 @@
   "permissions": [
     "management",
     "storage",
+    "tabs",
     "<all_urls>"
   ]
 }
diff --git a/go-back-with-backspace/pages/common.js b/go-back-with-backspace/pages/common.js
index 9fb28c5..d0e32e4 100644
--- a/go-back-with-backspace/pages/common.js
+++ b/go-back-with-backspace/pages/common.js
@@ -2,26 +2,35 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+// Define the customary shorthand for getElementById.
+var $ = document.getElementById.bind(document);
+
 // Load strings from messages.json into the HTML page. Each element that needs
 // an internationalized string should have an 'i18n' property holding the
-// name of the message to be used.
-function LoadInternationalizedStrings() {
+// name of the message to be used. If the element instead has an 'i18n-alt'
+// property, that message will be used for the alt and title attributes of the
+// element (generally an image).
+function loadInternationalizedStrings() {
   var all = document.querySelectorAll('[i18n]');
+  for (var i = 0; i < all.length; ++i)
+    all[i].textContent = chrome.i18n.getMessage(all[i].getAttribute('i18n'));
+
+  all = document.querySelectorAll('[i18n-alt]');
   for (var i = 0; i < all.length; ++i) {
-    var i18n = all[i].getAttribute('i18n');
-    if (i18n)
-      all[i].textContent = chrome.i18n.getMessage(i18n);
+      var message = chrome.i18n.getMessage(all[i].getAttribute('i18n-alt'));
+      all[i].alt = message;
+      all[i].title = message;
   }
 }
 
 // Open a pre-filled email to send feedback to the extension developers. The
 // initial content of the email depends on whether the URL of the current page
 // is provided.
-function reportPage(url) {
+function sendFeedback(opt_url) {
   var subject = chrome.i18n.getMessage('reportSubject');
   var body = '';
-  if (url)
-    body = chrome.i18n.getMessage('reportBodyWithURL', url);
+  if (opt_url)
+    body = chrome.i18n.getMessage('reportBodyWithURL', opt_url);
   else
     body = chrome.i18n.getMessage('reportBody');
   var msg = 'mailto:gobackwithbackspace@google.com' +
diff --git a/go-back-with-backspace/pages/feedback.png b/go-back-with-backspace/pages/feedback.png
new file mode 100755
index 0000000..0d05bef
--- /dev/null
+++ b/go-back-with-backspace/pages/feedback.png
Binary files differ
diff --git a/go-back-with-backspace/pages/options.css b/go-back-with-backspace/pages/options.css
index 936a461..ff17621 100644
--- a/go-back-with-backspace/pages/options.css
+++ b/go-back-with-backspace/pages/options.css
@@ -5,37 +5,55 @@
  */
 
 body {
-  padding: 10px;
+  background-color: white;
   direction: __MSG_@@bidi_dir__;
+  margin: 0;
+  padding: 10px;
 }
 
 textarea {
+  -webkit-padding-start: 4px;
+  margin-top: 2px;
   width: 100%;
-  margin: 0;
-  padding: 0;
 }
 
-.applets {
-  padding-top: 20px;
+input {
+  margin-bottom: 20px;
 }
 
-.whitelist {
-  padding-top: 10px;
-  padding-__MSG_@@bidi_start_edge__: 40px;
+fieldset {
+  border: solid 1px rgb(204, 204, 204);
+  border-radius: 3px;
+  padding: 4px 8px;
+}
+
+fieldset legend {
+  color: rgb(0, 51, 102);
+}
+
+.feedback-button {
+  padding: 5px 0;
+  text-align: end;
+}
+
+.setting {
+  margin: 2px 0;
+}
+
+.sub-setting {
+  -webkit-margin-start: 10px;
+  margin-top: 5px;
 }
 
 .buttons {
-  padding-top: 2px;
-  padding-bottom: 1px;
-  padding-__MSG_@@bidi_start_edge__: 20px;
-  padding-__MSG_@@bidi_end_edge__: 0px;
-  text-align: __MSG_@@bidi_end_edge__;
+  -webkit-margin-end: 0;
+  -webkit-margin-start: 20px;
+  margin-bottom: 1px;
+  margin-top: 2px;
+  text-align: end;
 }
 
 #error {
   color: red;
-}
-
-#report_page {
-  float: __MSG_@@bidi_start_edge__;
+  margin: 5px 0;
 }
diff --git a/go-back-with-backspace/pages/options.html b/go-back-with-backspace/pages/options.html
index aac5995..9375a9f 100644
--- a/go-back-with-backspace/pages/options.html
+++ b/go-back-with-backspace/pages/options.html
@@ -6,23 +6,32 @@
 </head>
 
 <body>
-  <span i18n="optionsBlacklist"></span><br>
-  <textarea id="blacklist" rows="8"></textarea><br>
+  <div class="feedback-button">
+    <a href="" i18n="sendFeedback" id="feedback-link"></a>
+  </div>
 
-  <div class="applets">
-    <input type="checkbox" id="disableInApplets">
-      <label i18n="optionsAppletCheckbox" for="disableInApplets"></label><br>
-    <div class="whitelist">
-      <span i18n="optionsWhitelist" class="toggled"></span><br>
-      <textarea id="whitelist" rows="8" class="toggled"></textarea><br>
+  <div>
+    <div class="setting">
+      <span i18n="optionsBlacklist"></span><br>
+      <textarea id="blacklist" rows="8"></textarea><br>
     </div>
+
+    <fieldset>
+      <legend i18n="optionsAppletGroup" id="fieldset-legend"></legend>
+      <input type="checkbox" id="disableInApplets" class="setting">
+      <label i18n="optionsAppletCheckbox" for="disableInApplets"></label>
+      <div class="sub-setting">
+        <span i18n="optionsWhitelist" class="toggled"></span><br>
+        <textarea id="whitelist" rows="8" class="toggled"></textarea>
+      </div>
+    </fieldset>
   </div>
 
   <div id="error">&nbsp;</div>
-  <a href="" i18n="sendFeedback" id="report_page"></a>
+
   <div class="buttons">
-    <button i18n="optionsSave" id="done_button"></button>
-    <button i18n="optionsCancel" id="cancel_button"></button>
+    <button i18n="optionsSave" id="done-button"></button>
+    <button i18n="optionsCancel" id="cancel-button"></button>
   </div>
 
   <script src="common.js"></script>
diff --git a/go-back-with-backspace/pages/options.js b/go-back-with-backspace/pages/options.js
index 9ff25d6..dfb03dc 100644
--- a/go-back-with-backspace/pages/options.js
+++ b/go-back-with-backspace/pages/options.js
@@ -2,33 +2,34 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+// Disable the whitelist field when it's not relevant.
+function updateEnabledInputs() {
+  $('whitelist').disabled = !$('disableInApplets').checked;
+}
+
 // Initialize the page.
 function init() {
-  LoadInternationalizedStrings();
-
-  var blacklist = document.getElementById('blacklist');
-  var checkbox = document.getElementById('disableInApplets');
-  var whitelist = document.getElementById('whitelist');
+  loadInternationalizedStrings();
 
   // Configure the textboxes, allowing 200 characters for JSON serialization
   // and key length.
-  blacklist.maxlength = chrome.storage.sync.QUOTA_BYTES_PER_ITEM - 200;
-  whitelist.maxlength = chrome.storage.sync.QUOTA_BYTES_PER_ITEM - 200;
+  $('blacklist').maxlength = chrome.storage.sync.QUOTA_BYTES_PER_ITEM - 200;
+  $('whitelist').maxlength = chrome.storage.sync.QUOTA_BYTES_PER_ITEM - 200;
 
   // Set event handlers.
-  document.getElementById('done_button').onclick = function() {
+  $('done-button').onclick = function() {
     chrome.storage.sync.set({
       // Split the lists into arrays at whitespace before saving.
-      blacklist:
-          document.getElementById('blacklist').value.split(/\s+/),
-      disableInApplets: document.getElementById('disableInApplets').checked,
-      whitelist:
-          document.getElementById('whitelist').value.split(/\s+/)
+      // TODO: Validate the items as URLs and display a warning in case of
+      // errors.
+      blacklist: $('blacklist').value.split(/\s+/),
+      disableInApplets: $('disableInApplets').checked,
+      whitelist: $('whitelist').value.split(/\s+/)
     }, function() {
       // One easy way to force an error for testing is to change "sync" to
       // "managed" in the chrome.storage.sync.set() call above.
       if (chrome.runtime.lastError) {
-        document.getElementById('error').textContent =
+        $('error').textContent =
             chrome.i18n.getMessage('errorSaving',
                                    chrome.runtime.lastError.message);
       } else {
@@ -37,12 +38,10 @@
     });
   };
 
-  document.getElementById('cancel_button').onclick = function() {
-    window.close();
-  };
-
-  document.getElementById('report_page').onclick = function() {
-    reportPage();
+  $('disableInApplets').onchange = updateEnabledInputs;
+  $('cancel-button').onclick = window.close.bind(window);
+  $('feedback-link').onclick = function() {
+    sendFeedback();
   };
 
   // Load saved settings into the form fields.
@@ -51,10 +50,12 @@
     disableInApplets: true,
     whitelist: []
   }, function(items) {
-    blacklist.value = items.blacklist.join('\n');
-    checkbox.checked = items.disableInApplets;
-    whitelist.value = items.whitelist.join('\n');
+    $('blacklist').value = items.blacklist.join('\n');
+    $('disableInApplets').checked = items.disableInApplets;
+    $('whitelist').value = items.whitelist.join('\n');
+
+    updateEnabledInputs();
   });
 }
 
-window.addEventListener('load', init, false);
+window.addEventListener('load', init);
diff --git a/go-back-with-backspace/pages/popup.css b/go-back-with-backspace/pages/popup.css
index 3dabde5..db1da7e 100644
--- a/go-back-with-backspace/pages/popup.css
+++ b/go-back-with-backspace/pages/popup.css
@@ -6,35 +6,85 @@
 
 body {
   direction: __MSG_@@bidi_dir__;
-  width: 300px;
+  margin: 0;
+  text-align: center;
+  width: 270px;
 }
 
-#list_edit_button, #messages  {
+h1 {
+  -webkit-margin-start: 8px;
+  font-size: 17px;
+  margin-top: 4px;
+  text-align: left;
+}
+
+.bottom-bar {
+  align-items: center;
+  background-color: rgb(241, 241, 241);
+  border-top: 1px solid rgb(204, 204, 204);
+  height: 33px;
+  margin-top: 2px;
+  padding: 0 5px;
+}
+
+.bottom-bar > span {
+  padding-top: 5px;
+}
+
+.button-image {
+  background-position: center;
+  background-repeat: no-repeat;
+  background-size: 20px 20px;
+  cursor: pointer;
+  height: 24px;
+  opacity: .54;
+  outline: none;
+  width: 24px;
+}
+
+#list-edit-button {
+  background-image: -webkit-linear-gradient(top,
+                                            rgb(77, 144, 254),
+                                            rgb(71, 135, 237));
+  border: 1px solid rgb(48, 121, 237);
+  border-radius: 2px;
+  color: white;
+  display: inline-block;
+  font-size: 13px;
+  height: 30px;
+  line-height: 30px;
   margin-bottom: 10px;
+  padding: 0 8px;
 }
 
-#list_edit_button {
-  margin-top: 10px;
+#list-edit-button:hover:enabled {
+  background-image: -webkit-linear-gradient(top,
+                                            rgb(77, 144, 254)
+                                            rgb(53, 122, 232));
+  border: 1px solid rgb(47, 91, 183);
 }
 
-#current_url {
-  text-overflow: ellipsis;
-  white-space: nowrap;
-  overflow: hidden;
+#list-edit-button:disabled {
+  opacity: 0.5;
+}
+
+#file-url-message {
+  margin-bottom: 5px;
 }
 
 #status {
   color: green;
+  line-height: 33px;
 }
 
 #status.error {
   color: red;
 }
 
-#report_page {
+#feedback-button {
   float: __MSG_@@bidi_start_edge__;
 }
 
-#open_options {
+#options-button {
   float: __MSG_@@bidi_end_edge__;
 }
diff --git a/go-back-with-backspace/pages/popup.html b/go-back-with-backspace/pages/popup.html
index ff4d6ed..be76b3b 100644
--- a/go-back-with-backspace/pages/popup.html
+++ b/go-back-with-backspace/pages/popup.html
@@ -7,20 +7,27 @@
 
 <body>
   <h1 i18n="extensionName"></h1>
-  <div id="current_url"></div>
-  <div>
-    <button i18n="popupAddBlacklist" id="list_edit_button"></button><br>
+  <button i18n="popupAddBlacklist" id="list-edit-button"></button><br>
+
+  <div id="file-url-message" hidden>
+    <a href="" i18n="popupFileURL" id="open-extensions"></a>
   </div>
 
-  <div id="messages">
-    <div id="status">&nbsp;</div>
-    <div id="file_url_message" hidden>
-      <a href="" i18n="popupFileURL" id="open_extensions"></a>
-    </div>
-  </div>
+  <div class="bottom-bar">
+    <span id="feedback-button">
+      <a id="feedback-link" class="image-link">
+        <img class="button-image" src="feedback.png" i18n-alt="sendFeedback">
+      </a>
+    </span>
 
-  <a href="" i18n="sendFeedback" id="report_page"></a>
-  <a href="" i18n="openOptions" id="open_options"></a>
+    <span id="status">&nbsp;</span>
+
+    <span id="options-button">
+      <a id="options-link" class="image-link">
+        <img class="button-image" src="settings.png" i18n-alt="openOptions">
+      </a>
+    </span>
+  </div>
 
   <script src="common.js"></script>
   <script src="popup.js"></script>
diff --git a/go-back-with-backspace/pages/popup.js b/go-back-with-backspace/pages/popup.js
index aa4666b..02fbf61 100644
--- a/go-back-with-backspace/pages/popup.js
+++ b/go-back-with-backspace/pages/popup.js
@@ -25,7 +25,7 @@
 // Update the popup display depending on the current options and the URL of
 // the current page.
 function updatePopup(options, url) {
-  var button = document.getElementById('list_edit_button');
+  var button = $('list-edit-button');
 
   if (!urlAllowed(url, options.file_ok)) {
     button.textContent = chrome.i18n.getMessage('popupAddBlacklist');
@@ -40,35 +40,32 @@
   // If the current page is prohibited because it's a file:// scheme, show a
   // link to chrome://extensions, where that setting can be changed.
   if (!options.file_ok && url.startsWith('file://')) {
-    document.getElementById('file_url_message').hidden = false;
-    document.getElementById('status').hidden = true;
+    $('file-url-message').hidden = false;
+    $('status').hidden = true;
   }
 }
 
 // Set the status text and display it in the error style.
 function setStatusError(message_id, placeholder) {
   setStatus(message_id, placeholder);
-  document.getElementById('status').classList.add('error');
+  $('status').classList.add('error');
 }
 
 // Set the status text.
 function setStatus(message_id, placeholder) {
-  var status = document.getElementById('status');
   if (placeholder === undefined)
-    status.textContent = chrome.i18n.getMessage(message_id);
+    $('status').textContent = chrome.i18n.getMessage(message_id);
   else
-    status.textContent = chrome.i18n.getMessage(message_id, placeholder);
+    $('status').textContent = chrome.i18n.getMessage(message_id, placeholder);
 }
 
 // Initialize the page.
 function init() {
-  LoadInternationalizedStrings();
+  loadInternationalizedStrings();
 
   // Load the active tab's URL, then set up page features that depend on it.
   chrome.tabs.query({active: true, currentWindow: true}, function(tabList) {
     var url = tabList[0].url;
-    var element = document.getElementById('current_url');
-    element.textContent = chrome.i18n.getMessage('popupCurrentURL', url);
 
     // Load settings.
     var options = {};
@@ -86,7 +83,7 @@
     });
 
     // Set event handlers that depend on the URL.
-    document.getElementById('list_edit_button').onclick = function() {
+    $('list-edit-button').onclick = function() {
       var index = options.blacklist.indexOf(url);
       if (index > -1)
         options.blacklist.splice(index, 1);
@@ -106,20 +103,20 @@
       });
     };
 
-    document.getElementById('report_page').onclick = function() {
-      reportPage(url);
+    $('feedback-link').onclick = function() {
+      sendFeedback(url);
     };
   });
 
   // Set event handlers that don't need the URL.
-  document.getElementById('open_options').onclick = function() {
+  $('options-link').onclick = function() {
     chrome.runtime.openOptionsPage();
   };
 
-  document.getElementById('open_extensions').onclick = function() {
+  $('open-extensions').onclick = function() {
     chrome.tabs.create({url: 'chrome://extensions', active:true});
     window.close();
   };
 }
 
-window.addEventListener('load', init, false);
+window.addEventListener('load', init);
diff --git a/go-back-with-backspace/pages/settings.png b/go-back-with-backspace/pages/settings.png
new file mode 100755
index 0000000..e84e188
--- /dev/null
+++ b/go-back-with-backspace/pages/settings.png
Binary files differ