Bug 1465581 - Export Screenshots 33.0.0 to Firefox (code excluding translations and Raven update); r=ianbicking,_6a68
authorBarry Chen <bchen@mozilla.com>
Mon, 18 Jun 2018 11:07:23 -0500
changeset 423263 3f8b00d17eea
parent 423262 3dda0c68e6bd
child 423264 927bbe6abacf
push id65407
push userjhirsch@mozilla.com
push dateThu, 21 Jun 2018 19:08:22 +0000
treeherderautoland@3f8b00d17eea [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersianbicking, _6a68
bugs1465581
milestone62.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1465581 - Export Screenshots 33.0.0 to Firefox (code excluding translations and Raven update); r=ianbicking,_6a68 MozReview-Commit-ID: FrvoD9G74mJ
browser/extensions/screenshots/bootstrap.js
browser/extensions/screenshots/install.rdf
browser/extensions/screenshots/moz.build
browser/extensions/screenshots/webextension/assertIsBlankDocument.js
browser/extensions/screenshots/webextension/assertIsTrusted.js
browser/extensions/screenshots/webextension/background/analytics.js
browser/extensions/screenshots/webextension/background/main.js
browser/extensions/screenshots/webextension/background/selectorLoader.js
browser/extensions/screenshots/webextension/background/takeshot.js
browser/extensions/screenshots/webextension/build/inlineSelectionCss.js
browser/extensions/screenshots/webextension/build/selection.js
browser/extensions/screenshots/webextension/build/thumbnailGenerator.js
browser/extensions/screenshots/webextension/icons/copied-notification.svg
browser/extensions/screenshots/webextension/icons/copy.png
browser/extensions/screenshots/webextension/log.js
browser/extensions/screenshots/webextension/manifest.json
browser/extensions/screenshots/webextension/randomString.js
browser/extensions/screenshots/webextension/selector/callBackground.js
browser/extensions/screenshots/webextension/selector/shooter.js
browser/extensions/screenshots/webextension/selector/ui.js
browser/extensions/screenshots/webextension/selector/uicontrol.js
--- a/browser/extensions/screenshots/bootstrap.js
+++ b/browser/extensions/screenshots/bootstrap.js
@@ -1,24 +1,21 @@
 /* globals ADDON_DISABLE Services CustomizableUI LegacyExtensionsUtils AppConstants PageActions */
 const ADDON_ID = "screenshots@mozilla.org";
 const TELEMETRY_ENABLED_PREF = "datareporting.healthreport.uploadEnabled";
 const PREF_BRANCH = "extensions.screenshots.";
 const USER_DISABLE_PREF = "extensions.screenshots.disabled";
 const UPLOAD_DISABLED_PREF = "extensions.screenshots.upload-disabled";
 const HISTORY_ENABLED_PREF = "places.history.enabled";
 
-const { interfaces: Ci, utils: Cu } = Components;
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "AddonManager",
                                "resource://gre/modules/AddonManager.jsm");
 ChromeUtils.defineModuleGetter(this, "AppConstants",
                                "resource://gre/modules/AppConstants.jsm");
-ChromeUtils.defineModuleGetter(this, "Console",
-                               "resource://gre/modules/Console.jsm");
 ChromeUtils.defineModuleGetter(this, "CustomizableUI",
                                "resource:///modules/CustomizableUI.jsm");
 ChromeUtils.defineModuleGetter(this, "LegacyExtensionsUtils",
                                "resource://gre/modules/LegacyExtensionsUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "PageActions",
                                "resource:///modules/PageActions.jsm");
 ChromeUtils.defineModuleGetter(this, "Services",
                                "resource://gre/modules/Services.jsm");
