show write-style entries for iframes in popup (#861)
* account for iframes in popup list/write-style and badge * fix and simplify openURL + onTabReady + message from popup * fixup! resolve about:blank iframes to their parent URL * fixup! don't underline iframe links until hovered * fix width bug in popup only when needed (Chrome 66-69) * fixup! reset styleIds on main page navigation * fixup! call updateCount explicitly on extension pages * fixup! ensure frame url is present * fixup! frameResults entry may be empty * fixup! init main frame first * fixup! track iframes via ports * fixup! reduce badge update rate during page load * fixup! cosmetics * fixup! don't add frames with errors * fixup! cosmetics
This commit is contained in:
		
							parent
							
								
									4bbce7cb9f
								
							
						
					
					
						commit
						8192fab1b8
					
				| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
  URLS ignoreChromeError usercssHelper
 | 
			
		||||
  styleManager msg navigatorUtil workerUtil contentScripts sync
 | 
			
		||||
  findExistingTab createTab activateTab isTabReplaceable getActiveTab
 | 
			
		||||
  iconManager tabManager */
 | 
			
		||||
  tabManager */
 | 
			
		||||
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -49,16 +49,27 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
 | 
			
		|||
 | 
			
		||||
  openEditor,
 | 
			
		||||
 | 
			
		||||
  updateIconBadge(count) {
 | 
			
		||||
    iconManager.updateIconBadge(this.sender.tab.id, count);
 | 
			
		||||
    return true;
 | 
			
		||||
  /* Same as openURL, the only extra prop in `opts` is `message` - it'll be sent when the tab is ready,
 | 
			
		||||
  which is needed in the popup, otherwise another extension could force the tab to open in foreground
 | 
			
		||||
  thus auto-closing the popup (in Chrome at least) and preventing the sendMessage code from running */
 | 
			
		||||
  openURL(opts) {
 | 
			
		||||
    const {message} = opts;
 | 
			
		||||
    return openURL(opts) // will pass the resolved value untouched when `message` is absent or falsy
 | 
			
		||||
      .then(message && (tab => tab.status === 'complete' ? tab : onTabReady(tab)))
 | 
			
		||||
      .then(message && (tab => msg.sendTab(tab.id, opts.message)));
 | 
			
		||||
    function onTabReady(tab) {
 | 
			
		||||
      return new Promise((resolve, reject) =>
 | 
			
		||||
        setTimeout(function ping(numTries = 10, delay = 100) {
 | 
			
		||||
          msg.sendTab(tab.id, {method: 'ping'})
 | 
			
		||||
            .catch(() => false)
 | 
			
		||||
            .then(pong => pong
 | 
			
		||||
              ? resolve(tab)
 | 
			
		||||
              : numTries && setTimeout(ping, delay, numTries - 1, delay * 1.5) ||
 | 
			
		||||
                reject('timeout'));
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // exposed for stuff that requires followup sendMessage() like popup::openSettings
 | 
			
		||||
  // that would fail otherwise if another extension forced the tab to open
 | 
			
		||||
  // in the foreground thus auto-closing the popup (in Chrome)
 | 
			
		||||
  openURL,
 | 
			
		||||
 | 
			
		||||
  optionsCustomizeHotkeys() {
 | 
			
		||||
    return browser.runtime.openOptionsPage()
 | 
			
		||||
      .then(() => new Promise(resolve => setTimeout(resolve, 100)))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,10 @@
 | 
			
		|||
/* global prefs debounce iconUtil FIREFOX CHROME VIVALDI tabManager */
 | 
			
		||||
/* global prefs debounce iconUtil FIREFOX CHROME VIVALDI tabManager navigatorUtil API_METHODS */
 | 
			
		||||
/* exported iconManager */
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const iconManager = (() => {
 | 
			
		||||
  const ICON_SIZES = FIREFOX || CHROME >= 2883 && !VIVALDI ? [16, 32] : [19, 38];
 | 
			
		||||
  const staleBadges = new Set();
 | 
			
		||||
 | 
			
		||||
  prefs.subscribe([
 | 
			
		||||
    'disableAll',
 | 
			
		||||
| 
						 | 
				
			
			@ -26,32 +27,51 @@ const iconManager = (() => {
 | 
			
		|||
    refreshAllIcons();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return {updateIconBadge};
 | 
			
		||||
  Object.assign(API_METHODS, {
 | 
			
		||||
    /** @param {(number|string)[]} styleIds
 | 
			
		||||
     * @param {boolean} [lazyBadge=false] preventing flicker during page load */
 | 
			
		||||
    updateIconBadge(styleIds, {lazyBadge} = {}) {
 | 
			
		||||
      // FIXME: in some cases, we only have to redraw the badge. is it worth a optimization?
 | 
			
		||||
      const {frameId, tab: {id: tabId}} = this.sender;
 | 
			
		||||
      const value = styleIds.length ? styleIds.map(Number) : undefined;
 | 
			
		||||
      tabManager.set(tabId, 'styleIds', frameId, value);
 | 
			
		||||
      debounce(refreshStaleBadges, frameId && lazyBadge ? 250 : 0);
 | 
			
		||||
      staleBadges.add(tabId);
 | 
			
		||||
      if (!frameId) refreshIcon(tabId, true);
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // FIXME: in some cases, we only have to redraw the badge. is it worth a optimization?
 | 
			
		||||
  function updateIconBadge(tabId, count, force = true) {
 | 
			
		||||
    tabManager.set(tabId, 'count', count);
 | 
			
		||||
    refreshIconBadgeText(tabId);
 | 
			
		||||
    refreshIcon(tabId, force);
 | 
			
		||||
  navigatorUtil.onCommitted(({tabId, frameId}) => {
 | 
			
		||||
    if (!frameId) tabManager.set(tabId, 'styleIds', undefined);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  chrome.runtime.onConnect.addListener(port => {
 | 
			
		||||
    if (port.name === 'iframe') {
 | 
			
		||||
      port.onDisconnect.addListener(onPortDisconnected);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  function onPortDisconnected({sender}) {
 | 
			
		||||
    if (tabManager.get(sender.tab.id, 'styleIds')) {
 | 
			
		||||
      API_METHODS.updateIconBadge.call({sender}, [], {lazyBadge: true});
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function refreshIconBadgeText(tabId) {
 | 
			
		||||
    const count = tabManager.get(tabId, 'count');
 | 
			
		||||
    iconUtil.setBadgeText({
 | 
			
		||||
      text: prefs.get('show-badge') && count ? String(count) : '',
 | 
			
		||||
      tabId
 | 
			
		||||
    });
 | 
			
		||||
    const text = prefs.get('show-badge') ? `${getStyleCount(tabId)}` : '';
 | 
			
		||||
    iconUtil.setBadgeText({tabId, text});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function getIconName(count = 0) {
 | 
			
		||||
  function getIconName(hasStyles = false) {
 | 
			
		||||
    const iconset = prefs.get('iconset') === 1 ? 'light/' : '';
 | 
			
		||||
    const postfix = prefs.get('disableAll') ? 'x' : !count ? 'w' : '';
 | 
			
		||||
    const postfix = prefs.get('disableAll') ? 'x' : !hasStyles ? 'w' : '';
 | 
			
		||||
    return `${iconset}$SIZE$${postfix}`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function refreshIcon(tabId, force = false) {
 | 
			
		||||
    const oldIcon = tabManager.get(tabId, 'icon');
 | 
			
		||||
    const newIcon = getIconName(tabManager.get(tabId, 'count'));
 | 
			
		||||
    const newIcon = getIconName(tabManager.get(tabId, 'styleIds', 0));
 | 
			
		||||
    // (changing the icon only for the main page, frameId = 0)
 | 
			
		||||
 | 
			
		||||
    if (!force && oldIcon === newIcon) {
 | 
			
		||||
      return;
 | 
			
		||||
| 
						 | 
				
			
			@ -73,6 +93,14 @@ const iconManager = (() => {
 | 
			
		|||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** @return {number | ''} */
 | 
			
		||||
  function getStyleCount(tabId) {
 | 
			
		||||
    const allIds = new Set();
 | 
			
		||||
    const data = tabManager.get(tabId, 'styleIds') || {};
 | 
			
		||||
    Object.values(data).forEach(frameIds => frameIds.forEach(id => allIds.add(id)));
 | 
			
		||||
    return allIds.size || '';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function refreshGlobalIcon() {
 | 
			
		||||
    iconUtil.setIcon({
 | 
			
		||||
      path: getIconPath(getIconName())
 | 
			
		||||
| 
						 | 
				
			
			@ -98,4 +126,11 @@ const iconManager = (() => {
 | 
			
		|||
      refreshIconBadgeText(tabId);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function refreshStaleBadges() {
 | 
			
		||||
    for (const tabId of staleBadges) {
 | 
			
		||||
      refreshIconBadgeText(tabId);
 | 
			
		||||
    }
 | 
			
		||||
    staleBadges.clear();
 | 
			
		||||
  }
 | 
			
		||||
})();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
/* global API_METHODS styleManager CHROME prefs iconManager */
 | 
			
		||||
/* global API_METHODS styleManager CHROME prefs */
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
API_METHODS.styleViaAPI = !CHROME && (() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -31,12 +31,13 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
 | 
			
		|||
        .then(maybeToggleObserver);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  function updateCount(request, {tab, frameId}) {
 | 
			
		||||
  function updateCount(request, sender) {
 | 
			
		||||
    const {tab, frameId} = sender;
 | 
			
		||||
    if (frameId) {
 | 
			
		||||
      throw new Error('we do not count styles for frames');
 | 
			
		||||
    }
 | 
			
		||||
    const {frameStyles} = getCachedData(tab.id, frameId);
 | 
			
		||||
    iconManager.updateIconBadge(tab.id, Object.keys(frameStyles).length);
 | 
			
		||||
    API_METHODS.updateIconBadge.call({sender}, Object.keys(frameStyles));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function styleApply({id = null, ignoreUrlCheck = false}, {tab, frameId, url}) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,17 +24,28 @@ const tabManager = (() => {
 | 
			
		|||
    onUpdate(fn) {
 | 
			
		||||
      listeners.push(fn);
 | 
			
		||||
    },
 | 
			
		||||
    get(tabId, key) {
 | 
			
		||||
      const meta = cache.get(tabId);
 | 
			
		||||
      return meta && meta[key];
 | 
			
		||||
    get(tabId, ...keys) {
 | 
			
		||||
      return keys.reduce((meta, key) => meta && meta[key], cache.get(tabId));
 | 
			
		||||
    },
 | 
			
		||||
    set(tabId, key, value) {
 | 
			
		||||
    /**
 | 
			
		||||
     * number of keys is arbitrary, last arg is value, `undefined` will delete the last key from meta
 | 
			
		||||
     * (tabId, 'foo', 123) will set tabId's meta to {foo: 123},
 | 
			
		||||
     * (tabId, 'foo', 'bar', 'etc', 123) will set tabId's meta to {foo: {bar: {etc: 123}}}
 | 
			
		||||
     */
 | 
			
		||||
    set(tabId, ...args) {
 | 
			
		||||
      let meta = cache.get(tabId);
 | 
			
		||||
      if (!meta) {
 | 
			
		||||
        meta = {};
 | 
			
		||||
        cache.set(tabId, meta);
 | 
			
		||||
      }
 | 
			
		||||
      meta[key] = value;
 | 
			
		||||
      const value = args.pop();
 | 
			
		||||
      const lastKey = args.pop();
 | 
			
		||||
      for (const key of args) meta = meta[key] || (meta[key] = {});
 | 
			
		||||
      if (value === undefined) {
 | 
			
		||||
        delete meta[lastKey];
 | 
			
		||||
      } else {
 | 
			
		||||
        meta[lastKey] = value;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    list() {
 | 
			
		||||
      return cache.keys();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,12 +9,25 @@
 | 
			
		|||
self.INJECTED !== 1 && (() => {
 | 
			
		||||
  self.INJECTED = 1;
 | 
			
		||||
 | 
			
		||||
  let IS_TAB = !chrome.tabs || location.pathname !== '/popup.html';
 | 
			
		||||
  const IS_FRAME = window !== parent;
 | 
			
		||||
  const STYLE_VIA_API = !chrome.app && document instanceof XMLDocument;
 | 
			
		||||
  const styleInjector = createStyleInjector({
 | 
			
		||||
    compare: (a, b) => a.id - b.id,
 | 
			
		||||
    onUpdate: onInjectorUpdate,
 | 
			
		||||
  });
 | 
			
		||||
  const initializing = init();
 | 
			
		||||
  /** @type chrome.runtime.Port */
 | 
			
		||||
  let port;
 | 
			
		||||
  let lazyBadge = IS_FRAME;
 | 
			
		||||
 | 
			
		||||
  // the popup needs a check as it's not a tab but can be opened in a tab manually for whatever reason
 | 
			
		||||
  if (!IS_TAB) {
 | 
			
		||||
    chrome.tabs.getCurrent(tab => {
 | 
			
		||||
      IS_TAB = Boolean(tab);
 | 
			
		||||
      if (tab && styleInjector.list.length) updateCount();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // save it now because chrome.runtime will be unavailable in the orphaned script
 | 
			
		||||
  const orphanEventId = chrome.runtime.id;
 | 
			
		||||
| 
						 | 
				
			
			@ -32,7 +45,7 @@ self.INJECTED !== 1 && (() => {
 | 
			
		|||
  let parentDomain;
 | 
			
		||||
 | 
			
		||||
  prefs.subscribe(['disableAll'], (key, value) => doDisableAll(value));
 | 
			
		||||
  if (window !== parent) {
 | 
			
		||||
  if (IS_FRAME) {
 | 
			
		||||
    prefs.subscribe(['exposeIframes'], updateExposeIframes);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -55,7 +68,7 @@ self.INJECTED !== 1 && (() => {
 | 
			
		|||
      // dynamic about: and javascript: iframes don't have an URL yet
 | 
			
		||||
      // so we'll try the parent frame which is guaranteed to have a real URL
 | 
			
		||||
      try {
 | 
			
		||||
        if (window !== parent) {
 | 
			
		||||
        if (IS_FRAME) {
 | 
			
		||||
          matchUrl = parent.location.href;
 | 
			
		||||
        }
 | 
			
		||||
      } catch (e) {}
 | 
			
		||||
| 
						 | 
				
			
			@ -153,19 +166,19 @@ self.INJECTED !== 1 && (() => {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  function updateCount() {
 | 
			
		||||
    if (window !== parent) {
 | 
			
		||||
      // we don't care about iframes
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (/^\w+?-extension:\/\/.+(popup|options)\.html$/.test(location.href)) {
 | 
			
		||||
      // popup and the option page are not tabs
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (STYLE_VIA_API) {
 | 
			
		||||
      API.styleViaAPI({method: 'updateCount'}).catch(msg.ignoreError);
 | 
			
		||||
    } else {
 | 
			
		||||
      API.updateIconBadge(styleInjector.list.length).catch(console.error);
 | 
			
		||||
    if (!IS_TAB) return;
 | 
			
		||||
    if (IS_FRAME) {
 | 
			
		||||
      if (!port && styleInjector.list.length) {
 | 
			
		||||
        port = chrome.runtime.connect({name: 'iframe'});
 | 
			
		||||
      } else if (port && !styleInjector.list.length) {
 | 
			
		||||
        port.disconnect();
 | 
			
		||||
      }
 | 
			
		||||
      if (lazyBadge && performance.now() > 1000) lazyBadge = false;
 | 
			
		||||
    }
 | 
			
		||||
    (STYLE_VIA_API ?
 | 
			
		||||
      API.styleViaAPI({method: 'updateCount'}) :
 | 
			
		||||
      API.updateIconBadge(styleInjector.list.map(style => style.id), {lazyBadge})
 | 
			
		||||
    ).catch(msg.ignoreError);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function orphanCheck() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
/* exported getActiveTab onTabReady stringAsRegExp getTabRealURL openURL
 | 
			
		||||
/* exported getTab getActiveTab onTabReady stringAsRegExp openURL ignoreChromeError
 | 
			
		||||
  getStyleWithNoCode tryRegExp sessionStorageHash download deepEqual
 | 
			
		||||
  closeCurrentTab capitalize CHROME_HAS_BORDER_BUG */
 | 
			
		||||
/* global promisify */
 | 
			
		||||
| 
						 | 
				
			
			@ -125,82 +125,6 @@ function getActiveTab() {
 | 
			
		|||
    .then(tabs => tabs[0]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getTabRealURL(tab) {
 | 
			
		||||
  return new Promise(resolve => {
 | 
			
		||||
    if (tab.url !== 'chrome://newtab/' || URLS.chromeProtectsNTP) {
 | 
			
		||||
      resolve(tab.url);
 | 
			
		||||
    } else {
 | 
			
		||||
      chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, frame => {
 | 
			
		||||
        resolve(frame && frame.url || '');
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Resolves when the [just created] tab is ready for communication.
 | 
			
		||||
 * @param {Number|Tab} tabOrId
 | 
			
		||||
 * @returns {Promise<?Tab>}
 | 
			
		||||
 */
 | 
			
		||||
function onTabReady(tabOrId) {
 | 
			
		||||
  let tabId, tab;
 | 
			
		||||
  if (Number.isInteger(tabOrId)) {
 | 
			
		||||
    tabId = tabOrId;
 | 
			
		||||
  } else {
 | 
			
		||||
    tab = tabOrId;
 | 
			
		||||
    tabId = tab && tab.id;
 | 
			
		||||
  }
 | 
			
		||||
  if (!tab) {
 | 
			
		||||
    return getTab(tabId).then(onTabReady);
 | 
			
		||||
  }
 | 
			
		||||
  if (tab.status === 'complete') {
 | 
			
		||||
    if (!FIREFOX || tab.url !== 'about:blank') {
 | 
			
		||||
      return Promise.resolve(tab);
 | 
			
		||||
    } else {
 | 
			
		||||
      return new Promise(resolve => {
 | 
			
		||||
        chrome.webNavigation.getFrame({tabId, frameId: 0}, frame => {
 | 
			
		||||
          ignoreChromeError();
 | 
			
		||||
          if (frame) {
 | 
			
		||||
            onTabReady(tab).then(resolve);
 | 
			
		||||
          } else {
 | 
			
		||||
            setTimeout(() => onTabReady(tabId).then(resolve));
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    chrome.webNavigation.onCommitted.addListener(onCommitted);
 | 
			
		||||
    chrome.webNavigation.onErrorOccurred.addListener(onErrorOccurred);
 | 
			
		||||
    chrome.tabs.onRemoved.addListener(onTabRemoved);
 | 
			
		||||
    chrome.tabs.onReplaced.addListener(onTabReplaced);
 | 
			
		||||
    function onCommitted(info) {
 | 
			
		||||
      if (info.tabId !== tabId) return;
 | 
			
		||||
      unregister();
 | 
			
		||||
      getTab(tab.id).then(resolve);
 | 
			
		||||
    }
 | 
			
		||||
    function onErrorOccurred(info) {
 | 
			
		||||
      if (info.tabId !== tabId) return;
 | 
			
		||||
      unregister();
 | 
			
		||||
      reject();
 | 
			
		||||
    }
 | 
			
		||||
    function onTabRemoved(removedTabId) {
 | 
			
		||||
      if (removedTabId !== tabId) return;
 | 
			
		||||
      unregister();
 | 
			
		||||
      reject();
 | 
			
		||||
    }
 | 
			
		||||
    function onTabReplaced(addedTabId, removedTabId) {
 | 
			
		||||
      onTabRemoved(removedTabId);
 | 
			
		||||
    }
 | 
			
		||||
    function unregister() {
 | 
			
		||||
      chrome.webNavigation.onCommitted.removeListener(onCommitted);
 | 
			
		||||
      chrome.webNavigation.onErrorOccurred.removeListener(onErrorOccurred);
 | 
			
		||||
      chrome.tabs.onRemoved.removeListener(onTabRemoved);
 | 
			
		||||
      chrome.tabs.onReplaced.removeListener(onTabReplaced);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function urlToMatchPattern(url, ignoreSearch) {
 | 
			
		||||
  // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns
 | 
			
		||||
  if (!/^(http|https|ws|wss|ftp|data|file)$/.test(url.protocol)) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -235,6 +235,7 @@
 | 
			
		|||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div id="write-style">
 | 
			
		||||
        <a id="write-for-frames" href="#" title="<IFRAME>..." hidden></a>
 | 
			
		||||
        <span id="write-style-for" i18n-text="writeStyleFor"></span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,12 +8,6 @@
 | 
			
		|||
  --outer-padding: 9px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
html {
 | 
			
		||||
  /* Chrome 66-?? adds a gap equal to the scrollbar width,
 | 
			
		||||
     which looks like a bug, see https://crbug.com/821143 */
 | 
			
		||||
  overflow: overlay;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
html, body {
 | 
			
		||||
  height: min-content;
 | 
			
		||||
  max-height: 600px;
 | 
			
		||||
| 
						 | 
				
			
			@ -313,6 +307,15 @@ a.configure[target="_blank"] .svg-icon.config {
 | 
			
		|||
  color: darkred;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.frame-url::before {
 | 
			
		||||
  content: "iframe: ";
 | 
			
		||||
  color: lightslategray;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.frame .style-name {
 | 
			
		||||
  font-weight: normal;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* entry menu */
 | 
			
		||||
.entry .menu {
 | 
			
		||||
  display: none;
 | 
			
		||||
| 
						 | 
				
			
			@ -516,13 +519,85 @@ body.blocked .actions > .main-controls {
 | 
			
		|||
  content: "\00ad"; /* "soft" hyphen */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#match {
 | 
			
		||||
.about-blank > .breadcrumbs {
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.about-blank > .breadcrumbs a {
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.match {
 | 
			
		||||
  overflow-wrap: break-word;
 | 
			
		||||
  display: block;
 | 
			
		||||
  flex-grow: 9;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.match[data-frame-id="0"] {
 | 
			
		||||
  min-width: 200px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.match[data-frame-id="0"] > .match {
 | 
			
		||||
  margin-top: .25em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.match:not([data-frame-id="0"]) a {
 | 
			
		||||
  text-decoration: none; /* not underlining iframe links until hovered to reduce visual noise */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.match .match {
 | 
			
		||||
  margin-left: .5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.match .match::before {
 | 
			
		||||
  content: "";
 | 
			
		||||
  width: .25rem;
 | 
			
		||||
  height: .25rem;
 | 
			
		||||
  margin-left: -.5rem;
 | 
			
		||||
  display: block;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  border-width: 1px;
 | 
			
		||||
  border-style: none none solid solid;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dupe > .breadcrumbs {
 | 
			
		||||
  opacity: .5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dupe:not([data-children]) {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#write-for-frames {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  width: 5px;
 | 
			
		||||
  height: 5px;
 | 
			
		||||
  margin-left: -12px;
 | 
			
		||||
  margin-top: 4px;
 | 
			
		||||
  --dash: transparent 2px, currentColor 2px, currentColor 3px, transparent 3px;
 | 
			
		||||
  background: linear-gradient(var(--dash)), linear-gradient(90deg, var(--dash));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#write-for-frames.expanded {
 | 
			
		||||
  background: linear-gradient(var(--dash));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#write-for-frames::after {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  margin: -2px;
 | 
			
		||||
  border: 1px solid currentColor;
 | 
			
		||||
  content: "";
 | 
			
		||||
  top: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#write-for-frames:not(.expanded) ~ .match:not([data-frame-id="0"]),
 | 
			
		||||
#write-for-frames:not(.expanded) ~ .match .match {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* "breadcrumbs" 'new style' links */
 | 
			
		||||
.breadcrumbs > .write-style-link {
 | 
			
		||||
  margin-left: 0
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										282
									
								
								popup/popup.js
									
									
									
									
									
								
							
							
						
						
									
										282
									
								
								popup/popup.js
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -1,39 +1,41 @@
 | 
			
		|||
/* global configDialog hotkeys onTabReady msg
 | 
			
		||||
  getActiveTab FIREFOX getTabRealURL URLS API onDOMready $ $$ prefs
 | 
			
		||||
/* global configDialog hotkeys msg
 | 
			
		||||
  getActiveTab CHROME FIREFOX URLS API onDOMready $ $$ prefs
 | 
			
		||||
  setupLivePrefs template t $create animateElement
 | 
			
		||||
  tryJSONparse debounce CHROME_HAS_BORDER_BUG */
 | 
			
		||||
  tryJSONparse CHROME_HAS_BORDER_BUG */
 | 
			
		||||
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
/** @type Element */
 | 
			
		||||
let installed;
 | 
			
		||||
/** @type string */
 | 
			
		||||
let tabURL;
 | 
			
		||||
let unsupportedURL;
 | 
			
		||||
const handleEvent = {};
 | 
			
		||||
 | 
			
		||||
const ABOUT_BLANK = 'about:blank';
 | 
			
		||||
const ENTRY_ID_PREFIX_RAW = 'style-';
 | 
			
		||||
const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW;
 | 
			
		||||
 | 
			
		||||
if (CHROME >= 3345 && CHROME < 3533) { // Chrome 66-69 adds a gap, https://crbug.com/821143
 | 
			
		||||
  document.head.appendChild($create('style', 'html { overflow: overlay }'));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
toggleSideBorders();
 | 
			
		||||
 | 
			
		||||
getActiveTab()
 | 
			
		||||
  .then(tab =>
 | 
			
		||||
    FIREFOX && tab.url === 'about:blank' && tab.status === 'loading'
 | 
			
		||||
    ? getTabRealURLFirefox(tab)
 | 
			
		||||
    : getTabRealURL(tab)
 | 
			
		||||
  )
 | 
			
		||||
  .then(url => Promise.all([
 | 
			
		||||
    (tabURL = URLS.supported(url) ? url : '') &&
 | 
			
		||||
      API.getStylesByUrl(tabURL),
 | 
			
		||||
    onDOMready().then(initPopup),
 | 
			
		||||
  ]))
 | 
			
		||||
  .then(([results]) => {
 | 
			
		||||
    if (!results) {
 | 
			
		||||
initTabUrls()
 | 
			
		||||
  .then(frames =>
 | 
			
		||||
    Promise.all([
 | 
			
		||||
      onDOMready().then(() => initPopup(frames)),
 | 
			
		||||
      ...frames
 | 
			
		||||
        .filter(f => f.url && !f.isDupe)
 | 
			
		||||
        .map(({url}) => API.getStylesByUrl(url).then(styles => ({styles, url}))),
 | 
			
		||||
    ]))
 | 
			
		||||
  .then(([, ...results]) => {
 | 
			
		||||
    if (results[0]) {
 | 
			
		||||
      showStyles(results);
 | 
			
		||||
    } else {
 | 
			
		||||
      // unsupported URL;
 | 
			
		||||
      unsupportedURL = true;
 | 
			
		||||
      $('#popup-manage-button').removeAttribute('title');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    showStyles(results.map(r => Object.assign(r.data, r)));
 | 
			
		||||
  })
 | 
			
		||||
  .catch(console.error);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -83,8 +85,32 @@ function toggleSideBorders(state = prefs.get('popup.borders')) {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function initTabUrls() {
 | 
			
		||||
  return getActiveTab()
 | 
			
		||||
    .then((tab = {}) =>
 | 
			
		||||
      FIREFOX && tab.status === 'loading' && tab.url === ABOUT_BLANK
 | 
			
		||||
        ? waitForTabUrlFF(tab)
 | 
			
		||||
        : tab)
 | 
			
		||||
    .then(tab => new Promise(resolve =>
 | 
			
		||||
      chrome.webNavigation.getAllFrames({tabId: tab.id}, frames =>
 | 
			
		||||
        resolve({frames, tab}))))
 | 
			
		||||
    .then(({frames, tab}) => {
 | 
			
		||||
      let url = tab.pendingUrl || tab.url || ''; // new Chrome uses pendingUrl while connecting
 | 
			
		||||
      frames = sortTabFrames(frames);
 | 
			
		||||
      if (url === 'chrome://newtab/' && !URLS.chromeProtectsNTP) {
 | 
			
		||||
        url = frames[0].url || '';
 | 
			
		||||
      }
 | 
			
		||||
      if (!URLS.supported(url)) {
 | 
			
		||||
        url = '';
 | 
			
		||||
        frames.length = 1;
 | 
			
		||||
      }
 | 
			
		||||
      tabURL = frames[0].url = url;
 | 
			
		||||
      return frames;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function initPopup() {
 | 
			
		||||
/** @param {chrome.webNavigation.GetAllFrameResultDetails[]} frames */
 | 
			
		||||
function initPopup(frames) {
 | 
			
		||||
  installed = $('#installed');
 | 
			
		||||
 | 
			
		||||
  setPopupWidth();
 | 
			
		||||
| 
						 | 
				
			
			@ -120,6 +146,13 @@ function initPopup() {
 | 
			
		|||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  frames.forEach(createWriterElement);
 | 
			
		||||
  if (frames.length > 1) {
 | 
			
		||||
    const el = $('#write-for-frames');
 | 
			
		||||
    el.hidden = false;
 | 
			
		||||
    el.onclick = () => el.classList.toggle('expanded');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getActiveTab().then(function ping(tab, retryCountdown = 10) {
 | 
			
		||||
    msg.sendTab(tab.id, {method: 'ping'}, {frameId: 0})
 | 
			
		||||
      .catch(() => false)
 | 
			
		||||
| 
						 | 
				
			
			@ -131,7 +164,7 @@ function initPopup() {
 | 
			
		|||
        // so we'll wait a bit to handle popup being invoked right after switching
 | 
			
		||||
        if (retryCountdown > 0 && (
 | 
			
		||||
            tab.status !== 'complete' ||
 | 
			
		||||
            FIREFOX && tab.url === 'about:blank')) {
 | 
			
		||||
            FIREFOX && tab.url === ABOUT_BLANK)) {
 | 
			
		||||
          setTimeout(ping, 100, tab, --retryCountdown);
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -166,24 +199,26 @@ function initPopup() {
 | 
			
		|||
        document.body.insertBefore(info, document.body.firstChild);
 | 
			
		||||
      });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  // Write new style links
 | 
			
		||||
  const writeStyle = $('#write-style');
 | 
			
		||||
  const matchTargets = document.createElement('span');
 | 
			
		||||
  const matchWrapper = document.createElement('span');
 | 
			
		||||
  matchWrapper.id = 'match';
 | 
			
		||||
  matchWrapper.appendChild(matchTargets);
 | 
			
		||||
/** @param {chrome.webNavigation.GetAllFrameResultDetails} frame */
 | 
			
		||||
function createWriterElement(frame) {
 | 
			
		||||
  const {url, frameId, parentFrameId, isDupe} = frame;
 | 
			
		||||
  const targets = $create('span');
 | 
			
		||||
 | 
			
		||||
  // For this URL
 | 
			
		||||
  const urlLink = template.writeStyle.cloneNode(true);
 | 
			
		||||
  const isAboutBlank = url === ABOUT_BLANK;
 | 
			
		||||
  Object.assign(urlLink, {
 | 
			
		||||
    href: 'edit.html?url-prefix=' + encodeURIComponent(tabURL),
 | 
			
		||||
    title: `url-prefix("${tabURL}")`,
 | 
			
		||||
    href: 'edit.html?url-prefix=' + encodeURIComponent(url),
 | 
			
		||||
    title: `url-prefix("${url}")`,
 | 
			
		||||
    tabIndex: isAboutBlank ? -1 : 0,
 | 
			
		||||
    textContent: prefs.get('popup.breadcrumbs.usePath')
 | 
			
		||||
      ? new URL(tabURL).pathname.slice(1)
 | 
			
		||||
      // this URL
 | 
			
		||||
      : t('writeStyleForURL').replace(/ /g, '\u00a0'),
 | 
			
		||||
    onclick: e => handleEvent.openEditor(e, {'url-prefix': tabURL}),
 | 
			
		||||
      ? new URL(url).pathname.slice(1)
 | 
			
		||||
      : frameId
 | 
			
		||||
        ? isAboutBlank ? url : 'URL'
 | 
			
		||||
        : t('writeStyleForURL').replace(/ /g, '\u00a0'), // this URL
 | 
			
		||||
    onclick: e => handleEvent.openEditor(e, {'url-prefix': url}),
 | 
			
		||||
  });
 | 
			
		||||
  if (prefs.get('popup.breadcrumbs')) {
 | 
			
		||||
    urlLink.onmouseenter =
 | 
			
		||||
| 
						 | 
				
			
			@ -191,10 +226,10 @@ function initPopup() {
 | 
			
		|||
    urlLink.onmouseleave =
 | 
			
		||||
      urlLink.onblur = () => urlLink.parentNode.classList.remove('url()');
 | 
			
		||||
  }
 | 
			
		||||
  matchTargets.appendChild(urlLink);
 | 
			
		||||
  targets.appendChild(urlLink);
 | 
			
		||||
 | 
			
		||||
  // For domain
 | 
			
		||||
  const domains = getDomains(tabURL);
 | 
			
		||||
  const domains = getDomains(url);
 | 
			
		||||
  for (const domain of domains) {
 | 
			
		||||
    const numParts = domain.length - domain.replace(/\./g, '').length + 1;
 | 
			
		||||
    // Don't include TLD
 | 
			
		||||
| 
						 | 
				
			
			@ -209,66 +244,92 @@ function initPopup() {
 | 
			
		|||
      onclick: e => handleEvent.openEditor(e, {domain}),
 | 
			
		||||
    });
 | 
			
		||||
    domainLink.setAttribute('subdomain', numParts > 1 ? 'true' : '');
 | 
			
		||||
    matchTargets.appendChild(domainLink);
 | 
			
		||||
    targets.appendChild(domainLink);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (prefs.get('popup.breadcrumbs')) {
 | 
			
		||||
    matchTargets.classList.add('breadcrumbs');
 | 
			
		||||
    matchTargets.appendChild(matchTargets.removeChild(matchTargets.firstElementChild));
 | 
			
		||||
    targets.classList.add('breadcrumbs');
 | 
			
		||||
    targets.appendChild(urlLink); // making it the last element
 | 
			
		||||
  }
 | 
			
		||||
  writeStyle.appendChild(matchWrapper);
 | 
			
		||||
 | 
			
		||||
  function getDomains(url) {
 | 
			
		||||
    let d = /.*?:\/*([^/:]+)|$/.exec(url)[1];
 | 
			
		||||
    if (!d || url.startsWith('file:')) {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
    const domains = [d];
 | 
			
		||||
    while (d.indexOf('.') !== -1) {
 | 
			
		||||
      d = d.substring(d.indexOf('.') + 1);
 | 
			
		||||
      domains.push(d);
 | 
			
		||||
    }
 | 
			
		||||
    return domains;
 | 
			
		||||
  }
 | 
			
		||||
  const root = $('#write-style');
 | 
			
		||||
  const parent = $(`[data-frame-id="${parentFrameId}"]`, root) || root;
 | 
			
		||||
  const child = $create({
 | 
			
		||||
    tag: 'span',
 | 
			
		||||
    className: `match${isDupe ? ' dupe' : ''}${isAboutBlank ? ' about-blank' : ''}`,
 | 
			
		||||
    dataset: {frameId},
 | 
			
		||||
    appendChild: targets,
 | 
			
		||||
  });
 | 
			
		||||
  parent.appendChild(child);
 | 
			
		||||
  parent.dataset.children = (Number(parent.dataset.children) || 0) + 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getDomains(url) {
 | 
			
		||||
  let d = url.split(/[/:]+/, 2)[1];
 | 
			
		||||
  if (!d || url.startsWith('file:')) {
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
  const domains = [d];
 | 
			
		||||
  while (d.includes('.')) {
 | 
			
		||||
    d = d.substring(d.indexOf('.') + 1);
 | 
			
		||||
    domains.push(d);
 | 
			
		||||
  }
 | 
			
		||||
  return domains;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @param {chrome.webNavigation.GetAllFrameResultDetails[]} frames */
 | 
			
		||||
function sortTabFrames(frames) {
 | 
			
		||||
  const unknown = new Map(frames.map(f => [f.frameId, f]));
 | 
			
		||||
  const known = new Map([[0, unknown.get(0) || {frameId: 0, url: ''}]]);
 | 
			
		||||
  unknown.delete(0);
 | 
			
		||||
  let lastSize = 0;
 | 
			
		||||
  while (unknown.size !== lastSize) {
 | 
			
		||||
    for (const [frameId, f] of unknown) {
 | 
			
		||||
      if (known.has(f.parentFrameId)) {
 | 
			
		||||
        unknown.delete(frameId);
 | 
			
		||||
        if (!f.errorOccurred) known.set(frameId, f);
 | 
			
		||||
        if (f.url === ABOUT_BLANK) f.url = known.get(f.parentFrameId).url;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    lastSize = unknown.size; // guard against an infinite loop due to a weird frame structure
 | 
			
		||||
  }
 | 
			
		||||
  const sortedFrames = [...known.values(), ...unknown.values()];
 | 
			
		||||
  const urls = new Set([ABOUT_BLANK]);
 | 
			
		||||
  for (const f of sortedFrames) {
 | 
			
		||||
    if (!f.url) f.url = '';
 | 
			
		||||
    f.isDupe = urls.has(f.url);
 | 
			
		||||
    urls.add(f.url);
 | 
			
		||||
  }
 | 
			
		||||
  return sortedFrames;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sortStyles(entries) {
 | 
			
		||||
  const enabledFirst = prefs.get('popup.enabledFirst');
 | 
			
		||||
  entries.sort((a, b) =>
 | 
			
		||||
    enabledFirst && a.styleMeta.enabled !== b.styleMeta.enabled ?
 | 
			
		||||
      (a.styleMeta.enabled ? -1 : 1) :
 | 
			
		||||
      a.styleMeta.name.localeCompare(b.styleMeta.name)
 | 
			
		||||
  );
 | 
			
		||||
  return entries.sort(({styleMeta: a}, {styleMeta: b}) =>
 | 
			
		||||
    Boolean(a.frameUrl) - Boolean(b.frameUrl) ||
 | 
			
		||||
    enabledFirst && Boolean(b.enabled) - Boolean(a.enabled) ||
 | 
			
		||||
    a.name.localeCompare(b.name));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showStyles(styles) {
 | 
			
		||||
  if (!styles) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!styles.length) {
 | 
			
		||||
function showStyles(frameResults) {
 | 
			
		||||
  const entries = new Map();
 | 
			
		||||
  frameResults.forEach(({styles = [], url}, index) => {
 | 
			
		||||
    styles.forEach(style => {
 | 
			
		||||
      const {id} = style.data;
 | 
			
		||||
      if (!entries.has(id)) {
 | 
			
		||||
        style.frameUrl = index === 0 ? '' : url;
 | 
			
		||||
        entries.set(id, createStyleElement(Object.assign(style.data, style)));
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  if (entries.size) {
 | 
			
		||||
    installed.append(...sortStyles([...entries.values()]));
 | 
			
		||||
  } else {
 | 
			
		||||
    installed.appendChild(template.noStyles.cloneNode(true));
 | 
			
		||||
    window.dispatchEvent(new Event('showStyles:done'));
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  const entries = styles.map(createStyleElement);
 | 
			
		||||
  sortStyles(entries);
 | 
			
		||||
  entries.forEach(e => installed.appendChild(e));
 | 
			
		||||
  window.dispatchEvent(new Event('showStyles:done'));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sortStylesInPlace() {
 | 
			
		||||
  if (!prefs.get('popup.autoResort')) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  const entries = $$('.entry', installed);
 | 
			
		||||
  if (!entries.length) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  sortStyles(entries);
 | 
			
		||||
  entries.forEach(e => installed.appendChild(e));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function createStyleElement(style) {
 | 
			
		||||
  let entry = $(ENTRY_ID_PREFIX + style.id);
 | 
			
		||||
| 
						 | 
				
			
			@ -356,6 +417,14 @@ function createStyleElement(style) {
 | 
			
		|||
  $('.exclude-by-domain', entry).title = getExcludeRule('domain');
 | 
			
		||||
  $('.exclude-by-url', entry).title = getExcludeRule('url');
 | 
			
		||||
 | 
			
		||||
  const {frameUrl} = style;
 | 
			
		||||
  if (frameUrl) {
 | 
			
		||||
    const sel = 'span.frame-url';
 | 
			
		||||
    const frameEl = $(sel, entry) || styleName.insertBefore($create(sel), styleName.lastChild);
 | 
			
		||||
    frameEl.title = frameUrl;
 | 
			
		||||
  }
 | 
			
		||||
  entry.classList.toggle('frame', Boolean(frameUrl));
 | 
			
		||||
 | 
			
		||||
  return entry;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -400,7 +469,11 @@ Object.assign(handleEvent, {
 | 
			
		|||
    event.stopPropagation();
 | 
			
		||||
    API
 | 
			
		||||
      .toggleStyle(handleEvent.getClickedStyleId(event), this.checked)
 | 
			
		||||
      .then(sortStylesInPlace);
 | 
			
		||||
      .then(() => {
 | 
			
		||||
        if (prefs.get('popup.autoResort')) {
 | 
			
		||||
          installed.append(...sortStyles($$('.entry', installed)));
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  toggleExclude(event, type) {
 | 
			
		||||
| 
						 | 
				
			
			@ -561,23 +634,17 @@ Object.assign(handleEvent, {
 | 
			
		|||
 | 
			
		||||
  openURLandHide(event) {
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
    const message = tryJSONparse(this.dataset.sendMessage);
 | 
			
		||||
    getActiveTab()
 | 
			
		||||
      .then(activeTab => API.openURL({
 | 
			
		||||
        url: this.href || this.dataset.href,
 | 
			
		||||
        index: activeTab.index + 1
 | 
			
		||||
        index: activeTab.index + 1,
 | 
			
		||||
        message: tryJSONparse(this.dataset.sendMessage),
 | 
			
		||||
      }))
 | 
			
		||||
      .then(tab => {
 | 
			
		||||
        if (message) {
 | 
			
		||||
          return onTabReady(tab)
 | 
			
		||||
            .then(() => msg.sendTab(tab.id, message));
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .then(window.close);
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  openManager(event) {
 | 
			
		||||
    if (event.button === 2 && unsupportedURL) return;
 | 
			
		||||
    if (event.button === 2 && !tabURL) return;
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
    if (!this.eventHandled) {
 | 
			
		||||
      // FIXME: this only works if popup is closed
 | 
			
		||||
| 
						 | 
				
			
			@ -640,32 +707,17 @@ function handleDelete(id) {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getTabRealURLFirefox(tab) {
 | 
			
		||||
  // wait for FF tab-on-demand to get a real URL (initially about:blank), 5 sec max
 | 
			
		||||
function waitForTabUrlFF(tab) {
 | 
			
		||||
  return new Promise(resolve => {
 | 
			
		||||
    function onNavigation({tabId, url, frameId}) {
 | 
			
		||||
      if (tabId === tab.id && frameId === 0) {
 | 
			
		||||
        detach();
 | 
			
		||||
        resolve(url);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function detach(timedOut) {
 | 
			
		||||
      if (timedOut) {
 | 
			
		||||
        resolve(tab.url);
 | 
			
		||||
      } else {
 | 
			
		||||
        debounce.unregister(detach);
 | 
			
		||||
      }
 | 
			
		||||
      chrome.webNavigation.onBeforeNavigate.removeListener(onNavigation);
 | 
			
		||||
      chrome.webNavigation.onCommitted.removeListener(onNavigation);
 | 
			
		||||
      chrome.tabs.onRemoved.removeListener(detach);
 | 
			
		||||
      chrome.tabs.onReplaced.removeListener(detach);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    chrome.webNavigation.onBeforeNavigate.addListener(onNavigation);
 | 
			
		||||
    chrome.webNavigation.onCommitted.addListener(onNavigation);
 | 
			
		||||
    chrome.tabs.onRemoved.addListener(detach);
 | 
			
		||||
    chrome.tabs.onReplaced.addListener(detach);
 | 
			
		||||
    debounce(detach, 5000, {timedOut: true});
 | 
			
		||||
    browser.tabs.onUpdated.addListener(...[
 | 
			
		||||
      function onUpdated(tabId, info, updatedTab) {
 | 
			
		||||
        if (info.url && tabId === tab.id) {
 | 
			
		||||
          chrome.tabs.onUpdated.removeListener(onUpdated);
 | 
			
		||||
          resolve(updatedTab);
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      ...'UpdateFilter' in browser.tabs ? [{tabId: tab.id}] : [],
 | 
			
		||||
      // TODO: remove both spreads and tabId check when strict_min_version >= 61
 | 
			
		||||
    ]);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user