simplify orphanCheck; use IIFE; fix comm issues

This commit is contained in:
tophf 2017-11-26 00:56:57 +03:00
parent babeb695c1
commit fb3554a351
2 changed files with 722 additions and 722 deletions

View File

@ -1,6 +1,7 @@
/* eslint no-var: 0 */ /* eslint no-var: 0 */
'use strict'; 'use strict';
(() => {
var ID_PREFIX = 'stylus-'; var ID_PREFIX = 'stylus-';
var ROOT = document.documentElement; var ROOT = document.documentElement;
var isOwnPage = location.protocol.endsWith('-extension:'); var isOwnPage = location.protocol.endsWith('-extension:');
@ -14,6 +15,7 @@ var docRootObserver;
requestStyles(); requestStyles();
chrome.runtime.onMessage.addListener(applyOnMessage); chrome.runtime.onMessage.addListener(applyOnMessage);
window.applyOnMessage = applyOnMessage;
if (!isOwnPage) { if (!isOwnPage) {
window.dispatchEvent(new CustomEvent(chrome.runtime.id)); window.dispatchEvent(new CustomEvent(chrome.runtime.id));
@ -257,10 +259,9 @@ function applyStyles(styles) {
function applySections(styleId, code) { function applySections(styleId, code) {
let el = document.getElementById(ID_PREFIX + styleId); const id = ID_PREFIX + styleId;
if (el) { let el = styleElements.get(id) || document.getElementById(id);
return; if (!el) {
}
if (document.documentElement instanceof SVGSVGElement) { if (document.documentElement instanceof SVGSVGElement) {
// SVG document style // SVG document style
el = document.createElementNS('http://www.w3.org/2000/svg', 'style'); el = document.createElementNS('http://www.w3.org/2000/svg', 'style');
@ -272,15 +273,15 @@ function applySections(styleId, code) {
el = document.createElement('style'); el = document.createElement('style');
} }
Object.assign(el, { Object.assign(el, {
styleId, id,
id: ID_PREFIX + styleId,
type: 'text/css', type: 'text/css',
textContent: code, textContent: code,
}); });
// SVG className is not a string, but an instance of SVGAnimatedString // SVG className is not a string, but an instance of SVGAnimatedString
el.classList.add('stylus'); el.classList.add('stylus');
addStyleElement(el); addStyleElement(el);
styleElements.set(el.id, el); }
styleElements.set(id, el);
disabledElements.delete(Number(styleId)); disabledElements.delete(Number(styleId));
return el; return el;
} }
@ -323,6 +324,22 @@ function replaceAll(newStyles) {
} }
function getStyleId(el) {
return parseInt(el.id.substr(ID_PREFIX.length));
}
function orphanCheck() {
if (chrome.i18n && chrome.i18n.getUILanguage()) {
return true;
}
// In Chrome content script is orphaned on an extension update/reload
// so we need to detach event listeners
[docRewriteObserver, docRootObserver].forEach(ob => ob && ob.takeRecords() && ob.disconnect());
window.removeEventListener(chrome.runtime.id, orphanCheck, true);
}
function initDocRewriteObserver() { function initDocRewriteObserver() {
// detect documentElement being rewritten from inside the script // detect documentElement being rewritten from inside the script
docRewriteObserver = new MutationObserver(mutations => { docRewriteObserver = new MutationObserver(mutations => {
@ -346,9 +363,7 @@ function initDocRewriteObserver() {
// re-add styles if we detect documentElement being recreated // re-add styles if we detect documentElement being recreated
function reinjectStyles() { function reinjectStyles() {
if (!styleElements) { if (!styleElements) {
if (orphanCheck) {
orphanCheck(); orphanCheck();
}
return; return;
} }
ROOT = document.documentElement; ROOT = document.documentElement;
@ -370,26 +385,32 @@ function initDocRootObserver() {
let lastRestorationTime = 0; let lastRestorationTime = 0;
let restorationCounter = 0; let restorationCounter = 0;
let observing = false; let observing = false;
let sorting = false;
// allow any types of elements between ours, except for the following:
const ORDERED_TAGS = ['head', 'body', 'style', 'link'];
docRootObserver = Object.assign(new MutationObserver(sortStyleElements), { init();
start({sort = false} = {}) { return;
function init() {
docRootObserver = new MutationObserver(sortStyleElements);
Object.assign(docRootObserver, {start, stop});
}
function start({sort = false} = {}) {
if (sort && sortStyleMap()) { if (sort && sortStyleMap()) {
sortStyleElements(); sortStyleElements();
} }
if (!observing) { if (!observing && ROOT) {
this.observe(ROOT, {childList: true}); docRootObserver.observe(ROOT, {childList: true});
observing = true; observing = true;
} }
}, }
stop() { function stop() {
if (observing) { if (observing) {
this.disconnect(); docRootObserver.disconnect();
observing = false; observing = false;
} }
}, }
});
return;
function sortStyleMap() { function sortStyleMap() {
const list = []; const list = [];
let prevStyleId = 0; let prevStyleId = 0;
@ -407,40 +428,60 @@ function initDocRootObserver() {
return true; return true;
} }
} }
function sortStyleElements() { function sortStyleElements() {
let prev = document.body || document.head; let expected = document.body || document.head;
if (!prev) { if (!expected || sorting) {
return; return;
} }
let appliedChanges = false; for (const el of styleElements.values()) {
for (const [idStr, el] of styleElements.entries()) { if (!isMovable(el)) {
if (!el.parentNode && disabledElements.has(getStyleId(idStr))) {
continue; continue;
} }
if (el.previousElementSibling === prev) { let prev = el.previousElementSibling;
prev = el; while (prev !== expected) {
continue; if (prev && isSkippable(prev)) {
expected = prev;
prev = prev.nextElementSibling;
} else if (!moveAfter(el, expected)) {
return;
} else {
break;
} }
if (!appliedChanges) { }
expected = el;
}
if (sorting) {
sorting = false;
docRootObserver.takeRecords();
setTimeout(start);
//docRootObserver.start();
}
}
function isMovable(el) {
return el.parentNode || !disabledElements.has(getStyleId(el));
}
function isSkippable(el) {
return !ORDERED_TAGS.includes(el.localName) ||
el.id.startsWith(ID_PREFIX) &&
el.id.endsWith('-ghost') &&
el.localName === 'style' &&
el.className === 'stylus';
}
function moveAfter(el, expected) {
if (!sorting) {
if (restorationLimitExceeded()) { if (restorationLimitExceeded()) {
return; return false;
} }
appliedChanges = true; sorting = true;
docRootObserver.stop(); docRootObserver.stop();
} }
prev.insertAdjacentElement('afterend', el); expected.insertAdjacentElement('afterend', el);
if (el.disabled !== disableAll) { if (el.disabled !== disableAll) {
// moving an element resets its 'disabled' state // moving an element resets its 'disabled' state
el.disabled = disableAll; el.disabled = disableAll;
} }
prev = el; return true;
} }
if (appliedChanges) {
docRootObserver.start();
}
}
function restorationLimitExceeded() { function restorationLimitExceeded() {
const t = performance.now(); const t = performance.now();
if (t - lastRestorationTime > 1000) { if (t - lastRestorationTime > 1000) {
@ -454,46 +495,4 @@ function initDocRootObserver() {
} }
} }
} }
})();
function getStyleId(el) {
return parseInt((el.id || el).substr(ID_PREFIX.length));
}
function orphanCheck() {
const port = chrome.runtime.connect();
if (port) {
port.disconnect();
return;
}
// we're orphaned due to an extension update
// we can detach the mutation observer
[docRewriteObserver, docRootObserver].forEach(ob => ob && ob.disconnect());
// we can detach event listeners
window.removeEventListener(chrome.runtime.id, orphanCheck, true);
// we can't detach chrome.runtime.onMessage because it's no longer connected internally
// we can destroy our globals in this context to free up memory
[ // functions
'addStyleElement',
'applyOnMessage',
'applySections',
'applyStyles',
'applyStyleState',
'doDisableAll',
'initDocRewriteObserver',
'initDocRootObserver',
'orphanCheck',
'removeStyle',
'replaceAll',
'requestStyles',
// variables
'ROOT',
'disabledElements',
'retiredStyleTimers',
'styleElements',
'docRewriteObserver',
'docRootObserver',
].forEach(fn => (window[fn] = null));
}

View File

@ -1,5 +1,6 @@
'use strict'; 'use strict';
(() => {
const FIREFOX = !chrome.app; const FIREFOX = !chrome.app;
const VIVALDI = chrome.app && /Vivaldi/.test(navigator.userAgent); const VIVALDI = chrome.app && /Vivaldi/.test(navigator.userAgent);
const OPERA = chrome.app && /OPR/.test(navigator.userAgent); const OPERA = chrome.app && /OPR/.test(navigator.userAgent);
@ -18,99 +19,6 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
} }
}); });
// TODO: remove the following statement when USO is fixed
document.documentElement.appendChild(document.createElement('script')).text = '(' +
function () {
let settings;
const originalResponseJson = Response.prototype.json;
document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) {
document.removeEventListener('stylusFixBuggyUSOsettings', _);
settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search);
if (!settings) {
Response.prototype.json = originalResponseJson;
}
});
Response.prototype.json = function (...args) {
return originalResponseJson.call(this, ...args).then(json => {
if (!settings || typeof ((json || {}).style_settings || {}).every !== 'function') {
return json;
}
Response.prototype.json = originalResponseJson;
const images = new Map();
for (const jsonSetting of json.style_settings) {
let value = settings.get('ik-' + jsonSetting.install_key);
if (!value
|| !jsonSetting.style_setting_options
|| !jsonSetting.style_setting_options[0]) {
continue;
}
if (value.startsWith('ik-')) {
value = value.replace(/^ik-/, '');
const defaultItem = jsonSetting.style_setting_options.find(item => item.default);
if (!defaultItem || defaultItem.install_key !== value) {
if (defaultItem) {
defaultItem.default = false;
}
jsonSetting.style_setting_options.some(item => {
if (item.install_key === value) {
item.default = true;
return true;
}
});
}
} else if (jsonSetting.setting_type === 'image') {
jsonSetting.style_setting_options.some(item => {
if (item.default) {
item.default = false;
return true;
}
});
images.set(jsonSetting.install_key, value);
} else {
const item = jsonSetting.style_setting_options[0];
if (item.value !== value && item.install_key === 'placeholder') {
item.value = value;
}
}
}
if (images.size) {
new MutationObserver((_, observer) => {
if (!document.getElementById('style-settings')) {
return;
}
observer.disconnect();
for (const [name, url] of images.entries()) {
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
if (elUrl) {
elUrl.value = url;
}
}
}).observe(document, {childList: true, subtree: true});
}
return json;
});
};
} + ')()';
// TODO: remove the following statement when USO pagination is fixed
if (location.search.includes('category=')) {
document.addEventListener('DOMContentLoaded', function _() {
document.removeEventListener('DOMContentLoaded', _);
new MutationObserver((_, observer) => {
if (!document.getElementById('pagination')) {
return;
}
observer.disconnect();
const category = '&' + location.search.match(/category=[^&]+/)[0];
const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])');
for (let i = 0; i < links.length; i++) {
links[i].href += category;
}
}).observe(document, {childList: true, subtree: true});
});
}
new MutationObserver((mutations, observer) => { new MutationObserver((mutations, observer) => {
if (document.body) { if (document.body) {
observer.disconnect(); observer.disconnect();
@ -188,7 +96,7 @@ function sendEvent(type, detail = null) {
function onClick(event) { function onClick(event) {
if (onClick.processing) { if (onClick.processing || !orphanCheck()) {
return; return;
} }
onClick.processing = true; onClick.processing = true;
@ -352,15 +260,108 @@ function onDOMready() {
function orphanCheck() { function orphanCheck() {
const port = chrome.runtime.connect(); if (chrome.i18n && chrome.i18n.getUILanguage()) {
if (port) {
port.disconnect();
return true; return true;
} }
// we're orphaned due to an extension update // In Chrome content script is orphaned on an extension update/reload
// we can detach event listeners // so we need to detach event listeners
window.removeEventListener(chrome.runtime.id + '-install', orphanCheck, true); window.removeEventListener(chrome.runtime.id + '-install', orphanCheck, true);
['Update', 'Install'].forEach(type => ['Update', 'Install'].forEach(type =>
['', 'Chrome', 'Opera'].forEach(browser => ['', 'Chrome', 'Opera'].forEach(browser =>
document.addEventListener('stylish' + type + browser, onClick))); document.addEventListener('stylish' + type + browser, onClick)));
} }
})();
// TODO: remove the following statement when USO is fixed
document.documentElement.appendChild(document.createElement('script')).text = '(' +
function () {
let settings;
const originalResponseJson = Response.prototype.json;
document.currentScript.remove();
document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) {
document.removeEventListener('stylusFixBuggyUSOsettings', _);
settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search);
if (!settings) {
Response.prototype.json = originalResponseJson;
}
});
Response.prototype.json = function (...args) {
return originalResponseJson.call(this, ...args).then(json => {
if (!settings || typeof ((json || {}).style_settings || {}).every !== 'function') {
return json;
}
Response.prototype.json = originalResponseJson;
const images = new Map();
for (const jsonSetting of json.style_settings) {
let value = settings.get('ik-' + jsonSetting.install_key);
if (!value
|| !jsonSetting.style_setting_options
|| !jsonSetting.style_setting_options[0]) {
continue;
}
if (value.startsWith('ik-')) {
value = value.replace(/^ik-/, '');
const defaultItem = jsonSetting.style_setting_options.find(item => item.default);
if (!defaultItem || defaultItem.install_key !== value) {
if (defaultItem) {
defaultItem.default = false;
}
jsonSetting.style_setting_options.some(item => {
if (item.install_key === value) {
item.default = true;
return true;
}
});
}
} else if (jsonSetting.setting_type === 'image') {
jsonSetting.style_setting_options.some(item => {
if (item.default) {
item.default = false;
return true;
}
});
images.set(jsonSetting.install_key, value);
} else {
const item = jsonSetting.style_setting_options[0];
if (item.value !== value && item.install_key === 'placeholder') {
item.value = value;
}
}
}
if (images.size) {
new MutationObserver((_, observer) => {
if (!document.getElementById('style-settings')) {
return;
}
observer.disconnect();
for (const [name, url] of images.entries()) {
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
if (elUrl) {
elUrl.value = url;
}
}
}).observe(document, {childList: true, subtree: true});
}
return json;
});
};
} + ')()';
// TODO: remove the following statement when USO pagination is fixed
if (location.search.includes('category=')) {
document.addEventListener('DOMContentLoaded', function _() {
document.removeEventListener('DOMContentLoaded', _);
new MutationObserver((_, observer) => {
if (!document.getElementById('pagination')) {
return;
}
observer.disconnect();
const category = '&' + location.search.match(/category=[^&]+/)[0];
const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])');
for (let i = 0; i < links.length; i++) {
links[i].href += category;
}
}).observe(document, {childList: true, subtree: true});
});
}