tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

browser-fullZoom.js (23918B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /**
      6 * Controls the "full zoom" setting and its site-specific preferences.
      7 */
      8 var FullZoom = {
      9  // Identifies the setting in the content prefs database.
     10  name: "browser.content.full-zoom",
     11 
     12  // browser.zoom.siteSpecific preference cache
     13  _siteSpecificPref: undefined,
     14 
     15  // browser.zoom.updateBackgroundTabs preference cache
     16  updateBackgroundTabs: undefined,
     17 
     18  // This maps the browser to monotonically increasing integer
     19  // tokens. _browserTokenMap[browser] is increased each time the zoom is
     20  // changed in the browser. See _getBrowserToken and _ignorePendingZoomAccesses.
     21  _browserTokenMap: new WeakMap(),
     22 
     23  // Stores initial locations if we receive onLocationChange
     24  // events before we're initialized.
     25  _initialLocations: new WeakMap(),
     26 
     27  get siteSpecific() {
     28    if (this._siteSpecificPref === undefined) {
     29      this._siteSpecificPref = Services.prefs.getBoolPref(
     30        "browser.zoom.siteSpecific"
     31      );
     32    }
     33    return this._siteSpecificPref;
     34  },
     35 
     36  // nsISupports
     37 
     38  QueryInterface: ChromeUtils.generateQI([
     39    "nsIObserver",
     40    "nsIContentPrefObserver",
     41    "nsISupportsWeakReference",
     42  ]),
     43 
     44  // Initialization & Destruction
     45 
     46  init: function FullZoom_init() {
     47    gBrowser.addEventListener("DoZoomEnlargeBy10", this);
     48    gBrowser.addEventListener("DoZoomReduceBy10", this);
     49    window.addEventListener("MozScaleGestureComplete", this);
     50 
     51    // Register ourselves with the service so we know when our pref changes.
     52    this._cps2 = Cc["@mozilla.org/content-pref/service;1"].getService(
     53      Ci.nsIContentPrefService2
     54    );
     55    this._cps2.addObserverForName(this.name, this);
     56 
     57    this.updateBackgroundTabs = Services.prefs.getBoolPref(
     58      "browser.zoom.updateBackgroundTabs"
     59    );
     60 
     61    // Listen for changes to the browser.zoom branch so we can enable/disable
     62    // updating background tabs and per-site saving and restoring of zoom levels.
     63    Services.prefs.addObserver("browser.zoom.", this, true);
     64 
     65    // If we received onLocationChange events for any of the current browsers
     66    // before we were initialized we want to replay those upon initialization.
     67    for (let browser of gBrowser.browsers) {
     68      if (this._initialLocations.has(browser)) {
     69        this.onLocationChange(...this._initialLocations.get(browser), browser);
     70      }
     71    }
     72 
     73    // This should be nulled after initialization.
     74    this._initialLocations = null;
     75  },
     76 
     77  destroy: function FullZoom_destroy() {
     78    Services.prefs.removeObserver("browser.zoom.", this);
     79    this._cps2.removeObserverForName(this.name, this);
     80    gBrowser.removeEventListener("DoZoomEnlargeBy10", this);
     81    gBrowser.removeEventListener("DoZoomReduceBy10", this);
     82    window.removeEventListener("MozScaleGestureComplete", this);
     83  },
     84 
     85  // Event Handlers
     86 
     87  // EventListener
     88 
     89  handleEvent: function FullZoom_handleEvent(event) {
     90    switch (event.type) {
     91      case "DoZoomEnlargeBy10":
     92        this.changeZoomBy(this._getTargetedBrowser(event), 0.1);
     93        break;
     94      case "DoZoomReduceBy10":
     95        this.changeZoomBy(this._getTargetedBrowser(event), -0.1);
     96        break;
     97      case "MozScaleGestureComplete": {
     98        let nonDefaultScalingZoom = event.detail != 1.0;
     99        this.updateCommands(nonDefaultScalingZoom);
    100        break;
    101      }
    102    }
    103  },
    104 
    105  // nsIObserver
    106 
    107  observe(aSubject, aTopic, aData) {
    108    switch (aTopic) {
    109      case "nsPref:changed":
    110        switch (aData) {
    111          case "browser.zoom.siteSpecific":
    112            // Invalidate pref cache.
    113            this._siteSpecificPref = undefined;
    114            break;
    115          case "browser.zoom.updateBackgroundTabs":
    116            this.updateBackgroundTabs = Services.prefs.getBoolPref(
    117              "browser.zoom.updateBackgroundTabs"
    118            );
    119            break;
    120          case "browser.zoom.full": {
    121            this.updateCommands();
    122            break;
    123          }
    124        }
    125        break;
    126    }
    127  },
    128 
    129  // nsIContentPrefObserver
    130 
    131  onContentPrefSet: function FullZoom_onContentPrefSet(
    132    aGroup,
    133    aName,
    134    aValue,
    135    aIsPrivate
    136  ) {
    137    this._onContentPrefChanged(aGroup, aValue, aIsPrivate);
    138  },
    139 
    140  onContentPrefRemoved: function FullZoom_onContentPrefRemoved(
    141    aGroup,
    142    aName,
    143    aIsPrivate
    144  ) {
    145    this._onContentPrefChanged(aGroup, undefined, aIsPrivate);
    146  },
    147 
    148  /**
    149   * Appropriately updates the zoom level after a content preference has
    150   * changed.
    151   *
    152   * @param aGroup  The group of the changed preference.
    153   * @param aValue  The new value of the changed preference.  Pass undefined to
    154   *                indicate the preference's removal.
    155   */
    156  _onContentPrefChanged: function FullZoom__onContentPrefChanged(
    157    aGroup,
    158    aValue,
    159    aIsPrivate
    160  ) {
    161    if (this._isNextContentPrefChangeInternal) {
    162      // Ignore changes that FullZoom itself makes.  This works because the
    163      // content pref service calls callbacks before notifying observers, and it
    164      // does both in the same turn of the event loop.
    165      delete this._isNextContentPrefChangeInternal;
    166      return;
    167    }
    168 
    169    let browser = gBrowser.selectedBrowser;
    170    if (!browser.currentURI) {
    171      return;
    172    }
    173 
    174    if (this._isPDFViewer(browser)) {
    175      return;
    176    }
    177 
    178    let ctxt = this._loadContextFromBrowser(browser);
    179    let domain = this._cps2.extractDomain(browser.currentURI.spec);
    180    if (aGroup) {
    181      if (aGroup == domain && ctxt.usePrivateBrowsing == aIsPrivate) {
    182        this._applyPrefToZoom(aValue, browser);
    183      }
    184      return;
    185    }
    186 
    187    // If the current page doesn't have a site-specific preference, then its
    188    // zoom should be set to the new global preference now that the global
    189    // preference has changed.
    190    let hasPref = false;
    191    let token = this._getBrowserToken(browser);
    192    this._cps2.getByDomainAndName(browser.currentURI.spec, this.name, ctxt, {
    193      handleResult() {
    194        hasPref = true;
    195      },
    196      handleCompletion: () => {
    197        if (!hasPref && token.isCurrent) {
    198          this._applyPrefToZoom(undefined, browser);
    199        }
    200      },
    201    });
    202  },
    203 
    204  // location change observer
    205 
    206  /**
    207   * Called when the location of a tab changes.
    208   * When that happens, we need to update the current zoom level if appropriate.
    209   *
    210   * @param aURI
    211   *        A URI object representing the new location.
    212   * @param aIsTabSwitch
    213   *        Whether this location change has happened because of a tab switch.
    214   * @param aBrowser
    215   *        (optional) browser object displaying the document
    216   */
    217  onLocationChange: function FullZoom_onLocationChange(
    218    aURI,
    219    aIsTabSwitch,
    220    aBrowser
    221  ) {
    222    let browser = aBrowser || gBrowser.selectedBrowser;
    223 
    224    // If we haven't been initialized yet but receive an onLocationChange
    225    // notification then let's store and replay it upon initialization.
    226    if (this._initialLocations) {
    227      this._initialLocations.set(browser, [aURI, aIsTabSwitch]);
    228      return;
    229    }
    230 
    231    // Ignore all pending async zoom accesses in the browser.  Pending accesses
    232    // that started before the location change will be prevented from applying
    233    // to the new location.
    234    this._ignorePendingZoomAccesses(browser);
    235 
    236    if (!aURI || (aIsTabSwitch && !this.siteSpecific)) {
    237      this._notifyOnLocationChange(browser);
    238      return;
    239    }
    240 
    241    if (aURI.spec == "about:blank") {
    242      if (
    243        !browser.contentPrincipal ||
    244        browser.contentPrincipal.isNullPrincipal
    245      ) {
    246        // For an about:blank with a null principal, zooming any amount does not
    247        // make any sense - so simply do 100%.
    248        this._applyPrefToZoom(
    249          1,
    250          browser,
    251          this._notifyOnLocationChange.bind(this, browser)
    252        );
    253      } else {
    254        // If it's not a null principal, there may be content loaded into it,
    255        // so use the global pref. This will avoid a cps2 roundtrip if we've
    256        // already loaded the global pref once. Really, this should probably
    257        // use the contentPrincipal's origin if it's an http(s) principal.
    258        // (See bug 1457597)
    259        this._applyPrefToZoom(
    260          undefined,
    261          browser,
    262          this._notifyOnLocationChange.bind(this, browser)
    263        );
    264      }
    265      return;
    266    }
    267 
    268    // Media documents should always start at 1, and are not affected by prefs.
    269    if (!aIsTabSwitch && browser.isSyntheticDocument) {
    270      ZoomManager.setZoomForBrowser(browser, 1);
    271      // _ignorePendingZoomAccesses already called above, so no need here.
    272      this._notifyOnLocationChange(browser);
    273      return;
    274    }
    275 
    276    // The PDF viewer zooming isn't handled by `ZoomManager`, ensure that the
    277    // browser zoom level always gets reset to 100% on load (to prevent the
    278    // UI elements of the PDF viewer from being zoomed in/out on load).
    279    if (this._isPDFViewer(browser)) {
    280      this._applyPrefToZoom(
    281        1,
    282        browser,
    283        this._notifyOnLocationChange.bind(this, browser)
    284      );
    285      return;
    286    }
    287 
    288    // See if the zoom pref is cached.
    289    let ctxt = this._loadContextFromBrowser(browser);
    290    let pref = this._cps2.getCachedByDomainAndName(aURI.spec, this.name, ctxt);
    291    if (pref) {
    292      this._applyPrefToZoom(
    293        pref.value,
    294        browser,
    295        this._notifyOnLocationChange.bind(this, browser)
    296      );
    297      return;
    298    }
    299 
    300    // It's not cached, so we have to asynchronously fetch it.
    301    let value = undefined;
    302    let token = this._getBrowserToken(browser);
    303    this._cps2.getByDomainAndName(aURI.spec, this.name, ctxt, {
    304      handleResult(resultPref) {
    305        value = resultPref.value;
    306      },
    307      handleCompletion: () => {
    308        if (!token.isCurrent) {
    309          this._notifyOnLocationChange(browser);
    310          return;
    311        }
    312        this._applyPrefToZoom(
    313          value,
    314          browser,
    315          this._notifyOnLocationChange.bind(this, browser)
    316        );
    317      },
    318    });
    319  },
    320 
    321  // update state of zoom menu items
    322 
    323  /**
    324   * Updates the current windows Zoom commands for zooming in, zooming out
    325   * and resetting the zoom level.
    326   *
    327   * @param {boolean} [forceResetEnabled=false]
    328   *   Set to true if the zoom reset command should be enabled regardless of
    329   *   whether or not the ZoomManager.zoom level is at 1.0. This is specifically
    330   *   for when using scaling zoom via the pinch gesture which doesn't cause
    331   *   the ZoomManager.zoom level to change.
    332   * @returns {Promise<void>}
    333   */
    334  updateCommands: async function FullZoom_updateCommands(
    335    forceResetEnabled = false
    336  ) {
    337    let zoomLevel = ZoomManager.zoom;
    338    let defaultZoomLevel = await ZoomUI.getGlobalValue();
    339    let reduceCmd = document.getElementById("cmd_fullZoomReduce");
    340    if (zoomLevel == ZoomManager.MIN) {
    341      reduceCmd.setAttribute("disabled", "true");
    342    } else {
    343      reduceCmd.removeAttribute("disabled");
    344    }
    345 
    346    let enlargeCmd = document.getElementById("cmd_fullZoomEnlarge");
    347    if (zoomLevel == ZoomManager.MAX) {
    348      enlargeCmd.setAttribute("disabled", "true");
    349    } else {
    350      enlargeCmd.removeAttribute("disabled");
    351    }
    352 
    353    let resetCmd = document.getElementById("cmd_fullZoomReset");
    354    if (zoomLevel == defaultZoomLevel && !forceResetEnabled) {
    355      resetCmd.setAttribute("disabled", "true");
    356    } else {
    357      resetCmd.removeAttribute("disabled");
    358    }
    359 
    360    let fullZoomCmd = document.getElementById("cmd_fullZoomToggle");
    361    fullZoomCmd.toggleAttribute("checked", !ZoomManager.useFullZoom);
    362  },
    363 
    364  // Setting & Pref Manipulation
    365 
    366  sendMessageToPDFViewer(browser, name) {
    367    try {
    368      browser.sendMessageToActor(name, {}, "Pdfjs");
    369    } catch (ex) {
    370      console.error(ex);
    371    }
    372  },
    373 
    374  /**
    375   * If browser in reader mode sends message to reader in order to decrease font size,
    376   * Otherwise reduces the zoom level of the page in the current browser.
    377   */
    378  async reduce() {
    379    let browser = gBrowser.selectedBrowser;
    380    if (browser.currentURI.spec.startsWith("about:reader")) {
    381      browser.sendMessageToActor("Reader:ZoomOut", {}, "AboutReader");
    382    } else if (this._isPDFViewer(browser)) {
    383      this.sendMessageToPDFViewer(browser, "PDFJS:ZoomOut");
    384    } else {
    385      ZoomManager.reduce();
    386      this._ignorePendingZoomAccesses(browser);
    387      await this._applyZoomToPref(browser);
    388    }
    389  },
    390 
    391  /**
    392   * If browser in reader mode sends message to reader in order to increase font size,
    393   * Otherwise enlarges the zoom level of the page in the current browser.
    394   */
    395  async enlarge() {
    396    let browser = gBrowser.selectedBrowser;
    397    if (browser.currentURI.spec.startsWith("about:reader")) {
    398      browser.sendMessageToActor("Reader:ZoomIn", {}, "AboutReader");
    399    } else if (this._isPDFViewer(browser)) {
    400      this.sendMessageToPDFViewer(browser, "PDFJS:ZoomIn");
    401    } else {
    402      ZoomManager.enlarge();
    403      this._ignorePendingZoomAccesses(browser);
    404      await this._applyZoomToPref(browser);
    405    }
    406  },
    407 
    408  /**
    409   * If browser in reader mode sends message to reader in order to increase font size,
    410   * Otherwise enlarges the zoom level of the page in the current browser.
    411   * This function is not async like reduce/enlarge, because it is invoked by our
    412   * event handler. This means that the call to _applyZoomToPref is not awaited and
    413   * will happen asynchronously.
    414   */
    415  changeZoomBy(aBrowser, aValue) {
    416    if (aBrowser.currentURI.spec.startsWith("about:reader")) {
    417      const message = aValue > 0 ? "Reader::ZoomIn" : "Reader:ZoomOut";
    418      aBrowser.sendMessageToActor(message, {}, "AboutReader");
    419      return;
    420    } else if (this._isPDFViewer(aBrowser)) {
    421      const message = aValue > 0 ? "PDFJS::ZoomIn" : "PDFJS:ZoomOut";
    422      this.sendMessageToPDFViewer(aBrowser, message);
    423      return;
    424    }
    425    let zoom = ZoomManager.getZoomForBrowser(aBrowser);
    426    zoom += aValue;
    427    if (zoom < ZoomManager.MIN) {
    428      zoom = ZoomManager.MIN;
    429    } else if (zoom > ZoomManager.MAX) {
    430      zoom = ZoomManager.MAX;
    431    }
    432    ZoomManager.setZoomForBrowser(aBrowser, zoom);
    433    this._ignorePendingZoomAccesses(aBrowser);
    434    this._applyZoomToPref(aBrowser);
    435  },
    436 
    437  /**
    438   * Sets the zoom level for the given browser to the given floating
    439   * point value, where 1 is the default zoom level.
    440   */
    441  setZoom(value, browser = gBrowser.selectedBrowser) {
    442    if (this._isPDFViewer(browser)) {
    443      return;
    444    }
    445    ZoomManager.setZoomForBrowser(browser, value);
    446    this._ignorePendingZoomAccesses(browser);
    447    this._applyZoomToPref(browser);
    448  },
    449 
    450  /**
    451   * Sets the zoom level of the page in the given browser to the global zoom
    452   * level.
    453   *
    454   * @return A promise which resolves when the zoom reset has been applied.
    455   */
    456  reset: function FullZoom_reset(browser = gBrowser.selectedBrowser) {
    457    let forceValue;
    458    if (browser.currentURI.spec.startsWith("about:reader")) {
    459      browser.sendMessageToActor("Reader:ResetZoom", {}, "AboutReader");
    460    } else if (this._isPDFViewer(browser)) {
    461      this.sendMessageToPDFViewer(browser, "PDFJS:ZoomReset");
    462      // Ensure that the UI elements of the PDF viewer won't be zoomed in/out
    463      // on reset, even if/when browser default zoom value is not set to 100%.
    464      forceValue = 1;
    465    }
    466    let token = this._getBrowserToken(browser);
    467    let result = ZoomUI.getGlobalValue().then(value => {
    468      if (token.isCurrent) {
    469        ZoomManager.setZoomForBrowser(browser, forceValue || value);
    470        this._ignorePendingZoomAccesses(browser);
    471      }
    472    });
    473    this._removePref(browser);
    474    return result;
    475  },
    476 
    477  /**
    478   * Called from the URL bar's inline zoom reset indicator button.
    479   */
    480  resetFromURLBar() {
    481    this.reset();
    482    this.resetScalingZoom();
    483  },
    484 
    485  resetScalingZoom: function FullZoom_resetScaling(
    486    browser = gBrowser.selectedBrowser
    487  ) {
    488    browser.browsingContext?.resetScalingZoom();
    489  },
    490 
    491  /**
    492   * Set the zoom level for a given browser.
    493   *
    494   * Per nsPresContext::setFullZoom, we can set the zoom to its current value
    495   * without significant impact on performance, as the setting is only applied
    496   * if it differs from the current setting.  In fact getting the zoom and then
    497   * checking ourselves if it differs costs more.
    498   *
    499   * And perhaps we should always set the zoom even if it was more expensive,
    500   * since nsDocumentViewer::SetTextZoom claims that child documents can have
    501   * a different text zoom (although it would be unusual), and it implies that
    502   * those child text zooms should get updated when the parent zoom gets set,
    503   * and perhaps the same is true for full zoom
    504   * (although nsDocumentViewer::SetFullZoom doesn't mention it).
    505   *
    506   * So when we apply new zoom values to the browser, we simply set the zoom.
    507   * We don't check first to see if the new value is the same as the current
    508   * one.
    509   *
    510   * @param aValue     The zoom level value.
    511   * @param aBrowser   The zoom is set in this browser.  Required.
    512   * @param aCallback  If given, it's asynchronously called when complete.
    513   */
    514  _applyPrefToZoom: function FullZoom__applyPrefToZoom(
    515    aValue,
    516    aBrowser,
    517    aCallback
    518  ) {
    519    // The browser is sometimes half-destroyed because this method is called
    520    // by content pref service callbacks, which themselves can be called at any
    521    // time, even after browsers are closed.
    522    if (
    523      !aBrowser.mInitialized ||
    524      aBrowser.isSyntheticDocument ||
    525      (!this.siteSpecific && aBrowser.tabHasCustomZoom)
    526    ) {
    527      this._executeSoon(aCallback);
    528      return;
    529    }
    530 
    531    if (aValue !== undefined && this.siteSpecific) {
    532      ZoomManager.setZoomForBrowser(aBrowser, this._ensureValid(aValue));
    533      this._ignorePendingZoomAccesses(aBrowser);
    534      this._executeSoon(aCallback);
    535      return;
    536    }
    537 
    538    // Above, we check if site-specific zoom is enabled before setting
    539    // the tab browser zoom, however global zoom should work independent
    540    // of the site-specific pref, so we do no checks here.
    541    let token = this._getBrowserToken(aBrowser);
    542    ZoomUI.getGlobalValue().then(value => {
    543      if (token.isCurrent) {
    544        ZoomManager.setZoomForBrowser(aBrowser, value);
    545        this._ignorePendingZoomAccesses(aBrowser);
    546      }
    547      this._executeSoon(aCallback);
    548    });
    549  },
    550 
    551  /**
    552   * Saves the zoom level of the page in the given browser to the content
    553   * prefs store.
    554   *
    555   * @param browser  The zoom of this browser will be saved.  Required.
    556   */
    557  _applyZoomToPref: function FullZoom__applyZoomToPref(browser) {
    558    if (!this.siteSpecific || browser.isSyntheticDocument) {
    559      // If site-specific zoom is disabled, we have called this function
    560      // to adjust our tab's zoom level. It is now considered "custom"
    561      // and we mark that here.
    562      browser.tabHasCustomZoom = !this.siteSpecific;
    563      return null;
    564    }
    565 
    566    return new Promise(resolve => {
    567      this._cps2.set(
    568        browser.currentURI.spec,
    569        this.name,
    570        ZoomManager.getZoomForBrowser(browser),
    571        this._loadContextFromBrowser(browser),
    572        {
    573          handleCompletion: () => {
    574            this._isNextContentPrefChangeInternal = true;
    575            resolve();
    576          },
    577        }
    578      );
    579    });
    580  },
    581 
    582  /**
    583   * Removes from the content prefs store the zoom level of the given browser.
    584   *
    585   * @param browser  The zoom of this browser will be removed.  Required.
    586   */
    587  _removePref: function FullZoom__removePref(browser) {
    588    if (browser.isSyntheticDocument) {
    589      return;
    590    }
    591    let ctxt = this._loadContextFromBrowser(browser);
    592    this._cps2.removeByDomainAndName(browser.currentURI.spec, this.name, ctxt, {
    593      handleCompletion: () => {
    594        this._isNextContentPrefChangeInternal = true;
    595      },
    596    });
    597  },
    598 
    599  // Utilities
    600 
    601  /**
    602   * Returns the zoom change token of the given browser.  Asynchronous
    603   * operations that access the given browser's zoom should use this method to
    604   * capture the token before starting and use token.isCurrent to determine if
    605   * it's safe to access the zoom when done.  If token.isCurrent is false, then
    606   * after the async operation started, either the browser's zoom was changed or
    607   * the browser was destroyed, and depending on what the operation is doing, it
    608   * may no longer be safe to set and get its zoom.
    609   *
    610   * @param browser  The token of this browser will be returned.
    611   * @return  An object with an "isCurrent" getter.
    612   */
    613  _getBrowserToken: function FullZoom__getBrowserToken(browser) {
    614    let map = this._browserTokenMap;
    615    if (!map.has(browser)) {
    616      map.set(browser, 0);
    617    }
    618    return {
    619      token: map.get(browser),
    620      get isCurrent() {
    621        // At this point, the browser may have been destructed and unbound but
    622        // its outer ID not removed from the map because outer-window-destroyed
    623        // hasn't been received yet.  In that case, the browser is unusable, it
    624        // has no properties, so return false.  Check for this case by getting a
    625        // property, say, docShell.
    626        return map.get(browser) === this.token && browser.mInitialized;
    627      },
    628    };
    629  },
    630 
    631  /**
    632   * Returns the browser that the supplied zoom event is associated with.
    633   *
    634   * @param event  The zoom event.
    635   * @return  The associated browser element, if one exists, otherwise null.
    636   */
    637  _getTargetedBrowser: function FullZoom__getTargetedBrowser(event) {
    638    let target = event.originalTarget;
    639 
    640    // With remote content browsers, the event's target is the browser
    641    // we're looking for.
    642    const XUL_NS =
    643      "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
    644    if (
    645      window.XULElement.isInstance(target) &&
    646      target.localName == "browser" &&
    647      target.namespaceURI == XUL_NS
    648    ) {
    649      return target;
    650    }
    651 
    652    // With in-process content browsers, the event's target is the content
    653    // document.
    654    if (target.nodeType == Node.DOCUMENT_NODE) {
    655      return target.ownerGlobal.docShell.chromeEventHandler;
    656    }
    657 
    658    throw new Error("Unexpected zoom event source");
    659  },
    660 
    661  /**
    662   * Increments the zoom change token for the given browser so that pending
    663   * async operations know that it may be unsafe to access they zoom when they
    664   * finish.
    665   *
    666   * @param browser  Pending accesses in this browser will be ignored.
    667   */
    668  _ignorePendingZoomAccesses: function FullZoom__ignorePendingZoomAccesses(
    669    browser
    670  ) {
    671    let map = this._browserTokenMap;
    672    map.set(browser, (map.get(browser) || 0) + 1);
    673  },
    674 
    675  _ensureValid: function FullZoom__ensureValid(aValue) {
    676    // Note that undefined is a valid value for aValue that indicates a known-
    677    // not-to-exist value.
    678    if (isNaN(aValue)) {
    679      return 1;
    680    }
    681 
    682    if (aValue < ZoomManager.MIN) {
    683      return ZoomManager.MIN;
    684    }
    685 
    686    if (aValue > ZoomManager.MAX) {
    687      return ZoomManager.MAX;
    688    }
    689 
    690    return aValue;
    691  },
    692 
    693  /**
    694   * Gets the load context from the given Browser.
    695   *
    696   * @param Browser  The Browser whose load context will be returned.
    697   * @return        The nsILoadContext of the given Browser.
    698   */
    699  _loadContextFromBrowser: function FullZoom__loadContextFromBrowser(browser) {
    700    return browser.loadContext;
    701  },
    702 
    703  /**
    704   * Asynchronously broadcasts "browser-fullZoom:location-change" so that
    705   * listeners can be notified when the zoom levels on those pages change.
    706   * The notification is always asynchronous so that observers are guaranteed a
    707   * consistent behavior.
    708   */
    709  _notifyOnLocationChange: function FullZoom__notifyOnLocationChange(browser) {
    710    this._executeSoon(function () {
    711      Services.obs.notifyObservers(browser, "browser-fullZoom:location-change");
    712    });
    713  },
    714 
    715  _executeSoon: function FullZoom__executeSoon(callback) {
    716    if (!callback) {
    717      return;
    718    }
    719    Services.tm.dispatchToMainThread(callback);
    720  },
    721 
    722  _isPDFViewer(browser) {
    723    return !!(
    724      browser.contentPrincipal.spec == "resource://pdf.js/web/viewer.html"
    725    );
    726  },
    727 };