Add options and a browser-action popup.

Add an options page with a blacklist, an option to disable the extension
in embedded content, and a whitelist to override that option. Add a popup
to easily add/remove the current page from the blacklist. Add a feedback
link that pre-fills an email. Update the build script with the new files.

Options page, popup, and email text are placeholders, pending design support.

BUG=639639, 639645
R=ojan@chromium.org

Review URL: https://codereview.chromium.org//2325963003 .
diff --git a/go-back-with-backspace/_locales/en/messages.json b/go-back-with-backspace/_locales/en/messages.json
index 92dfd7b..4649c05 100644
--- a/go-back-with-backspace/_locales/en/messages.json
+++ b/go-back-with-backspace/_locales/en/messages.json
@@ -1,10 +1,116 @@
 {
   "extensionName": {
     "message": "Go Back With Backspace",
-    "description": "Name of this extension."
+    "description": "Name of this extension, used in Chrome's UI as well as in the extension's own UI pages."
   },
   "extensionDescription": {
     "message": "Re-enables the backspace key as a back navigation button (except when writing text).",
     "description": "Brief description of this extension."
+  },
+  "installedTitle": {
+    "message": "Go Back With Backspace Installed",
+    "description": "title of the installation notes page shown on first installation."
+  },
+  "installedHeading": {
+    "message": "Go Back With Backspace installation notes",
+    "description": "Page heading for the installation notes page."
+  },
+  "installedNotes": {
+    "message": "Thank you for installing the \"Go Back With Backspace\" extension. After this extension is newly installed, enabled, or updated, any open tabs will need to be reloaded for the changes to take effect.",
+    "description": "Content of the installation notes page, explaining how to get the extension to workimmediately after installation."
+  },
+  "optionsTitle": {
+    "message": "Go Back With Backspace Options",
+    "description": "Used in the page title and top heading of the options page."
+  },
+  "optionsBlacklist": {
+    "message": "Backspace should never navigate 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.).",
+    "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:",
+    "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",
+    "description": "Label for the button that saves settings and closes the dialog."
+  },
+  "optionsCancel": {
+    "message": "Cancel",
+    "description": "Label for the button that closes the dialog without saving changes."
+  },
+  "popupTitle": {
+    "message": "Go Back With Backspace",
+    "description": "Title for the small popup window associated with the browser action."
+  },
+  "popupCurrentURL": {
+    "message": "Current page: $URL$",
+    "description": "Label indicating the current page's URL, as shown in the browser-action popup. Long URLs will be truncated.",
+    "placeholders": {
+      "url": {
+        "content": "$1",
+        "example": "https://www.google.com"
+      }
+    }
+  },
+  "popupDisallowedURL": {
+    "message": "The extension is unable to work 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": {
+    "message": "Allow this extension access to file URLs",
+    "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",
+    "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",
+    "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": {
+    "message": "Change saved.",
+    "description": "Status message shown in the browser action popup when the current page has been added or removed from the white- or blacklist."
+  },
+  "errorSaving": {
+    "message": "Error saving change: $ERROR$",
+    "description": "Status message shown in either the options or the popup when changes to the settings couldn't be saved.",
+    "placeholders": {
+      "error": {
+        "content": "$1",
+        "example": "Storage is full."
+      }
+    }
+  },
+  "openOptions": {
+    "message": "Options",
+    "description": "Label for the link to open the extension's options UI."
+  },
+  "sendFeedback": {
+    "message": "Send feedback",
+    "description": "Label for the link in the popup to email feedback to the extension developers."
+  },
+  "reportSubject": {
+    "message": "Go Back With Backspace feedback",
+    "description": "Initial Subject of the feedback email created by the 'Send feedback' button."
+  },
+  "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)."
+  },
+  "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.)",
+    "description": "Initial body of the feedback email created by the 'Send feedback' button in places where we have a relevant URL (e.g. the browser action popup).",
+    "placeholders": {
+      "url": {
+        "content": "$1",
+        "example": "http://www.google.com"
+      }
+    }
   }
 }
diff --git a/go-back-with-backspace/assets/icon19.png b/go-back-with-backspace/assets/icon19.png
new file mode 100644
index 0000000..07778ec
--- /dev/null
+++ b/go-back-with-backspace/assets/icon19.png
Binary files differ
diff --git a/go-back-with-backspace/assets/icon38.png b/go-back-with-backspace/assets/icon38.png
new file mode 100644
index 0000000..07778ec
--- /dev/null
+++ b/go-back-with-backspace/assets/icon38.png
Binary files differ
diff --git a/go-back-with-backspace/background.js b/go-back-with-backspace/background.js
index dada8c8..0c822a5 100644
--- a/go-back-with-backspace/background.js
+++ b/go-back-with-backspace/background.js
@@ -1,6 +1,10 @@
+// Copyright 2016 Google Inc. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
 // Put up an informative message on first install.
 chrome.runtime.onInstalled.addListener(function(details) {
   if (details.reason == "install") {
-    chrome.tabs.create({url: "installed.html"});
+    chrome.tabs.create({url: "pages/installed.html"});
   }
 });
