diff --git a/content/apply.js b/content/apply.js index 7b35b1fc..28d4390c 100644 --- a/content/apply.js +++ b/content/apply.js @@ -1,42 +1,32 @@ -/* eslint no-var: 0 */ /* global msg API prefs createStyleInjector */ -/* exported APPLY */ 'use strict'; -// some weird bug in new Chrome: the content script gets injected multiple times -// define a constant so it throws when redefined -const APPLY = (() => { - const CHROME = chrome.app ? parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]) : NaN; +// Chrome reruns content script when documentElement is replaced. +// Note, we're checking against a literal `1`, not just `if (truthy)`, +// because is exposed per HTML spec as a global variable and `window.INJECTED`. + +// eslint-disable-next-line no-unused-expressions +self.INJECTED !== 1 && (() => { + self.INJECTED = 1; + const STYLE_VIA_API = !chrome.app && document instanceof XMLDocument; - const IS_OWN_PAGE = location.protocol.endsWith('-extension:'); - const setStyleContent = createSetStyleContent(); const styleInjector = createStyleInjector({ compare: (a, b) => a.id - b.id, - setStyleContent, - onUpdate: onInjectorUpdate - }); - const docRootObserver = createDocRootObserver({ - onChange: () => { - if (styleInjector.outOfOrder()) { - styleInjector.sort(); - return true; - } - } - }); - const docRewriteObserver = createDocRewriteObserver({ - onChange: () => { - docRootObserver.evade(styleInjector.sort); - } + onUpdate: onInjectorUpdate, }); const initializing = init(); + // save it now because chrome.runtime will be unavailable in the orphaned script + const orphanEventId = chrome.runtime.id; + let isOrphaned; + // firefox doesn't orphanize content scripts so the old elements stay + if (!chrome.app) styleInjector.clearOrphans(); + msg.onTab(applyOnMessage); - if (!IS_OWN_PAGE) { - window.dispatchEvent(new CustomEvent(chrome.runtime.id, { - detail: pageObject({method: 'orphan'}) - })); - window.addEventListener(chrome.runtime.id, orphanCheck, true); + if (!chrome.tabs) { + window.dispatchEvent(new CustomEvent(orphanEventId)); + window.addEventListener(orphanEventId, orphanCheck, true); } let parentDomain; @@ -47,148 +37,20 @@ const APPLY = (() => { } function onInjectorUpdate() { - if (!IS_OWN_PAGE && styleInjector.list.length) { - docRewriteObserver.start(); - docRootObserver.start(); - } else { - docRewriteObserver.stop(); - docRootObserver.stop(); + if (!isOrphaned) { + updateCount(); + updateExposeIframes(); } - updateCount(); - updateExposeIframes(); } function init() { - if (STYLE_VIA_API) { - return API.styleViaAPI({method: 'styleApply'}); - } - return API.getSectionsByUrl(getMatchUrl()) - .then(result => - applyStyles(result) - .then(() => { - // CSS transition bug workaround: since we insert styles asynchronously, - // the browsers, especially Firefox, may apply all transitions on page load - if (styleInjector.list.some(s => s.code.includes('transition'))) { - applyTransitionPatch(); - } - }) - ); - } - - function pageObject(target) { - // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Sharing_objects_with_page_scripts - const obj = new window.Object(); - Object.assign(obj, target); - return obj; - } - - function createSetStyleContent() { - // FF59+ bug workaround - // See https://github.com/openstyles/stylus/issues/461 - // Since it's easy to spoof the browser version in pre-Quantum FF we're checking - // for getPreventDefault which got removed in FF59 https://bugzil.la/691151 - const EVENT_NAME = chrome.runtime.id; - let ready; - return (el, content, disabled) => - checkPageScript().then(ok => { - if (!ok) { - el.textContent = content; - // https://github.com/openstyles/stylus/issues/693 - el.disabled = disabled; - } else { - const detail = pageObject({ - method: 'setStyleContent', - id: el.id, - content, - disabled - }); - window.dispatchEvent(new CustomEvent(EVENT_NAME, {detail})); - } - }); - - function checkPageScript() { - if (!ready) { - ready = CHROME || IS_OWN_PAGE || Event.prototype.getPreventDefault ? - Promise.resolve(false) : injectPageScript(); - } - return ready; - } - - function injectPageScript() { - const scriptContent = EVENT_NAME => { - document.currentScript.remove(); - const available = checkStyleApplied(); - if (available) { - window.addEventListener(EVENT_NAME, function handler(e) { - const {method, id, content, disabled} = e.detail; - if (method === 'setStyleContent') { - const el = document.getElementById(id); - if (!el) { - return; - } - el.textContent = content; - el.disabled = disabled; - } else if (method === 'orphan') { - window.removeEventListener(EVENT_NAME, handler); - } - }, true); - } - window.dispatchEvent(new CustomEvent(EVENT_NAME, {detail: { - method: 'init', - available - }})); - - function checkStyleApplied() { - const style = document.createElement('style'); - document.documentElement.appendChild(style); - const applied = Boolean(style.sheet); - style.remove(); - return applied; - } - }; - const code = `(${scriptContent})(${JSON.stringify(EVENT_NAME)})`; - // make sure it works in XML - const script = document.createElementNS('http://www.w3.org/1999/xhtml', 'script'); - const {resolve, promise} = deferred(); - // use inline script because using src is too slow - // https://github.com/openstyles/stylus/pull/766 - script.text = code; - script.onerror = resolveFalse; - window.addEventListener('error', resolveFalse); - window.addEventListener(EVENT_NAME, handleInit); - (document.head || document.documentElement).appendChild(script); - // injection failed if handleInit is not called. - resolveFalse(); - return promise.then(result => { - script.remove(); - window.removeEventListener(EVENT_NAME, handleInit); - window.removeEventListener('error', resolveFalse); - return result; - }); - - function resolveFalse() { - resolve(false); - } - - function handleInit(e) { - if (e.detail.method === 'init') { - resolve(e.detail.available); - } - } - } - } - - function deferred() { - const o = {}; - o.promise = new Promise((resolve, reject) => { - o.resolve = resolve; - o.reject = reject; - }); - return o; + return STYLE_VIA_API ? + API.styleViaAPI({method: 'styleApply'}) : + API.getSectionsByUrl(getMatchUrl()).then(styleInjector.apply); } function getMatchUrl() { - var matchUrl = location.href; + let matchUrl = location.href; if (!matchUrl.match(/^(http|file|chrome|ftp)/)) { // 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 @@ -227,7 +89,7 @@ const APPLY = (() => { if (!sections[request.style.id]) { styleInjector.remove(request.style.id); } else { - applyStyles(sections); + styleInjector.apply(sections); } }); } else { @@ -238,13 +100,13 @@ const APPLY = (() => { case 'styleAdded': if (request.style.enabled) { API.getSectionsByUrl(getMatchUrl(), request.style.id) - .then(applyStyles); + .then(styleInjector.apply); } break; case 'urlChanged': API.getSectionsByUrl(getMatchUrl()) - .then(replaceAll); + .then(styleInjector.replace); break; case 'backgroundReady': @@ -272,17 +134,12 @@ const APPLY = (() => { } function fetchParentDomain() { - if (parentDomain) { - return Promise.resolve(); - } - return msg.send({ - method: 'invokeAPI', - name: 'getTabUrlPrefix', - args: [] - }) - .then(newDomain => { - parentDomain = newDomain; - }); + return parentDomain ? + Promise.resolve() : + API.getTabUrlPrefix() + .then(newDomain => { + parentDomain = newDomain; + }); } function updateExposeIframes() { @@ -306,166 +163,22 @@ const APPLY = (() => { } if (STYLE_VIA_API) { API.styleViaAPI({method: 'updateCount'}).catch(msg.ignoreError); - return; + } else { + API.updateIconBadge(styleInjector.list.length).catch(console.error); } - // we have to send the tabId so we can't use `sendBg` that is used by `API` - msg.send({ - method: 'invokeAPI', - name: 'updateIconBadge', - args: [styleInjector.list.length] - }).catch(console.error); } - function rootReady() { - if (document.documentElement) { - return Promise.resolve(); - } - return new Promise(resolve => { - new MutationObserver((mutations, observer) => { - if (document.documentElement) { - observer.disconnect(); - resolve(); - } - }).observe(document, {childList: true}); - }); - } - - function applyStyles(sections) { - const styles = Object.values(sections); - if (!styles.length) { - return Promise.resolve(); - } - return rootReady().then(() => - docRootObserver.evade(() => - styleInjector.addMany( - styles.map(s => ({id: s.id, code: s.code.join('')})) - ) - ) - ); - } - - function replaceAll(newStyles) { - styleInjector.replaceAll( - Object.values(newStyles) - .map(s => ({id: s.id, code: s.code.join('')})) - ); - } - - function applyTransitionPatch() { - // CSS transition bug workaround: since we insert styles asynchronously, - // the browsers, especially Firefox, may apply all transitions on page load - const el = styleInjector.createStyle('transition-patch'); - // FIXME: this will trigger docRootObserver and cause a resort. We should - // move this function into style-injector. - document.documentElement.appendChild(el); - setStyleContent(el, ` - :root:not(#\\0):not(#\\0) * { - transition: none !important; - } - `) - .then(afterPaint) - .then(() => { - el.remove(); - }); - } - - function afterPaint() { - return new Promise(resolve => { - requestAnimationFrame(() => { - setTimeout(resolve); - }); - }); - } - - function orphanCheck(e) { - if (e && e.detail.method !== 'orphan') { - return; - } - if (chrome.i18n && chrome.i18n.getUILanguage()) { - return true; - } + function orphanCheck() { + try { + if (chrome.i18n.getUILanguage()) return; + } catch (e) {} // In Chrome content script is orphaned on an extension update/reload // so we need to detach event listeners + window.removeEventListener(orphanEventId, orphanCheck, true); + isOrphaned = true; styleInjector.clear(); - window.removeEventListener(chrome.runtime.id, orphanCheck, true); try { msg.off(applyOnMessage); } catch (e) {} } - - function createDocRewriteObserver({onChange}) { - // detect documentElement being rewritten from inside the script - let root; - let observing = false; - let timer; - const observer = new MutationObserver(check); - return {start, stop}; - - function start() { - if (observing) return; - // detect dynamic iframes rewritten after creation by the embedder i.e. externally - root = document.documentElement; - timer = setTimeout(check); - observer.observe(document, {childList: true}); - observing = true; - } - - function stop() { - if (!observing) return; - clearTimeout(timer); - observer.disconnect(); - observing = false; - } - - function check() { - if (root !== document.documentElement) { - root = document.documentElement; - onChange(); - } - } - } - - function createDocRootObserver({onChange}) { - let digest = 0; - let lastCalledTime = NaN; - let observing = false; - const observer = new MutationObserver(() => { - if (digest) { - if (performance.now() - lastCalledTime > 1000) { - digest = 0; - } else if (digest > 5) { - throw new Error('The page keeps generating mutations. Skip the event.'); - } - } - if (onChange()) { - digest++; - lastCalledTime = performance.now(); - } - }); - return {start, stop, evade}; - - function start() { - if (observing) return; - observer.observe(document.documentElement, {childList: true}); - observing = true; - } - - function stop() { - if (!observing) return; - // FIXME: do we need this? - observer.takeRecords(); - observer.disconnect(); - observing = false; - } - - function evade(fn) { - if (!observing) { - return fn(); - } - stop(); - const r = fn(); - start(); - return r; - } - } })(); diff --git a/content/style-injector.js b/content/style-injector.js index 5024d944..1b496b4c 100644 --- a/content/style-injector.js +++ b/content/style-injector.js @@ -1,89 +1,183 @@ -/* exported createStyleInjector */ 'use strict'; -function createStyleInjector({compare, setStyleContent, onUpdate}) { - const CHROME = chrome.app ? parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]) : NaN; +self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({ + compare, + onUpdate = () => {}, +}) => { const PREFIX = 'stylus-'; + const PATCH_ID = 'transition-patch'; // styles are out of order if any of these elements is injected between them const ORDERED_TAGS = new Set(['head', 'body', 'frameset', 'style', 'link']); + const IS_OWN_PAGE = Boolean(chrome.tabs); + // detect Chrome 65 via a feature it added since browser version can be spoofed + const isChromePre65 = chrome.app && typeof Worklet !== 'function'; + const docRewriteObserver = RewriteObserver(_sort); + const docRootObserver = RootObserver(_sortIfNeeded); const list = []; const table = new Map(); - let enabled = true; + let isEnabled = true; + let isTransitionPatched; + // will store the original method refs because the page can override them + let creationDoc, createElement, createElementNS; return { - // manipulation - add, - addMany, - remove, - update, + apply, clear, - replaceAll, - - // method + clearOrphans, + remove, + replace, toggle, - sort, - - // state - outOfOrder, list, - - // static util - createStyle }; - function outOfOrder() { - if (!list.length) { - return false; + function apply(styleMap) { + const styles = _styleMapToArray(styleMap); + return !styles.length ? + Promise.resolve([]) : + docRootObserver.evade(() => { + if (!isTransitionPatched) _applyTransitionPatch(styles); + const els = styles.map(_apply); + _emitUpdate(); + return els; + }); + } + + function clear() { + for (const style of list) { + style.el.remove(); } - let el = list[0].el; - if (el.parentNode !== document.documentElement) { - return true; - } - let i = 0; - while (el) { - if (i < list.length && el === list[i].el) { - i++; - } else if (ORDERED_TAGS.has(el.localName)) { - return true; + list.length = 0; + table.clear(); + _emitUpdate(); + } + + function clearOrphans() { + for (const el of document.querySelectorAll(`style[id^="${PREFIX}"].stylus`)) { + const id = el.id.slice(PREFIX.length); + if (/^\d+$/.test(id) || id === PATCH_ID) { + el.remove(); } - el = el.nextSibling; } - // some styles are not injected to the document - return i < list.length; - } - - function addMany(styles) { - const pending = Promise.all(styles.map(_add)); - emitUpdate(); - return pending; - } - - function add(style) { - const pending = _add(style); - emitUpdate(); - return pending; - } - - function _add(style) { - if (table.has(style.id)) { - return update(style); - } - style.el = createStyle(style.id); - const pending = setStyleContent(style.el, style.code, !enabled); - table.set(style.id, style); - const nextIndex = list.findIndex(i => compare(i, style) > 0); - if (nextIndex < 0) { - document.documentElement.appendChild(style.el); - list.push(style); - } else { - document.documentElement.insertBefore(style.el, list[nextIndex].el); - list.splice(nextIndex, 0, style); - } - return pending; } function remove(id) { _remove(id); - emitUpdate(); + _emitUpdate(); + } + + function replace(styleMap) { + const styles = _styleMapToArray(styleMap); + const added = new Set(styles.map(s => s.id)); + const removed = []; + for (const style of list) { + if (!added.has(style.id)) { + removed.push(style.id); + } + } + styles.forEach(_apply); + removed.forEach(_remove); + _emitUpdate(); + } + + function toggle(_enabled) { + if (isEnabled === _enabled) return; + isEnabled = _enabled; + for (const style of list) { + style.el.disabled = !isEnabled; + } + } + + function _add(style) { + const el = style.el = _createStyle(style.id, style.code); + table.set(style.id, style); + const nextIndex = list.findIndex(i => compare(i, style) > 0); + if (nextIndex < 0) { + document.documentElement.appendChild(el); + list.push(style); + } else { + document.documentElement.insertBefore(el, list[nextIndex].el); + list.splice(nextIndex, 0, style); + } + // moving an element resets its 'disabled' state + el.disabled = !isEnabled; + return el; + } + + function _apply(style) { + return table.has(style.id) ? _update(style) : _add(style); + } + + function _applyTransitionPatch(styles) { + // CSS transition bug workaround: since we insert styles asynchronously, + // the browsers, especially Firefox, may apply all transitions on page load + isTransitionPatched = document.readyState === 'complete'; + if (isTransitionPatched || !styles.some(s => s.code.includes('transition'))) { + return; + } + const el = _createStyle(PATCH_ID, ` + :root:not(#\\0):not(#\\0) * { + transition: none !important; + } + `); + document.documentElement.appendChild(el); + // wait for the next paint to complete + // note: requestAnimationFrame won't fire in inactive tabs + requestAnimationFrame(() => setTimeout(() => el.remove())); + } + + function _createStyle(id, code = '') { + if (!creationDoc) _initCreationDoc(); + let el; + if (document.documentElement instanceof SVGSVGElement) { + // SVG document style + el = createElementNS.call(creationDoc, 'http://www.w3.org/2000/svg', 'style'); + } else if (document instanceof XMLDocument) { + // XML document style + el = createElementNS.call(creationDoc, 'http://www.w3.org/1999/xhtml', 'style'); + } else { + // HTML document style; also works on HTML-embedded SVG + el = createElement.call(creationDoc, 'style'); + } + if (id) { + el.id = `${PREFIX}${id}`; + const oldEl = document.getElementById(el.id); + if (oldEl) oldEl.id += '-superseded-by-Stylus'; + } + el.type = 'text/css'; + // SVG className is not a string, but an instance of SVGAnimatedString + el.classList.add('stylus'); + el.textContent = code; + return el; + } + + function _emitUpdate() { + if (!IS_OWN_PAGE && list.length) { + docRewriteObserver.start(); + docRootObserver.start(); + } else { + docRewriteObserver.stop(); + docRootObserver.stop(); + } + onUpdate(); + } + + /* + FF59+ workaround: allow the page to read our sheets, https://github.com/openstyles/stylus/issues/461 + First we're trying the page context document where inline styles may be forbidden by CSP + https://bugzilla.mozilla.org/show_bug.cgi?id=1579345#c3 + and since userAgent.navigator can be spoofed via about:config or devtools, + we're checking for getPreventDefault that was removed in FF59 + */ + function _initCreationDoc() { + creationDoc = !Event.prototype.getPreventDefault && document.wrappedJSObject; + if (creationDoc) { + ({createElement, createElementNS} = creationDoc); + const el = document.documentElement.appendChild(_createStyle()); + const isApplied = el.sheet; + el.remove(); + if (isApplied) return; + } + creationDoc = document; + ({createElement, createElementNS} = document); } function _remove(id) { @@ -94,91 +188,149 @@ function createStyleInjector({compare, setStyleContent, onUpdate}) { style.el.remove(); } - function update({id, code}) { + function _sort() { + docRootObserver.evade(() => { + list.sort(compare); + for (const style of list) { + // moving an element resets its 'disabled' state + document.documentElement.appendChild(style.el); + style.el.disabled = !isEnabled; + } + }); + } + + function _sortIfNeeded() { + let needsSort; + let el = list.length && list[0].el; + if (!el) { + needsSort = false; + } else if (el.parentNode !== creationDoc.documentElement) { + needsSort = true; + } else { + let i = 0; + while (el) { + if (i < list.length && el === list[i].el) { + i++; + } else if (ORDERED_TAGS.has(el.localName)) { + needsSort = true; + break; + } + el = el.nextElementSibling; + } + // some styles are not injected to the document + if (i < list.length) needsSort = true; + } + if (needsSort) _sort(); + return needsSort; + } + + function _styleMapToArray(styleMap) { + return Object.values(styleMap).map(s => ({ + id: s.id, + code: s.code.join(''), + })); + } + + function _update({id, code}) { const style = table.get(id); if (style.code === code) return; style.code = code; // workaround for Chrome devtools bug fixed in v65 - // https://github.com/openstyles/stylus/commit/0fa391732ba8e35fa68f326a560fc04c04b8608b - let oldEl; - if (CHROME < 3321) { - oldEl = style.el; - oldEl.id = ''; - style.el = createStyle(id); + if (isChromePre65) { + const oldEl = style.el; + style.el = _createStyle(id, code); oldEl.parentNode.insertBefore(style.el, oldEl.nextSibling); - style.el.disabled = !enabled; - } - return setStyleContent(style.el, code, !enabled) - .then(() => oldEl && oldEl.remove()); - } - - function createStyle(id) { - let el; - if (document.documentElement instanceof SVGSVGElement) { - // SVG document style - el = document.createElementNS('http://www.w3.org/2000/svg', 'style'); - } else if (document instanceof XMLDocument) { - // XML document style - el = document.createElementNS('http://www.w3.org/1999/xhtml', 'style'); + oldEl.remove(); } else { - // HTML document style; also works on HTML-embedded SVG - el = document.createElement('style'); + style.el.textContent = code; } - el.id = `${PREFIX}${id}`; - el.type = 'text/css'; - // SVG className is not a string, but an instance of SVGAnimatedString - el.classList.add('stylus'); - return el; + // https://github.com/openstyles/stylus/issues/693 + style.el.disabled = !isEnabled; } - function clear() { - for (const style of list) { - style.el.remove(); - } - list.length = 0; - table.clear(); - emitUpdate(); - } + function RewriteObserver(onChange) { + // detect documentElement being rewritten from inside the script + let root; + let observing = false; + let timer; + const observer = new MutationObserver(_check); + return {start, stop}; - function toggle(_enabled) { - if (enabled === _enabled) return; - enabled = _enabled; - for (const style of list) { - style.el.disabled = !enabled; + function start() { + if (observing) return; + // detect dynamic iframes rewritten after creation by the embedder i.e. externally + root = document.documentElement; + timer = setTimeout(_check); + observer.observe(document, {childList: true}); + observing = true; } - } - function sort() { - list.sort(compare); - for (const style of list) { - // FIXME: do we need this? - // const copy = document.importNode(el, true); - // el.textContent += ' '; // invalidate CSSOM cache - document.documentElement.appendChild(style.el); - // moving an element resets its 'disabled' state - style.el.disabled = !enabled; + function stop() { + if (!observing) return; + clearTimeout(timer); + observer.disconnect(); + observing = false; } - } - function emitUpdate() { - if (onUpdate) { - onUpdate(); - } - } - - function replaceAll(styles) { - const added = new Set(styles.map(s => s.id)); - const removed = []; - for (const style of list) { - if (!added.has(style.id)) { - removed.push(style.id); + function _check() { + if (root !== document.documentElement) { + root = document.documentElement; + onChange(); } } - // FIXME: is it possible that `docRootObserver` breaks the process? - return Promise.all(styles.map(_add)) - .then(() => { - removed.forEach(_remove); - emitUpdate(); - }); } -} + + function RootObserver(onChange) { + let digest = 0; + let lastCalledTime = NaN; + let observing = false; + const observer = new MutationObserver(() => { + if (digest) { + if (performance.now() - lastCalledTime > 1000) { + digest = 0; + } else if (digest > 5) { + throw new Error('The page keeps generating mutations. Skip the event.'); + } + } + if (onChange()) { + digest++; + lastCalledTime = performance.now(); + } + }); + return {evade, start, stop}; + + function evade(fn) { + const restore = observing && start; + stop(); + return new Promise(resolve => _run(fn, resolve, _waitForRoot)) + .then(restore); + } + + function start() { + if (observing) return; + observer.observe(document.documentElement, {childList: true}); + observing = true; + } + + function stop() { + if (!observing) return; + // FIXME: do we need this? + observer.takeRecords(); + observer.disconnect(); + observing = false; + } + + function _run(fn, resolve, wait) { + if (document.documentElement) { + resolve(fn()); + return true; + } + if (wait) wait(fn, resolve); + } + + function _waitForRoot(...args) { + new MutationObserver((_, observer) => _run(...args) && observer.disconnect()) + .observe(document, {childList: true}); + } + } +}; diff --git a/js/msg.js b/js/msg.js index 5bbef7a4..f7e6bafd 100644 --- a/js/msg.js +++ b/js/msg.js @@ -1,9 +1,8 @@ /* global promisify deepCopy */ -/* exported msg API */ // deepCopy is only used if the script is executed in extension pages. 'use strict'; -const msg = (() => { +self.msg = self.INJECTED === 1 ? self.msg : (() => { const runtimeSend = promisify(chrome.runtime.sendMessage.bind(chrome.runtime)); const tabSend = chrome.tabs && promisify(chrome.tabs.sendMessage.bind(chrome.tabs)); const tabQuery = chrome.tabs && promisify(chrome.tabs.query.bind(chrome.tabs)); @@ -239,9 +238,15 @@ const msg = (() => { } })(); -const API = new Proxy({}, { +self.API = self.INJECTED === 1 ? self.API : new Proxy({ + // Handlers for these methods need sender.tab.id which is set by `send` as it uses messaging, + // unlike `sendBg` which invokes the background page directly in our own extension tabs + getTabUrlPrefix: true, + updateIconBadge: true, + styleViaAPI: true, +}, { get: (target, name) => - (...args) => Promise.resolve(msg.sendBg({ + (...args) => Promise.resolve(self.msg[target[name] ? 'send' : 'sendBg']({ method: 'invokeAPI', name, args diff --git a/js/polyfill.js b/js/polyfill.js index 78665c85..2b95b10d 100644 --- a/js/polyfill.js +++ b/js/polyfill.js @@ -1,12 +1,19 @@ 'use strict'; -(() => { +// eslint-disable-next-line no-unused-expressions +self.INJECTED !== 1 && (() => { + if (!Object.entries) { Object.entries = obj => Object.keys(obj).map(k => [k, obj[k]]); } if (!Object.values) { Object.values = obj => Object.keys(obj).map(k => obj[k]); } + + // the above was shared by content scripts and workers, + // the rest is only needed for our extension pages + if (!self.chrome || !self.chrome.tabs) return; + if (typeof document === 'object') { const ELEMENT_METH = { append: { diff --git a/js/prefs.js b/js/prefs.js index 4ac7cf3d..b227d2c6 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -1,8 +1,7 @@ /* global promisify */ -/* exported prefs */ 'use strict'; -const prefs = (() => { +self.prefs = self.INJECTED === 1 ? self.prefs : (() => { const defaults = { 'openEditInWindow': false, // new editor opens in a own browser window 'windowPosition': {}, // detached window position diff --git a/js/promisify.js b/js/promisify.js index 8362b00d..89605a37 100644 --- a/js/promisify.js +++ b/js/promisify.js @@ -1,4 +1,3 @@ -/* exported promisify */ 'use strict'; /* Convert chrome APIs into promises. Example: @@ -7,8 +6,8 @@ Convert chrome APIs into promises. Example: storageSyncGet(['key']).then(result => {...}); */ -function promisify(fn) { - return (...args) => +self.promisify = self.INJECTED === 1 ? self.promisify : fn => + (...args) => new Promise((resolve, reject) => { fn(...args, (...result) => { if (chrome.runtime.lastError) { @@ -21,4 +20,3 @@ function promisify(fn) { ); }); }); -}