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
|
FIREFOX: false
|
||||||
OPERA: false
|
OPERA: false
|
||||||
URLS: false
|
URLS: false
|
||||||
|
BG: false
|
||||||
notifyAllTabs: false
|
notifyAllTabs: false
|
||||||
refreshAllTabs: false
|
|
||||||
updateIcon: false
|
|
||||||
getActiveTab: false
|
getActiveTab: false
|
||||||
getActiveTabRealURL: false
|
getActiveTabRealURL: false
|
||||||
getTabRealURL: false
|
getTabRealURL: false
|
||||||
openURL: false
|
openURL: false
|
||||||
activateTab: false
|
activateTab: false
|
||||||
stringAsRegExp: false
|
stringAsRegExp: false
|
||||||
wildcardAsRegExp: false
|
|
||||||
ignoreChromeError: 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
|
# localization.js
|
||||||
template: false
|
template: false
|
||||||
t: false
|
t: false
|
||||||
|
@ -37,31 +45,13 @@ globals:
|
||||||
# dom.js
|
# dom.js
|
||||||
onDOMready: false
|
onDOMready: false
|
||||||
scrollElementIntoView: false
|
scrollElementIntoView: false
|
||||||
|
enforceInputRange: false
|
||||||
animateElement: false
|
animateElement: false
|
||||||
$: false
|
$: false
|
||||||
$$: false
|
$$: false
|
||||||
# storage.js
|
# prefs.js
|
||||||
prefs: false
|
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
|
setupLivePrefs: false
|
||||||
enforceInputRange: false
|
|
||||||
getCodeMirrorThemes: false
|
|
||||||
styleSectionsEqual: false
|
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
accessor-pairs: [2]
|
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';
|
'use strict';
|
||||||
|
|
||||||
chrome.webNavigation.onBeforeNavigate.addListener(data => {
|
chrome.webNavigation.onBeforeNavigate.addListener(data => {
|
||||||
|
@ -39,9 +39,9 @@ function webNavigationListener(method, data) {
|
||||||
|
|
||||||
// messaging
|
// messaging
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener(onBackgroundMessage);
|
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||||
|
|
||||||
function onBackgroundMessage(request, sender, sendResponse) {
|
function onRuntimeMessage(request, sender, sendResponse) {
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
|
|
||||||
case 'getStyles':
|
case 'getStyles':
|
||||||
|
@ -61,9 +61,7 @@ function onBackgroundMessage(request, sender, sendResponse) {
|
||||||
return KEEP_CHANNEL_OPEN;
|
return KEEP_CHANNEL_OPEN;
|
||||||
|
|
||||||
case 'invalidateCache':
|
case 'invalidateCache':
|
||||||
if (typeof invalidateCache != 'undefined') {
|
invalidateCache(false, request);
|
||||||
invalidateCache(false, request);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'healthCheck':
|
case 'healthCheck':
|
||||||
|
@ -101,8 +99,8 @@ if ('commands' in chrome) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// context menus
|
// context menus
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
const contextMenus = {
|
var contextMenus = {
|
||||||
'show-badge': {
|
'show-badge': {
|
||||||
title: 'menuShowBadge',
|
title: 'menuShowBadge',
|
||||||
click: info => prefs.set(info.menuItemId, info.checked),
|
click: info => prefs.set(info.menuItemId, info.checked),
|
||||||
|
@ -123,7 +121,7 @@ const contextMenus = {
|
||||||
// Vivaldi: Vivaldi/#
|
// Vivaldi: Vivaldi/#
|
||||||
if (/Vivaldi\/[\d.]+$/.test(navigator.userAgent)
|
if (/Vivaldi\/[\d.]+$/.test(navigator.userAgent)
|
||||||
|| /Safari\/[\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 = {
|
contextMenus.editDeleteText = {
|
||||||
title: 'editDeleteText',
|
title: 'editDeleteText',
|
||||||
contexts: ['editable'],
|
contexts: ['editable'],
|
||||||
|
@ -172,8 +170,11 @@ chrome.tabs.onAttached.addListener((tabId, data) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
var codeMirrorThemes; // eslint-disable-line no-var
|
// eslint-disable-next-line no-var
|
||||||
getCodeMirrorThemes(themes => (codeMirrorThemes = themes));
|
var codeMirrorThemes;
|
||||||
|
getCodeMirrorThemes().then(themes => {
|
||||||
|
codeMirrorThemes = themes;
|
||||||
|
});
|
||||||
|
|
||||||
// do not use prefs.get('version', null) as it might not yet be available
|
// do not use prefs.get('version', null) as it might not yet be available
|
||||||
chrome.storage.local.get('version', prefs => {
|
chrome.storage.local.get('version', prefs => {
|
||||||
|
@ -198,6 +199,9 @@ chrome.storage.local.get('version', prefs => {
|
||||||
injectContentScripts();
|
injectContentScripts();
|
||||||
|
|
||||||
function injectContentScripts() {
|
function injectContentScripts() {
|
||||||
|
// expand * as .*?
|
||||||
|
const wildcardAsRegExp = (s, flags) =>
|
||||||
|
new RegExp(s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&').replace(/\*/g, '.*?'), flags);
|
||||||
const contentScripts = chrome.runtime.getManifest().content_scripts;
|
const contentScripts = chrome.runtime.getManifest().content_scripts;
|
||||||
for (const cs of contentScripts) {
|
for (const cs of contentScripts) {
|
||||||
cs.matches = cs.matches.map(m => (
|
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';
|
'use strict';
|
||||||
|
|
||||||
const STYLISH_DUMP_FILE_EXT = '.txt';
|
const STYLISH_DUMP_FILE_EXT = '.txt';
|
||||||
|
@ -47,8 +47,15 @@ function importFromFile({fileTypeFilter, file} = {}) {
|
||||||
|
|
||||||
|
|
||||||
function importFromString(jsonString) {
|
function importFromString(jsonString) {
|
||||||
const json = runTryCatch(() => Array.from(JSON.parse(jsonString))) || [];
|
if (!BG) {
|
||||||
const oldStyles = json.length && deepCopyStyles();
|
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(
|
const oldStylesByName = json.length && new Map(
|
||||||
oldStyles.map(style => [style.name.trim(), style]));
|
oldStyles.map(style => [style.name.trim(), style]));
|
||||||
const stats = {
|
const stats = {
|
||||||
|
@ -60,18 +67,19 @@ function importFromString(jsonString) {
|
||||||
invalid: {names: [], legend: 'invalid skipped'},
|
invalid: {names: [], legend: 'invalid skipped'},
|
||||||
};
|
};
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
let lastRepaint = performance.now();
|
||||||
return new Promise(proceed);
|
return new Promise(proceed);
|
||||||
|
|
||||||
function proceed(resolve) {
|
function proceed(resolve) {
|
||||||
while (index < json.length) {
|
while (index < json.length) {
|
||||||
const item = json[index++];
|
const item = json[index++];
|
||||||
if (!item || !item.name || !item.name.trim() || typeof item != 'object'
|
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 || '')}`);
|
stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
item.name = item.name.trim();
|
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 byName = oldStylesByName.get(item.name);
|
||||||
const oldStyle = byId && byId.name.trim() == item.name || !byName ? byId : byName;
|
const oldStyle = byId && byId.name.trim() == item.name || !byName ? byId : byName;
|
||||||
if (oldStyle == byName && byName) {
|
if (oldStyle == byName && byName) {
|
||||||
|
@ -81,16 +89,22 @@ function importFromString(jsonString) {
|
||||||
const metaEqual = oldStyleKeys &&
|
const metaEqual = oldStyleKeys &&
|
||||||
oldStyleKeys.length == Object.keys(item).length &&
|
oldStyleKeys.length == Object.keys(item).length &&
|
||||||
oldStyleKeys.every(k => k == 'sections' || oldStyle[k] === item[k]);
|
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) {
|
if (metaEqual && codeEqual) {
|
||||||
stats.unchanged.names.push(oldStyle.name);
|
stats.unchanged.names.push(oldStyle.name);
|
||||||
stats.unchanged.ids.push(oldStyle.id);
|
stats.unchanged.ids.push(oldStyle.id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
saveStyle(Object.assign(item, {
|
// using saveStyle directly since json was parsed in background page context
|
||||||
|
BG.saveStyle(Object.assign(item, {
|
||||||
reason: 'import',
|
reason: 'import',
|
||||||
notify: false,
|
notify: false,
|
||||||
})).then(style => {
|
})).then(style => {
|
||||||
|
handleUpdate(style, {reason: 'import'});
|
||||||
|
if (performance.now() - lastRepaint > 1000) {
|
||||||
|
scrollElementIntoView($('#style-' + style.id));
|
||||||
|
lastRepaint = performance.now();
|
||||||
|
}
|
||||||
setTimeout(proceed, 0, resolve);
|
setTimeout(proceed, 0, resolve);
|
||||||
if (!oldStyle) {
|
if (!oldStyle) {
|
||||||
stats.added.names.push(style.name);
|
stats.added.names.push(style.name);
|
||||||
|
@ -120,17 +134,22 @@ function importFromString(jsonString) {
|
||||||
stats.metaOnly.names.length +
|
stats.metaOnly.names.length +
|
||||||
stats.codeOnly.names.length +
|
stats.codeOnly.names.length +
|
||||||
stats.added.names.length;
|
stats.added.names.length;
|
||||||
Promise.resolve(numChanged && refreshAllTabs()).then(() => {
|
Promise.resolve(numChanged && BG.refreshAllTabs()).then(() => {
|
||||||
scrollTo(0, 0);
|
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)
|
const report = Object.keys(stats)
|
||||||
.filter(kind => stats[kind].names.length)
|
.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>
|
<summary><b>${stats[kind].names.length} ${stats[kind].legend}</b></summary>
|
||||||
<small>` + stats[kind].names.map((name, i) =>
|
<small>${listNames(kind).join('')}</small>
|
||||||
`<div data-id="${stats[kind].ids[i]}">${name}</div>`).join('') + `
|
|
||||||
</small>
|
|
||||||
</details>`)
|
</details>`)
|
||||||
.join('');
|
.join('');
|
||||||
|
scrollTo(0, 0);
|
||||||
messageBox({
|
messageBox({
|
||||||
title: 'Finished importing styles',
|
title: 'Finished importing styles',
|
||||||
contents: report || 'Nothing was changed.',
|
contents: report || 'Nothing was changed.',
|
||||||
|
@ -155,7 +174,7 @@ function importFromString(jsonString) {
|
||||||
];
|
];
|
||||||
index = 0;
|
index = 0;
|
||||||
return new Promise(undoNextId)
|
return new Promise(undoNextId)
|
||||||
.then(refreshAllTabs)
|
.then(BG.refreshAllTabs)
|
||||||
.then(() => messageBox({
|
.then(() => messageBox({
|
||||||
title: 'Import has been undone',
|
title: 'Import has been undone',
|
||||||
contents: newIds.length + ' styles were reverted.',
|
contents: newIds.length + ' styles were reverted.',
|
||||||
|
@ -167,14 +186,14 @@ function importFromString(jsonString) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const id = newIds[index++];
|
const id = newIds[index++];
|
||||||
deleteStyle(id, {notify: false}).then(id => {
|
deleteStyleSafe({id, notify: false}).then(id => {
|
||||||
const oldStyle = oldStylesById.get(id);
|
const oldStyle = oldStylesById.get(id);
|
||||||
if (oldStyle) {
|
if (oldStyle) {
|
||||||
saveStyle(Object.assign(oldStyle, {
|
saveStyleSafe(Object.assign(oldStyle, {
|
||||||
reason: 'undoImport',
|
reason: 'import',
|
||||||
notify: false,
|
notify: false,
|
||||||
}))
|
})).then(() =>
|
||||||
.then(() => setTimeout(undoNextId, 0, resolve));
|
setTimeout(undoNextId, 0, resolve));
|
||||||
} else {
|
} else {
|
||||||
setTimeout(undoNextId, 0, resolve);
|
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) {
|
function limitString(s, limit = 100) {
|
||||||
return s.length <= limit ? s : s.substr(0, limit) + '...';
|
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) {
|
function $(selector, base = document) {
|
||||||
// we have ids with . like #manage.onlyEdited which look like #id.class
|
// we have ids with . like #manage.onlyEdited which look like #id.class
|
||||||
// so since getElementById is superfast we'll try it anyway
|
// so since getElementById is superfast we'll try it anyway
|
||||||
|
|
|
@ -645,8 +645,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="dom.js"></script>
|
<script src="dom.js"></script>
|
||||||
<script src="storage.js"></script>
|
|
||||||
<script src="messaging.js"></script>
|
<script src="messaging.js"></script>
|
||||||
|
<script src="prefs.js"></script>
|
||||||
<script src="localization.js"></script>
|
<script src="localization.js"></script>
|
||||||
<script src="apply.js"></script>
|
<script src="apply.js"></script>
|
||||||
<script src="edit.js"></script>
|
<script src="edit.js"></script>
|
||||||
|
|
11
edit.js
11
edit.js
|
@ -252,7 +252,8 @@ function initCodeMirror() {
|
||||||
} else {
|
} else {
|
||||||
// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
|
// 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]);
|
themeControl.innerHTML = optionsHtmlFromArray([theme == "default" ? t("defaultTheme") : theme]);
|
||||||
getCodeMirrorThemes(function(themes) {
|
BG.getCodeMirrorThemes().then(themes => {
|
||||||
|
BG.codeMirrorThemes = themes;
|
||||||
themeControl.innerHTML = optionsHtmlFromArray(themes);
|
themeControl.innerHTML = optionsHtmlFromArray(themes);
|
||||||
themeControl.selectedIndex = Math.max(0, themes.indexOf(theme));
|
themeControl.selectedIndex = Math.max(0, themes.indexOf(theme));
|
||||||
});
|
});
|
||||||
|
@ -1333,7 +1334,7 @@ function save() {
|
||||||
}
|
}
|
||||||
var name = document.getElementById("name").value;
|
var name = document.getElementById("name").value;
|
||||||
var enabled = document.getElementById("enabled").checked;
|
var enabled = document.getElementById("enabled").checked;
|
||||||
saveStyle({
|
saveStyleSafe({
|
||||||
id: styleId,
|
id: styleId,
|
||||||
name: name,
|
name: name,
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
|
@ -1815,7 +1816,9 @@ function getParams() {
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
|
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||||
|
|
||||||
|
function onRuntimeMessage(request) {
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
case "styleUpdated":
|
case "styleUpdated":
|
||||||
if (styleId && styleId == request.style.id && request.reason != 'editSave') {
|
if (styleId && styleId == request.style.id && request.reason != 'editSave') {
|
||||||
|
@ -1838,7 +1841,7 @@ chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
|
||||||
document.execCommand('delete');
|
document.execCommand('delete');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
function getComputedHeight(el) {
|
function getComputedHeight(el) {
|
||||||
var compStyle = getComputedStyle(el);
|
var compStyle = getComputedStyle(el);
|
||||||
|
|
|
@ -116,9 +116,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="dom.js"></script>
|
<script src="dom.js"></script>
|
||||||
<script src="health.js"></script>
|
|
||||||
<script src="storage.js"></script>
|
|
||||||
<script src="messaging.js"></script>
|
<script src="messaging.js"></script>
|
||||||
|
<script src="prefs.js"></script>
|
||||||
<script src="apply.js"></script>
|
<script src="apply.js"></script>
|
||||||
<script src="localization.js"></script>
|
<script src="localization.js"></script>
|
||||||
<script src="manage.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) {
|
switch (msg.method) {
|
||||||
case 'styleUpdated':
|
case 'styleUpdated':
|
||||||
case 'styleAdded':
|
case 'styleAdded':
|
||||||
|
@ -37,7 +39,7 @@ chrome.runtime.onMessage.addListener(msg => {
|
||||||
handleDelete(msg.id);
|
handleDelete(msg.id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
|
||||||
function initGlobalEvents() {
|
function initGlobalEvents() {
|
||||||
|
@ -151,8 +153,6 @@ function createStyleElement({style, name}) {
|
||||||
(style.enabled ? 'enabled' : 'disabled') +
|
(style.enabled ? 'enabled' : 'disabled') +
|
||||||
(style.updateUrl ? ' updatable' : ''),
|
(style.updateUrl ? ' updatable' : ''),
|
||||||
id: 'style-' + style.id,
|
id: 'style-' + style.id,
|
||||||
styleId: style.id,
|
|
||||||
styleNameLowerCase: name || style.name.toLocaleLowerCase(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
parts.nameLink.textContent = style.name;
|
parts.nameLink.textContent = style.name;
|
||||||
|
@ -216,6 +216,8 @@ function createStyleElement({style, name}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const newEntry = parts.entry.cloneNode(true);
|
const newEntry = parts.entry.cloneNode(true);
|
||||||
|
newEntry.styleId = style.id;
|
||||||
|
newEntry.styleNameLowerCase = name || style.name.toLocaleLowerCase();
|
||||||
const newTargets = $('.targets', newEntry);
|
const newTargets = $('.targets', newEntry);
|
||||||
if (numTargets) {
|
if (numTargets) {
|
||||||
newTargets.parentElement.replaceChild(targets, newTargets);
|
newTargets.parentElement.replaceChild(targets, newTargets);
|
||||||
|
@ -282,7 +284,10 @@ Object.assign(handleEvent, {
|
||||||
},
|
},
|
||||||
|
|
||||||
toggle(event, entry) {
|
toggle(event, entry) {
|
||||||
enableStyle(entry.styleId, this.matches('.enable') || this.checked);
|
saveStyleSafe({
|
||||||
|
id: entry.styleId,
|
||||||
|
enabled: this.matches('.enable') || this.checked,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
check(event, entry) {
|
check(event, entry) {
|
||||||
|
@ -291,7 +296,7 @@ Object.assign(handleEvent, {
|
||||||
|
|
||||||
update(event, entry) {
|
update(event, entry) {
|
||||||
// update everything but name
|
// update everything but name
|
||||||
saveStyle(Object.assign(entry.updatedCode, {
|
saveStyleSafe(Object.assign(entry.updatedCode, {
|
||||||
id: entry.styleId,
|
id: entry.styleId,
|
||||||
name: null,
|
name: null,
|
||||||
reason: 'update',
|
reason: 'update',
|
||||||
|
@ -300,7 +305,7 @@ Object.assign(handleEvent, {
|
||||||
|
|
||||||
delete(event, entry) {
|
delete(event, entry) {
|
||||||
const id = entry.styleId;
|
const id = entry.styleId;
|
||||||
const {name} = cachedStyles.byId.get(id) || {};
|
const {name} = BG.cachedStyles.byId.get(id) || {};
|
||||||
animateElement(entry, {className: 'highlight'});
|
animateElement(entry, {className: 'highlight'});
|
||||||
messageBox({
|
messageBox({
|
||||||
title: t('deleteStyleConfirm'),
|
title: t('deleteStyleConfirm'),
|
||||||
|
@ -310,7 +315,7 @@ Object.assign(handleEvent, {
|
||||||
})
|
})
|
||||||
.then(({button, enter, esc}) => {
|
.then(({button, enter, esc}) => {
|
||||||
if (button == 0 || enter) {
|
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 element = createStyleElement({style});
|
||||||
const oldElement = $('#style-' + style.id, installed);
|
const oldElement = $('#style-' + style.id, installed);
|
||||||
if (oldElement) {
|
if (oldElement) {
|
||||||
|
@ -354,8 +359,8 @@ function handleUpdate(style, {reason, quiet} = {}) {
|
||||||
installed.insertBefore(element, findNextElement(style));
|
installed.insertBefore(element, findNextElement(style));
|
||||||
if (reason != 'import') {
|
if (reason != 'import') {
|
||||||
animateElement(element, {className: 'highlight'});
|
animateElement(element, {className: 'highlight'});
|
||||||
|
scrollElementIntoView(element);
|
||||||
}
|
}
|
||||||
scrollElementIntoView(element);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -465,7 +470,7 @@ function checkUpdate(element) {
|
||||||
|
|
||||||
class Updater {
|
class Updater {
|
||||||
constructor(element) {
|
constructor(element) {
|
||||||
const style = cachedStyles.byId.get(element.styleId);
|
const style = BG.cachedStyles.byId.get(element.styleId);
|
||||||
Object.assign(this, {
|
Object.assign(this, {
|
||||||
element,
|
element,
|
||||||
id: style.id,
|
id: style.id,
|
||||||
|
@ -504,7 +509,7 @@ class Updater {
|
||||||
|
|
||||||
handleJson(forceUpdate, json) {
|
handleJson(forceUpdate, json) {
|
||||||
return getStylesSafe({id: this.id}).then(([style]) => {
|
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});
|
this.display({json: needsUpdate && json});
|
||||||
return needsUpdate;
|
return needsUpdate;
|
||||||
});
|
});
|
||||||
|
@ -598,7 +603,7 @@ function searchStyles({immediately, container}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const element of (container || installed).children) {
|
for (const element of (container || installed).children) {
|
||||||
const style = cachedStyles.byId.get(element.styleId) || {};
|
const style = BG.cachedStyles.byId.get(element.styleId) || {};
|
||||||
if (style) {
|
if (style) {
|
||||||
const isMatching = !query
|
const isMatching = !query
|
||||||
|| isMatchingText(style.name)
|
|| isMatchingText(style.name)
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
"*://*/*"
|
"*://*/*"
|
||||||
],
|
],
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": ["messaging.js", "storage.js", "background.js", "update.js"]
|
"scripts": ["messaging.js", "storage.js", "prefs.js", "background.js", "update.js"]
|
||||||
},
|
},
|
||||||
"commands": {
|
"commands": {
|
||||||
"openManage": {
|
"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';
|
'use strict';
|
||||||
|
|
||||||
// keep message channel open for sendResponse in chrome.runtime.onMessage listener
|
// 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 OPERA = /OPR/.test(navigator.userAgent);
|
||||||
const URLS = {
|
const URLS = {
|
||||||
ownOrigin: chrome.runtime.getURL(''),
|
ownOrigin: chrome.runtime.getURL(''),
|
||||||
optionsUI: new Set([
|
optionsUI: [
|
||||||
chrome.runtime.getURL('options/index.html'),
|
chrome.runtime.getURL('options/index.html'),
|
||||||
'chrome://extensions/?options=' + chrome.runtime.id,
|
'chrome://extensions/?options=' + chrome.runtime.id,
|
||||||
]),
|
],
|
||||||
configureCommands: OPERA ? 'opera://settings/configureCommands'
|
configureCommands:
|
||||||
: 'chrome://extensions/configureCommands',
|
OPERA ? 'opera://settings/configureCommands'
|
||||||
|
: 'chrome://extensions/configureCommands',
|
||||||
};
|
};
|
||||||
const RX_SUPPORTED_URLS = new RegExp(`^(file|https?|ftps?):|^${URLS.ownOrigin}`);
|
const RX_SUPPORTED_URLS = new RegExp(`^(file|https?|ftps?):|^${URLS.ownOrigin}`);
|
||||||
|
|
||||||
document.documentElement.classList.toggle('firefox', FIREFOX);
|
let BG = chrome.extension.getBackgroundPage();
|
||||||
document.documentElement.classList.toggle('opera', OPERA);
|
|
||||||
|
|
||||||
|
if (!BG || BG != window) {
|
||||||
|
document.documentElement.classList.toggle('firefox', FIREFOX);
|
||||||
|
document.documentElement.classList.toggle('opera', OPERA);
|
||||||
|
}
|
||||||
|
|
||||||
function notifyAllTabs(request) {
|
function notifyAllTabs(msg) {
|
||||||
// list all tabs including chrome-extension:// which can be ours
|
const originalMessage = msg;
|
||||||
if (request.codeIsUpdated === false && request.style) {
|
if (msg.codeIsUpdated === false && msg.style) {
|
||||||
request = Object.assign({}, request, {
|
msg = Object.assign({}, msg, {
|
||||||
style: getStyleWithNoCode(request.style)
|
style: getStyleWithNoCode(msg.style)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const affectsAll = !request.affects || request.affects.all;
|
const affectsAll = !msg.affects || msg.affects.all;
|
||||||
const affectsOwnOrigin = !affectsAll && (request.affects.editor || request.affects.manager);
|
const affectsOwnOrigin = !affectsAll && (msg.affects.editor || msg.affects.manager);
|
||||||
const affectsTabs = affectsAll || affectsOwnOrigin;
|
const affectsTabs = affectsAll || affectsOwnOrigin;
|
||||||
const affectsIcon = affectsAll || request.affects.icon;
|
const affectsIcon = affectsAll || msg.affects.icon;
|
||||||
const affectsPopup = affectsAll || request.affects.popup;
|
const affectsPopup = affectsAll || msg.affects.popup;
|
||||||
if (affectsTabs || affectsIcon) {
|
if (affectsTabs || affectsIcon) {
|
||||||
|
// list all tabs including chrome-extension:// which can be ours
|
||||||
chrome.tabs.query(affectsOwnOrigin ? {url: URLS.ownOrigin + '*'} : {}, tabs => {
|
chrome.tabs.query(affectsOwnOrigin ? {url: URLS.ownOrigin + '*'} : {}, tabs => {
|
||||||
for (const tab of tabs) {
|
for (const tab of tabs) {
|
||||||
if (affectsTabs || URLS.optionsUI.has(tab.url)) {
|
if (affectsTabs || URLS.optionsUI.includes(tab.url)) {
|
||||||
chrome.tabs.sendMessage(tab.id, request);
|
chrome.tabs.sendMessage(tab.id, msg);
|
||||||
}
|
}
|
||||||
if (affectsIcon) {
|
if (affectsIcon && BG) {
|
||||||
updateIcon(tab);
|
BG.updateIcon(tab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// notify self: the message no longer is sent to the origin in new Chrome
|
// notify self: the message no longer is sent to the origin in new Chrome
|
||||||
if (window.applyOnMessage) {
|
if (typeof onRuntimeMessage != 'undefined') {
|
||||||
applyOnMessage(request);
|
onRuntimeMessage(originalMessage);
|
||||||
} else if (window.onBackgroundMessage) {
|
}
|
||||||
onBackgroundMessage(request);
|
// notify apply.js on own pages
|
||||||
|
if (typeof applyOnMessage != 'undefined') {
|
||||||
|
applyOnMessage(originalMessage);
|
||||||
}
|
}
|
||||||
// notify background page and all open popups
|
// notify background page and all open popups
|
||||||
if (affectsPopup || request.prefs) {
|
if (affectsPopup || msg.prefs) {
|
||||||
chrome.runtime.sendMessage(request);
|
chrome.runtime.sendMessage(msg);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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() {
|
function ignoreChromeError() {
|
||||||
chrome.runtime.lastError; // eslint-disable-line no-unused-expressions
|
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">
|
<link rel="stylesheet" href="index.css">
|
||||||
<script src="/dom.js"></script>
|
<script src="/dom.js"></script>
|
||||||
<script src="/localization.js"></script>
|
<script src="/localization.js"></script>
|
||||||
<script src="/apply.js"></script>
|
|
||||||
<script src="/storage.js"></script>
|
|
||||||
<script src="/messaging.js"></script>
|
<script src="/messaging.js"></script>
|
||||||
|
<script src="/prefs.js"></script>
|
||||||
|
<script src="/apply.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
/* global update */
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
setupLivePrefs([
|
setupLivePrefs([
|
||||||
'show-badge',
|
'show-badge',
|
||||||
'popup.stylesFirst',
|
'popup.stylesFirst',
|
||||||
|
@ -33,7 +31,7 @@ document.onclick = e => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function check() {
|
function check() {
|
||||||
chrome.extension.getBackgroundPage().update.perform((cmd, value) => {
|
BG.update.perform((cmd, value) => {
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
case 'count':
|
case 'count':
|
||||||
total = value;
|
total = value;
|
||||||
|
|
|
@ -57,9 +57,8 @@
|
||||||
|
|
||||||
<script src="dom.js"></script>
|
<script src="dom.js"></script>
|
||||||
<script src="localization.js"></script>
|
<script src="localization.js"></script>
|
||||||
<script src="health.js"></script>
|
|
||||||
<script src="storage.js"></script>
|
|
||||||
<script src="messaging.js"></script>
|
<script src="messaging.js"></script>
|
||||||
|
<script src="prefs.js"></script>
|
||||||
<script src="apply.js"></script>
|
<script src="apply.js"></script>
|
||||||
<script src="popup.js"></script>
|
<script src="popup.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
28
popup.js
28
popup.js
|
@ -1,4 +1,3 @@
|
||||||
/* global SLOPPY_REGEXP_PREFIX, compileStyleRegExps */
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
let installed;
|
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) {
|
switch (msg.method) {
|
||||||
case 'styleAdded':
|
case 'styleAdded':
|
||||||
case 'styleUpdated':
|
case 'styleUpdated':
|
||||||
|
@ -38,7 +38,7 @@ chrome.runtime.onMessage.addListener(msg => {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
|
||||||
function setPopupWidth(width = prefs.get('popupWidth')) {
|
function setPopupWidth(width = prefs.get('popupWidth')) {
|
||||||
|
@ -117,7 +117,7 @@ function initPopup(url) {
|
||||||
matchTargets.appendChild(urlLink);
|
matchTargets.appendChild(urlLink);
|
||||||
|
|
||||||
// For domain
|
// For domain
|
||||||
const domains = getDomains(url);
|
const domains = BG.getDomains(url);
|
||||||
for (const domain of domains) {
|
for (const domain of domains) {
|
||||||
// Don't include TLD
|
// Don't include TLD
|
||||||
if (domains.length > 1 && !domain.includes('.')) {
|
if (domains.length > 1 && !domain.includes('.')) {
|
||||||
|
@ -252,7 +252,7 @@ Object.assign(handleEvent, {
|
||||||
},
|
},
|
||||||
|
|
||||||
toggle(event) {
|
toggle(event) {
|
||||||
saveStyle({
|
saveStyleSafe({
|
||||||
id: handleEvent.getClickedStyleId(event),
|
id: handleEvent.getClickedStyleId(event),
|
||||||
enabled: this.type == 'checkbox' ? this.checked : this.matches('.enable'),
|
enabled: this.type == 'checkbox' ? this.checked : this.matches('.enable'),
|
||||||
});
|
});
|
||||||
|
@ -263,7 +263,7 @@ Object.assign(handleEvent, {
|
||||||
const box = $('#confirm');
|
const box = $('#confirm');
|
||||||
box.dataset.display = true;
|
box.dataset.display = true;
|
||||||
box.style.cssText = '';
|
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="ok"]', box).onclick = () => confirm(true);
|
||||||
$('[data-cmd="cancel"]', box).onclick = () => confirm(false);
|
$('[data-cmd="cancel"]', box).onclick = () => confirm(false);
|
||||||
window.onkeydown = event => {
|
window.onkeydown = event => {
|
||||||
|
@ -278,7 +278,7 @@ Object.assign(handleEvent, {
|
||||||
animateElement(box, {className: 'lights-on'})
|
animateElement(box, {className: 'lights-on'})
|
||||||
.then(() => (box.dataset.display = false));
|
.then(() => (box.dataset.display = false));
|
||||||
if (ok) {
|
if (ok) {
|
||||||
deleteStyle(id).then(() => {
|
deleteStyleSafe({id}).then(() => {
|
||||||
// update view with 'No styles installed for this site' message
|
// update view with 'No styles installed for this site' message
|
||||||
if (!installed.children.length) {
|
if (!installed.children.length) {
|
||||||
showStyles([]);
|
showStyles([]);
|
||||||
|
@ -297,7 +297,7 @@ Object.assign(handleEvent, {
|
||||||
entry.appendChild(info);
|
entry.appendChild(info);
|
||||||
},
|
},
|
||||||
|
|
||||||
closeExplanation(event) {
|
closeExplanation() {
|
||||||
$('#regexp-explanation').remove();
|
$('#regexp-explanation').remove();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -347,7 +347,7 @@ function handleUpdate(style) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Add an entry when a new style for the current url is installed
|
// 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';
|
$('#unavailable').style.display = 'none';
|
||||||
createStyleElement({style});
|
createStyleElement({style});
|
||||||
}
|
}
|
||||||
|
@ -368,13 +368,15 @@ function handleDelete(id) {
|
||||||
*/
|
*/
|
||||||
function detectSloppyRegexps({entry, style}) {
|
function detectSloppyRegexps({entry, style}) {
|
||||||
const {
|
const {
|
||||||
appliedSections = getApplicableSections({style, matchUrl: tabURL}),
|
appliedSections =
|
||||||
wannabeSections = getApplicableSections({style, matchUrl: tabURL, strictRegexp: false}),
|
BG.getApplicableSections({style, matchUrl: tabURL}),
|
||||||
|
wannabeSections =
|
||||||
|
BG.getApplicableSections({style, matchUrl: tabURL, strictRegexp: false}),
|
||||||
} = style;
|
} = style;
|
||||||
|
|
||||||
compileStyleRegExps({style, compileAll: true});
|
BG.compileStyleRegExps({style, compileAll: true});
|
||||||
entry.hasInvalidRegexps = wannabeSections.some(section =>
|
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;
|
entry.sectionsSkipped = wannabeSections.length - appliedSections.length;
|
||||||
|
|
||||||
if (!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 cachedStyles: true */
|
||||||
/* global handleUpdate, handleDelete */
|
|
||||||
'use strict';
|
'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) {
|
function getDatabase(ready, error) {
|
||||||
const dbOpenRequest = window.indexedDB.open('stylish', 2);
|
const dbOpenRequest = window.indexedDB.open('stylish', 2);
|
||||||
dbOpenRequest.onsuccess = event => {
|
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) {
|
function getStyles(options, callback) {
|
||||||
if (cachedStyles.list) {
|
if (cachedStyles.list) {
|
||||||
callback(filterStyles(options));
|
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({
|
function filterStyles({
|
||||||
enabled,
|
enabled,
|
||||||
url = null,
|
url = null,
|
||||||
|
@ -174,10 +93,10 @@ function filterStyles({
|
||||||
id = id === null ? null : Number(id);
|
id = id === null ? null : Number(id);
|
||||||
|
|
||||||
if (enabled === null
|
if (enabled === null
|
||||||
&& url === null
|
&& url === null
|
||||||
&& id === null
|
&& id === null
|
||||||
&& matchUrl === null
|
&& matchUrl === null
|
||||||
&& asHash != true) {
|
&& 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
|
//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;
|
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) {
|
function saveStyle(style) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
getDatabase(db => {
|
getDatabase(db => {
|
||||||
|
@ -312,9 +201,6 @@ function saveStyle(style) {
|
||||||
style, codeIsUpdated, reason,
|
style, codeIsUpdated, reason,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (typeof handleUpdate != 'undefined') {
|
|
||||||
handleUpdate(style, {reason});
|
|
||||||
}
|
|
||||||
resolve(style);
|
resolve(style);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -340,9 +226,6 @@ function saveStyle(style) {
|
||||||
if (notify) {
|
if (notify) {
|
||||||
notifyAllTabs({method: 'styleAdded', style, reason});
|
notifyAllTabs({method: 'styleAdded', style, reason});
|
||||||
}
|
}
|
||||||
if (typeof handleUpdate != 'undefined') {
|
|
||||||
handleUpdate(style, {reason});
|
|
||||||
}
|
|
||||||
resolve(style);
|
resolve(style);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -350,24 +233,7 @@ function saveStyle(style) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function addMissingStyleTargets(style) {
|
function deleteStyle({id, notify = true}) {
|
||||||
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} = {}) {
|
|
||||||
return new Promise(resolve =>
|
return new Promise(resolve =>
|
||||||
getDatabase(db => {
|
getDatabase(db => {
|
||||||
const tx = db.transaction(['styles'], 'readwrite');
|
const tx = db.transaction(['styles'], 'readwrite');
|
||||||
|
@ -377,61 +243,12 @@ function deleteStyle(id, {notify = true} = {}) {
|
||||||
if (notify) {
|
if (notify) {
|
||||||
notifyAllTabs({method: 'styleDeleted', id});
|
notifyAllTabs({method: 'styleDeleted', id});
|
||||||
}
|
}
|
||||||
if (typeof handleDelete != 'undefined') {
|
|
||||||
handleDelete(id);
|
|
||||||
}
|
|
||||||
resolve(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}) {
|
function getApplicableSections({style, matchUrl, strictRegexp = true, stopOnFirst}) {
|
||||||
//let t0 = 0;
|
//let t0 = 0;
|
||||||
const sections = [];
|
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) {
|
function styleSectionsEqual(styleA, styleB) {
|
||||||
if (!styleA.sections || !styleB.sections) {
|
if (!styleA.sections || !styleB.sections) {
|
||||||
return undefined;
|
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';
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* globals getStyles */
|
/* eslint brace-style: 1, arrow-parens: 1, space-before-function-paren: 1, arrow-body-style: 1 */
|
||||||
|
/* globals getStyles, saveStyle */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// TODO: refactor to make usable in manage::Updater
|
// TODO: refactor to make usable in manage::Updater
|
||||||
|
|
Loading…
Reference in New Issue
Block a user