globally disable CSS transitions for a moment during page opening

the problem we fix is that since we add the styles asynchronously, the browsers, esp. Firefox, sometimes apply transitions from the null/default state to the one specified in the injected CSS.

supersedes 72e8213b and 4dbca46b
This commit is contained in:
tophf 2017-09-03 11:12:18 +03:00
parent 0c205df108
commit 519d745f59
5 changed files with 111 additions and 14 deletions

View File

@ -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

View File

@ -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));
}
}

View File

@ -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;
}

View File

@ -7,12 +7,6 @@
<link rel="stylesheet" href="msgbox/msgbox.css">
<style id="style-overrides"></style>
<style id="firefox-transitions-bug-suppressor">
/* increased specificity to override sane selectors in user styles */
html#stylus.firefox #stylus-manage #header *:not(body) {
transition: none !important;
}
</style>
<!-- Notes:
* Chrome doesn't garbage-collect (or even leaks) SVG <symbol> referenced via <use> so we'll embed the code directly

View File

@ -76,10 +76,6 @@ onDOMready().then(onBackgroundReady).then(() => {
});
filterOnChange({forceRefilter: true});
if (FIREFOX) {
$('#firefox-transitions-bug-suppressor').remove();
}
});