diff --git a/go-back-with-backspace/build-zip.sh b/go-back-with-backspace/build-zip.sh
index eeb9c92..302583c 100755
--- a/go-back-with-backspace/build-zip.sh
+++ b/go-back-with-backspace/build-zip.sh
@@ -4,17 +4,32 @@
 
 # Constants
 readonly ZIP_FILE='go-back-with-backspace.zip'
-# The individual icon files are listed so an error will be raised if one is
-# missing.
-declare -ar ZIP_CONTENTS=(_locales/
+# The individual files within directories are listed so an error will be
+# raised if one is missing.
+declare -ar ZIP_CONTENTS=(LICENSE
+                          _locales/
+                          background.js
+                          content_script.js
                           icons/
                           icons/icon16.png
+                          icons/icon19.png
                           icons/icon32.png
+                          icons/icon38.png
                           icons/icon48.png
                           icons/icon128.png
-                          content_script.js
                           is_editable.js
                           manifest.json
+                          pages/
+                          pages/common.js
+                          pages/installed.css
+                          pages/installed.html
+                          pages/installed.js
+                          pages/options.css
+                          pages/options.html
+                          pages/options.js
+                          pages/popup.css
+                          pages/popup.html
+                          pages/popup.js
                           readme.txt)
 
 # Remove backup files.
diff --git a/go-back-with-backspace/content_script.js b/go-back-with-backspace/content_script.js
index ea6cecb..f80ab2a 100644
--- a/go-back-with-backspace/content_script.js
+++ b/go-back-with-backspace/content_script.js
@@ -1,18 +1,77 @@
-// Listen for shift-backspace or unmodified backspace and navigate if not in
-// an editable field. We capture the event at the Window to let any handlers
-// or listeners registered on the Document have a chance to handle it first.
-window.addEventListener('keydown', function(e) {
+// Copyright 2016 Google Inc. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Load the options, then register a keyboard listener.  We capture the event
+// at the Window to let any handlers or listeners registered on the Document
+// have a chance to handle it first.
+var options;
+chrome.storage.sync.get({
+  blacklist: [],
+  disableInApplets: true,
+  whitelist: []
+}, function(items) {
+  options = items;
+  window.addEventListener('keydown', function(e) {
+    handleBackspace(e);
+  });
+});
+
+// Update the local options when they're changed externally.
+chrome.storage.onChanged.addListener(function(changes, area) {
+  if (area === 'sync') {
+    if (changes.blacklist)
+      options.blacklist = changes.blacklist.newValue;
+    if (changes.disableInApplets)
+      options.disableInApplets = changes.disableInApplets.newValue;
+    if (changes.whitelist)
+      options.whitelist = changes.whitelist.newValue;
+  }
+});
+
+// Check for shift-backspace or unmodified backspace and navigate if
+// applicable.
+function handleBackspace(e) {
+  if (e.defaultPrevented ||
+      e.key !== 'Backspace' ||
+      e.altKey ||
+      e.ctrlKey ||
+      e.metaKey)
+    return;
+
+  // The blacklist overrides everything.
+  var url = window.location.href;
+  if (options.blacklist.includes(url))
+    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 (e.key === 'Backspace' &&
-      !e.defaultPrevented &&
-      !e.altKey &&
-      !e.ctrlKey &&
-      !e.metaKey &&
-      !isEditable(document.activeElement)) {
-    e.shiftKey ? window.history.forward(): window.history.back();
-    e.preventDefault();
+  if (!options.whitelist.includes(url) &&
+      disabledInApplet(document.activeElement))
+    return;
+  if (isEditable(document.activeElement))
+    return;
+
+  e.shiftKey ? window.history.forward(): window.history.back();
+  e.preventDefault();
+}
+
+// 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)
+    return false;
+
+  var nodeName = target.nodeName.toUpperCase();
+  var nodeType = target.type || '';
+  nodeType = nodeType.toLowerCase();
+  if ((nodeName === 'EMBED' || nodeName === 'OBJECT') &&
+      (nodeType === 'application/x-shockwave-flash' ||
+       nodeType === 'application/java')) {
+    return true;
   }
-});
+  return false;
+}
diff --git a/go-back-with-backspace/installed.html b/go-back-with-backspace/installed.html
deleted file mode 100644
index 87b26ef..0000000
--- a/go-back-with-backspace/installed.html
+++ /dev/null
@@ -1,13 +0,0 @@
-<html>
-  <head>
-    <title>Go Back With Backspace Installed</title>
-  </head>
-  <body>
-    <h1>Go Back With Backspace installation notes</h1>
-    <img src="icons/icon128.png" style="float:left">
-    <p style="margin:40 0 0 0">Thank you for installing the "Go Back With
-      Backspace" extension.</p>
-    <p>After this extension is newly installed, enabled, or updated, any open
-      tabs will need to be reloaded for the changes to take effect.</p>
-  </body>
-</html>
diff --git a/go-back-with-backspace/is_editable.js b/go-back-with-backspace/is_editable.js
index 9e26cdc..6f28ed7 100644
--- a/go-back-with-backspace/is_editable.js
+++ b/go-back-with-backspace/is_editable.js
@@ -1,3 +1,7 @@
+// Copyright 2016 Google Inc. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
 // Determine whether focus is in an editable text field.
 function isEditable(target) {
   // Elements may be explicitly marked as editable.
diff --git a/go-back-with-backspace/manifest.json b/go-back-with-backspace/manifest.json
index f16ae59..9d314c8 100644
--- a/go-back-with-backspace/manifest.json
+++ b/go-back-with-backspace/manifest.json
@@ -2,7 +2,7 @@
   "name": "__MSG_extensionName__",
   "description": "__MSG_extensionDescription__",
   "default_locale": "en",
-  "version": "1.5",
+  "version": "1.6",
   "manifest_version": 2,
   "minimum_chrome_version": "52",
   "icons": {
@@ -15,6 +15,13 @@
     "scripts": ["background.js"],
     "persistent": false
   },
+  "browser_action": {
+    "default_icon": {
+      "19": "icons/icon19.png",
+      "38": "icons/icon38.png"
+    },
+    "default_popup": "pages/popup.html"
+  },
   "content_scripts": [
     {
       "matches": ["<all_urls>"],
@@ -22,5 +29,13 @@
       "all_frames": true,
       "run_at": "document_start"
     }
+  ],
+  "options_ui": {
+    "page": "pages/options.html",
+    "chrome_style": true
+  },
+  "permissions": [
+    "storage",
+    "activeTab"
   ]
 }
diff --git a/go-back-with-backspace/pages/common.js b/go-back-with-backspace/pages/common.js
new file mode 100644
index 0000000..9fb28c5
--- /dev/null
+++ b/go-back-with-backspace/pages/common.js
@@ -0,0 +1,33 @@
+// Copyright 2016 Google Inc. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// 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() {
+  var all = document.querySelectorAll('[i18n]');
+  for (var i = 0; i < all.length; ++i) {
+    var i18n = all[i].getAttribute('i18n');
+    if (i18n)
+      all[i].textContent = chrome.i18n.getMessage(i18n);
+  }
+}
+
+// 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) {
+  var subject = chrome.i18n.getMessage('reportSubject');
+  var body = '';
+  if (url)
+    body = chrome.i18n.getMessage('reportBodyWithURL', url);
+  else
+    body = chrome.i18n.getMessage('reportBody');
+  var msg = 'mailto:gobackwithbackspace@google.com' +
+      '?subject=' + encodeURIComponent(subject) +
+      '&body=' + encodeURIComponent(body);
+  chrome.tabs.create({
+    url: msg,
+    active: true});
+}
diff --git a/go-back-with-backspace/pages/installed.css b/go-back-with-backspace/pages/installed.css
new file mode 100644
index 0000000..10f24fe
--- /dev/null
+++ b/go-back-with-backspace/pages/installed.css
@@ -0,0 +1,21 @@
+/**
+ * Copyright 2016 Google Inc. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+body {
+  direction: __MSG_@@bidi_dir__;
+}
+
+#icon {
+  float: __MSG_@@bidi_start_edge__;
+}
+
+#notes {
+  margin-top: 40px;
+}
+
+#button_div {
+  margin-top: 10px;
+}
\ No newline at end of file
diff --git a/go-back-with-backspace/pages/installed.html b/go-back-with-backspace/pages/installed.html
new file mode 100644
index 0000000..fb52cbd
--- /dev/null
+++ b/go-back-with-backspace/pages/installed.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title i18n="installedTitle"></title>
+  <link rel="stylesheet" type="text/css" href="installed.css">
+</head>
+<body>
+  <h1 i18n="installedHeading"></h1>
+  <img id="icon" src="../icons/icon128.png">
+  <div i18n="installedNotes" id="notes"></div>
+
+  <div id="button_div">
+    <a href="" i18n="sendFeedback" id="report_page"></a>
+    <a href="" i18n="openOptions" id="open_options"></a>
+  </div>
+
+  <script src="common.js"></script>
+  <script src="installed.js"></script>
+</body>
+</html>
diff --git a/go-back-with-backspace/pages/installed.js b/go-back-with-backspace/pages/installed.js
new file mode 100644
index 0000000..b069050
--- /dev/null
+++ b/go-back-with-backspace/pages/installed.js
@@ -0,0 +1,19 @@
+// Copyright 2016 Google Inc. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Initialize the page.
+function init() {
+  LoadInternationalizedStrings();
+
+  document.getElementById('report_page').onclick = function() {
+    reportPage();
+  };
+
+  document.getElementById('open_options').onclick = function() {
+    chrome.runtime.openOptionsPage();
+  };
+
+}
+
+window.addEventListener('load', init, false);
diff --git a/go-back-with-backspace/pages/options.css b/go-back-with-backspace/pages/options.css
new file mode 100644
index 0000000..936a461
--- /dev/null
+++ b/go-back-with-backspace/pages/options.css
@@ -0,0 +1,41 @@
+/**
+ * Copyright 2016 Google Inc. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+body {
+  padding: 10px;
+  direction: __MSG_@@bidi_dir__;
+}
+
+textarea {
+  width: 100%;
+  margin: 0;
+  padding: 0;
+}
+
+.applets {
+  padding-top: 20px;
+}
+
+.whitelist {
+  padding-top: 10px;
+  padding-__MSG_@@bidi_start_edge__: 40px;
+}
+
+.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__;
+}
+
+#error {
+  color: red;
+}
+
+#report_page {
+  float: __MSG_@@bidi_start_edge__;
+}
diff --git a/go-back-with-backspace/pages/options.html b/go-back-with-backspace/pages/options.html
new file mode 100644
index 0000000..aac5995
--- /dev/null
+++ b/go-back-with-backspace/pages/options.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title i18n="optionsTitle">TITLE</title>
+  <link rel="stylesheet" type="text/css" href="options.css">
+</head>
+
+<body>
+  <span i18n="optionsBlacklist"></span><br>
+  <textarea id="blacklist" rows="8"></textarea><br>
+
+  <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>
+
+  <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>
+  </div>
+
+  <script src="common.js"></script>
+  <script src="options.js"></script>
+</body>
+</html>
diff --git a/go-back-with-backspace/pages/options.js b/go-back-with-backspace/pages/options.js
new file mode 100644
index 0000000..9ff25d6
--- /dev/null
+++ b/go-back-with-backspace/pages/options.js
@@ -0,0 +1,60 @@
+// Copyright 2016 Google Inc. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Initialize the page.
+function init() {
+  LoadInternationalizedStrings();
+
+  var blacklist = document.getElementById('blacklist');
+  var checkbox = document.getElementById('disableInApplets');
+  var whitelist = document.getElementById('whitelist');
+
+  // 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;
+
+  // Set event handlers.
+  document.getElementById('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+/)
+    }, 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 =
+            chrome.i18n.getMessage('errorSaving',
+                                   chrome.runtime.lastError.message);
+      } else {
+        window.close();
+      }
+    });
+  };
+
+  document.getElementById('cancel_button').onclick = function() {
+    window.close();
+  };
+
+  document.getElementById('report_page').onclick = function() {
+    reportPage();
+  };
+
+  // Load saved settings into the form fields.
+  chrome.storage.sync.get({
+    blacklist: [],
+    disableInApplets: true,
+    whitelist: []
+  }, function(items) {
+    blacklist.value = items.blacklist.join('\n');
+    checkbox.checked = items.disableInApplets;
+    whitelist.value = items.whitelist.join('\n');
+  });
+}
+
+window.addEventListener('load', init, false);
diff --git a/go-back-with-backspace/pages/popup.css b/go-back-with-backspace/pages/popup.css
new file mode 100644
index 0000000..3dabde5
--- /dev/null
+++ b/go-back-with-backspace/pages/popup.css
@@ -0,0 +1,40 @@
+/**
+ * Copyright 2016 Google Inc. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+body {
+  direction: __MSG_@@bidi_dir__;
+  width: 300px;
+}
+
+#list_edit_button, #messages  {
+  margin-bottom: 10px;
+}
+
+#list_edit_button {
+  margin-top: 10px;
+}
+
+#current_url {
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  overflow: hidden;
+}
+
+#status {
+  color: green;
+}
+
+#status.error {
+  color: red;
+}
+
+#report_page {
+  float: __MSG_@@bidi_start_edge__;
+}
+
+#open_options {
+  float: __MSG_@@bidi_end_edge__;
+}
diff --git a/go-back-with-backspace/pages/popup.html b/go-back-with-backspace/pages/popup.html
new file mode 100644
index 0000000..ff4d6ed
--- /dev/null
+++ b/go-back-with-backspace/pages/popup.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title i18n="popupTitle"></title>
+  <link rel="stylesheet" type="text/css" href="popup.css">
+</head>
+
+<body>
+  <h1 i18n="extensionName"></h1>
+  <div id="current_url"></div>
+  <div>
+    <button i18n="popupAddBlacklist" id="list_edit_button"></button><br>
+  </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>
+
+  <a href="" i18n="sendFeedback" id="report_page"></a>
+  <a href="" i18n="openOptions" id="open_options"></a>
+
+  <script src="common.js"></script>
+  <script src="popup.js"></script>
+</body>
+</html>
diff --git a/go-back-with-backspace/pages/popup.js b/go-back-with-backspace/pages/popup.js
new file mode 100644
index 0000000..aa4666b
--- /dev/null
+++ b/go-back-with-backspace/pages/popup.js
@@ -0,0 +1,125 @@
+// Copyright 2016 Google Inc. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Return true if the extension is permitted to run on the given URL, with
+// the indicated permission for file:// schemes.
+function urlAllowed(url, file_ok) {
+  // Extensions are prohibited on the Chrome Web Store.
+  if (url.startsWith('https://chrome.google.com/webstore/'))
+    return false;
+
+  // Extensions are permitted to run on pages with schemes http, https, and
+  // ftp, plus file if enabled.
+  if (url.startsWith('http://') ||
+      url.startsWith('https://') ||
+      url.startsWith('ftp://'))
+    return true;
+
+  if (file_ok && url.startsWith('file://'))
+    return true;
+
+  return false;
+}
+
+// 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');
+
+  if (!urlAllowed(url, options.file_ok)) {
+    button.textContent = chrome.i18n.getMessage('popupAddBlacklist');
+    button.disabled = true;
+    setStatusError('popupDisallowedURL');
+  } else if (options.blacklist.includes(url)) {
+    button.textContent = chrome.i18n.getMessage('popupRemoveBlacklist');
+  } else {
+    button.textContent = chrome.i18n.getMessage('popupAddBlacklist');
+  }
+
+  // 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;
+  }
+}
+
+// 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');
+}
+
+// Set the status text.
+function setStatus(message_id, placeholder) {
+  var status = document.getElementById('status');
+  if (placeholder === undefined)
+    status.textContent = chrome.i18n.getMessage(message_id);
+  else
+    status.textContent = chrome.i18n.getMessage(message_id, placeholder);
+}
+
+// Initialize the page.
+function init() {
+  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 = {};
+    chrome.storage.sync.get({
+      blacklist: [],
+      disableInApplets: true,
+      whitelist: []
+    }, function(items) {
+      options = items;
+      // Read the extension file-access permission and update the popup UI.
+      chrome.extension.isAllowedFileSchemeAccess(function(file_ok) {
+        options.file_ok = file_ok;
+        updatePopup(options, url);
+      });
+    });
+
+    // Set event handlers that depend on the URL.
+    document.getElementById('list_edit_button').onclick = function() {
+      var index = options.blacklist.indexOf(url);
+      if (index > -1)
+        options.blacklist.splice(index, 1);
+      else
+        options.blacklist.push(url);
+
+      chrome.storage.sync.set({
+        blacklist: options.blacklist,
+        whitelist: options.whitelist
+      }, function() {
+        if (chrome.runtime.lastError) {
+          setStatusError('errorSaving', chrome.runtime.lastError.message);
+        } else {
+          updatePopup(options, url);
+          setStatus('popupStatusSaved');
+        }
+      });
+    };
+
+    document.getElementById('report_page').onclick = function() {
+      reportPage(url);
+    };
+  });
+
+  // Set event handlers that don't need the URL.
+  document.getElementById('open_options').onclick = function() {
+    chrome.runtime.openOptionsPage();
+  };
+
+  document.getElementById('open_extensions').onclick = function() {
+    chrome.tabs.create({url: 'chrome://extensions', active:true});
+    window.close();
+  };
+}
+
+window.addEventListener('load', init, false);