@@ -58,17 +55,17 @@ const appStartupObserver = {
   unregister() {
     Services.obs.removeObserver(this, "sessionstore-windows-restored", false); // eslint-disable-line mozilla/no-useless-parameters
   },
 
   observe() {
     appStartupDone();
     this.unregister();
   }
-}
+};
 
 const LibraryButton = {
   ITEM_ID: "appMenu-library-screenshots",
 
   init(webExtension) {
     this._initialized = true;
     const permissionPages = [...webExtension.extension.permissions].filter(p => (/^https?:\/\//i).test(p));
     if (permissionPages.length > 1) {
--- a/browser/extensions/screenshots/install.rdf
+++ b/browser/extensions/screenshots/install.rdf
@@ -7,14 +7,14 @@
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!--Firefox-->
         <em:minVersion>57.0a1</em:minVersion>
         <em:maxVersion>*</em:maxVersion>
       </Description>
     </em:targetApplication>
     <em:type>2</em:type>
-    <em:version>32.1.0</em:version>
+    <em:version>33.0.0</em:version>
     <em:bootstrap>true</em:bootstrap>
     <em:homepageURL>https://screenshots.firefox.com/</em:homepageURL>
     <em:multiprocessCompatible>true</em:multiprocessCompatible>
   </Description>
 </RDF>
--- a/browser/extensions/screenshots/moz.build
+++ b/browser/extensions/screenshots/moz.build
@@ -402,26 +402,27 @@ FINAL_TARGET_FILES.features['screenshots
 ]
 
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["build"] += [
   'webextension/build/buildSettings.js',
   'webextension/build/inlineSelectionCss.js',
   'webextension/build/onboardingCss.js',
   'webextension/build/onboardingHtml.js',
   'webextension/build/raven.js',
+  'webextension/build/selection.js',
   'webextension/build/shot.js',
   'webextension/build/thumbnailGenerator.js'
 ]
 
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"]["icons"] += [
   'webextension/icons/back-highlight.svg',
   'webextension/icons/back.svg',
   'webextension/icons/cancel.svg',
   'webextension/icons/cloud.svg',
-  'webextension/icons/copy.png',
+  'webextension/icons/copied-notification.svg',
   'webextension/icons/copy.svg',
   'webextension/icons/done.svg',
   'webextension/icons/download.svg',
   'webextension/icons/help-16.svg',
   'webextension/icons/icon-highlight-32-v2.svg',
   'webextension/icons/icon-v2.svg',
   'webextension/icons/icon-welcome-face-without-eyes.svg',
   'webextension/icons/menu-fullpage.svg',
--- a/browser/extensions/screenshots/webextension/assertIsBlankDocument.js
+++ b/browser/extensions/screenshots/webextension/assertIsBlankDocument.js
@@ -3,10 +3,10 @@
     Should be applied *inside* catcher.watchFunction
 */
 this.assertIsBlankDocument = function assertIsBlankDocument(doc) {
   if (doc.documentURI !== browser.extension.getURL("blank.html")) {
     const exc = new Error("iframe URL does not match expected blank.html");
     exc.foundURL = doc.documentURI;
     throw exc;
   }
-}
+};
 null;
--- a/browser/extensions/screenshots/webextension/assertIsTrusted.js
+++ b/browser/extensions/screenshots/webextension/assertIsTrusted.js
@@ -11,10 +11,10 @@ this.assertIsTrusted = function assertIs
     }
     if (!event.isTrusted) {
       const exc = new Error(`Received untrusted event (type: ${event.type})`);
       exc.noPopup = true;
       throw exc;
     }
     return handlerFunction.call(this, event);
   };
-}
+};
 null;
--- a/browser/extensions/screenshots/webextension/background/analytics.js
+++ b/browser/extensions/screenshots/webextension/background/analytics.js
@@ -24,17 +24,17 @@ this.analytics = (function() {
       return;
     }
 
     const eventsUrl = `${main.getBackend()}/event`;
     const deviceId = auth.getDeviceId();
     const sendTime = Date.now();
 
     pendingEvents.forEach(event => {
-      event.queueTime = sendTime - event.eventTime
+      event.queueTime = sendTime - event.eventTime;
       log.info(`sendEvent ${event.event}/${event.action}/${event.label || "none"} ${JSON.stringify(event.options)}`);
     });
 
     const body = JSON.stringify({deviceId, events: pendingEvents});
     const fetchRequest = fetch(eventsUrl, Object.assign({body}, fetchOptions));
     fetchWatcher(fetchRequest);
     pendingEvents = [];
   }
