Bug 1465581 - Export Screenshots 33.0.0 to Firefox (code excluding translations and Raven update); r?ianbicking draft
authorBarry Chen <bchen@mozilla.com>
Mon, 18 Jun 2018 11:07:23 -0500
changeset 808168 2cf9020b6313
parent 808167 503dae1f3464
push id113300
push userbmo:bchen@mozilla.com
push dateMon, 18 Jun 2018 16:09:31 +0000
reviewersianbicking
bugs1465581
milestone62.0a1
Bug 1465581 - Export Screenshots 33.0.0 to Firefox (code excluding translations and Raven update); r?ianbicking 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;
         }