Isolate storage.js in background context
To prevent cross-page leaks we need to create/copy prefs and cachedStyles inside the background page context. * storage.js is now used only in the background page * messaging.js now contains less bg-specific methods and more common methods. Added saveStyleSafe, deleteStyleSafe which automatically invoke onRuntimeMessage of the current page or just handleUpdate/handleDelete when notify:false * prefs.js with 'prefs' for background and UI pages: separate objects because a UI page may load before the background page and it can read prefs from localStorage/sync/defaults
This commit is contained in:
parent
7f6d3e241a
commit
5c8d1950a7
36
.eslintrc
36
.eslintrc
|
@ -15,17 +15,25 @@ globals:
|
|||
FIREFOX: false
|
||||
OPERA: false
|
||||
URLS: false
|
||||
BG: false
|
||||
notifyAllTabs: false
|
||||
refreshAllTabs: false
|
||||
updateIcon: false
|
||||
getActiveTab: false
|
||||
getActiveTabRealURL: false
|
||||
getTabRealURL: false
|
||||
openURL: false
|
||||
activateTab: false
|
||||
stringAsRegExp: false
|
||||
wildcardAsRegExp: false
|
||||
ignoreChromeError: false
|
||||
tryCatch: false
|
||||
tryRegExp: false
|
||||
tryJSONparse: false
|
||||
debounce: false
|
||||
deepCopy: false
|
||||
onBackgroundReady: false
|
||||
deleteStyleSafe: false
|
||||
getStylesSafe: false
|
||||
saveStyleSafe: false
|
||||
sessionStorageHash: false
|
||||
# localization.js
|
||||
template: false
|
||||
t: false
|
||||
|
@ -37,31 +45,13 @@ globals:
|
|||
# dom.js
|
||||
onDOMready: false
|
||||
scrollElementIntoView: false
|
||||
enforceInputRange: false
|
||||
animateElement: false
|
||||
$: false
|
||||
$$: false
|
||||
# storage.js
|
||||
# prefs.js
|
||||
prefs: false
|
||||
cachedStyles: false
|
||||
sessionStorageHash: false
|
||||
getStylesSafe: false
|
||||
invalidateCache: false
|
||||
saveStyle: false
|
||||
enableStyle: false
|
||||
deleteStyle: false
|
||||
fixBoolean: false
|
||||
getDomains: false
|
||||
getType: false
|
||||
getApplicableSections: false
|
||||
isCheckbox: false
|
||||
runTryCatch: false
|
||||
tryRegExp: false
|
||||
tryJSONparse: false
|
||||
debounce: false
|
||||
setupLivePrefs: false
|
||||
enforceInputRange: false
|
||||
getCodeMirrorThemes: false
|
||||
styleSectionsEqual: false
|
||||
|
||||
rules:
|
||||
accessor-pairs: [2]
|
||||
|
|
175
background.js
175
background.js
|
@ -1,4 +1,4 @@
|
|||
/* global getDatabase, getStyles, reportError */
|
||||
/* global getDatabase, getStyles, saveStyle, reportError, invalidateCache */
|
||||
'use strict';
|
||||
|
||||
chrome.webNavigation.onBeforeNavigate.addListener(data => {
|
||||
|
@ -39,9 +39,9 @@ function webNavigationListener(method, data) {
|
|||
|
||||
// messaging
|
||||
|
||||
chrome.runtime.onMessage.addListener(onBackgroundMessage);
|
||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||
|
||||
function onBackgroundMessage(request, sender, sendResponse) {
|
||||
function onRuntimeMessage(request, sender, sendResponse) {
|
||||
switch (request.method) {
|
||||
|
||||
case 'getStyles':
|
||||
|
@ -61,9 +61,7 @@ function onBackgroundMessage(request, sender, sendResponse) {
|
|||
return KEEP_CHANNEL_OPEN;
|
||||
|
||||
case 'invalidateCache':
|
||||
if (typeof invalidateCache != 'undefined') {
|
||||
invalidateCache(false, request);
|
||||
}
|
||||
invalidateCache(false, request);
|
||||
break;
|
||||
|
||||
case 'healthCheck':
|
||||
|
@ -101,8 +99,8 @@ if ('commands' in chrome) {
|
|||
}
|
||||
|
||||
// context menus
|
||||
|
||||
const contextMenus = {
|
||||
// eslint-disable-next-line no-var
|
||||
var contextMenus = {
|
||||
'show-badge': {
|
||||
title: 'menuShowBadge',
|
||||
click: info => prefs.set(info.menuItemId, info.checked),
|
||||
|
@ -123,7 +121,7 @@ const contextMenus = {
|
|||
// Vivaldi: Vivaldi/#
|
||||
if (/Vivaldi\/[\d.]+$/.test(navigator.userAgent)
|
||||
|| /Safari\/[\d.]+$/.test(navigator.userAgent)
|
||||
&& ![...navigator.plugins].some(p => p.name == 'Shockwave Flash')) {
|
||||
&& !Array.from(navigator.plugins).some(p => p.name == 'Shockwave Flash')) {
|
||||
contextMenus.editDeleteText = {
|
||||
title: 'editDeleteText',
|
||||
contexts: ['editable'],
|
||||
|
@ -172,8 +170,11 @@ chrome.tabs.onAttached.addListener((tabId, data) => {
|
|||
});
|
||||
});
|
||||
|
||||
var codeMirrorThemes; // eslint-disable-line no-var
|
||||
getCodeMirrorThemes(themes => (codeMirrorThemes = themes));
|
||||
// eslint-disable-next-line no-var
|
||||
var codeMirrorThemes;
|
||||
getCodeMirrorThemes().then(themes => {
|
||||
codeMirrorThemes = themes;
|
||||
});
|
||||
|
||||
// do not use prefs.get('version', null) as it might not yet be available
|
||||
chrome.storage.local.get('version', prefs => {
|
||||
|
@ -198,6 +199,9 @@ chrome.storage.local.get('version', prefs => {
|
|||
injectContentScripts();
|
||||
|
||||
function injectContentScripts() {
|
||||
// expand * as .*?
|
||||
const wildcardAsRegExp = (s, flags) =>
|
||||
new RegExp(s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&').replace(/\*/g, '.*?'), flags);
|
||||
const contentScripts = chrome.runtime.getManifest().content_scripts;
|
||||
for (const cs of contentScripts) {
|
||||
cs.matches = cs.matches.map(m => (
|
||||
|
@ -227,3 +231,152 @@ function injectContentScripts() {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function refreshAllTabs() {
|
||||
return new Promise(resolve => {
|
||||
// list all tabs including chrome-extension:// which can be ours
|
||||
chrome.tabs.query({}, tabs => {
|
||||
const lastTab = tabs[tabs.length - 1];
|
||||
for (const tab of tabs) {
|
||||
getStyles({matchUrl: tab.url, enabled: true, asHash: true}, styles => {
|
||||
const message = {method: 'styleReplaceAll', styles};
|
||||
chrome.tabs.sendMessage(tab.id, message);
|
||||
updateIcon(tab, styles);
|
||||
if (tab == lastTab) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function updateIcon(tab, styles) {
|
||||
// while NTP is still loading only process the request for its main frame with a real url
|
||||
// (but when it's loaded we should process style toggle requests from popups, for example)
|
||||
const isNTP = tab.url == 'chrome://newtab/';
|
||||
if (isNTP && tab.status != 'complete' || tab.id < 0) {
|
||||
return;
|
||||
}
|
||||
if (styles) {
|
||||
// check for not-yet-existing tabs e.g. omnibox instant search
|
||||
chrome.tabs.get(tab.id, () => {
|
||||
if (!chrome.runtime.lastError) {
|
||||
stylesReceived(styles);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (isNTP) {
|
||||
getTabRealURL(tab).then(url =>
|
||||
getStyles({matchUrl: url, enabled: true, asHash: true}, stylesReceived));
|
||||
} else {
|
||||
getStyles({matchUrl: tab.url, enabled: true, asHash: true}, stylesReceived);
|
||||
}
|
||||
|
||||
function stylesReceived(styles) {
|
||||
let numStyles = styles.length;
|
||||
if (numStyles === undefined) {
|
||||
// for 'styles' asHash:true fake the length by counting numeric ids manually
|
||||
numStyles = 0;
|
||||
for (const id of Object.keys(styles)) {
|
||||
numStyles += id.match(/^\d+$/) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll');
|
||||
const postfix = disableAll ? 'x' : numStyles == 0 ? 'w' : '';
|
||||
const color = prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal');
|
||||
const text = prefs.get('show-badge') && numStyles ? String(numStyles) : '';
|
||||
chrome.browserAction.setIcon({
|
||||
tabId: tab.id,
|
||||
path: {
|
||||
// Material Design 2016 new size is 16px
|
||||
16: `images/icon/16${postfix}.png`,
|
||||
32: `images/icon/32${postfix}.png`,
|
||||
// Chromium forks or non-chromium browsers may still use the traditional 19px
|
||||
19: `images/icon/19${postfix}.png`,
|
||||
38: `images/icon/38${postfix}.png`,
|
||||
// TODO: add Edge preferred sizes: 20, 25, 30, 40
|
||||
},
|
||||
}, () => {
|
||||
if (!chrome.runtime.lastError) {
|
||||
// Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor
|
||||
chrome.browserAction.setBadgeBackgroundColor({color});
|
||||
chrome.browserAction.setBadgeText({text, tabId: tab.id});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getCodeMirrorThemes() {
|
||||
if (!chrome.runtime.getPackageDirectoryEntry) {
|
||||
return Promise.resolve([
|
||||
'3024-day',
|
||||
'3024-night',
|
||||
'abcdef',
|
||||
'ambiance',
|
||||
'ambiance-mobile',
|
||||
'base16-dark',
|
||||
'base16-light',
|
||||
'bespin',
|
||||
'blackboard',
|
||||
'cobalt',
|
||||
'colorforth',
|
||||
'dracula',
|
||||
'duotone-dark',
|
||||
'duotone-light',
|
||||
'eclipse',
|
||||
'elegant',
|
||||
'erlang-dark',
|
||||
'hopscotch',
|
||||
'icecoder',
|
||||
'isotope',
|
||||
'lesser-dark',
|
||||
'liquibyte',
|
||||
'material',
|
||||
'mbo',
|
||||
'mdn-like',
|
||||
'midnight',
|
||||
'monokai',
|
||||
'neat',
|
||||
'neo',
|
||||
'night',
|
||||
'panda-syntax',
|
||||
'paraiso-dark',
|
||||
'paraiso-light',
|
||||
'pastel-on-dark',
|
||||
'railscasts',
|
||||
'rubyblue',
|
||||
'seti',
|
||||
'solarized',
|
||||
'the-matrix',
|
||||
'tomorrow-night-bright',
|
||||
'tomorrow-night-eighties',
|
||||
'ttcn',
|
||||
'twilight',
|
||||
'vibrant-ink',
|
||||
'xq-dark',
|
||||
'xq-light',
|
||||
'yeti',
|
||||
'zenburn',
|
||||
]);
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
chrome.runtime.getPackageDirectoryEntry(rootDir => {
|
||||
rootDir.getDirectory('codemirror/theme', {create: false}, themeDir => {
|
||||
themeDir.createReader().readEntries(entries => {
|
||||
resolve([
|
||||
chrome.i18n.getMessage('defaultTheme')
|
||||
].concat(
|
||||
entries.filter(entry => entry.isFile)
|
||||
.sort((a, b) => (a.name < b.name ? -1 : 1))
|
||||
.map(entry => entry.name.replace(/\.css$/, ''))
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global messageBox */
|
||||
/* global messageBox, handleUpdate */
|
||||
'use strict';
|
||||
|
||||
const STYLISH_DUMP_FILE_EXT = '.txt';
|
||||
|
@ -47,8 +47,15 @@ function importFromFile({fileTypeFilter, file} = {}) {
|
|||
|
||||
|
||||
function importFromString(jsonString) {
|
||||
const json = runTryCatch(() => Array.from(JSON.parse(jsonString))) || [];
|
||||
const oldStyles = json.length && deepCopyStyles();
|
||||
if (!BG) {
|
||||
onBackgroundReady().then(() => importFromString(jsonString));
|
||||
return;
|
||||
}
|
||||
const json = BG.tryJSONparse(jsonString) || []; // create object in background context
|
||||
if (typeof json.slice != 'function') {
|
||||
json.length = 0;
|
||||
}
|
||||
const oldStyles = json.length && BG.deepCopy(BG.cachedStyles.list || []);
|
||||
const oldStylesByName = json.length && new Map(
|
||||
oldStyles.map(style => [style.name.trim(), style]));
|
||||
const stats = {
|
||||
|
@ -60,18 +67,19 @@ function importFromString(jsonString) {
|
|||
invalid: {names: [], legend: 'invalid skipped'},
|
||||
};
|
||||
let index = 0;
|
||||
let lastRepaint = performance.now();
|
||||
return new Promise(proceed);
|
||||
|
||||
function proceed(resolve) {
|
||||
while (index < json.length) {
|
||||
const item = json[index++];
|
||||
if (!item || !item.name || !item.name.trim() || typeof item != 'object'
|
||||
|| (item.sections && !(item.sections instanceof Array))) {
|
||||
|| (item.sections && typeof item.sections.slice != 'function')) {
|
||||
stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`);
|
||||
continue;
|
||||
}
|
||||
item.name = item.name.trim();
|
||||
const byId = cachedStyles.byId.get(item.id);
|
||||
const byId = BG.cachedStyles.byId.get(item.id);
|
||||
const byName = oldStylesByName.get(item.name);
|
||||
const oldStyle = byId && byId.name.trim() == item.name || !byName ? byId : byName;
|
||||
if (oldStyle == byName && byName) {
|
||||
|
@ -81,16 +89,22 @@ function importFromString(jsonString) {
|
|||
const metaEqual = oldStyleKeys &&
|
||||
oldStyleKeys.length == Object.keys(item).length &&
|
||||
oldStyleKeys.every(k => k == 'sections' || oldStyle[k] === item[k]);
|
||||
const codeEqual = oldStyle && styleSectionsEqual(oldStyle, item);
|
||||
const codeEqual = oldStyle && BG.styleSectionsEqual(oldStyle, item);
|
||||
if (metaEqual && codeEqual) {
|
||||
stats.unchanged.names.push(oldStyle.name);
|
||||
stats.unchanged.ids.push(oldStyle.id);
|
||||
continue;
|
||||
}
|
||||
saveStyle(Object.assign(item, {
|
||||
// using saveStyle directly since json was parsed in background page context
|
||||
BG.saveStyle(Object.assign(item, {
|
||||
reason: 'import',
|
||||
notify: false,
|
||||
})).then(style => {
|
||||
handleUpdate(style, {reason: 'import'});
|
||||
if (performance.now() - lastRepaint > 1000) {
|
||||
scrollElementIntoView($('#style-' + style.id));
|
||||
lastRepaint = performance.now();
|
||||
}
|
||||
setTimeout(proceed, 0, resolve);
|
||||
if (!oldStyle) {
|
||||
stats.added.names.push(style.name);
|
||||
|
@ -120,17 +134,22 @@ function importFromString(jsonString) {
|
|||
stats.metaOnly.names.length +
|
||||
stats.codeOnly.names.length +
|
||||
stats.added.names.length;
|
||||
Promise.resolve(numChanged && refreshAllTabs()).then(() => {
|
||||
scrollTo(0, 0);
|
||||
Promise.resolve(numChanged && BG.refreshAllTabs()).then(() => {
|
||||
const listNames = kind => {
|
||||
const {ids, names} = stats[kind];
|
||||
return ids
|
||||
? names.map((name, i) => `<div data-id="${ids[i]}">${name}</div>`)
|
||||
: names.map(name => `<div>${name}</div>`);
|
||||
};
|
||||
const report = Object.keys(stats)
|
||||
.filter(kind => stats[kind].names.length)
|
||||
.map(kind => `<details data-id="${kind}">
|
||||
.map(kind =>
|
||||
`<details data-id="${kind}">
|
||||
<summary><b>${stats[kind].names.length} ${stats[kind].legend}</b></summary>
|
||||
<small>` + stats[kind].names.map((name, i) =>
|
||||
`<div data-id="${stats[kind].ids[i]}">${name}</div>`).join('') + `
|
||||
</small>
|
||||
<small>${listNames(kind).join('')}</small>
|
||||
</details>`)
|
||||
.join('');
|
||||
scrollTo(0, 0);
|
||||
messageBox({
|
||||
title: 'Finished importing styles',
|
||||
contents: report || 'Nothing was changed.',
|
||||
|
@ -155,7 +174,7 @@ function importFromString(jsonString) {
|
|||
];
|
||||
index = 0;
|
||||
return new Promise(undoNextId)
|
||||
.then(refreshAllTabs)
|
||||
.then(BG.refreshAllTabs)
|
||||
.then(() => messageBox({
|
||||
title: 'Import has been undone',
|
||||
contents: newIds.length + ' styles were reverted.',
|
||||
|
@ -167,14 +186,14 @@ function importFromString(jsonString) {
|
|||
return;
|
||||
}
|
||||
const id = newIds[index++];
|
||||
deleteStyle(id, {notify: false}).then(id => {
|
||||
deleteStyleSafe({id, notify: false}).then(id => {
|
||||
const oldStyle = oldStylesById.get(id);
|
||||
if (oldStyle) {
|
||||
saveStyle(Object.assign(oldStyle, {
|
||||
reason: 'undoImport',
|
||||
saveStyleSafe(Object.assign(oldStyle, {
|
||||
reason: 'import',
|
||||
notify: false,
|
||||
}))
|
||||
.then(() => setTimeout(undoNextId, 0, resolve));
|
||||
})).then(() =>
|
||||
setTimeout(undoNextId, 0, resolve));
|
||||
} else {
|
||||
setTimeout(undoNextId, 0, resolve);
|
||||
}
|
||||
|
@ -198,25 +217,6 @@ function importFromString(jsonString) {
|
|||
}
|
||||
}
|
||||
|
||||
function deepCopyStyles() {
|
||||
const clonedStyles = [];
|
||||
for (let style of cachedStyles.list || []) {
|
||||
style = Object.assign({}, style);
|
||||
style.sections = style.sections.slice();
|
||||
for (let i = 0, section; (section = style.sections[i]); i++) {
|
||||
const copy = style.sections[i] = Object.assign({}, section);
|
||||
for (const propName in copy) {
|
||||
const prop = copy[propName];
|
||||
if (prop instanceof Array) {
|
||||
copy[propName] = prop.slice();
|
||||
}
|
||||
}
|
||||
}
|
||||
clonedStyles.push(style);
|
||||
}
|
||||
return clonedStyles;
|
||||
}
|
||||
|
||||
function limitString(s, limit = 100) {
|
||||
return s.length <= limit ? s : s.substr(0, limit) + '...';
|
||||
}
|
||||
|
|
15
dom.js
15
dom.js
|
@ -50,6 +50,21 @@ function animateElement(element, {className, remove = false}) {
|
|||
}
|
||||
|
||||
|
||||
function enforceInputRange(element) {
|
||||
const min = Number(element.min);
|
||||
const max = Number(element.max);
|
||||
const onChange = () => {
|
||||
const value = Number(element.value);
|
||||
if (value < min || value > max) {
|
||||
element.value = Math.max(min, Math.min(max, value));
|
||||
}
|
||||
};
|
||||
onChange();
|
||||
element.addEventListener('change', onChange);
|
||||
element.addEventListener('input', onChange);
|
||||
}
|
||||
|
||||
|
||||
function $(selector, base = document) {
|
||||
// we have ids with . like #manage.onlyEdited which look like #id.class
|
||||
// so since getElementById is superfast we'll try it anyway
|
||||
|
|
|
@ -645,8 +645,8 @@
|
|||
</template>
|
||||
|
||||
<script src="dom.js"></script>
|
||||
<script src="storage.js"></script>
|
||||
<script src="messaging.js"></script>
|
||||
<script src="prefs.js"></script>
|
||||
<script src="localization.js"></script>
|
||||
<script src="apply.js"></script>
|
||||
<script src="edit.js"></script>
|
||||
|
|
11
edit.js
11
edit.js
|
@ -252,7 +252,8 @@ function initCodeMirror() {
|
|||
} else {
|
||||
// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
|
||||
themeControl.innerHTML = optionsHtmlFromArray([theme == "default" ? t("defaultTheme") : theme]);
|
||||
getCodeMirrorThemes(function(themes) {
|
||||
BG.getCodeMirrorThemes().then(themes => {
|
||||
BG.codeMirrorThemes = themes;
|
||||
themeControl.innerHTML = optionsHtmlFromArray(themes);
|
||||
themeControl.selectedIndex = Math.max(0, themes.indexOf(theme));
|
||||
});
|
||||
|
@ -1333,7 +1334,7 @@ function save() {
|
|||
}
|
||||
var name = document.getElementById("name").value;
|
||||
var enabled = document.getElementById("enabled").checked;
|
||||
saveStyle({
|
||||
saveStyleSafe({
|
||||
id: styleId,
|
||||
name: name,
|
||||
enabled: enabled,
|
||||
|
@ -1815,7 +1816,9 @@ function getParams() {
|
|||
return params;
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
|
||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||
|
||||
function onRuntimeMessage(request) {
|
||||
switch (request.method) {
|
||||
case "styleUpdated":
|
||||
if (styleId && styleId == request.style.id && request.reason != 'editSave') {
|
||||
|
@ -1838,7 +1841,7 @@ chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
|
|||
document.execCommand('delete');
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getComputedHeight(el) {
|
||||
var compStyle = getComputedStyle(el);
|
||||
|
|
|
@ -116,9 +116,8 @@
|
|||
</template>
|
||||
|
||||
<script src="dom.js"></script>
|
||||
<script src="health.js"></script>
|
||||
<script src="storage.js"></script>
|
||||
<script src="messaging.js"></script>
|
||||
<script src="prefs.js"></script>
|
||||
<script src="apply.js"></script>
|
||||
<script src="localization.js"></script>
|
||||
<script src="manage.js"></script>
|
||||
|
|
31
manage.js
31
manage.js
|
@ -27,7 +27,9 @@ Promise.all([
|
|||
});
|
||||
|
||||
|
||||
chrome.runtime.onMessage.addListener(msg => {
|
||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||
|
||||
function onRuntimeMessage(msg) {
|
||||
switch (msg.method) {
|
||||
case 'styleUpdated':
|
||||
case 'styleAdded':
|
||||
|
@ -37,7 +39,7 @@ chrome.runtime.onMessage.addListener(msg => {
|
|||
handleDelete(msg.id);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function initGlobalEvents() {
|
||||
|
@ -151,8 +153,6 @@ function createStyleElement({style, name}) {
|
|||
(style.enabled ? 'enabled' : 'disabled') +
|
||||
(style.updateUrl ? ' updatable' : ''),
|
||||
id: 'style-' + style.id,
|
||||
styleId: style.id,
|
||||
styleNameLowerCase: name || style.name.toLocaleLowerCase(),
|
||||
});
|
||||
|
||||
parts.nameLink.textContent = style.name;
|
||||
|
@ -216,6 +216,8 @@ function createStyleElement({style, name}) {
|
|||
}
|
||||
|
||||
const newEntry = parts.entry.cloneNode(true);
|
||||
newEntry.styleId = style.id;
|
||||
newEntry.styleNameLowerCase = name || style.name.toLocaleLowerCase();
|
||||
const newTargets = $('.targets', newEntry);
|
||||
if (numTargets) {
|
||||
newTargets.parentElement.replaceChild(targets, newTargets);
|
||||
|
@ -282,7 +284,10 @@ Object.assign(handleEvent, {
|
|||
},
|
||||
|
||||
toggle(event, entry) {
|
||||
enableStyle(entry.styleId, this.matches('.enable') || this.checked);
|
||||
saveStyleSafe({
|
||||
id: entry.styleId,
|
||||
enabled: this.matches('.enable') || this.checked,
|
||||
});
|
||||
},
|
||||
|
||||
check(event, entry) {
|
||||
|
@ -291,7 +296,7 @@ Object.assign(handleEvent, {
|
|||
|
||||
update(event, entry) {
|
||||
// update everything but name
|
||||
saveStyle(Object.assign(entry.updatedCode, {
|
||||
saveStyleSafe(Object.assign(entry.updatedCode, {
|
||||
id: entry.styleId,
|
||||
name: null,
|
||||
reason: 'update',
|
||||
|
@ -300,7 +305,7 @@ Object.assign(handleEvent, {
|
|||
|
||||
delete(event, entry) {
|
||||
const id = entry.styleId;
|
||||
const {name} = cachedStyles.byId.get(id) || {};
|
||||
const {name} = BG.cachedStyles.byId.get(id) || {};
|
||||
animateElement(entry, {className: 'highlight'});
|
||||
messageBox({
|
||||
title: t('deleteStyleConfirm'),
|
||||
|
@ -310,7 +315,7 @@ Object.assign(handleEvent, {
|
|||
})
|
||||
.then(({button, enter, esc}) => {
|
||||
if (button == 0 || enter) {
|
||||
deleteStyle(id);
|
||||
deleteStyleSafe({id});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -335,7 +340,7 @@ Object.assign(handleEvent, {
|
|||
});
|
||||
|
||||
|
||||
function handleUpdate(style, {reason, quiet} = {}) {
|
||||
function handleUpdate(style, {reason} = {}) {
|
||||
const element = createStyleElement({style});
|
||||
const oldElement = $('#style-' + style.id, installed);
|
||||
if (oldElement) {
|
||||
|
@ -354,8 +359,8 @@ function handleUpdate(style, {reason, quiet} = {}) {
|
|||
installed.insertBefore(element, findNextElement(style));
|
||||
if (reason != 'import') {
|
||||
animateElement(element, {className: 'highlight'});
|
||||
scrollElementIntoView(element);
|
||||
}
|
||||
scrollElementIntoView(element);
|
||||
}
|
||||
|
||||
|
||||
|
@ -465,7 +470,7 @@ function checkUpdate(element) {
|
|||
|
||||
class Updater {
|
||||
constructor(element) {
|
||||
const style = cachedStyles.byId.get(element.styleId);
|
||||
const style = BG.cachedStyles.byId.get(element.styleId);
|
||||
Object.assign(this, {
|
||||
element,
|
||||
id: style.id,
|
||||
|
@ -504,7 +509,7 @@ class Updater {
|
|||
|
||||
handleJson(forceUpdate, json) {
|
||||
return getStylesSafe({id: this.id}).then(([style]) => {
|
||||
const needsUpdate = forceUpdate || !styleSectionsEqual(style, json);
|
||||
const needsUpdate = forceUpdate || !BG.styleSectionsEqual(style, json);
|
||||
this.display({json: needsUpdate && json});
|
||||
return needsUpdate;
|
||||
});
|
||||
|
@ -598,7 +603,7 @@ function searchStyles({immediately, container}) {
|
|||
}
|
||||
|
||||
for (const element of (container || installed).children) {
|
||||
const style = cachedStyles.byId.get(element.styleId) || {};
|
||||
const style = BG.cachedStyles.byId.get(element.styleId) || {};
|
||||
if (style) {
|
||||
const isMatching = !query
|
||||
|| isMatchingText(style.name)
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
"*://*/*"
|
||||
],
|
||||
"background": {
|
||||
"scripts": ["messaging.js", "storage.js", "background.js", "update.js"]
|
||||
"scripts": ["messaging.js", "storage.js", "prefs.js", "background.js", "update.js"]
|
||||
},
|
||||
"commands": {
|
||||
"openManage": {
|
||||
|
|
294
messaging.js
294
messaging.js
|
@ -1,4 +1,4 @@
|
|||
/* global getStyleWithNoCode, applyOnMessage, onBackgroundMessage, getStyles */
|
||||
/* global BG: true, onRuntimeMessage, applyOnMessage, handleUpdate, handleDelete */
|
||||
'use strict';
|
||||
|
||||
// keep message channel open for sendResponse in chrome.runtime.onMessage listener
|
||||
|
@ -7,134 +7,59 @@ const FIREFOX = /Firefox/.test(navigator.userAgent);
|
|||
const OPERA = /OPR/.test(navigator.userAgent);
|
||||
const URLS = {
|
||||
ownOrigin: chrome.runtime.getURL(''),
|
||||
optionsUI: new Set([
|
||||
optionsUI: [
|
||||
chrome.runtime.getURL('options/index.html'),
|
||||
'chrome://extensions/?options=' + chrome.runtime.id,
|
||||
]),
|
||||
configureCommands: OPERA ? 'opera://settings/configureCommands'
|
||||
: 'chrome://extensions/configureCommands',
|
||||
],
|
||||
configureCommands:
|
||||
OPERA ? 'opera://settings/configureCommands'
|
||||
: 'chrome://extensions/configureCommands',
|
||||
};
|
||||
const RX_SUPPORTED_URLS = new RegExp(`^(file|https?|ftps?):|^${URLS.ownOrigin}`);
|
||||
|
||||
document.documentElement.classList.toggle('firefox', FIREFOX);
|
||||
document.documentElement.classList.toggle('opera', OPERA);
|
||||
let BG = chrome.extension.getBackgroundPage();
|
||||
|
||||
if (!BG || BG != window) {
|
||||
document.documentElement.classList.toggle('firefox', FIREFOX);
|
||||
document.documentElement.classList.toggle('opera', OPERA);
|
||||
}
|
||||
|
||||
function notifyAllTabs(request) {
|
||||
// list all tabs including chrome-extension:// which can be ours
|
||||
if (request.codeIsUpdated === false && request.style) {
|
||||
request = Object.assign({}, request, {
|
||||
style: getStyleWithNoCode(request.style)
|
||||
function notifyAllTabs(msg) {
|
||||
const originalMessage = msg;
|
||||
if (msg.codeIsUpdated === false && msg.style) {
|
||||
msg = Object.assign({}, msg, {
|
||||
style: getStyleWithNoCode(msg.style)
|
||||
});
|
||||
}
|
||||
const affectsAll = !request.affects || request.affects.all;
|
||||
const affectsOwnOrigin = !affectsAll && (request.affects.editor || request.affects.manager);
|
||||
const affectsAll = !msg.affects || msg.affects.all;
|
||||
const affectsOwnOrigin = !affectsAll && (msg.affects.editor || msg.affects.manager);
|
||||
const affectsTabs = affectsAll || affectsOwnOrigin;
|
||||
const affectsIcon = affectsAll || request.affects.icon;
|
||||
const affectsPopup = affectsAll || request.affects.popup;
|
||||
const affectsIcon = affectsAll || msg.affects.icon;
|
||||
const affectsPopup = affectsAll || msg.affects.popup;
|
||||
if (affectsTabs || affectsIcon) {
|
||||
// list all tabs including chrome-extension:// which can be ours
|
||||
chrome.tabs.query(affectsOwnOrigin ? {url: URLS.ownOrigin + '*'} : {}, tabs => {
|
||||
for (const tab of tabs) {
|
||||
if (affectsTabs || URLS.optionsUI.has(tab.url)) {
|
||||
chrome.tabs.sendMessage(tab.id, request);
|
||||
if (affectsTabs || URLS.optionsUI.includes(tab.url)) {
|
||||
chrome.tabs.sendMessage(tab.id, msg);
|
||||
}
|
||||
if (affectsIcon) {
|
||||
updateIcon(tab);
|
||||
if (affectsIcon && BG) {
|
||||
BG.updateIcon(tab);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// notify self: the message no longer is sent to the origin in new Chrome
|
||||
if (window.applyOnMessage) {
|
||||
applyOnMessage(request);
|
||||
} else if (window.onBackgroundMessage) {
|
||||
onBackgroundMessage(request);
|
||||
if (typeof onRuntimeMessage != 'undefined') {
|
||||
onRuntimeMessage(originalMessage);
|
||||
}
|
||||
// notify apply.js on own pages
|
||||
if (typeof applyOnMessage != 'undefined') {
|
||||
applyOnMessage(originalMessage);
|
||||
}
|
||||
// notify background page and all open popups
|
||||
if (affectsPopup || request.prefs) {
|
||||
chrome.runtime.sendMessage(request);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function refreshAllTabs() {
|
||||
return new Promise(resolve => {
|
||||
// list all tabs including chrome-extension:// which can be ours
|
||||
chrome.tabs.query({}, tabs => {
|
||||
const lastTab = tabs[tabs.length - 1];
|
||||
for (const tab of tabs) {
|
||||
getStyles({matchUrl: tab.url, enabled: true, asHash: true}, styles => {
|
||||
const message = {method: 'styleReplaceAll', styles};
|
||||
if (tab.url == location.href && typeof applyOnMessage !== 'undefined') {
|
||||
applyOnMessage(message);
|
||||
} else {
|
||||
chrome.tabs.sendMessage(tab.id, message);
|
||||
}
|
||||
updateIcon(tab, styles);
|
||||
if (tab == lastTab) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function updateIcon(tab, styles) {
|
||||
// while NTP is still loading only process the request for its main frame with a real url
|
||||
// (but when it's loaded we should process style toggle requests from popups, for example)
|
||||
const isNTP = tab.url == 'chrome://newtab/';
|
||||
if (isNTP && tab.status != 'complete' || tab.id < 0) {
|
||||
return;
|
||||
}
|
||||
if (styles) {
|
||||
// check for not-yet-existing tabs e.g. omnibox instant search
|
||||
chrome.tabs.get(tab.id, () => {
|
||||
if (!chrome.runtime.lastError) {
|
||||
stylesReceived(styles);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (isNTP) {
|
||||
getTabRealURL(tab).then(url =>
|
||||
getStyles({matchUrl: url, enabled: true, asHash: true}, stylesReceived));
|
||||
} else {
|
||||
getStyles({matchUrl: tab.url, enabled: true, asHash: true}, stylesReceived);
|
||||
}
|
||||
|
||||
function stylesReceived(styles) {
|
||||
let numStyles = styles.length;
|
||||
if (numStyles === undefined) {
|
||||
// for 'styles' asHash:true fake the length by counting numeric ids manually
|
||||
numStyles = 0;
|
||||
for (const id of Object.keys(styles)) {
|
||||
numStyles += id.match(/^\d+$/) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll');
|
||||
const postfix = disableAll ? 'x' : numStyles == 0 ? 'w' : '';
|
||||
const color = prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal');
|
||||
const text = prefs.get('show-badge') && numStyles ? String(numStyles) : '';
|
||||
chrome.browserAction.setIcon({
|
||||
tabId: tab.id,
|
||||
path: {
|
||||
// Material Design 2016 new size is 16px
|
||||
16: `images/icon/16${postfix}.png`,
|
||||
32: `images/icon/32${postfix}.png`,
|
||||
// Chromium forks or non-chromium browsers may still use the traditional 19px
|
||||
19: `images/icon/19${postfix}.png`,
|
||||
38: `images/icon/38${postfix}.png`,
|
||||
// TODO: add Edge preferred sizes: 20, 25, 30, 40
|
||||
},
|
||||
}, () => {
|
||||
if (!chrome.runtime.lastError) {
|
||||
// Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor
|
||||
chrome.browserAction.setBadgeBackgroundColor({color});
|
||||
chrome.browserAction.setBadgeText({text, tabId: tab.id});
|
||||
}
|
||||
});
|
||||
if (affectsPopup || msg.prefs) {
|
||||
chrome.runtime.sendMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -211,12 +136,153 @@ function stringAsRegExp(s, flags) {
|
|||
}
|
||||
|
||||
|
||||
// expands * as .*?
|
||||
function wildcardAsRegExp(s, flags) {
|
||||
return new RegExp(s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&').replace(/\*/g, '.*?'), flags);
|
||||
}
|
||||
|
||||
|
||||
function ignoreChromeError() {
|
||||
chrome.runtime.lastError; // eslint-disable-line no-unused-expressions
|
||||
}
|
||||
|
||||
|
||||
function getStyleWithNoCode(style) {
|
||||
const stripped = Object.assign({}, style, {sections: []});
|
||||
for (const section of style.sections) {
|
||||
stripped.sections.push(Object.assign({}, section, {code: null}));
|
||||
}
|
||||
return stripped;
|
||||
}
|
||||
|
||||
|
||||
// js engine can't optimize the entire function if it contains try-catch
|
||||
// so we should keep it isolated from normal code in a minimal wrapper
|
||||
// Update: might get fixed in V8 TurboFan in the future
|
||||
function tryCatch(func, ...args) {
|
||||
try {
|
||||
return func(...args);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
||||
function tryRegExp(regexp) {
|
||||
try {
|
||||
return new RegExp(regexp);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
||||
function tryJSONparse(jsonString) {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
||||
function debounce(fn, delay, ...args) {
|
||||
const timers = debounce.timers = debounce.timers || new Map();
|
||||
debounce.run = debounce.run || ((fn, ...args) => {
|
||||
timers.delete(fn);
|
||||
fn(...args);
|
||||
});
|
||||
clearTimeout(timers.get(fn));
|
||||
timers.set(fn, setTimeout(debounce.run, delay, fn, ...args));
|
||||
}
|
||||
|
||||
|
||||
function deepCopy(obj) {
|
||||
if (!obj || typeof obj != 'object') {
|
||||
return obj;
|
||||
} else {
|
||||
const emptyCopy = Object.create(Object.getPrototypeOf(obj));
|
||||
return deepMerge(emptyCopy, obj);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function deepMerge(target, ...args) {
|
||||
for (const obj of args) {
|
||||
for (const k in obj) {
|
||||
const value = obj[k];
|
||||
if (!value || typeof value != 'object') {
|
||||
target[k] = value;
|
||||
} else if (typeof value.slice == 'function') {
|
||||
const arrayCopy = target[k] = target[k] || [];
|
||||
for (const element of value) {
|
||||
arrayCopy.push(deepCopy(element));
|
||||
}
|
||||
} else if (k in target) {
|
||||
deepMerge(target[k], value);
|
||||
} else {
|
||||
target[k] = deepCopy(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
|
||||
function sessionStorageHash(name) {
|
||||
return {
|
||||
name,
|
||||
value: tryCatch(JSON.parse, sessionStorage[name]) || {},
|
||||
set(k, v) {
|
||||
this.value[k] = v;
|
||||
this.updateStorage();
|
||||
},
|
||||
unset(k) {
|
||||
delete this.value[k];
|
||||
this.updateStorage();
|
||||
},
|
||||
updateStorage() {
|
||||
sessionStorage[this.name] = JSON.stringify(this.value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function onBackgroundReady() {
|
||||
return BG ? Promise.resolve() : new Promise(ping);
|
||||
function ping(resolve) {
|
||||
chrome.runtime.sendMessage({method: 'healthCheck'}, health => {
|
||||
if (health !== undefined) {
|
||||
BG = chrome.extension.getBackgroundPage();
|
||||
resolve();
|
||||
} else {
|
||||
ping(resolve);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage
|
||||
function getStylesSafe(options) {
|
||||
return new Promise(resolve => {
|
||||
if (BG) {
|
||||
BG.getStyles(options, resolve);
|
||||
} else {
|
||||
onBackgroundReady().then(() =>
|
||||
BG.getStyles(options, resolve));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function saveStyleSafe(style) {
|
||||
return onBackgroundReady()
|
||||
.then(() => BG.saveStyle(BG.deepCopy(style)))
|
||||
.then(savedStyle => {
|
||||
if (style.notify === false) {
|
||||
handleUpdate(savedStyle, style);
|
||||
}
|
||||
return savedStyle;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function deleteStyleSafe({id, notify = true} = {}) {
|
||||
return onBackgroundReady()
|
||||
.then(() => BG.deleteStyle({id, notify}))
|
||||
.then(() => {
|
||||
if (!notify) {
|
||||
handleDelete(id);
|
||||
}
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
<link rel="stylesheet" href="index.css">
|
||||
<script src="/dom.js"></script>
|
||||
<script src="/localization.js"></script>
|
||||
<script src="/apply.js"></script>
|
||||
<script src="/storage.js"></script>
|
||||
<script src="/messaging.js"></script>
|
||||
<script src="/prefs.js"></script>
|
||||
<script src="/apply.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
/* global update */
|
||||
'use strict';
|
||||
|
||||
|
||||
setupLivePrefs([
|
||||
'show-badge',
|
||||
'popup.stylesFirst',
|
||||
|
@ -33,7 +31,7 @@ document.onclick = e => {
|
|||
}
|
||||
|
||||
function check() {
|
||||
chrome.extension.getBackgroundPage().update.perform((cmd, value) => {
|
||||
BG.update.perform((cmd, value) => {
|
||||
switch (cmd) {
|
||||
case 'count':
|
||||
total = value;
|
||||
|
|
|
@ -57,9 +57,8 @@
|
|||
|
||||
<script src="dom.js"></script>
|
||||
<script src="localization.js"></script>
|
||||
<script src="health.js"></script>
|
||||
<script src="storage.js"></script>
|
||||
<script src="messaging.js"></script>
|
||||
<script src="prefs.js"></script>
|
||||
<script src="apply.js"></script>
|
||||
<script src="popup.js"></script>
|
||||
</head>
|
||||
|
|
28
popup.js
28
popup.js
|
@ -1,4 +1,3 @@
|
|||
/* global SLOPPY_REGEXP_PREFIX, compileStyleRegExps */
|
||||
'use strict';
|
||||
|
||||
let installed;
|
||||
|
@ -17,8 +16,9 @@ getActiveTabRealURL().then(url => {
|
|||
});
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||
|
||||
chrome.runtime.onMessage.addListener(msg => {
|
||||
function onRuntimeMessage(msg) {
|
||||
switch (msg.method) {
|
||||
case 'styleAdded':
|
||||
case 'styleUpdated':
|
||||
|
@ -38,7 +38,7 @@ chrome.runtime.onMessage.addListener(msg => {
|
|||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function setPopupWidth(width = prefs.get('popupWidth')) {
|
||||
|
@ -117,7 +117,7 @@ function initPopup(url) {
|
|||
matchTargets.appendChild(urlLink);
|
||||
|
||||
// For domain
|
||||
const domains = getDomains(url);
|
||||
const domains = BG.getDomains(url);
|
||||
for (const domain of domains) {
|
||||
// Don't include TLD
|
||||
if (domains.length > 1 && !domain.includes('.')) {
|
||||
|
@ -252,7 +252,7 @@ Object.assign(handleEvent, {
|
|||
},
|
||||
|
||||
toggle(event) {
|
||||
saveStyle({
|
||||
saveStyleSafe({
|
||||
id: handleEvent.getClickedStyleId(event),
|
||||
enabled: this.type == 'checkbox' ? this.checked : this.matches('.enable'),
|
||||
});
|
||||
|
@ -263,7 +263,7 @@ Object.assign(handleEvent, {
|
|||
const box = $('#confirm');
|
||||
box.dataset.display = true;
|
||||
box.style.cssText = '';
|
||||
$('b', box).textContent = (cachedStyles.byId.get(id) || {}).name;
|
||||
$('b', box).textContent = (BG.cachedStyles.byId.get(id) || {}).name;
|
||||
$('[data-cmd="ok"]', box).onclick = () => confirm(true);
|
||||
$('[data-cmd="cancel"]', box).onclick = () => confirm(false);
|
||||
window.onkeydown = event => {
|
||||
|
@ -278,7 +278,7 @@ Object.assign(handleEvent, {
|
|||
animateElement(box, {className: 'lights-on'})
|
||||
.then(() => (box.dataset.display = false));
|
||||
if (ok) {
|
||||
deleteStyle(id).then(() => {
|
||||
deleteStyleSafe({id}).then(() => {
|
||||
// update view with 'No styles installed for this site' message
|
||||
if (!installed.children.length) {
|
||||
showStyles([]);
|
||||
|
@ -297,7 +297,7 @@ Object.assign(handleEvent, {
|
|||
entry.appendChild(info);
|
||||
},
|
||||
|
||||
closeExplanation(event) {
|
||||
closeExplanation() {
|
||||
$('#regexp-explanation').remove();
|
||||
},
|
||||
|
||||
|
@ -347,7 +347,7 @@ function handleUpdate(style) {
|
|||
return;
|
||||
}
|
||||
// Add an entry when a new style for the current url is installed
|
||||
if (tabURL && getApplicableSections({style, matchUrl: tabURL, stopOnFirst: true}).length) {
|
||||
if (tabURL && BG.getApplicableSections({style, matchUrl: tabURL, stopOnFirst: true}).length) {
|
||||
$('#unavailable').style.display = 'none';
|
||||
createStyleElement({style});
|
||||
}
|
||||
|
@ -368,13 +368,15 @@ function handleDelete(id) {
|
|||
*/
|
||||
function detectSloppyRegexps({entry, style}) {
|
||||
const {
|
||||
appliedSections = getApplicableSections({style, matchUrl: tabURL}),
|
||||
wannabeSections = getApplicableSections({style, matchUrl: tabURL, strictRegexp: false}),
|
||||
appliedSections =
|
||||
BG.getApplicableSections({style, matchUrl: tabURL}),
|
||||
wannabeSections =
|
||||
BG.getApplicableSections({style, matchUrl: tabURL, strictRegexp: false}),
|
||||
} = style;
|
||||
|
||||
compileStyleRegExps({style, compileAll: true});
|
||||
BG.compileStyleRegExps({style, compileAll: true});
|
||||
entry.hasInvalidRegexps = wannabeSections.some(section =>
|
||||
section.regexps.some(rx => !cachedStyles.regexps.has(rx)));
|
||||
section.regexps.some(rx => !BG.cachedStyles.regexps.has(rx)));
|
||||
entry.sectionsSkipped = wannabeSections.length - appliedSections.length;
|
||||
|
||||
if (!appliedSections.length) {
|
||||
|
|
265
prefs.js
Normal file
265
prefs.js
Normal file
|
@ -0,0 +1,265 @@
|
|||
/* global prefs: true, contextMenus */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var prefs = new function Prefs() {
|
||||
const defaults = {
|
||||
'openEditInWindow': false, // new editor opens in a own browser window
|
||||
'windowPosition': {}, // detached window position
|
||||
'show-badge': true, // display text on popup menu icon
|
||||
'disableAll': false, // boss key
|
||||
|
||||
'popup.breadcrumbs': true, // display 'New style' links as URL breadcrumbs
|
||||
'popup.breadcrumbs.usePath': false, // use URL path for 'this URL'
|
||||
'popup.enabledFirst': true, // display enabled styles before disabled styles
|
||||
'popup.stylesFirst': true, // display enabled styles before disabled styles
|
||||
|
||||
'manage.onlyEnabled': false, // display only enabled styles
|
||||
'manage.onlyEdited': false, // display only styles created locally
|
||||
'manage.newUI': true, // use the new compact layout
|
||||
'manage.newUI.favicons': true, // show favicons for the sites in applies-to
|
||||
'manage.newUI.targets': 3, // max number of applies-to targets visible: 0 = none
|
||||
|
||||
'editor.options': {}, // CodeMirror.defaults.*
|
||||
'editor.lineWrapping': true, // word wrap
|
||||
'editor.smartIndent': true, // 'smart' indent
|
||||
'editor.indentWithTabs': false, // smart indent with tabs
|
||||
'editor.tabSize': 4, // tab width, in spaces
|
||||
'editor.keyMap': navigator.appVersion.indexOf('Windows') > 0 ? 'sublime' : 'default',
|
||||
'editor.theme': 'default', // CSS theme
|
||||
'editor.beautify': { // CSS beautifier
|
||||
selector_separator_newline: true,
|
||||
newline_before_open_brace: false,
|
||||
newline_after_open_brace: true,
|
||||
newline_between_properties: true,
|
||||
newline_before_close_brace: true,
|
||||
newline_between_rules: false,
|
||||
end_with_newline: false,
|
||||
space_around_selector_separator: true,
|
||||
},
|
||||
'editor.lintDelay': 500, // lint gutter marker update delay, ms
|
||||
'editor.lintReportDelay': 4500, // lint report update delay, ms
|
||||
'editor.matchHighlight': 'token', // token = token/word under cursor even if nothing is selected
|
||||
// selection = only when something is selected
|
||||
// '' (empty string) = disabled
|
||||
|
||||
'badgeDisabled': '#8B0000', // badge background color when disabled
|
||||
'badgeNormal': '#006666', // badge background color
|
||||
|
||||
'popupWidth': 246, // popup width in pixels
|
||||
|
||||
'updateInterval': 0 // user-style automatic update interval, hour
|
||||
};
|
||||
const values = deepCopy(defaults);
|
||||
|
||||
const affectsIcon = [
|
||||
'show-badge',
|
||||
'disableAll',
|
||||
'badgeDisabled',
|
||||
'badgeNormal',
|
||||
];
|
||||
|
||||
// coalesce multiple pref changes in broadcast
|
||||
let broadcastPrefs = {};
|
||||
|
||||
Object.defineProperty(this, 'readOnlyValues', {value: {}});
|
||||
|
||||
Object.assign(Prefs.prototype, {
|
||||
|
||||
get(key, defaultValue) {
|
||||
if (key in values) {
|
||||
return values[key];
|
||||
}
|
||||
if (defaultValue !== undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (key in defaults) {
|
||||
return defaults[key];
|
||||
}
|
||||
console.warn("No default preference for '%s'", key);
|
||||
},
|
||||
|
||||
getAll() {
|
||||
return deepCopy(values);
|
||||
},
|
||||
|
||||
set(key, value, {noBroadcast, noSync} = {}) {
|
||||
const oldValue = deepCopy(values[key]);
|
||||
values[key] = value;
|
||||
defineReadonlyProperty(this.readOnlyValues, key, value);
|
||||
if (!noBroadcast && !equal(value, oldValue)) {
|
||||
this.broadcast(key, value, {noSync});
|
||||
}
|
||||
localStorage[key] = typeof defaults[key] == 'object'
|
||||
? JSON.stringify(value)
|
||||
: value;
|
||||
},
|
||||
|
||||
remove: key => this.set(key, undefined),
|
||||
|
||||
reset: key => this.set(key, deepCopy(defaults[key])),
|
||||
|
||||
broadcast(key, value, {noSync} = {}) {
|
||||
broadcastPrefs[key] = value;
|
||||
debounce(doBroadcast);
|
||||
if (!noSync) {
|
||||
debounce(doSyncSet);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Unlike sync, HTML5 localStorage is ready at browser startup
|
||||
// so we'll mirror the prefs to avoid using the wrong defaults
|
||||
// during the startup phase
|
||||
for (const key in defaults) {
|
||||
const defaultValue = defaults[key];
|
||||
let value = localStorage[key];
|
||||
if (typeof value == 'string') {
|
||||
switch (typeof defaultValue) {
|
||||
case 'boolean':
|
||||
value = value.toLowerCase() === 'true';
|
||||
break;
|
||||
case 'number':
|
||||
value |= 0;
|
||||
break;
|
||||
case 'object':
|
||||
value = tryJSONparse(value) || defaultValue;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
value = defaultValue;
|
||||
}
|
||||
this.set(key, value, {noBroadcast: true});
|
||||
}
|
||||
|
||||
getSync().get('settings', ({settings: synced} = {}) => {
|
||||
if (synced) {
|
||||
for (const key in defaults) {
|
||||
if (key == 'popupWidth' && synced[key] != values.popupWidth) {
|
||||
// this is a fix for the period when popupWidth wasn't synced
|
||||
// TODO: remove it in a couple of months
|
||||
continue;
|
||||
}
|
||||
if (key in synced) {
|
||||
this.set(key, synced[key], {noSync: true});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof contextMenus !== 'undefined') {
|
||||
for (const id in contextMenus) {
|
||||
if (typeof values[id] == 'boolean') {
|
||||
this.broadcast(id, values[id], {noSync: true});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (area == 'sync' && 'settings' in changes) {
|
||||
const synced = changes.settings.newValue;
|
||||
if (synced) {
|
||||
for (const key in defaults) {
|
||||
if (key in synced) {
|
||||
this.set(key, synced[key], {noSync: true});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// user manually deleted our settings, we'll recreate them
|
||||
getSync().set({'settings': values});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function doBroadcast() {
|
||||
const affects = {all: 'disableAll' in broadcastPrefs};
|
||||
if (!affects.all) {
|
||||
for (const key in broadcastPrefs) {
|
||||
affects.icon = affects.icon || affectsIcon.includes(key);
|
||||
affects.popup = affects.popup || key.startsWith('popup');
|
||||
affects.editor = affects.editor || key.startsWith('editor');
|
||||
affects.manager = affects.manager || key.startsWith('manage');
|
||||
}
|
||||
}
|
||||
notifyAllTabs({method: 'prefChanged', prefs: broadcastPrefs, affects});
|
||||
broadcastPrefs = {};
|
||||
}
|
||||
|
||||
function doSyncSet() {
|
||||
getSync().set({'settings': values});
|
||||
}
|
||||
|
||||
// Polyfill for Firefox < 53 https://bugzilla.mozilla.org/show_bug.cgi?id=1220494
|
||||
function getSync() {
|
||||
if ('sync' in chrome.storage) {
|
||||
return chrome.storage.sync;
|
||||
}
|
||||
const crappyStorage = {};
|
||||
return {
|
||||
get(key, callback) {
|
||||
callback(crappyStorage[key] || {});
|
||||
},
|
||||
set(source, callback) {
|
||||
for (const property in source) {
|
||||
if (source.hasOwnProperty(property)) {
|
||||
crappyStorage[property] = source[property];
|
||||
}
|
||||
}
|
||||
callback();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function defineReadonlyProperty(obj, key, value) {
|
||||
const copy = deepCopy(value);
|
||||
if (typeof copy == 'object') {
|
||||
Object.freeze(copy);
|
||||
}
|
||||
Object.defineProperty(obj, key, {value: copy, configurable: true});
|
||||
}
|
||||
|
||||
function equal(a, b) {
|
||||
if (!a || !b || typeof a != 'object' || typeof b != 'object') {
|
||||
return a === b;
|
||||
}
|
||||
if (Object.keys(a).length != Object.keys(b).length) {
|
||||
return false;
|
||||
}
|
||||
for (const k in a) {
|
||||
if (a[k] !== b[k]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}();
|
||||
|
||||
|
||||
// Accepts an array of pref names (values are fetched via prefs.get)
|
||||
// and establishes a two-way connection between the document elements and the actual prefs
|
||||
function setupLivePrefs(IDs) {
|
||||
const localIDs = {};
|
||||
IDs.forEach(function(id) {
|
||||
localIDs[id] = true;
|
||||
updateElement(id).addEventListener('change', function() {
|
||||
prefs.set(this.id, isCheckbox(this) ? this.checked : this.value);
|
||||
});
|
||||
});
|
||||
chrome.runtime.onMessage.addListener(msg => {
|
||||
if (msg.prefs) {
|
||||
for (const prefName in msg.prefs) {
|
||||
if (prefName in localIDs) {
|
||||
updateElement(prefName, msg.prefs[prefName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
function updateElement(id, value) {
|
||||
const el = document.getElementById(id);
|
||||
el[isCheckbox(el) ? 'checked' : 'value'] = value || prefs.get(id);
|
||||
el.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
|
||||
return el;
|
||||
}
|
||||
function isCheckbox(el) {
|
||||
return el.localName == 'input' && el.type == 'checkbox';
|
||||
}
|
||||
}
|
758
storage.js
758
storage.js
|
@ -1,7 +1,28 @@
|
|||
/* global cachedStyles: true, prefs: true, contextMenus: false */
|
||||
/* global handleUpdate, handleDelete */
|
||||
/* global cachedStyles: true */
|
||||
'use strict';
|
||||
|
||||
const RX_NAMESPACE = new RegExp([/[\s\r\n]*/,
|
||||
/(@namespace[\s\r\n]+(?:[^\s\r\n]+[\s\r\n]+)?url\(http:\/\/.*?\);)/,
|
||||
/[\s\r\n]*/].map(rx => rx.source).join(''), 'g');
|
||||
const RX_CSS_COMMENTS = /\/\*[\s\S]*?\*\//g;
|
||||
const SLOPPY_REGEXP_PREFIX = '\0';
|
||||
|
||||
// Note, only 'var'-declared variables are visible from another extension page
|
||||
// eslint-disable-next-line no-var
|
||||
var cachedStyles = {
|
||||
list: null,
|
||||
byId: new Map(),
|
||||
filters: new Map(),
|
||||
regexps: new Map(),
|
||||
urlDomains: new Map(),
|
||||
emptyCode: new Map(), // entire code is comments/whitespace/@namespace
|
||||
mutex: {
|
||||
inProgress: false,
|
||||
onDone: [],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
function getDatabase(ready, error) {
|
||||
const dbOpenRequest = window.indexedDB.open('stylish', 2);
|
||||
dbOpenRequest.onsuccess = event => {
|
||||
|
@ -24,54 +45,6 @@ function getDatabase(ready, error) {
|
|||
}
|
||||
|
||||
|
||||
const RX_NAMESPACE = new RegExp([/[\s\r\n]*/,
|
||||
/(@namespace[\s\r\n]+(?:[^\s\r\n]+[\s\r\n]+)?url\(http:\/\/.*?\);)/,
|
||||
/[\s\r\n]*/].map(rx => rx.source).join(''), 'g');
|
||||
const RX_CSS_COMMENTS = /\/\*[\s\S]*?\*\//g;
|
||||
const SLOPPY_REGEXP_PREFIX = '\0';
|
||||
|
||||
// Let manage/popup/edit reuse background page variables
|
||||
// Note, only 'var'-declared variables are visible from another extension page
|
||||
// eslint-disable-next-line no-var
|
||||
var cachedStyles, prefs;
|
||||
(() => {
|
||||
const bg = chrome.extension.getBackgroundPage();
|
||||
cachedStyles = bg && bg.cachedStyles || {
|
||||
bg,
|
||||
list: null,
|
||||
byId: new Map(),
|
||||
filters: new Map(),
|
||||
regexps: new Map(),
|
||||
urlDomains: new Map(),
|
||||
emptyCode: new Map(), // entire code is comments/whitespace/@namespace
|
||||
mutex: {
|
||||
inProgress: false,
|
||||
onDone: [],
|
||||
},
|
||||
};
|
||||
prefs = bg && bg.prefs;
|
||||
})();
|
||||
|
||||
|
||||
// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage
|
||||
function getStylesSafe(options) {
|
||||
return new Promise(resolve => {
|
||||
if (cachedStyles.bg) {
|
||||
getStyles(options, resolve);
|
||||
return;
|
||||
}
|
||||
chrome.runtime.sendMessage(Object.assign({method: 'getStyles'}, options), styles => {
|
||||
if (!styles) {
|
||||
resolve(getStylesSafe(options));
|
||||
} else {
|
||||
cachedStyles = chrome.extension.getBackgroundPage().cachedStyles;
|
||||
resolve(styles);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function getStyles(options, callback) {
|
||||
if (cachedStyles.list) {
|
||||
callback(filterStyles(options));
|
||||
|
@ -107,60 +80,6 @@ function getStyles(options, callback) {
|
|||
}
|
||||
|
||||
|
||||
function getStyleWithNoCode(style) {
|
||||
const stripped = Object.assign({}, style, {sections: []});
|
||||
for (const section of style.sections) {
|
||||
stripped.sections.push(Object.assign({}, section, {code: null}));
|
||||
}
|
||||
return stripped;
|
||||
}
|
||||
|
||||
|
||||
function invalidateCache(andNotify, {added, updated, deletedId} = {}) {
|
||||
// prevent double-add on echoed invalidation
|
||||
const cached = added && cachedStyles.byId.get(added.id);
|
||||
if (cached) {
|
||||
return;
|
||||
}
|
||||
if (andNotify) {
|
||||
chrome.runtime.sendMessage({method: 'invalidateCache', added, updated, deletedId});
|
||||
}
|
||||
if (!cachedStyles.list) {
|
||||
return;
|
||||
}
|
||||
if (updated) {
|
||||
const cached = cachedStyles.byId.get(updated.id);
|
||||
if (cached) {
|
||||
Object.assign(cached, updated);
|
||||
//console.debug('cache: updated', updated);
|
||||
}
|
||||
cachedStyles.filters.clear();
|
||||
return;
|
||||
}
|
||||
if (added) {
|
||||
cachedStyles.list.push(added);
|
||||
cachedStyles.byId.set(added.id, added);
|
||||
//console.debug('cache: added', added);
|
||||
cachedStyles.filters.clear();
|
||||
return;
|
||||
}
|
||||
if (deletedId != undefined) {
|
||||
const deletedStyle = (cachedStyles.byId.get(deletedId) || {}).style;
|
||||
if (deletedStyle) {
|
||||
const cachedIndex = cachedStyles.list.indexOf(deletedStyle);
|
||||
cachedStyles.list.splice(cachedIndex, 1);
|
||||
cachedStyles.byId.delete(deletedId);
|
||||
//console.debug('cache: deleted', deletedStyle);
|
||||
cachedStyles.filters.clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
cachedStyles.list = null;
|
||||
//console.debug('cache cleared');
|
||||
cachedStyles.filters.clear();
|
||||
}
|
||||
|
||||
|
||||
function filterStyles({
|
||||
enabled,
|
||||
url = null,
|
||||
|
@ -174,10 +93,10 @@ function filterStyles({
|
|||
id = id === null ? null : Number(id);
|
||||
|
||||
if (enabled === null
|
||||
&& url === null
|
||||
&& id === null
|
||||
&& matchUrl === null
|
||||
&& asHash != true) {
|
||||
&& url === null
|
||||
&& id === null
|
||||
&& matchUrl === null
|
||||
&& asHash != true) {
|
||||
//console.debug('%c%s filterStyles SKIPPED LOOP %s', 'color:gray', (performance.now() - t0).toFixed(1), enabled, id, asHash, strictRegexp, matchUrl); // eslint-disable-line max-len
|
||||
return cachedStyles.list;
|
||||
}
|
||||
|
@ -247,36 +166,6 @@ function filterStyles({
|
|||
}
|
||||
|
||||
|
||||
function cleanupCachedFilters({force = false} = {}) {
|
||||
if (!force) {
|
||||
// sliding timer for 1 second
|
||||
clearTimeout(cleanupCachedFilters.timeout);
|
||||
cleanupCachedFilters.timeout = setTimeout(cleanupCachedFilters, 1000, {force: true});
|
||||
return;
|
||||
}
|
||||
const size = cachedStyles.filters.size;
|
||||
const oldestHit = cachedStyles.filters.values().next().value.lastHit;
|
||||
const now = Date.now();
|
||||
const timeSpan = now - oldestHit;
|
||||
const recencyWeight = 5 / size;
|
||||
const hitWeight = 1 / 4; // we make ~4 hits per URL
|
||||
const lastHitWeight = 10;
|
||||
// delete the oldest 10%
|
||||
[...cachedStyles.filters.entries()]
|
||||
.map(([id, v], index) => ({
|
||||
id,
|
||||
weight:
|
||||
index * recencyWeight +
|
||||
v.hits * hitWeight +
|
||||
(v.lastHit - oldestHit) / timeSpan * lastHitWeight,
|
||||
}))
|
||||
.sort((a, b) => a.weight - b.weight)
|
||||
.slice(0, size / 10 + 1)
|
||||
.forEach(({id}) => cachedStyles.filters.delete(id));
|
||||
cleanupCachedFilters.timeout = 0;
|
||||
}
|
||||
|
||||
|
||||
function saveStyle(style) {
|
||||
return new Promise(resolve => {
|
||||
getDatabase(db => {
|
||||
|
@ -312,9 +201,6 @@ function saveStyle(style) {
|
|||
style, codeIsUpdated, reason,
|
||||
});
|
||||
}
|
||||
if (typeof handleUpdate != 'undefined') {
|
||||
handleUpdate(style, {reason});
|
||||
}
|
||||
resolve(style);
|
||||
};
|
||||
};
|
||||
|
@ -340,9 +226,6 @@ function saveStyle(style) {
|
|||
if (notify) {
|
||||
notifyAllTabs({method: 'styleAdded', style, reason});
|
||||
}
|
||||
if (typeof handleUpdate != 'undefined') {
|
||||
handleUpdate(style, {reason});
|
||||
}
|
||||
resolve(style);
|
||||
};
|
||||
});
|
||||
|
@ -350,24 +233,7 @@ function saveStyle(style) {
|
|||
}
|
||||
|
||||
|
||||
function addMissingStyleTargets(style) {
|
||||
style.sections = (style.sections || []).map(section =>
|
||||
Object.assign({
|
||||
urls: [],
|
||||
urlPrefixes: [],
|
||||
domains: [],
|
||||
regexps: [],
|
||||
}, section)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function enableStyle(id, enabled) {
|
||||
return saveStyle({id, enabled});
|
||||
}
|
||||
|
||||
|
||||
function deleteStyle(id, {notify = true} = {}) {
|
||||
function deleteStyle({id, notify = true}) {
|
||||
return new Promise(resolve =>
|
||||
getDatabase(db => {
|
||||
const tx = db.transaction(['styles'], 'readwrite');
|
||||
|
@ -377,61 +243,12 @@ function deleteStyle(id, {notify = true} = {}) {
|
|||
if (notify) {
|
||||
notifyAllTabs({method: 'styleDeleted', id});
|
||||
}
|
||||
if (typeof handleDelete != 'undefined') {
|
||||
handleDelete(id);
|
||||
}
|
||||
resolve(id);
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
function reportError(...args) {
|
||||
for (const arg of args) {
|
||||
if ('message' in arg) {
|
||||
console.log(arg.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function fixBoolean(b) {
|
||||
if (typeof b != 'undefined') {
|
||||
return b != 'false';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
function getDomains(url) {
|
||||
if (url.indexOf('file:') == 0) {
|
||||
return [];
|
||||
}
|
||||
let d = /.*?:\/*([^/:]+)/.exec(url)[1];
|
||||
const domains = [d];
|
||||
while (d.indexOf('.') != -1) {
|
||||
d = d.substring(d.indexOf('.') + 1);
|
||||
domains.push(d);
|
||||
}
|
||||
return domains;
|
||||
}
|
||||
|
||||
|
||||
function getType(o) {
|
||||
if (typeof o == 'undefined' || typeof o == 'string') {
|
||||
return typeof o;
|
||||
}
|
||||
// with the persistent cachedStyles the Array reference is usually different
|
||||
// so let's check for e.g. type of 'every' which is only present on arrays
|
||||
// (in the context of our extension)
|
||||
if (o instanceof Array || typeof o.every == 'function') {
|
||||
return 'array';
|
||||
}
|
||||
console.warn('Unsupported type:', o);
|
||||
return 'undefined';
|
||||
}
|
||||
|
||||
|
||||
function getApplicableSections({style, matchUrl, strictRegexp = true, stopOnFirst}) {
|
||||
//let t0 = 0;
|
||||
const sections = [];
|
||||
|
@ -518,392 +335,6 @@ function getApplicableSections({style, matchUrl, strictRegexp = true, stopOnFirs
|
|||
}
|
||||
|
||||
|
||||
function isCheckbox(el) {
|
||||
return el.localName == 'input' && el.type == 'checkbox';
|
||||
}
|
||||
|
||||
|
||||
// js engine can't optimize the entire function if it contains try-catch
|
||||
// so we should keep it isolated from normal code in a minimal wrapper
|
||||
// Update: might get fixed in V8 TurboFan in the future
|
||||
function runTryCatch(func, ...args) {
|
||||
try {
|
||||
return func(...args);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
||||
function tryRegExp(regexp) {
|
||||
try {
|
||||
return new RegExp(regexp);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
||||
function tryJSONparse(jsonString) {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
||||
function debounce(fn, delay, ...args) {
|
||||
const timers = debounce.timers = debounce.timers || new Map();
|
||||
debounce.run = debounce.run || ((fn, ...args) => {
|
||||
timers.delete(fn);
|
||||
fn(...args);
|
||||
});
|
||||
clearTimeout(timers.get(fn));
|
||||
timers.set(fn, setTimeout(debounce.run, delay, fn, ...args));
|
||||
}
|
||||
|
||||
|
||||
prefs = prefs || new function Prefs() {
|
||||
const defaults = {
|
||||
'openEditInWindow': false, // new editor opens in a own browser window
|
||||
'windowPosition': {}, // detached window position
|
||||
'show-badge': true, // display text on popup menu icon
|
||||
'disableAll': false, // boss key
|
||||
|
||||
'popup.breadcrumbs': true, // display 'New style' links as URL breadcrumbs
|
||||
'popup.breadcrumbs.usePath': false, // use URL path for 'this URL'
|
||||
'popup.enabledFirst': true, // display enabled styles before disabled styles
|
||||
'popup.stylesFirst': true, // display enabled styles before disabled styles
|
||||
|
||||
'manage.onlyEnabled': false, // display only enabled styles
|
||||
'manage.onlyEdited': false, // display only styles created locally
|
||||
'manage.newUI': true, // use the new compact layout
|
||||
'manage.newUI.favicons': true, // show favicons for the sites in applies-to
|
||||
'manage.newUI.targets': 3, // max number of applies-to targets visible: 0 = none
|
||||
|
||||
'editor.options': {}, // CodeMirror.defaults.*
|
||||
'editor.lineWrapping': true, // word wrap
|
||||
'editor.smartIndent': true, // 'smart' indent
|
||||
'editor.indentWithTabs': false, // smart indent with tabs
|
||||
'editor.tabSize': 4, // tab width, in spaces
|
||||
'editor.keyMap': navigator.appVersion.indexOf('Windows') > 0 ? 'sublime' : 'default',
|
||||
'editor.theme': 'default', // CSS theme
|
||||
'editor.beautify': { // CSS beautifier
|
||||
selector_separator_newline: true,
|
||||
newline_before_open_brace: false,
|
||||
newline_after_open_brace: true,
|
||||
newline_between_properties: true,
|
||||
newline_before_close_brace: true,
|
||||
newline_between_rules: false,
|
||||
end_with_newline: false,
|
||||
space_around_selector_separator: true,
|
||||
},
|
||||
'editor.lintDelay': 500, // lint gutter marker update delay, ms
|
||||
'editor.lintReportDelay': 4500, // lint report update delay, ms
|
||||
'editor.matchHighlight': 'token', // token = token/word under cursor even if nothing is selected
|
||||
// selection = only when something is selected
|
||||
// '' (empty string) = disabled
|
||||
|
||||
'badgeDisabled': '#8B0000', // badge background color when disabled
|
||||
'badgeNormal': '#006666', // badge background color
|
||||
|
||||
'popupWidth': 246, // popup width in pixels
|
||||
|
||||
'updateInterval': 0 // user-style automatic update interval, hour
|
||||
};
|
||||
const values = deepCopy(defaults);
|
||||
|
||||
const affectsIcon = [
|
||||
'show-badge',
|
||||
'disableAll',
|
||||
'badgeDisabled',
|
||||
'badgeNormal',
|
||||
];
|
||||
|
||||
// coalesce multiple pref changes in broadcast
|
||||
let broadcastPrefs = {};
|
||||
|
||||
function doBroadcast() {
|
||||
const affects = {all: 'disableAll' in broadcastPrefs};
|
||||
if (!affects.all) {
|
||||
for (const key in broadcastPrefs) {
|
||||
affects.icon = affects.icon || affectsIcon.includes(key);
|
||||
affects.popup = affects.popup || key.startsWith('popup');
|
||||
affects.editor = affects.editor || key.startsWith('editor');
|
||||
affects.manager = affects.manager || key.startsWith('manage');
|
||||
}
|
||||
}
|
||||
notifyAllTabs({method: 'prefChanged', prefs: broadcastPrefs, affects});
|
||||
broadcastPrefs = {};
|
||||
}
|
||||
|
||||
function doSyncSet() {
|
||||
getSync().set({'settings': values});
|
||||
}
|
||||
|
||||
Object.defineProperty(this, 'readOnlyValues', {value: {}});
|
||||
|
||||
Object.assign(Prefs.prototype, {
|
||||
|
||||
get(key, defaultValue) {
|
||||
if (key in values) {
|
||||
return values[key];
|
||||
}
|
||||
if (defaultValue !== undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (key in defaults) {
|
||||
return defaults[key];
|
||||
}
|
||||
console.warn("No default preference for '%s'", key);
|
||||
},
|
||||
|
||||
getAll() {
|
||||
return deepCopy(values);
|
||||
},
|
||||
|
||||
set(key, value, {noBroadcast, noSync} = {}) {
|
||||
const oldValue = deepCopy(values[key]);
|
||||
values[key] = value;
|
||||
defineReadonlyProperty(this.readOnlyValues, key, value);
|
||||
if (!noBroadcast && !equal(value, oldValue)) {
|
||||
this.broadcast(key, value, {noSync});
|
||||
}
|
||||
localStorage[key] = typeof defaults[key] == 'object'
|
||||
? JSON.stringify(value)
|
||||
: value;
|
||||
},
|
||||
|
||||
remove: key => this.set(key, undefined),
|
||||
|
||||
reset: key => this.set(key, deepCopy(defaults[key])),
|
||||
|
||||
broadcast(key, value, {noSync} = {}) {
|
||||
broadcastPrefs[key] = value;
|
||||
debounce(doBroadcast);
|
||||
if (!noSync) {
|
||||
debounce(doSyncSet);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Unlike sync, HTML5 localStorage is ready at browser startup
|
||||
// so we'll mirror the prefs to avoid using the wrong defaults
|
||||
// during the startup phase
|
||||
for (const key in defaults) {
|
||||
const defaultValue = defaults[key];
|
||||
let value = localStorage[key];
|
||||
if (typeof value == 'string') {
|
||||
switch (typeof defaultValue) {
|
||||
case 'boolean':
|
||||
value = value.toLowerCase() === 'true';
|
||||
break;
|
||||
case 'number':
|
||||
value |= 0;
|
||||
break;
|
||||
case 'object':
|
||||
value = tryJSONparse(value) || defaultValue;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
value = defaultValue;
|
||||
}
|
||||
this.set(key, value, {noBroadcast: true});
|
||||
}
|
||||
|
||||
getSync().get('settings', ({settings: synced}) => {
|
||||
if (synced) {
|
||||
for (const key in defaults) {
|
||||
if (key == 'popupWidth' && synced[key] != values.popupWidth) {
|
||||
// this is a fix for the period when popupWidth wasn't synced
|
||||
// TODO: remove it in a couple of months before the summer 2017
|
||||
continue;
|
||||
}
|
||||
if (key in synced) {
|
||||
this.set(key, synced[key], {noSync: true});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof contextMenus !== 'undefined') {
|
||||
for (const id in contextMenus) {
|
||||
if (typeof values[id] == 'boolean') {
|
||||
this.broadcast(id, values[id], {noSync: true});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (area == 'sync' && 'settings' in changes) {
|
||||
const synced = changes.settings.newValue;
|
||||
if (synced) {
|
||||
for (const key in defaults) {
|
||||
if (key in synced) {
|
||||
this.set(key, synced[key], {noSync: true});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// user manually deleted our settings, we'll recreate them
|
||||
getSync().set({'settings': values});
|
||||
}
|
||||
}
|
||||
});
|
||||
}();
|
||||
|
||||
|
||||
// Accepts an array of pref names (values are fetched via prefs.get)
|
||||
// and establishes a two-way connection between the document elements and the actual prefs
|
||||
function setupLivePrefs(IDs) {
|
||||
const localIDs = {};
|
||||
IDs.forEach(function(id) {
|
||||
localIDs[id] = true;
|
||||
updateElement(id).addEventListener('change', function() {
|
||||
prefs.set(this.id, isCheckbox(this) ? this.checked : this.value);
|
||||
});
|
||||
});
|
||||
chrome.runtime.onMessage.addListener(msg => {
|
||||
if (msg.prefs) {
|
||||
for (const prefName in msg.prefs) {
|
||||
if (prefName in localIDs) {
|
||||
updateElement(prefName, msg.prefs[prefName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
function updateElement(id, value) {
|
||||
const el = document.getElementById(id);
|
||||
el[isCheckbox(el) ? 'checked' : 'value'] = value || prefs.get(id);
|
||||
el.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
|
||||
return el;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function enforceInputRange(element) {
|
||||
const min = Number(element.min);
|
||||
const max = Number(element.max);
|
||||
const onChange = () => {
|
||||
const value = Number(element.value);
|
||||
if (value < min || value > max) {
|
||||
element.value = Math.max(min, Math.min(max, value));
|
||||
}
|
||||
};
|
||||
onChange();
|
||||
element.addEventListener('change', onChange);
|
||||
element.addEventListener('input', onChange);
|
||||
}
|
||||
|
||||
|
||||
function getCodeMirrorThemes(callback) {
|
||||
chrome.runtime.getPackageDirectoryEntry(function(rootDir) {
|
||||
rootDir.getDirectory('codemirror/theme', {create: false}, function(themeDir) {
|
||||
themeDir.createReader().readEntries(function(entries) {
|
||||
const themes = [chrome.i18n.getMessage('defaultTheme')];
|
||||
entries
|
||||
.filter(entry => entry.isFile)
|
||||
.sort((a, b) => (a.name < b.name ? -1 : 1))
|
||||
.forEach(function(entry) {
|
||||
themes.push(entry.name.replace(/\.css$/, ''));
|
||||
});
|
||||
if (callback) {
|
||||
callback(themes);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function sessionStorageHash(name) {
|
||||
return {
|
||||
name,
|
||||
value: runTryCatch(JSON.parse, sessionStorage[name]) || {},
|
||||
set(k, v) {
|
||||
this.value[k] = v;
|
||||
this.updateStorage();
|
||||
},
|
||||
unset(k) {
|
||||
delete this.value[k];
|
||||
this.updateStorage();
|
||||
},
|
||||
updateStorage() {
|
||||
sessionStorage[this.name] = JSON.stringify(this.value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function deepCopy(obj) {
|
||||
if (!obj || typeof obj != 'object') {
|
||||
return obj;
|
||||
} else {
|
||||
const emptyCopy = Object.create(Object.getPrototypeOf(obj));
|
||||
return deepMerge(emptyCopy, obj);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function deepMerge(target, ...args) {
|
||||
for (const obj of args) {
|
||||
for (const k in obj) {
|
||||
const value = obj[k];
|
||||
if (!value || typeof value != 'object') {
|
||||
target[k] = value;
|
||||
} else if (k in target) {
|
||||
deepMerge(target[k], value);
|
||||
} else if (typeof value.slice == 'function') {
|
||||
target[k] = value.slice();
|
||||
} else {
|
||||
target[k] = deepCopy(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
|
||||
function equal(a, b) {
|
||||
if (!a || !b || typeof a != 'object' || typeof b != 'object') {
|
||||
return a === b;
|
||||
}
|
||||
if (Object.keys(a).length != Object.keys(b).length) {
|
||||
return false;
|
||||
}
|
||||
for (const k in a) {
|
||||
if (a[k] !== b[k]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function defineReadonlyProperty(obj, key, value) {
|
||||
const copy = deepCopy(value);
|
||||
if (typeof copy == 'object') {
|
||||
Object.freeze(copy);
|
||||
}
|
||||
Object.defineProperty(obj, key, {value: copy, configurable: true});
|
||||
}
|
||||
|
||||
|
||||
// Polyfill for Firefox < 53 https://bugzilla.mozilla.org/show_bug.cgi?id=1220494
|
||||
function getSync() {
|
||||
if ('sync' in chrome.storage) {
|
||||
return chrome.storage.sync;
|
||||
}
|
||||
const crappyStorage = {};
|
||||
return {
|
||||
get(key, callback) {
|
||||
callback(crappyStorage[key] || {});
|
||||
},
|
||||
set(source, callback) {
|
||||
for (const property in source) {
|
||||
if (source.hasOwnProperty(property)) {
|
||||
crappyStorage[property] = source[property];
|
||||
}
|
||||
}
|
||||
callback();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function styleSectionsEqual(styleA, styleB) {
|
||||
if (!styleA.sections || !styleB.sections) {
|
||||
return undefined;
|
||||
|
@ -990,3 +421,136 @@ function compileStyleRegExps({style, compileAll}) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function invalidateCache(andNotify, {added, updated, deletedId} = {}) {
|
||||
// prevent double-add on echoed invalidation
|
||||
const cached = added && cachedStyles.byId.get(added.id);
|
||||
if (cached) {
|
||||
return;
|
||||
}
|
||||
if (andNotify) {
|
||||
chrome.runtime.sendMessage({method: 'invalidateCache', added, updated, deletedId});
|
||||
}
|
||||
if (!cachedStyles.list) {
|
||||
return;
|
||||
}
|
||||
if (updated) {
|
||||
const cached = cachedStyles.byId.get(updated.id);
|
||||
if (cached) {
|
||||
Object.assign(cached, updated);
|
||||
//console.debug('cache: updated', updated);
|
||||
}
|
||||
cachedStyles.filters.clear();
|
||||
return;
|
||||
}
|
||||
if (added) {
|
||||
cachedStyles.list.push(added);
|
||||
cachedStyles.byId.set(added.id, added);
|
||||
//console.debug('cache: added', added);
|
||||
cachedStyles.filters.clear();
|
||||
return;
|
||||
}
|
||||
if (deletedId != undefined) {
|
||||
const deletedStyle = (cachedStyles.byId.get(deletedId) || {}).style;
|
||||
if (deletedStyle) {
|
||||
const cachedIndex = cachedStyles.list.indexOf(deletedStyle);
|
||||
cachedStyles.list.splice(cachedIndex, 1);
|
||||
cachedStyles.byId.delete(deletedId);
|
||||
//console.debug('cache: deleted', deletedStyle);
|
||||
cachedStyles.filters.clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
cachedStyles.list = null;
|
||||
//console.debug('cache cleared');
|
||||
cachedStyles.filters.clear();
|
||||
}
|
||||
|
||||
|
||||
function cleanupCachedFilters({force = false} = {}) {
|
||||
if (!force) {
|
||||
// sliding timer for 1 second
|
||||
clearTimeout(cleanupCachedFilters.timeout);
|
||||
cleanupCachedFilters.timeout = setTimeout(cleanupCachedFilters, 1000, {force: true});
|
||||
return;
|
||||
}
|
||||
const size = cachedStyles.filters.size;
|
||||
const oldestHit = cachedStyles.filters.values().next().value.lastHit;
|
||||
const now = Date.now();
|
||||
const timeSpan = now - oldestHit;
|
||||
const recencyWeight = 5 / size;
|
||||
const hitWeight = 1 / 4; // we make ~4 hits per URL
|
||||
const lastHitWeight = 10;
|
||||
// delete the oldest 10%
|
||||
[...cachedStyles.filters.entries()]
|
||||
.map(([id, v], index) => ({
|
||||
id,
|
||||
weight:
|
||||
index * recencyWeight +
|
||||
v.hits * hitWeight +
|
||||
(v.lastHit - oldestHit) / timeSpan * lastHitWeight,
|
||||
}))
|
||||
.sort((a, b) => a.weight - b.weight)
|
||||
.slice(0, size / 10 + 1)
|
||||
.forEach(({id}) => cachedStyles.filters.delete(id));
|
||||
cleanupCachedFilters.timeout = 0;
|
||||
}
|
||||
|
||||
|
||||
function addMissingStyleTargets(style) {
|
||||
style.sections = (style.sections || []).map(section =>
|
||||
Object.assign({
|
||||
urls: [],
|
||||
urlPrefixes: [],
|
||||
domains: [],
|
||||
regexps: [],
|
||||
}, section)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function reportError(...args) {
|
||||
for (const arg of args) {
|
||||
if ('message' in arg) {
|
||||
console.log(arg.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function fixBoolean(b) {
|
||||
if (typeof b != 'undefined') {
|
||||
return b != 'false';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
function getDomains(url) {
|
||||
if (url.indexOf('file:') == 0) {
|
||||
return [];
|
||||
}
|
||||
let d = /.*?:\/*([^/:]+)/.exec(url)[1];
|
||||
const domains = [d];
|
||||
while (d.indexOf('.') != -1) {
|
||||
d = d.substring(d.indexOf('.') + 1);
|
||||
domains.push(d);
|
||||
}
|
||||
return domains;
|
||||
}
|
||||
|
||||
|
||||
function getType(o) {
|
||||
if (typeof o == 'undefined' || typeof o == 'string') {
|
||||
return typeof o;
|
||||
}
|
||||
// with the persistent cachedStyles the Array reference is usually different
|
||||
// so let's check for e.g. type of 'every' which is only present on arrays
|
||||
// (in the context of our extension)
|
||||
if (o instanceof Array || typeof o.every == 'function') {
|
||||
return 'array';
|
||||
}
|
||||
console.warn('Unsupported type:', o);
|
||||
return 'undefined';
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user