--- a/browser/extensions/screenshots/webextension/background/main.js
+++ b/browser/extensions/screenshots/webextension/background/main.js
@@ -125,29 +125,37 @@ this.main = (function() {
     }));
   });
 
   function forceOnboarding() {
     return browser.tabs.create({url: getOnboardingUrl()});
   }
 
   exports.onClickedContextMenu = catcher.watchFunction((info, tab) => {
-    if (!tab) {
-      // Not in a page/tab context, ignore
-      return;
-    }
-    if (!urlEnabled(tab.url)) {
-      senderror.showError({
-        popupMessage: "UNSHOOTABLE_PAGE"
-      });
-      return;
-    }
-    catcher.watchPromise(
+    catcher.watchPromise(hasSeenOnboarding.then(onboarded => {
+      if (!tab) {
+        // Not in a page/tab context, ignore
+        return;
+      }
+      if (!urlEnabled(tab.url)) {
+        if (!onboarded) {
+          sendEvent("goto-onboarding", "selection-button", {incognito: tab.incognito});
+          forceOnboarding();
+          return;
+        }
+        senderror.showError({
+          popupMessage: "UNSHOOTABLE_PAGE"
+        });
+        return;
+      }
+      // No need to catch() here because of watchPromise().
+      // eslint-disable-next-line promise/catch-or-return
       toggleSelector(tab)
-        .then(() => sendEvent("start-shot", "context-menu", {incognito: tab.incognito})));
+        .then(() => sendEvent("start-shot", "context-menu", {incognito: tab.incognito}));
+    }));
   });
 
   function urlEnabled(url) {
     if (shouldOpenMyShots(url)) {
       return true;
     }
     if (isShotOrMyShotPage(url) || /^(?:about|data|moz-extension):/i.test(url) || isBlacklistedUrl(url)) {
       return false;
@@ -195,17 +203,17 @@ this.main = (function() {
       .then(() => browser.tabs.create({url: backend + "/shots"})));
   });
 
   communication.register("openShot", (sender, {url, copied}) => {
     if (copied) {
       const id = makeUuid();
       return browser.notifications.create(id, {
         type: "basic",
-        iconUrl: "../icons/copy.png",
+        iconUrl: "../icons/copied-notification.svg",
         title: browser.i18n.getMessage("notificationLinkCopiedTitle"),
         message: browser.i18n.getMessage("notificationLinkCopiedDetails", pasteSymbol)
       });
     }
     return null;
   });
 
   // This is used for truncated full page downloads and copy to clipboards.
@@ -229,22 +237,22 @@ this.main = (function() {
 
   communication.register("copyShotToClipboard", (sender, blob) => {
     return blobConverters.blobToArray(blob).then(buffer => {
       return browser.clipboard.setImageData(
         buffer, blob.type.split("/", 2)[1]).then(() => {
           catcher.watchPromise(communication.sendToBootstrap("incrementCount", {scalar: "copy"}));
           return browser.notifications.create({
             type: "basic",
-            iconUrl: "../icons/copy.png",
+            iconUrl: "../icons/copied-notification.svg",
             title: browser.i18n.getMessage("notificationImageCopiedTitle"),
             message: browser.i18n.getMessage("notificationImageCopiedDetails", pasteSymbol)
           });
         });
-    })
+    });
   });
 
   communication.register("downloadShot", (sender, info) => {
     // 'data:' urls don't work directly, let's use a Blob
     // see http://stackoverflow.com/questions/40269862/save-data-uri-as-file-using-downloads-download-api
     const blob = blobConverters.dataUrlToBlob(info.url);
     const url = URL.createObjectURL(blob);
     let downloadId;
@@ -252,23 +260,28 @@ this.main = (function() {
       if (!downloadId || downloadId !== change.id) {
         return;
       }
       if (change.state && change.state.current !== "in_progress") {
         URL.revokeObjectURL(url);
         browser.downloads.onChanged.removeListener(onChangedCallback);
       }
     });
-    browser.downloads.onChanged.addListener(onChangedCallback)
+    browser.downloads.onChanged.addListener(onChangedCallback);
     catcher.watchPromise(communication.sendToBootstrap("incrementCount", {scalar: "download"}));
     return browser.windows.getLastFocused().then(windowInfo => {
       return browser.downloads.download({
         url,
         incognito: windowInfo.incognito,
         filename: info.filename
+      }).catch((error) => {
+        // We are not logging error message when user cancels download
+        if (error && error.message && !error.message.includes("canceled")) {
+          log.error(error.message);
+        }
       }).then((id) => {
         downloadId = id;
       });
     });
   });
 
   communication.register("closeSelector", (sender) => {
     setIconActive(false, sender.tab.id);
--- a/browser/extensions/screenshots/webextension/background/selectorLoader.js
+++ b/browser/extensions/screenshots/webextension/background/selectorLoader.js
@@ -20,16 +20,17 @@ this.selectorLoader = (function() {
     "background/selectorLoader.js",
     "selector/callBackground.js",
     "selector/util.js"
   ];
 
   const selectorScripts = [
     "clipboard.js",
     "makeUuid.js",
+    "build/selection.js",
     "build/shot.js",
     "randomString.js",
     "domainFromUrl.js",
     "build/inlineSelectionCss.js",
     "selector/documentMetadata.js",
     "selector/ui.js",
     "selector/shooter.js",
     "selector/uicontrol.js"
@@ -153,14 +154,14 @@ this.selectorLoader = (function() {
 
   exports.toggle = function(tabId, hasSeenOnboarding) {
     return exports.unloadIfLoaded(tabId)
       .then(wasLoaded => {
         if (!wasLoaded) {
           exports.loadModules(tabId, hasSeenOnboarding);
         }
         return !wasLoaded;
-      })
+      });
   };
 
   return exports;
 })();
 null;
--- a/browser/extensions/screenshots/webextension/background/takeshot.js
+++ b/browser/extensions/screenshots/webextension/background/takeshot.js
@@ -52,17 +52,17 @@ this.takeshot = (function() {
       return thumbnailGenerator.createThumbnailUrl(shot);
     }).then((thumbnailImage) => {
       if (buildSettings.uploadBinary) {
         thumbnailBlob = thumbnailImage;
       } else {
         shot.thumbnail = thumbnailImage;
       }
     }).then(() => {
-      return browser.tabs.create({url: shot.creatingUrl})
+      return browser.tabs.create({url: shot.creatingUrl});
     }).then((tab) => {
       openedTab = tab;
       sendEvent("internal", "open-shot-tab");
       return uploadShot(shot, imageBlob, thumbnailBlob);
     }).then(() => {
       return browser.tabs.update(openedTab.id, {url: shot.viewUrl, loadReplace: true}).then(
         null,
         (error) => {
--- a/browser/extensions/screenshots/webextension/build/inlineSelectionCss.js
+++ b/browser/extensions/screenshots/webextension/build/inlineSelectionCss.js
@@ -18,16 +18,18 @@ window.inlineSelectionCss = `
   text-decoration: none;
   transition: background 150ms cubic-bezier(0.07, 0.95, 0, 1), border 150ms cubic-bezier(0.07, 0.95, 0, 1);
   user-select: none;
   white-space: nowrap; }
   .button.small, .small.highlight-button-cancel, .small.highlight-button-save, .small.highlight-button-download, .small.highlight-button-copy, .small.preview-button-save {
     height: 32px;
     line-height: 32px;
     padding: 0 8px; }
+  .button.active, .active.highlight-button-cancel, .active.highlight-button-save, .active.highlight-button-download, .active.highlight-button-copy, .active.preview-button-save {
+    background-color: #dedede; }
   .button.tiny, .tiny.highlight-button-cancel, .tiny.highlight-button-save, .tiny.highlight-button-download, .tiny.highlight-button-copy, .tiny.preview-button-save {
     font-size: 14px;
     height: 26px;
     border: 1px solid #c7c7c7; }
     .button.tiny:hover, .tiny.highlight-button-cancel:hover, .tiny.highlight-button-save:hover, .tiny.highlight-button-download:hover, .tiny.highlight-button-copy:hover, .tiny.preview-button-save:hover, .button.tiny:focus, .tiny.highlight-button-cancel:focus, .tiny.highlight-button-save:focus, .tiny.highlight-button-download:focus, .tiny.highlight-button-copy:focus, .tiny.preview-button-save:focus {
       background: #ededf0;
       border-color: #989898; }
     .button.tiny:active, .tiny.highlight-button-cancel:active, .tiny.highlight-button-save:active, .tiny.highlight-button-download:active, .tiny.highlight-button-copy:active, .tiny.preview-button-save:active {
@@ -448,16 +450,19 @@ window.inlineSelectionCss = `
   position: fixed;
   top: 0;
   width: 100%;
   z-index: 9999999999; }
   body.hcm .preview-overlay {
     background-color: black;
     opacity: 0.7; }
 
+.precision-cursor {
+  cursor: crosshair; }
+
 .highlight {
   border-radius: 2px;
   border: 2px dashed rgba(255, 255, 255, 0.8);
   box-sizing: border-box;
   cursor: move;
   position: absolute;
   z-index: 9999999999; }
   body.hcm .highlight {
new file mode 100644
--- /dev/null
+++ b/browser/extensions/screenshots/webextension/build/selection.js
@@ -0,0 +1,121 @@
+this.selection = (function () {let exports={}; class Selection {
+  constructor(x1, y1, x2, y2) {
+    this.x1 = x1;
+    this.y1 = y1;
+    this.x2 = x2;
+    this.y2 = y2;
+  }
+
+  get top() {
+    return Math.min(this.y1, this.y2);
+  }
+  set top(val) {
+    if (this.y1 < this.y2) {
+      this.y1 = val;
+    } else {
+      this.y2 = val;
+    }
+  }
+
+  get bottom() {
+    return Math.max(this.y1, this.y2);
+  }
+  set bottom(val) {
+    if (this.y1 > this.y2) {
+      this.y1 = val;
+    } else {
+      this.y2 = val;
+    }
+  }
+
+  get left() {
+    return Math.min(this.x1, this.x2);
+  }
+  set left(val) {
+    if (this.x1 < this.x2) {
+      this.x1 = val;
+    } else {
+      this.x2 = val;
+    }
+  }
+
+  get right() {
+    return Math.max(this.x1, this.x2);
+  }
+  set right(val) {
+    if (this.x1 > this.x2) {
+      this.x1 = val;
+    } else {
+      this.x2 = val;
+    }
+  }
+
+  get width() {
+    return Math.abs(this.x2 - this.x1);
+  }
+  get height() {
+    return Math.abs(this.y2 - this.y1);
+  }
+
+  rect() {
+    return {
+      top: Math.floor(this.top),
+      left: Math.floor(this.left),
+      bottom: Math.floor(this.bottom),
+      right: Math.floor(this.right)
+    };
+  }
+
+  union(other) {
+    return new Selection(
+      Math.min(this.left, other.left),
+      Math.min(this.top, other.top),
+      Math.max(this.right, other.right),
+      Math.max(this.bottom, other.bottom)
+    );
+  }
+
+  /** Sort x1/x2 and y1/y2 so x1<x2, y1<y2 */
+  sortCoords() {
+    if (this.x1 > this.x2) {
+      [this.x1, this.x2] = [this.x2, this.x1];
+    }
+    if (this.y1 > this.y2) {
+      [this.y1, this.y2] = [this.y2, this.y1];
+    }
+  }
+
+  clone() {
+    return new Selection(this.x1, this.y1, this.x2, this.y2);
+  }
+
+  toJSON() {
+    return {
+      left: this.left,
+      right: this.right,
+      top: this.top,
+      bottom: this.bottom
+    };
+  }
+
+  static getBoundingClientRect(el) {
+    if (!el.getBoundingClientRect) {
+      // Typically the <html> element or somesuch
+      return null;
+    }
+    const rect = el.getBoundingClientRect();
+    if (!rect) {
+      return null;
+    }
+    return new Selection(rect.left, rect.top, rect.right, rect.bottom);
+  }
+}
+
+if (typeof exports !== "undefined") {
+  exports.Selection = Selection;
+}
+
+return exports;
+})();
+null;
+
--- a/browser/extensions/screenshots/webextension/build/thumbnailGenerator.js
+++ b/browser/extensions/screenshots/webextension/build/thumbnailGenerator.js
@@ -1,17 +1,17 @@
 this.thumbnailGenerator = (function () {let exports={}; // This is used in addon/webextension/background/takeshot.js,
 // server/src/pages/shot/controller.js, and
 // server/scr/pages/shotindex/view.js. It is used in a browser
 // environment.
 
 // Resize down 1/2 at a time produces better image quality.
 // Not quite as good as using a third-party filter (which will be
 // slower), but good enough.
-const maxResizeScaleFactor = 0.5
+const maxResizeScaleFactor = 0.5;
 
 // The shot will be scaled or cropped down to 210px on x, and cropped or
 // scaled down to a maximum of 280px on y.
 // x: 210
 // y: <= 280
 const maxThumbnailWidth = 210;
 const maxThumbnailHeight = 280;
 
@@ -44,17 +44,17 @@ function getThumbnailDimensions(imageWid
                                     maxThumbnailHeight / (maxThumbnailWidth / imageWidth));
   }
 
   return {
     width: thumbnailImageWidth,
     height: thumbnailImageHeight,
     scaledX,
     scaledY
-  }
+  };
 }
 
 /**
  * @param {dataUrl} String Data URL of the original image.
  * @param {int} imageHeight Height in pixels of the original image.
  * @param {int} imageWidth Width in pixels of the original image.
  * @param {String} urlOrBlob 'blob' for a blob, otherwise data url.
  * @returns A promise that resolves to the data URL or blob of the thumbnail image, or null.
@@ -103,25 +103,25 @@ function createThumbnail(dataUrl, imageW
 
       if (thumbnailCanvas.width <= thumbnailDimensions.width ||
         thumbnailCanvas.height <= thumbnailDimensions.height) {
         if (urlOrBlob === "blob") {
           thumbnailCanvas.toBlob((blob) => {
             resolve(blob);
           });
         } else {
-          resolve(thumbnailCanvas.toDataURL("image/png"))
+          resolve(thumbnailCanvas.toDataURL("image/png"));
         }
         return;
       }
 
       srcWidth = destWidth;
       srcHeight = destHeight;
       thumbnailImage.src = thumbnailCanvas.toDataURL();
-    }
+    };
     thumbnailImage.src = dataUrl;
   });
 }
 
 function createThumbnailUrl(shot) {
   const image = shot.getClip(shot.clipNames()[0]).image;
   if (!image.url) {
     return Promise.resolve(null);
new file mode 100644
--- /dev/null
+++ b/browser/extensions/screenshots/webextension/icons/copied-notification.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"><path fill="context-fill" d="M44.121 24.879l-9-9A3 3 0 0 0 33 15h-3v-3a3 3 0 0 0-.879-2.121l-9-9A3 3 0 0 0 18 0H9a6 6 0 0 0-6 6v21a6 6 0 0 0 6 6h9v9a6 6 0 0 0 6 6h15a6 6 0 0 0 6-6V27a3 3 0 0 0-.879-2.121zM37.758 27H33v-4.758zm-15-15H18V7.242zM18 21v6H9V6h6v7.5a1.5 1.5 0 0 0 1.5 1.5H24a6 6 0 0 0-6 6zm6 21V21h6v7.5a1.5 1.5 0 0 0 1.5 1.5H39v12z"/></svg>
\ No newline at end of file
deleted file mode 100644
index 85f952d7b68d4873e6d4631fe1ebf51bd1893f2b..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/browser/extensions/screenshots/webextension/log.js
+++ b/browser/extensions/screenshots/webextension/log.js
@@ -23,26 +23,26 @@ this.log = (function() {
       }
     }
   }
 
   function stub() {}
   exports.debug = exports.info = exports.warn = exports.error = stub;
 
   if (shouldLog.debug) {
-    exports.debug = console.debug.bind(console);
+    exports.debug = console.debug;
   }
 
   if (shouldLog.info) {
-    exports.info = console.info.bind(console);
+    exports.info = console.info;
   }
 
   if (shouldLog.warn) {
-    exports.warn = console.warn.bind(console);
+    exports.warn = console.warn;
   }
 
   if (shouldLog.error) {
-    exports.error = console.error.bind(console);
+    exports.error = console.error;
   }
 
   return exports;
 })();
 null;
--- a/browser/extensions/screenshots/webextension/manifest.json
+++ b/browser/extensions/screenshots/webextension/manifest.json
@@ -1,12 +1,12 @@
 {
   "manifest_version": 2,
   "name": "Firefox Screenshots",
-  "version": "32.1.0",
+  "version": "33.0.0",
   "description": "__MSG_addonDescription__",
   "author": "__MSG_addonAuthorsList__",
   "homepage_url": "https://github.com/mozilla-services/screenshots",
   "applications": {
     "gecko": {
       "id": "screenshots@mozilla.org",
       "strict_min_version": "57.0a1"
     }
--- a/browser/extensions/screenshots/webextension/randomString.js
+++ b/browser/extensions/screenshots/webextension/randomString.js
@@ -2,13 +2,13 @@
 
 "use strict";
 
 this.randomString = function randomString(length, chars) {
   const randomStringChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
   chars = chars || randomStringChars;
   let result = "";
   for (let i = 0; i < length; i++) {
-    result += chars[Math.floor(Math.random() * chars.length)]
+    result += chars[Math.floor(Math.random() * chars.length)];
   }
   return result;
-}
+};
 null;
--- a/browser/extensions/screenshots/webextension/selector/callBackground.js
+++ b/browser/extensions/screenshots/webextension/selector/callBackground.js
@@ -18,10 +18,10 @@ this.callBackground = function callBackg
       throw exc;
     } else {
       log.error("Unexpected background result:", result);
       const exc = new Error(`Bad response type from background page: ${result && result.type || undefined}`);
       exc.resultType = result ? (result.type || "undefined") : "undefined result";
       throw exc;
     }
   });
-}
+};
 null;
--- a/browser/extensions/screenshots/webextension/selector/shooter.js
+++ b/browser/extensions/screenshots/webextension/selector/shooter.js
@@ -203,17 +203,17 @@ this.shooter = (function() { // eslint-d
             url: dataUrl,
             type,
             location: selectedPos
           }
         });
         ui.triggerDownload(dataUrl, shotObject.filename);
         uicontrol.deactivate();
       }));
-    }))
+    }));
   };
 
   let copyInProgress = null;
   exports.copyShot = function(selectedPos, previewDataUrl, type) {
     // This is pretty slow. We'll ignore additional user triggered copy events
     // while it is in progress.
     if (copyInProgress) {
       return;
@@ -223,17 +223,17 @@ this.shooter = (function() { // eslint-d
       copyInProgress = null;
     }, 5000);
 
     const unsetCopyInProgress = () => {
       if (copyInProgress) {
         clearTimeout(copyInProgress);
         copyInProgress = null;
       }
-    }
+    };
     const shotPromise = previewDataUrl ? Promise.resolve(previewDataUrl) : screenshotPageAsync(selectedPos, type);
     catcher.watchPromise(shotPromise.then(dataUrl => {
       const blob = blobConverters.dataUrlToBlob(dataUrl);
       catcher.watchPromise(callBackground("copyShotToClipboard", blob).then(() => {
         uicontrol.deactivate();
         unsetCopyInProgress();
       }, unsetCopyInProgress));
     }));
--- a/browser/extensions/screenshots/webextension/selector/ui.js
+++ b/browser/extensions/screenshots/webextension/selector/ui.js
@@ -15,17 +15,17 @@ this.ui = (function() { // eslint-disabl
            el.classList.contains("visible") ||
            el.classList.contains("full-page") ||
            el.classList.contains("cancel-shot"))) {
         return true;
       }
       el = el.parentNode;
     }
     return false;
-  }
+  };
 
   const substitutedCss = inlineSelectionCss.replace(/MOZ_EXTENSION([^"]+)/g, (match, filename) => {
     return browser.extension.getURL(filename);
   });
 
   function makeEl(tagName, className) {
     if (!iframe.document()) {
       throw new Error("Attempted makeEl before iframe was initialized");
@@ -76,17 +76,17 @@ this.ui = (function() { // eslint-disabl
     doc.body.removeChild(el);
     // When Windows is in High Contrast mode, Firefox replaces background
     // image URLs with the string "none".
     return (computed && computed.backgroundImage === "none");
   }
 
   const isDownloadOnly = exports.isDownloadOnly = function() {
     return window.downloadOnly;
-  }
+  };
 
   // the download notice is rendered in iframes that match the document height
   // or the window height. If parent iframe matches window height, pass in true
   function renderDownloadNotice(initAtBottom = false) {
     const notice = makeEl("table", "notice");
     notice.innerHTML = `
       <div class="notice-tooltip">
         <p data-l10n-id="downloadOnlyDetails"></p>
@@ -273,25 +273,25 @@ this.ui = (function() { // eslint-disabl
         if (!this.element) {
           this.element = initializeIframe();
           this.element.id = "firefox-screenshots-preselection-iframe";
           this.element.style.setProperty("position", "fixed", "important");
           this.element.style.width = "100%";
           this.element.style.height = "100%";
           this.element.addEventListener("load", watchFunction(() => {
             this.document = this.element.contentDocument;
-            assertIsBlankDocument(this.document)
+            assertIsBlankDocument(this.document);
             // eslint-disable-next-line no-unsanitized/property
             this.document.documentElement.innerHTML = `
                <head>
                 <style>${substitutedCss}</style>
                 <title></title>
                </head>
                <body>
-                 <div class="preview-overlay">
+                 <div class="preview-overlay precision-cursor">
                    <div class="fixed-container">
                      <div class="face-container">
                        <div class="eye left"><div class="eyeball"></div></div>
                        <div class="eye right"><div class="eyeball"></div></div>
                        <div class="face"></div>
                      </div>
                      <div class="preview-instructions" data-l10n-id="screenshotInstructions"></div>
                      <button class="cancel-shot">${browser.i18n.getMessage("cancelScreenshot")}</button>
@@ -660,17 +660,17 @@ this.ui = (function() { // eslint-disabl
     // when a user ends scrolling or ends resizing a window
     delayExecution(delay, cb) {
       let timer;
       return function() {
         if (typeof timer !== "undefined") {
           clearTimeout(timer);
         }
         timer = setTimeout(cb, delay);
-      }
+      };
     },
 
     remove() {
       if (this.downloadNotice) {
         window.removeEventListener("scroll", this.windowChangeStop, true);
         window.removeEventListener("resize", this.windowChangeStop, true);
       }
       for (const name of ["el", "bgTop", "bgLeft", "bgRight", "bgBottom", "downloadNotice"]) {
--- a/browser/extensions/screenshots/webextension/selector/uicontrol.js
+++ b/browser/extensions/screenshots/webextension/selector/uicontrol.js
@@ -1,10 +1,10 @@
 /* globals log, catcher, util, ui, slides */
-/* globals shooter, callBackground, selectorLoader, assertIsTrusted, buildSettings */
+/* globals shooter, callBackground, selectorLoader, assertIsTrusted, buildSettings, selection */
 
 "use strict";
 
 this.uicontrol = (function() {
   const exports = {};
 
   /** ********************************************************
    * selection
@@ -58,16 +58,17 @@ this.uicontrol = (function() {
   const MAX_DETECT_WIDTH = Math.max(window.innerWidth + 100, 1000);
   // This is how close (in pixels) you can get to the edge of the window and then
   // it will scroll:
   const SCROLL_BY_EDGE = 20;
   // This is how wide the inboard scrollbars are, generally 0 except on Mac
   const SCROLLBAR_WIDTH = (window.navigator.platform.match(/Mac/i)) ? 17 : 0;
 
 
+  const { Selection } = selection;
   const { sendEvent } = shooter;
   const log = global.log;
 
   function round10(n) {
     return Math.floor(n / 10) * 10;
   }
 
   function eventOptionsForBox(box) {
@@ -181,17 +182,17 @@ this.uicontrol = (function() {
         .catch(() => {
           // Handled in communication.js
         });
     },
     onClickVisible: () => {
       sendEvent("capture-visible", "selection-button");
       selectedPos = new Selection(
         window.scrollX, window.scrollY,
-        window.scrollX + window.innerWidth, window.scrollY + window.innerHeight);
+        window.scrollX + document.documentElement.clientWidth, window.scrollY + window.innerHeight);
       captureType = "visible";
       setState("previewing");
     },
     onClickFullPage: () => {
       sendEvent("capture-full-page", "selection-button");
       captureType = "fullPage";
       let width = getDocumentWidth();
       if (width > MAX_PAGE_WIDTH) {
@@ -250,134 +251,16 @@ this.uicontrol = (function() {
   let selectedPos;
   let resizeDirection;
   let resizeStartPos;
   let resizeStartSelected;
   let resizeHasMoved;
   let mouseupNoAutoselect = false;
   let autoDetectRect;
 
-  /** Represents a selection box: */
-  class Selection {
-    constructor(x1, y1, x2, y2) {
-      this.x1 = x1;
-      this.y1 = y1;
-      this.x2 = x2;
-      this.y2 = y2;
-    }
-
-    rect() {
-      return {
-        top: Math.floor(this.top),
-        left: Math.floor(this.left),
-        bottom: Math.floor(this.bottom),
-        right: Math.floor(this.right)
-      };
-    }
-
-    get top() {
-      return Math.min(this.y1, this.y2);
-    }
-    set top(val) {
-      if (this.y1 < this.y2) {
-        this.y1 = val;
-      } else {
-        this.y2 = val;
-      }
-    }
-
-    get bottom() {
-      return Math.max(this.y1, this.y2);
-    }
-    set bottom(val) {
-      if (this.y1 > this.y2) {
-        this.y1 = val;
-      } else {
-        this.y2 = val;
-      }
-    }
-
-    get left() {
-      return Math.min(this.x1, this.x2);
-    }
-    set left(val) {
-      if (this.x1 < this.x2) {
-        this.x1 = val;
-      } else {
-        this.x2 = val;
-      }
-    }
-
-    get right() {
-      return Math.max(this.x1, this.x2);
-    }
-    set right(val) {
-      if (this.x1 > this.x2) {
-        this.x1 = val;
-      } else {
-        this.x2 = val;
-      }
-    }
-
-    get width() {
-      return Math.abs(this.x1 - this.x2);
-    }
-    get height() {
-      return Math.abs(this.y1 - this.y2);
-    }
-
-    /** Sort x1/x2 and y1/y2 so x1<x2, y1<y2 */
-    sortCoords() {
-      if (this.x1 > this.x2) {
-        const tmp = this.x2;
-        this.x2 = this.x1;
-        this.x1 = tmp;
-      }
-      if (this.y1 > this.y2) {
-        const tmp = this.y2;
-        this.y2 = this.y1;
-        this.y1 = tmp;
-      }
-    }
-
-    union(other) {
-      return new Selection(
-        Math.min(this.left, other.left),
-        Math.min(this.top, other.top),
-        Math.max(this.right, other.right),
-        Math.max(this.bottom, other.bottom)
-      );
-    }
-
-    clone() {
-      return new Selection(this.x1, this.y1, this.x2, this.y2);
-    }
-
-    toJSON() {
-      return {
-        left: this.left,
-        right: this.right,
-        top: this.top,
-        bottom: this.bottom
-      };
-    }
-  }
-
-  Selection.getBoundingClientRect = function(el) {
-    if (!el.getBoundingClientRect) {
-      // Typically the <html> element or somesuch
-      return null;
-    }
-    const rect = el.getBoundingClientRect();
-    if (!rect) {
-      return null;
-    }
-    return new Selection(rect.left, rect.top, rect.right, rect.bottom);
-  };
-
   /** Represents a single x/y point, typically for a mouse click that doesn't have a drag: */
   class Pos {
     constructor(x, y) {
       this.x = x;
       this.y = y;
     }
 
     elementFromPoint() {
@@ -455,17 +338,17 @@ this.uicontrol = (function() {
       let el;
       if (event.target.classList && event.target.classList.contains("preview-overlay")) {
         // The hover is on the overlay, so we need to figure out the real element
         el = ui.iframe.getElementFromPoint(
           event.pageX + window.scrollX - window.pageXOffset,
           event.pageY + window.scrollY - window.pageYOffset
         );
         const xpos = Math.floor(10 * (event.pageX - window.innerWidth / 2) / window.innerWidth);
-        const ypos = Math.floor(10 * (event.pageY - window.innerHeight / 2) / window.innerHeight)
+        const ypos = Math.floor(10 * (event.pageY - window.innerHeight / 2) / window.innerHeight);
 
         for (let i = 0; i < 2; i++) {
           const move = `translate(${xpos}px, ${ypos}px)`;
           event.target.getElementsByClassName("eyeball")[i].style.transform = move;
         }
       } else {
         // The hover is on the element we care about, so we use that
         el = event.target;
@@ -904,30 +787,30 @@ this.uicontrol = (function() {
       return;
     }
     addHandlers();
     if (shouldOnboard) {
       setState("onboarding");
     } else {
       setState("crosshairs");
     }
-  }
+  };
 
   function isFrameset() {
     return document.body.tagName === "FRAMESET";
   }
 
   exports.deactivate = function() {
     try {
       sendEvent("internal", "deactivate");
       setState("cancel");
       callBackground("closeSelector");
       selectorLoader.unloadModules();
     } catch (e) {
-      log.error("Error in deactivate", e)
+      log.error("Error in deactivate", e);
       // Sometimes this fires so late that the document isn't available
       // We don't care about the exception, so we swallow it here
     }
   };
 
   let unloadTime = 0;
 
   exports.unload = function() {
@@ -936,17 +819,17 @@ this.uicontrol = (function() {
     removeHandlers();
   };
 
   /** *********************************************
    * Event handlers
    */
 
   const primedDocumentHandlers = new Map();
-  let registeredDocumentHandlers = []
+  let registeredDocumentHandlers = [];
 
   function addHandlers() {
     ["mouseup", "mousedown", "mousemove", "click"].forEach((eventName) => {
       const fn = watchFunction(assertIsTrusted((function(eventName, event) {
         if (typeof event.button === "number" && event.button !== 0) {
           // Not a left click
           return undefined;
         }