diff --git a/background/background.js b/background/background.js index 454239fa..87ad49eb 100644 --- a/background/background.js +++ b/background/background.js @@ -1,4 +1,5 @@ /* global dbExec, getStyles, saveStyle */ +/* global handleCssTransitionBug */ 'use strict'; // eslint-disable-next-line no-var @@ -211,7 +212,10 @@ contextMenus = Object.assign({ function webNavigationListener(method, {url, tabId, frameId}) { getStyles({matchUrl: url, enabled: true, asHash: true}).then(styles => { - if (method && !url.startsWith('chrome:') && tabId >= 0) { + if (method && URLS.supported(url) && tabId >= 0) { + if (method === 'styleApply') { + handleCssTransitionBug(tabId, frameId, styles); + } chrome.tabs.sendMessage(tabId, { method, // ping own page so it retrieves the styles directly diff --git a/background/storage.js b/background/storage.js index bc566874..d4554e75 100644 --- a/background/storage.js +++ b/background/storage.js @@ -8,6 +8,11 @@ const RX_CSS_COMMENTS = /\/\*[\s\S]*?\*\//g; // eslint-disable-next-line no-var var SLOPPY_REGEXP_PREFIX = '\0'; +// CSS transition bug workaround: since we insert styles asynchronously, +// the browsers, especially Firefox, may apply all transitions on page load +const CSS_TRANSITION_SUPPRESSOR = '* { transition: none !important; }'; +const RX_CSS_TRANSITION_DETECTOR = /([\s\n;/{]|-webkit-|-moz-)transition[\s\n]*:[\s\n]*(?!none)/; + // Note, only 'var'-declared variables are visible from another extension page // eslint-disable-next-line no-var var cachedStyles = { @@ -16,6 +21,7 @@ var cachedStyles = { filters: new Map(), // filterStyles() parameters mapped to the returned results, 10k max regexps: new Map(), // compiled style regexps urlDomains: new Map(), // getDomain() results for 100 last checked urls + needTransitionPatch: new Map(), // FF bug workaround mutex: { inProgress: false, // while getStyles() is reading IndexedDB all subsequent calls onDone: [], // to getStyles() are queued and resolved when the first one finishes @@ -517,6 +523,7 @@ function invalidateCache({added, updated, deletedId} = {}) { if (cached) { Object.assign(cached, updated); cachedStyles.filters.clear(); + cachedStyles.needTransitionPatch.delete(id); return; } else { added = updated; @@ -527,6 +534,7 @@ function invalidateCache({added, updated, deletedId} = {}) { cachedStyles.list.push(added); cachedStyles.byId.set(added.id, added); cachedStyles.filters.clear(); + cachedStyles.needTransitionPatch.delete(id); } return; } @@ -536,11 +544,13 @@ function invalidateCache({added, updated, deletedId} = {}) { cachedStyles.list.splice(cachedIndex, 1); cachedStyles.byId.delete(deletedId); cachedStyles.filters.clear(); + cachedStyles.needTransitionPatch.delete(id); return; } } cachedStyles.list = null; cachedStyles.filters.clear(); + cachedStyles.needTransitionPatch.clear(id); } @@ -612,3 +622,78 @@ function calcStyleDigest(style) { return parts.join(''); } } + + +function handleCssTransitionBug(tabId, frameId, styles) { + for (let id in styles) { + id |= 0; + if (!id) { + continue; + } + let need = cachedStyles.needTransitionPatch.get(id); + if (need === false) { + continue; + } + if (need !== true) { + need = styles[id].some(sectionContainsTransitions); + cachedStyles.needTransitionPatch.set(id, need); + if (!need) { + continue; + } + } + if (FIREFOX) { + patchFirefox(); + } else { + styles.needTransitionPatch = true; + } + break; + } + + function patchFirefox() { + browser.tabs.insertCSS(tabId, { + frameId, + code: CSS_TRANSITION_SUPPRESSOR, + cssOrigin: 'user', + runAt: 'document_start', + matchAboutBlank: true, + }).then(() => setTimeout(() => { + browser.tabs.removeCSS(tabId, { + frameId, + code: CSS_TRANSITION_SUPPRESSOR, + cssOrigin: 'user', + matchAboutBlank: true, + }).catch(ignoreChromeError); + })).catch(ignoreChromeError); + } + + function sectionContainsTransitions(section) { + let code = section.code; + const firstTransition = code.indexOf('transition'); + if (firstTransition < 0) { + return false; + } + const firstCmt = code.indexOf('/*'); + // check the part before the first comment + if (firstCmt < 0 || firstTransition < firstCmt) { + if (quickCheckAround(code, firstTransition)) { + return true; + } else if (firstCmt < 0) { + return false; + } + } + // check the rest + const lastCmt = code.lastIndexOf('*/'); + if (lastCmt < firstCmt) { + // the comment is unclosed and we already checked the preceding part + return false; + } + let mid = code.slice(firstCmt, lastCmt + 2); + mid = mid.indexOf('*/') === mid.length - 2 ? '' : mid.replace(RX_CSS_COMMENTS, ''); + code = mid + code.slice(lastCmt + 2); + return quickCheckAround(code) || RX_CSS_TRANSITION_DETECTOR.test(code); + } + + function quickCheckAround(code, pos = code.indexOf('transition')) { + return RX_CSS_TRANSITION_DETECTOR.test(code.substr(Math.max(0, pos - 10), 50)); + } +} diff --git a/content/apply.js b/content/apply.js index f29b6e4a..c93ebc7d 100644 --- a/content/apply.js +++ b/content/apply.js @@ -199,8 +199,25 @@ function applyStyles(styles) { // which is already autogenerated at this moment ROOT = document.head; } + if (styles.needTransitionPatch) { + // CSS transition bug workaround: since we insert styles asynchronously, + // the browsers, especially Firefox, may apply all transitions on page load + delete styles.needTransitionPatch; + const className = chrome.runtime.id + '-transition-bug-fix'; + const docId = document.documentElement.id ? '#' + document.documentElement.id : ''; + document.documentElement.classList.add(className); + applySections(0, ` + ${docId}.${className}:root * { + transition: none !important; + } + `); + setTimeout(() => { + removeStyle({id: 0}); + document.documentElement.classList.remove(className); + }); + } for (const id in styles) { - applySections(id, styles[id]); + applySections(id, styles[id].map(section => section.code).join('\n')); } initDocRewriteObserver(); initDocRootObserver(); @@ -215,7 +232,7 @@ function applyStyles(styles) { } -function applySections(styleId, sections) { +function applySections(styleId, code) { let el = document.getElementById(ID_PREFIX + styleId); if (el) { return; @@ -234,11 +251,12 @@ function applySections(styleId, sections) { id: ID_PREFIX + styleId, className: 'stylus', type: 'text/css', - textContent: sections.map(section => section.code).join('\n'), + textContent: code, }); addStyleElement(el); styleElements.set(el.id, el); disabledElements.delete(Number(styleId)); + return el; } diff --git a/manage.html b/manage.html index 6548ed03..bd6f73d8 100644 --- a/manage.html +++ b/manage.html @@ -7,12 +7,6 @@ -