commit
e48e1ab874
|
@ -1,3 +1,2 @@
|
|||
beautify/
|
||||
codemirror/
|
||||
csslint/
|
||||
vendor/
|
||||
vendor-overwrites/
|
||||
|
|
|
@ -82,7 +82,7 @@ rules:
|
|||
dot-location: [2, property]
|
||||
dot-notation: [0]
|
||||
eol-last: [2]
|
||||
eqeqeq: [0]
|
||||
eqeqeq: [1, always]
|
||||
func-call-spacing: [2, never]
|
||||
func-name-matching: [0]
|
||||
func-names: [0]
|
||||
|
@ -132,7 +132,7 @@ rules:
|
|||
no-duplicate-imports: [2]
|
||||
no-else-return: [0]
|
||||
no-empty-character-class: [2]
|
||||
no-empty-function: [0]
|
||||
no-empty-function: [1]
|
||||
no-empty-pattern: [2]
|
||||
no-empty: [2, {allowEmptyCatch: true}]
|
||||
no-eq-null: [2]
|
||||
|
@ -166,7 +166,7 @@ rules:
|
|||
no-mixed-operators: [0]
|
||||
no-mixed-requires: [2, true]
|
||||
no-mixed-spaces-and-tabs: [2]
|
||||
no-multi-spaces: [0]
|
||||
no-multi-spaces: [2, {ignoreEOLComments: true}]
|
||||
no-multi-str: [2]
|
||||
no-multiple-empty-lines: [2, {max: 2, maxEOF: 0, maxBOF: 0}]
|
||||
no-native-reassign: [2]
|
||||
|
@ -249,7 +249,7 @@ rules:
|
|||
sort-imports: [0]
|
||||
sort-keys: [0]
|
||||
space-before-blocks: [2, always]
|
||||
space-before-function-paren: [2, never]
|
||||
space-before-function-paren: [2, {anonymous: always, asyncArrow: always, named: never}]
|
||||
space-in-parens: [2, never]
|
||||
space-infix-ops: [2]
|
||||
space-unary-ops: [2]
|
||||
|
|
33
.github/CONTRIBUTING.md
vendored
Normal file
33
.github/CONTRIBUTING.md
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
# Contributing to Stylus
|
||||
|
||||
1. [Getting Involved](#getting-involved)
|
||||
2. [How to Report Issues](#how-to-report-issues)
|
||||
3. [Adding Tranlations](#adding-translations)
|
||||
4. [Core Style Guide](#core-style-guide)
|
||||
5. [Getting Started](#getting-started)
|
||||
|
||||
## Getting Involved
|
||||
|
||||
There are a number of ways to get involved with the development of Stylus. Even if you've never contributed to an Open Source project before, we're always looking for help by identifying issues and suggesting improvements.
|
||||
|
||||
## How to Report issues
|
||||
|
||||
When an issue is opened, a template is provided. Please answer these questions as thoroughly as possible. If we were psychic, we'd be hanging out in casinos playing poker until they kicked us out. We can't read your mind! Please provide step-by-step direction on how to reproduce the issue as well as the browser, operating system and versions of each.
|
||||
|
||||
When adding a feature request, please include
|
||||
|
||||
## Adding Translations
|
||||
|
||||
You can help us translate the extension on [Transifex](https://www.transifex.com/github-7/Stylus). When `messages.json` file is ready to be merged, please open a new bug report in [stylus/issues](https://github.com/openstyles/stylus/issues).
|
||||
|
||||
## Core Style Guide
|
||||
|
||||
* Use the provided `.editorconfig` file with your code editor. Don't know what that is? Then check out http://editorconfig.org/.
|
||||
|
||||
## Getting Started
|
||||
|
||||
* First open an issue to discuss your changes.
|
||||
* Then download, fork or clone this repository.
|
||||
<!-- * Use [node.js](http://nodejs.org/) to run `npm install`. -->
|
||||
* Make any changes within a branch of this repository (not the `master` branch).
|
||||
* Submit a pull request and include a reference to the initial issue with the discussion.
|
28
.github/ISSUE_TEMPLATE.md
vendored
Normal file
28
.github/ISSUE_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
<!--
|
||||
Thank you for reporting an issue. Please make sure that your style is up to
|
||||
date and you checked the recent commits that your issue wasn't recently
|
||||
addressed. To update:
|
||||
|
||||
Make sure to first update DIRECTLY from https://userstyles.org/styles/37035/
|
||||
(see https://github.com/JasonBarnabe/stylish-chrome/issues/179 to know why) or,
|
||||
if using the GitHub-Dark script, use the "Force Update Style" button, then
|
||||
force refresh the web page (Windows: Ctrl+F5; Mac/Apple: Apple+R or Command+R;
|
||||
Linux: F5).
|
||||
|
||||
If the issue persists, please help us identifying the cause by providing these
|
||||
details:
|
||||
-->
|
||||
|
||||
* **Browser**:
|
||||
* **Operating System**:
|
||||
* **Screenshot**:
|
||||
|
||||
* **HTML of the section where the issue occurs**:
|
||||
|
||||
<!-- You can get the HTML by right click on the element, look for the
|
||||
highlighted node in the DevTools, right click it and select
|
||||
Copy -> Outer HTML -->
|
||||
|
||||
````html
|
||||
|
||||
````
|
|
@ -16,13 +16,15 @@ See the [help docs](http://userstyles.org/help/stylish_chrome) or [ask in userst
|
|||
|
||||
## Contributing
|
||||
|
||||
The source is hosted on [GitHub](https://github.com/schomery/stylus) and pull requests are welcome.
|
||||
The source is hosted on [GitHub](https://github.com/openstyles/stylus) and pull requests are welcome.
|
||||
|
||||
You can help us translate the extension on [Transifex](https://www.transifex.com/github-7/Stylus). When `messages.json` file is ready to be merged, please open a new bug report in [stylus/issues](https://github.com/schomery/stylus/issues).
|
||||
You can help us translate the extension on [Transifex](https://www.transifex.com/github-7/Stylus). When `messages.json` file is ready to be merged, please open a new bug report in [stylus/issues](https://github.com/openstyles/stylus/issues).
|
||||
|
||||
See our [contributing](./.github/CONTRIBUTING.md) page for more details.
|
||||
|
||||
## License
|
||||
|
||||
For copyright status of the "codemirror" directory, see codemirror/LICENSE. Everything else is:
|
||||
For copyright status of the "codemirror" directory, see [codemirror/LICENSE](https://github.com/openstyles/stylus/blob/master/src/vendor/codemirror/LICENSE). Everything else is:
|
||||
|
||||
Copyright (C) 2005-2014 Jason Barnabe <jason.barnabe@gmail.com>
|
||||
|
||||
|
|
|
@ -26,14 +26,14 @@ chrome.webNavigation.onHistoryStateUpdated.addListener(data =>
|
|||
chrome.webNavigation.onReferenceFragmentUpdated.addListener(data =>
|
||||
webNavigationListener('styleReplaceAll', data));
|
||||
|
||||
chrome.tabs.onAttached.addListener((tabId, data) => {
|
||||
chrome.tabs.onAttached.addListener(tabId => {
|
||||
// When an edit page gets attached or detached, remember its state
|
||||
// so we can do the same to the next one to open.
|
||||
chrome.tabs.get(tabId, tab => {
|
||||
if (tab.url.startsWith(URLS.ownOrigin + 'edit.html')) {
|
||||
chrome.windows.get(tab.windowId, {populate: true}, win => {
|
||||
// If there's only one tab in this window, it's been dragged to new window
|
||||
prefs.set('openEditInWindow', win.tabs.length == 1);
|
||||
prefs.set('openEditInWindow', win.tabs.length === 1);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -59,13 +59,13 @@ updateIcon({id: undefined}, {});
|
|||
const manifest = chrome.runtime.getManifest();
|
||||
// Open FAQs page once after installation to guide new users.
|
||||
// Do not display it in development mode.
|
||||
if (reason == 'install' && manifest.update_url) {
|
||||
if (reason === 'install' && manifest.update_url) {
|
||||
setTimeout(openURL, 100, {
|
||||
url: 'http://add0n.com/stylus.html'
|
||||
});
|
||||
}
|
||||
// reset L10N cache on update
|
||||
if (reason == 'update') {
|
||||
if (reason === 'update') {
|
||||
localStorage.L10N = JSON.stringify({
|
||||
browserUIlanguage: chrome.i18n.getUILanguage(),
|
||||
});
|
||||
|
@ -98,7 +98,7 @@ updateIcon({id: undefined}, {});
|
|||
// browser commands
|
||||
browserCommands = {
|
||||
openManage() {
|
||||
openURL({url: '/manage.html'});
|
||||
openURL({url: 'manage.html'});
|
||||
},
|
||||
styleDisableAll(info) {
|
||||
prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll'));
|
||||
|
@ -138,7 +138,7 @@ contextMenus = Object.assign({
|
|||
const item = Object.assign({id}, contextMenus[id]);
|
||||
const prefValue = prefs.readOnlyValues[id];
|
||||
item.title = chrome.i18n.getMessage(item.title);
|
||||
if (!item.type && typeof prefValue == 'boolean') {
|
||||
if (!item.type && typeof prefValue === 'boolean') {
|
||||
item.type = 'checkbox';
|
||||
item.checked = prefValue;
|
||||
}
|
||||
|
@ -151,7 +151,7 @@ contextMenus = Object.assign({
|
|||
};
|
||||
createContextMenus();
|
||||
prefs.subscribe((id, checked) => {
|
||||
if (id == 'editor.contextDelete') {
|
||||
if (id === 'editor.contextDelete') {
|
||||
if (checked) {
|
||||
createContextMenus([id]);
|
||||
} else {
|
||||
|
@ -160,7 +160,7 @@ contextMenus = Object.assign({
|
|||
} else {
|
||||
chrome.contextMenus.update(id, {checked}, ignoreChromeError);
|
||||
}
|
||||
}, Object.keys(contextMenus).filter(key => typeof prefs.readOnlyValues[key] == 'boolean'));
|
||||
}, Object.keys(contextMenus).filter(key => typeof prefs.readOnlyValues[key] === 'boolean'));
|
||||
}
|
||||
|
||||
// *************************************************************************
|
||||
|
@ -176,7 +176,7 @@ contextMenus = Object.assign({
|
|||
.replace(/\*/g, '.*?'), flags);
|
||||
for (const cs of contentScripts) {
|
||||
cs.matches = cs.matches.map(m => (
|
||||
m == ALL_URLS ? m : wildcardAsRegExp(m)
|
||||
m === ALL_URLS ? m : wildcardAsRegExp(m)
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -191,8 +191,8 @@ contextMenus = Object.assign({
|
|||
|
||||
const pingCS = (cs, {id, url}) => {
|
||||
cs.matches.some(match => {
|
||||
if ((match == ALL_URLS || url.match(match))
|
||||
&& (!url.startsWith('chrome') || url == NTP)) {
|
||||
if ((match === ALL_URLS || url.match(match))
|
||||
&& (!url.startsWith('chrome') || url === NTP)) {
|
||||
chrome.tabs.sendMessage(id, PING, pong => {
|
||||
if (!pong) {
|
||||
injectCS(cs, id);
|
||||
|
@ -229,7 +229,7 @@ function webNavigationListener(method, {url, tabId, frameId}) {
|
|||
});
|
||||
}
|
||||
// main page frame id is 0
|
||||
if (frameId == 0) {
|
||||
if (frameId === 0) {
|
||||
updateIcon({id: tabId, url}, styles);
|
||||
}
|
||||
});
|
||||
|
@ -258,7 +258,7 @@ function updateIcon(tab, styles) {
|
|||
}
|
||||
}
|
||||
const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll');
|
||||
const postfix = disableAll ? 'x' : numStyles == 0 ? 'w' : '';
|
||||
const postfix = disableAll ? 'x' : numStyles === 0 ? 'w' : '';
|
||||
const color = prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal');
|
||||
const text = prefs.get('show-badge') && numStyles ? String(numStyles) : '';
|
||||
const iconset = ['', 'light/'][prefs.get('iconset')] || '';
|
|
@ -63,7 +63,7 @@ function dbExec(method, data) {
|
|||
reject(event);
|
||||
},
|
||||
onupgradeneeded(event) {
|
||||
if (event.oldVersion == 0) {
|
||||
if (event.oldVersion === 0) {
|
||||
event.target.result.createObjectStore('styles', {
|
||||
keyPath: 'id',
|
||||
autoIncrement: true,
|
||||
|
@ -111,15 +111,17 @@ function filterStyles({
|
|||
asHash = null,
|
||||
strictRegexp = true, // used by the popup to detect bad regexps
|
||||
} = {}) {
|
||||
enabled = enabled === null || typeof enabled == 'boolean' ? enabled :
|
||||
typeof enabled == 'string' ? enabled == 'true' : null;
|
||||
enabled = enabled === null || typeof enabled === 'boolean' ? enabled :
|
||||
typeof enabled === 'string' ? enabled === 'true' : null;
|
||||
id = id === null ? null : Number(id);
|
||||
|
||||
if (enabled === null
|
||||
&& url === null
|
||||
&& id === null
|
||||
&& matchUrl === null
|
||||
&& asHash != true) {
|
||||
if (
|
||||
enabled === null &&
|
||||
url === null &&
|
||||
id === null &&
|
||||
matchUrl === null &&
|
||||
asHash !== true
|
||||
) {
|
||||
return cachedStyles.list;
|
||||
}
|
||||
const blankHash = asHash && {
|
||||
|
@ -189,10 +191,11 @@ function filterStylesInternal({
|
|||
|
||||
const needSections = asHash || matchUrl !== null;
|
||||
|
||||
for (let i = 0, style; (style = styles[i]); i++) {
|
||||
if ((enabled === null || style.enabled == enabled)
|
||||
&& (url === null || style.url == url)
|
||||
&& (id === null || style.id == id)) {
|
||||
let style;
|
||||
for (let i = 0; (style = styles[i]); i++) {
|
||||
if ((enabled === null || style.enabled === enabled)
|
||||
&& (url === null || style.url === url)
|
||||
&& (id === null || style.id === id)) {
|
||||
const sections = needSections &&
|
||||
getApplicableSections({style, matchUrl, strictRegexp, stopOnFirst: !asHash});
|
||||
if (asHash) {
|
||||
|
@ -230,17 +233,18 @@ function saveStyle(style) {
|
|||
if (!style.name) {
|
||||
delete style.name;
|
||||
}
|
||||
let existed, codeIsUpdated;
|
||||
if (reason == 'update' || reason == 'update-digest') {
|
||||
let existed;
|
||||
let codeIsUpdated;
|
||||
if (reason === 'update' || reason === 'update-digest') {
|
||||
return calcStyleDigest(style).then(digest => {
|
||||
style.originalDigest = digest;
|
||||
return decide();
|
||||
});
|
||||
}
|
||||
if (reason == 'import') {
|
||||
if (reason === 'import') {
|
||||
style.originalDigest = style.originalDigest || style.styleDigest; // TODO: remove in the future
|
||||
delete style.styleDigest; // TODO: remove in the future
|
||||
if (typeof style.originalDigest != 'string' || style.originalDigest.length != 40) {
|
||||
if (typeof style.originalDigest !== 'string' || style.originalDigest.length !== 40) {
|
||||
delete style.originalDigest;
|
||||
}
|
||||
}
|
||||
|
@ -253,7 +257,7 @@ function saveStyle(style) {
|
|||
return dbExec('get', id).then((event, store) => {
|
||||
const oldStyle = event.target.result;
|
||||
existed = Boolean(oldStyle);
|
||||
if (reason == 'update-digest' && oldStyle.originalDigest == style.originalDigest) {
|
||||
if (reason === 'update-digest' && oldStyle.originalDigest === style.originalDigest) {
|
||||
return style;
|
||||
}
|
||||
codeIsUpdated = !existed || 'sections' in style && !styleSectionsEqual(style, oldStyle);
|
||||
|
@ -287,7 +291,7 @@ function saveStyle(style) {
|
|||
}
|
||||
|
||||
function done(event) {
|
||||
if (reason == 'update-digest') {
|
||||
if (reason === 'update-digest') {
|
||||
return style;
|
||||
}
|
||||
style.id = style.id || event.target.result;
|
||||
|
@ -365,14 +369,14 @@ function getApplicableSections({style, matchUrl, strictRegexp = true, stopOnFirs
|
|||
function arraySomeMatches(array, matchUrl, strictRegexp) {
|
||||
for (const regexp of array) {
|
||||
for (let pass = 1; pass <= (strictRegexp ? 1 : 2); pass++) {
|
||||
const cacheKey = pass == 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp;
|
||||
const cacheKey = pass === 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp;
|
||||
let rx = cachedStyles.regexps.get(cacheKey);
|
||||
if (rx == false) {
|
||||
if (rx === false) {
|
||||
// invalid regexp
|
||||
break;
|
||||
}
|
||||
if (!rx) {
|
||||
const anchored = pass == 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$';
|
||||
const anchored = pass === 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$';
|
||||
rx = tryRegExp(anchored);
|
||||
cachedStyles.regexps.set(cacheKey, rx || false);
|
||||
if (!rx) {
|
||||
|
@ -413,7 +417,7 @@ function styleSectionsEqual({sections: a}, {sections: b}) {
|
|||
if (!a || !b) {
|
||||
return undefined;
|
||||
}
|
||||
if (a.length != b.length) {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
const checkedInB = [];
|
||||
|
@ -430,16 +434,16 @@ function styleSectionsEqual({sections: a}, {sections: b}) {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a == b);
|
||||
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b);
|
||||
}
|
||||
|
||||
function equalOrEmpty(a, b, telltale, comparator) {
|
||||
const typeA = a && typeof a[telltale] == 'function';
|
||||
const typeB = b && typeof b[telltale] == 'function';
|
||||
const typeA = a && typeof a[telltale] === 'function';
|
||||
const typeB = b && typeof b[telltale] === 'function';
|
||||
return (
|
||||
(a === null || a === undefined || (typeA && !a.length)) &&
|
||||
(b === null || b === undefined || (typeB && !b.length))
|
||||
) || typeA && typeB && a.length == b.length && comparator(a, b);
|
||||
) || typeA && typeB && a.length === b.length && comparator(a, b);
|
||||
}
|
||||
|
||||
function arrayMirrors(array1, array2) {
|
||||
|
@ -523,12 +527,12 @@ function cleanupCachedFilters({force = false} = {}) {
|
|||
|
||||
|
||||
function getDomains(url) {
|
||||
if (url.indexOf('file:') == 0) {
|
||||
if (url.indexOf('file:') === 0) {
|
||||
return [];
|
||||
}
|
||||
let d = /.*?:\/*([^/:]+)/.exec(url)[1];
|
||||
const domains = [d];
|
||||
while (d.indexOf('.') != -1) {
|
||||
while (d.indexOf('.') !== -1) {
|
||||
d = d.substring(d.indexOf('.') + 1);
|
||||
domains.push(d);
|
||||
}
|
|
@ -68,17 +68,17 @@ var updater = {
|
|||
});
|
||||
|
||||
function maybeFetchMd5(digest) {
|
||||
if (!ignoreDigest && style.originalDigest && style.originalDigest != digest) {
|
||||
if (!ignoreDigest && style.originalDigest && style.originalDigest !== digest) {
|
||||
return Promise.reject(updater.EDITED);
|
||||
}
|
||||
return download(style.md5Url);
|
||||
}
|
||||
|
||||
function maybeFetchCode(md5) {
|
||||
if (!md5 || md5.length != 32) {
|
||||
if (!md5 || md5.length !== 32) {
|
||||
return Promise.reject(updater.ERROR_MD5);
|
||||
}
|
||||
if (md5 == style.originalMd5 && style.originalDigest && !ignoreDigest) {
|
||||
if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
|
||||
return Promise.reject(updater.SAME_MD5);
|
||||
}
|
||||
return download(style.updateUrl);
|
||||
|
@ -109,8 +109,8 @@ var updater = {
|
|||
return json
|
||||
&& json.sections
|
||||
&& json.sections.length
|
||||
&& typeof json.sections.every == 'function'
|
||||
&& typeof json.sections[0].code == 'string';
|
||||
&& typeof json.sections.every === 'function'
|
||||
&& typeof json.sections[0].code === 'string';
|
||||
}
|
||||
},
|
||||
|
|
@ -28,7 +28,7 @@ function requestStyles(options, callback = applyStyles) {
|
|||
// dynamic about: and javascript: iframes don't have an URL yet
|
||||
// so we'll try the parent frame which is guaranteed to have a real URL
|
||||
try {
|
||||
if (window != parent) {
|
||||
if (window !== parent) {
|
||||
matchUrl = parent.location.href;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
@ -49,7 +49,7 @@ function requestStyles(options, callback = applyStyles) {
|
|||
|
||||
|
||||
function applyOnMessage(request, sender, sendResponse) {
|
||||
if (request.styles == 'DIY') {
|
||||
if (request.styles === 'DIY') {
|
||||
// Do-It-Yourself tells our built-in pages to fetch the styles directly
|
||||
// which is faster because IPC messaging JSON-ifies everything internally
|
||||
requestStyles({}, styles => {
|
||||
|
@ -114,7 +114,7 @@ function doDisableAll(disable = disableAll) {
|
|||
disableAll = disable;
|
||||
Array.prototype.forEach.call(document.styleSheets, stylesheet => {
|
||||
if (stylesheet.ownerNode.matches(`STYLE.stylus[id^="${ID_PREFIX}"]`)
|
||||
&& stylesheet.disabled != disable) {
|
||||
&& stylesheet.disabled !== disable) {
|
||||
stylesheet.disabled = disable;
|
||||
}
|
||||
});
|
||||
|
@ -122,14 +122,14 @@ function doDisableAll(disable = disableAll) {
|
|||
|
||||
|
||||
function doExposeIframes(state = exposeIframes) {
|
||||
if (state === exposeIframes || window == parent) {
|
||||
if (state === exposeIframes || window === parent) {
|
||||
return;
|
||||
}
|
||||
exposeIframes = state;
|
||||
const attr = document.documentElement.getAttribute('stylus-iframe');
|
||||
if (state && attr != '') {
|
||||
if (state && attr !== '') {
|
||||
document.documentElement.setAttribute('stylus-iframe', '');
|
||||
} else if (!state && attr == '') {
|
||||
} else if (!state && attr === '') {
|
||||
document.documentElement.removeAttribute('stylus-iframe');
|
||||
}
|
||||
}
|
||||
|
@ -193,7 +193,7 @@ function applyStyles(styles) {
|
|||
}
|
||||
if (document.head
|
||||
&& document.head.firstChild
|
||||
&& document.head.firstChild.id == 'xml-viewer-style') {
|
||||
&& document.head.firstChild.id === 'xml-viewer-style') {
|
||||
// when site response is application/xml Chrome displays our style elements
|
||||
// under document.documentElement as plain text so we need to move them into HEAD
|
||||
// which is already autogenerated at this moment
|
||||
|
@ -293,7 +293,7 @@ function initDocRewriteObserver() {
|
|||
for (let m = mutations.length; --m >= 0;) {
|
||||
const added = mutations[m].addedNodes;
|
||||
for (let n = added.length; --n >= 0;) {
|
||||
if (added[n].localName == 'html') {
|
||||
if (added[n].localName === 'html') {
|
||||
reinjectStyles();
|
||||
return;
|
||||
}
|
||||
|
@ -303,7 +303,7 @@ function initDocRewriteObserver() {
|
|||
docRewriteObserver.observe(document, {childList: true});
|
||||
// detect dynamic iframes rewritten after creation by the embedder i.e. externally
|
||||
setTimeout(() => {
|
||||
if (document.documentElement != ROOT) {
|
||||
if (document.documentElement !== ROOT) {
|
||||
reinjectStyles();
|
||||
}
|
||||
});
|
|
@ -1,360 +1,360 @@
|
|||
'use strict';
|
||||
|
||||
const CHROMIUM = /Chromium/.test(navigator.userAgent); // non-Windows Chromium
|
||||
const FIREFOX = /Firefox/.test(navigator.userAgent);
|
||||
const VIVALDI = /Vivaldi/.test(navigator.userAgent);
|
||||
const OPERA = /OPR/.test(navigator.userAgent);
|
||||
|
||||
document.addEventListener('stylishUpdate', onUpdateClicked);
|
||||
document.addEventListener('stylishUpdateChrome', onUpdateClicked);
|
||||
document.addEventListener('stylishUpdateOpera', onUpdateClicked);
|
||||
|
||||
document.addEventListener('stylishInstall', onInstallClicked);
|
||||
document.addEventListener('stylishInstallChrome', onInstallClicked);
|
||||
document.addEventListener('stylishInstallOpera', onInstallClicked);
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
// orphaned content script check
|
||||
if (msg.method == 'ping') {
|
||||
sendResponse(true);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: remove the following statement when USO is fixed
|
||||
document.documentElement.appendChild(document.createElement('script')).text = '(' +
|
||||
function() {
|
||||
let settings;
|
||||
document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) {
|
||||
document.removeEventListener('stylusFixBuggyUSOsettings', _);
|
||||
settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search);
|
||||
});
|
||||
const originalResponseJson = Response.prototype.json;
|
||||
Response.prototype.json = function(...args) {
|
||||
return originalResponseJson.call(this, ...args).then(json => {
|
||||
Response.prototype.json = originalResponseJson;
|
||||
if (!settings || typeof ((json || {}).style_settings || {}).every != 'function') {
|
||||
return json;
|
||||
}
|
||||
const images = new Map();
|
||||
for (const jsonSetting of json.style_settings) {
|
||||
let value = settings.get('ik-' + jsonSetting.install_key);
|
||||
if (!value
|
||||
|| !jsonSetting.style_setting_options
|
||||
|| !jsonSetting.style_setting_options[0]) {
|
||||
continue;
|
||||
}
|
||||
if (value.startsWith('ik-')) {
|
||||
value = value.replace(/^ik-/, '');
|
||||
const defaultItem = jsonSetting.style_setting_options.find(item => item.default);
|
||||
if (!defaultItem || defaultItem.install_key != value) {
|
||||
if (defaultItem) {
|
||||
defaultItem.default = false;
|
||||
}
|
||||
jsonSetting.style_setting_options.some(item => {
|
||||
if (item.install_key == value) {
|
||||
item.default = true;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (jsonSetting.setting_type == 'image') {
|
||||
jsonSetting.style_setting_options.some(item => {
|
||||
if (item.default) {
|
||||
item.default = false;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
images.set(jsonSetting.install_key, value);
|
||||
} else {
|
||||
const item = jsonSetting.style_setting_options[0];
|
||||
if (item.value !== value && item.install_key == 'placeholder') {
|
||||
item.value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (images.size) {
|
||||
new MutationObserver((_, observer) => {
|
||||
if (!document.getElementById('style-settings')) {
|
||||
return;
|
||||
}
|
||||
observer.disconnect();
|
||||
for (const [name, url] of images.entries()) {
|
||||
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
|
||||
const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
|
||||
if (elUrl) {
|
||||
elUrl.value = url;
|
||||
}
|
||||
}
|
||||
}).observe(document, {childList: true, subtree: true});
|
||||
}
|
||||
return json;
|
||||
});
|
||||
};
|
||||
} + ')()';
|
||||
|
||||
// TODO: remove the following statement when USO pagination is fixed
|
||||
if (location.search.includes('category=')) {
|
||||
document.addEventListener('DOMContentLoaded', function _() {
|
||||
document.removeEventListener('DOMContentLoaded', _);
|
||||
new MutationObserver((_, observer) => {
|
||||
if (!document.getElementById('pagination')) {
|
||||
return;
|
||||
}
|
||||
observer.disconnect();
|
||||
const category = '&' + location.search.match(/category=[^&]+/)[0];
|
||||
const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])');
|
||||
for (let i = 0; i < links.length; i++) {
|
||||
links[i].href += category;
|
||||
}
|
||||
}).observe(document, {childList: true, subtree: true});
|
||||
});
|
||||
}
|
||||
|
||||
new MutationObserver((mutations, observer) => {
|
||||
if (document.body) {
|
||||
observer.disconnect();
|
||||
// TODO: remove the following statement when USO pagination title is fixed
|
||||
document.title = document.title.replace(/^\d+&category=/, '');
|
||||
chrome.runtime.sendMessage({
|
||||
method: 'getStyles',
|
||||
url: getMeta('stylish-id-url') || location.href
|
||||
}, checkUpdatability);
|
||||
}
|
||||
}).observe(document.documentElement, {childList: true});
|
||||
|
||||
/* since we are using "stylish-code-chrome" meta key on all browsers and
|
||||
US.o does not provide "advanced settings" on this url if browser is not Chrome,
|
||||
we need to fix this URL using "stylish-update-url" meta key
|
||||
*/
|
||||
function getStyleURL() {
|
||||
const url = getMeta('stylish-code-chrome');
|
||||
// TODO: remove when USO is fixed
|
||||
const directUrl = getMeta('stylish-update-url');
|
||||
if (directUrl.includes('?') && !url.includes('?')) {
|
||||
/* get custom settings from the update url */
|
||||
return Object.assign(new URL(url), {
|
||||
search: (new URL(directUrl)).search
|
||||
}).href;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function checkUpdatability([installedStyle]) {
|
||||
// TODO: remove the following statement when USO is fixed
|
||||
document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', {
|
||||
detail: installedStyle && installedStyle.updateUrl,
|
||||
}));
|
||||
if (!installedStyle) {
|
||||
sendEvent('styleCanBeInstalledChrome');
|
||||
return;
|
||||
}
|
||||
const md5Url = getMeta('stylish-md5-url');
|
||||
if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) {
|
||||
getResource(md5Url).then(md5 => {
|
||||
reportUpdatable(md5 != installedStyle.originalMd5);
|
||||
});
|
||||
} else {
|
||||
getResource(getStyleURL()).then(code => {
|
||||
reportUpdatable(code === null ||
|
||||
!styleSectionsEqual(JSON.parse(code), installedStyle));
|
||||
});
|
||||
}
|
||||
|
||||
function reportUpdatable(isUpdatable) {
|
||||
sendEvent(
|
||||
isUpdatable
|
||||
? 'styleCanBeUpdatedChrome'
|
||||
: 'styleAlreadyInstalledChrome',
|
||||
{
|
||||
updateUrl: installedStyle.updateUrl
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function sendEvent(type, detail = null) {
|
||||
if (FIREFOX) {
|
||||
type = type.replace('Chrome', '');
|
||||
} else if (OPERA || VIVALDI) {
|
||||
type = type.replace('Chrome', 'Opera');
|
||||
}
|
||||
detail = {detail};
|
||||
if (typeof cloneInto != 'undefined') {
|
||||
// Firefox requires explicit cloning, however USO can't process our messages anyway
|
||||
// because USO tries to use a global "event" variable deprecated in Firefox
|
||||
detail = cloneInto(detail, document); // eslint-disable-line no-undef
|
||||
}
|
||||
onDOMready().then(() => {
|
||||
document.dispatchEvent(new CustomEvent(type, detail));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function onInstallClicked() {
|
||||
if (!orphanCheck || !orphanCheck()) {
|
||||
return;
|
||||
}
|
||||
getResource(getMeta('stylish-description'))
|
||||
.then(name => saveStyleCode('styleInstall', name))
|
||||
.then(() => getResource(getMeta('stylish-install-ping-url-chrome')));
|
||||
}
|
||||
|
||||
|
||||
function onUpdateClicked() {
|
||||
if (!orphanCheck || !orphanCheck()) {
|
||||
return;
|
||||
}
|
||||
chrome.runtime.sendMessage({
|
||||
method: 'getStyles',
|
||||
url: getMeta('stylish-id-url') || location.href,
|
||||
}, ([style]) => {
|
||||
saveStyleCode('styleUpdate', style.name, {id: style.id});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function saveStyleCode(message, name, addProps) {
|
||||
return new Promise(resolve => {
|
||||
if (!confirm(chrome.i18n.getMessage(message, [name]))) {
|
||||
return;
|
||||
}
|
||||
enableUpdateButton(false);
|
||||
getResource(getStyleURL()).then(code => {
|
||||
chrome.runtime.sendMessage(
|
||||
Object.assign(JSON.parse(code), addProps, {
|
||||
method: 'saveStyle',
|
||||
reason: 'update',
|
||||
}),
|
||||
style => {
|
||||
if (message == 'styleUpdate' && style.updateUrl.includes('?')) {
|
||||
enableUpdateButton(true);
|
||||
} else {
|
||||
sendEvent('styleInstalledChrome');
|
||||
}
|
||||
}
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
function enableUpdateButton(state) {
|
||||
const button = document.getElementById('update_style_button');
|
||||
if (button) {
|
||||
button.style.cssText = state ? '' :
|
||||
'pointer-events: none !important; opacity: .25 !important;';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getMeta(name) {
|
||||
const e = document.querySelector(`link[rel="${name}"]`);
|
||||
return e ? e.getAttribute('href') : null;
|
||||
}
|
||||
|
||||
|
||||
function getResource(url) {
|
||||
return new Promise(resolve => {
|
||||
if (url.startsWith('#')) {
|
||||
resolve(document.getElementById(url.slice(1)).textContent);
|
||||
} else {
|
||||
chrome.runtime.sendMessage({method: 'download', url}, resolve);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function styleSectionsEqual({sections: a}, {sections: b}) {
|
||||
if (!a || !b) {
|
||||
return undefined;
|
||||
}
|
||||
if (a.length != b.length) {
|
||||
return false;
|
||||
}
|
||||
const checkedInB = [];
|
||||
return a.every(sectionA => b.some(sectionB => {
|
||||
if (!checkedInB.includes(sectionB) && propertiesEqual(sectionA, sectionB)) {
|
||||
checkedInB.push(sectionB);
|
||||
return true;
|
||||
}
|
||||
}));
|
||||
|
||||
function propertiesEqual(secA, secB) {
|
||||
for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
|
||||
if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a == b);
|
||||
}
|
||||
|
||||
function equalOrEmpty(a, b, telltale, comparator) {
|
||||
const typeA = a && typeof a[telltale] == 'function';
|
||||
const typeB = b && typeof b[telltale] == 'function';
|
||||
return (
|
||||
(a === null || a === undefined || (typeA && !a.length)) &&
|
||||
(b === null || b === undefined || (typeB && !b.length))
|
||||
) || typeA && typeB && a.length == b.length && comparator(a, b);
|
||||
}
|
||||
|
||||
function arrayMirrors(array1, array2) {
|
||||
for (const el of array1) {
|
||||
if (array2.indexOf(el) < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const el of array2) {
|
||||
if (array1.indexOf(el) < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function onDOMready() {
|
||||
if (document.readyState != 'loading') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
document.addEventListener('DOMContentLoaded', function _() {
|
||||
document.removeEventListener('DOMContentLoaded', _);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function orphanCheck() {
|
||||
const port = chrome.runtime.connect();
|
||||
if (port) {
|
||||
port.disconnect();
|
||||
return true;
|
||||
}
|
||||
// we're orphaned due to an extension update
|
||||
// we can detach event listeners
|
||||
document.removeEventListener('stylishUpdate', onUpdateClicked);
|
||||
document.removeEventListener('stylishUpdateChrome', onUpdateClicked);
|
||||
document.removeEventListener('stylishUpdateOpera', onUpdateClicked);
|
||||
|
||||
document.removeEventListener('stylishInstall', onInstallClicked);
|
||||
document.removeEventListener('stylishInstallChrome', onInstallClicked);
|
||||
document.removeEventListener('stylishInstallOpera', onInstallClicked);
|
||||
|
||||
// we can't detach chrome.runtime.onMessage because it's no longer connected internally
|
||||
// we can destroy global functions in this context to free up memory
|
||||
[
|
||||
'checkUpdatability',
|
||||
'getMeta',
|
||||
'getResource',
|
||||
'onDOMready',
|
||||
'onInstallClicked',
|
||||
'onUpdateClicked',
|
||||
'orphanCheck',
|
||||
'saveStyleCode',
|
||||
'sendEvent',
|
||||
'styleSectionsEqual',
|
||||
].forEach(fn => (window[fn] = null));
|
||||
}
|
||||
'use strict';
|
||||
|
||||
const CHROMIUM = /Chromium/.test(navigator.userAgent); // non-Windows Chromium
|
||||
const FIREFOX = /Firefox/.test(navigator.userAgent);
|
||||
const VIVALDI = /Vivaldi/.test(navigator.userAgent);
|
||||
const OPERA = /OPR/.test(navigator.userAgent);
|
||||
|
||||
document.addEventListener('stylishUpdate', onUpdateClicked);
|
||||
document.addEventListener('stylishUpdateChrome', onUpdateClicked);
|
||||
document.addEventListener('stylishUpdateOpera', onUpdateClicked);
|
||||
|
||||
document.addEventListener('stylishInstall', onInstallClicked);
|
||||
document.addEventListener('stylishInstallChrome', onInstallClicked);
|
||||
document.addEventListener('stylishInstallOpera', onInstallClicked);
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
// orphaned content script check
|
||||
if (msg.method === 'ping') {
|
||||
sendResponse(true);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: remove the following statement when USO is fixed
|
||||
document.documentElement.appendChild(document.createElement('script')).text = '(' +
|
||||
function () {
|
||||
let settings;
|
||||
document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) {
|
||||
document.removeEventListener('stylusFixBuggyUSOsettings', _);
|
||||
settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search);
|
||||
});
|
||||
const originalResponseJson = Response.prototype.json;
|
||||
Response.prototype.json = function (...args) {
|
||||
return originalResponseJson.call(this, ...args).then(json => {
|
||||
Response.prototype.json = originalResponseJson;
|
||||
if (!settings || typeof ((json || {}).style_settings || {}).every !== 'function') {
|
||||
return json;
|
||||
}
|
||||
const images = new Map();
|
||||
for (const jsonSetting of json.style_settings) {
|
||||
let value = settings.get('ik-' + jsonSetting.install_key);
|
||||
if (!value
|
||||
|| !jsonSetting.style_setting_options
|
||||
|| !jsonSetting.style_setting_options[0]) {
|
||||
continue;
|
||||
}
|
||||
if (value.startsWith('ik-')) {
|
||||
value = value.replace(/^ik-/, '');
|
||||
const defaultItem = jsonSetting.style_setting_options.find(item => item.default);
|
||||
if (!defaultItem || defaultItem.install_key !== value) {
|
||||
if (defaultItem) {
|
||||
defaultItem.default = false;
|
||||
}
|
||||
jsonSetting.style_setting_options.some(item => {
|
||||
if (item.install_key === value) {
|
||||
item.default = true;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (jsonSetting.setting_type === 'image') {
|
||||
jsonSetting.style_setting_options.some(item => {
|
||||
if (item.default) {
|
||||
item.default = false;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
images.set(jsonSetting.install_key, value);
|
||||
} else {
|
||||
const item = jsonSetting.style_setting_options[0];
|
||||
if (item.value !== value && item.install_key === 'placeholder') {
|
||||
item.value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (images.size) {
|
||||
new MutationObserver((_, observer) => {
|
||||
if (!document.getElementById('style-settings')) {
|
||||
return;
|
||||
}
|
||||
observer.disconnect();
|
||||
for (const [name, url] of images.entries()) {
|
||||
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
|
||||
const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
|
||||
if (elUrl) {
|
||||
elUrl.value = url;
|
||||
}
|
||||
}
|
||||
}).observe(document, {childList: true, subtree: true});
|
||||
}
|
||||
return json;
|
||||
});
|
||||
};
|
||||
} + ')()';
|
||||
|
||||
// TODO: remove the following statement when USO pagination is fixed
|
||||
if (location.search.includes('category=')) {
|
||||
document.addEventListener('DOMContentLoaded', function _() {
|
||||
document.removeEventListener('DOMContentLoaded', _);
|
||||
new MutationObserver((_, observer) => {
|
||||
if (!document.getElementById('pagination')) {
|
||||
return;
|
||||
}
|
||||
observer.disconnect();
|
||||
const category = '&' + location.search.match(/category=[^&]+/)[0];
|
||||
const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])');
|
||||
for (let i = 0; i < links.length; i++) {
|
||||
links[i].href += category;
|
||||
}
|
||||
}).observe(document, {childList: true, subtree: true});
|
||||
});
|
||||
}
|
||||
|
||||
new MutationObserver((mutations, observer) => {
|
||||
if (document.body) {
|
||||
observer.disconnect();
|
||||
// TODO: remove the following statement when USO pagination title is fixed
|
||||
document.title = document.title.replace(/^\d+&category=/, '');
|
||||
chrome.runtime.sendMessage({
|
||||
method: 'getStyles',
|
||||
url: getMeta('stylish-id-url') || location.href
|
||||
}, checkUpdatability);
|
||||
}
|
||||
}).observe(document.documentElement, {childList: true});
|
||||
|
||||
/* since we are using "stylish-code-chrome" meta key on all browsers and
|
||||
US.o does not provide "advanced settings" on this url if browser is not Chrome,
|
||||
we need to fix this URL using "stylish-update-url" meta key
|
||||
*/
|
||||
function getStyleURL() {
|
||||
const url = getMeta('stylish-code-chrome');
|
||||
// TODO: remove when USO is fixed
|
||||
const directUrl = getMeta('stylish-update-url');
|
||||
if (directUrl.includes('?') && !url.includes('?')) {
|
||||
/* get custom settings from the update url */
|
||||
return Object.assign(new URL(url), {
|
||||
search: (new URL(directUrl)).search
|
||||
}).href;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function checkUpdatability([installedStyle]) {
|
||||
// TODO: remove the following statement when USO is fixed
|
||||
document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', {
|
||||
detail: installedStyle && installedStyle.updateUrl,
|
||||
}));
|
||||
if (!installedStyle) {
|
||||
sendEvent('styleCanBeInstalledChrome');
|
||||
return;
|
||||
}
|
||||
const md5Url = getMeta('stylish-md5-url');
|
||||
if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) {
|
||||
getResource(md5Url).then(md5 => {
|
||||
reportUpdatable(md5 !== installedStyle.originalMd5);
|
||||
});
|
||||
} else {
|
||||
getResource(getStyleURL()).then(code => {
|
||||
reportUpdatable(code === null ||
|
||||
!styleSectionsEqual(JSON.parse(code), installedStyle));
|
||||
});
|
||||
}
|
||||
|
||||
function reportUpdatable(isUpdatable) {
|
||||
sendEvent(
|
||||
isUpdatable
|
||||
? 'styleCanBeUpdatedChrome'
|
||||
: 'styleAlreadyInstalledChrome',
|
||||
{
|
||||
updateUrl: installedStyle.updateUrl
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function sendEvent(type, detail = null) {
|
||||
if (FIREFOX) {
|
||||
type = type.replace('Chrome', '');
|
||||
} else if (OPERA || VIVALDI) {
|
||||
type = type.replace('Chrome', 'Opera');
|
||||
}
|
||||
detail = {detail};
|
||||
if (typeof cloneInto !== 'undefined') {
|
||||
// Firefox requires explicit cloning, however USO can't process our messages anyway
|
||||
// because USO tries to use a global "event" variable deprecated in Firefox
|
||||
detail = cloneInto(detail, document); // eslint-disable-line no-undef
|
||||
}
|
||||
onDOMready().then(() => {
|
||||
document.dispatchEvent(new CustomEvent(type, detail));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function onInstallClicked() {
|
||||
if (!orphanCheck || !orphanCheck()) {
|
||||
return;
|
||||
}
|
||||
getResource(getMeta('stylish-description'))
|
||||
.then(name => saveStyleCode('styleInstall', name))
|
||||
.then(() => getResource(getMeta('stylish-install-ping-url-chrome')));
|
||||
}
|
||||
|
||||
|
||||
function onUpdateClicked() {
|
||||
if (!orphanCheck || !orphanCheck()) {
|
||||
return;
|
||||
}
|
||||
chrome.runtime.sendMessage({
|
||||
method: 'getStyles',
|
||||
url: getMeta('stylish-id-url') || location.href,
|
||||
}, ([style]) => {
|
||||
saveStyleCode('styleUpdate', style.name, {id: style.id});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function saveStyleCode(message, name, addProps) {
|
||||
return new Promise(resolve => {
|
||||
if (!confirm(chrome.i18n.getMessage(message, [name]))) {
|
||||
return;
|
||||
}
|
||||
enableUpdateButton(false);
|
||||
getResource(getStyleURL()).then(code => {
|
||||
chrome.runtime.sendMessage(
|
||||
Object.assign(JSON.parse(code), addProps, {
|
||||
method: 'saveStyle',
|
||||
reason: 'update',
|
||||
}),
|
||||
style => {
|
||||
if (message === 'styleUpdate' && style.updateUrl.includes('?')) {
|
||||
enableUpdateButton(true);
|
||||
} else {
|
||||
sendEvent('styleInstalledChrome');
|
||||
}
|
||||
}
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
function enableUpdateButton(state) {
|
||||
const button = document.getElementById('update_style_button');
|
||||
if (button) {
|
||||
button.style.cssText = state ? '' :
|
||||
'pointer-events: none !important; opacity: .25 !important;';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getMeta(name) {
|
||||
const e = document.querySelector(`link[rel="${name}"]`);
|
||||
return e ? e.getAttribute('href') : null;
|
||||
}
|
||||
|
||||
|
||||
function getResource(url) {
|
||||
return new Promise(resolve => {
|
||||
if (url.startsWith('#')) {
|
||||
resolve(document.getElementById(url.slice(1)).textContent);
|
||||
} else {
|
||||
chrome.runtime.sendMessage({method: 'download', url}, resolve);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function styleSectionsEqual({sections: a}, {sections: b}) {
|
||||
if (!a || !b) {
|
||||
return undefined;
|
||||
}
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
const checkedInB = [];
|
||||
return a.every(sectionA => b.some(sectionB => {
|
||||
if (!checkedInB.includes(sectionB) && propertiesEqual(sectionA, sectionB)) {
|
||||
checkedInB.push(sectionB);
|
||||
return true;
|
||||
}
|
||||
}));
|
||||
|
||||
function propertiesEqual(secA, secB) {
|
||||
for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
|
||||
if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b);
|
||||
}
|
||||
|
||||
function equalOrEmpty(a, b, telltale, comparator) {
|
||||
const typeA = a && typeof a[telltale] === 'function';
|
||||
const typeB = b && typeof b[telltale] === 'function';
|
||||
return (
|
||||
(a === null || a === undefined || (typeA && !a.length)) &&
|
||||
(b === null || b === undefined || (typeB && !b.length))
|
||||
) || typeA && typeB && a.length === b.length && comparator(a, b);
|
||||
}
|
||||
|
||||
function arrayMirrors(array1, array2) {
|
||||
for (const el of array1) {
|
||||
if (array2.indexOf(el) < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const el of array2) {
|
||||
if (array1.indexOf(el) < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function onDOMready() {
|
||||
if (document.readyState !== 'loading') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
document.addEventListener('DOMContentLoaded', function _() {
|
||||
document.removeEventListener('DOMContentLoaded', _);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function orphanCheck() {
|
||||
const port = chrome.runtime.connect();
|
||||
if (port) {
|
||||
port.disconnect();
|
||||
return true;
|
||||
}
|
||||
// we're orphaned due to an extension update
|
||||
// we can detach event listeners
|
||||
document.removeEventListener('stylishUpdate', onUpdateClicked);
|
||||
document.removeEventListener('stylishUpdateChrome', onUpdateClicked);
|
||||
document.removeEventListener('stylishUpdateOpera', onUpdateClicked);
|
||||
|
||||
document.removeEventListener('stylishInstall', onInstallClicked);
|
||||
document.removeEventListener('stylishInstallChrome', onInstallClicked);
|
||||
document.removeEventListener('stylishInstallOpera', onInstallClicked);
|
||||
|
||||
// we can't detach chrome.runtime.onMessage because it's no longer connected internally
|
||||
// we can destroy global functions in this context to free up memory
|
||||
[
|
||||
'checkUpdatability',
|
||||
'getMeta',
|
||||
'getResource',
|
||||
'onDOMready',
|
||||
'onInstallClicked',
|
||||
'onUpdateClicked',
|
||||
'orphanCheck',
|
||||
'saveStyleCode',
|
||||
'sendEvent',
|
||||
'styleSectionsEqual',
|
||||
].forEach(fn => (window[fn] = null));
|
||||
}
|
977
edit.html
977
edit.html
|
@ -1,792 +1,213 @@
|
|||
<html id="stylus">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
||||
|
||||
<script src="dom.js"></script>
|
||||
<script src="messaging.js"></script>
|
||||
<script src="prefs.js"></script>
|
||||
<script src="localization.js"></script>
|
||||
<script src="apply.js"></script>
|
||||
<script src="edit.js"></script>
|
||||
<script src="js/dom.js"></script>
|
||||
<script src="js/messaging.js"></script>
|
||||
<script src="js/prefs.js"></script>
|
||||
<script src="js/localization.js"></script>
|
||||
<script src="content/apply.js"></script>
|
||||
<link rel="stylesheet" href="edit/edit.css">
|
||||
<script src="edit/edit.js"></script>
|
||||
|
||||
<script src="codemirror/lib/codemirror.js"></script>
|
||||
<link rel="stylesheet" href="codemirror/lib/codemirror.css">
|
||||
<script src="codemirror/mode/css/css.js"></script>
|
||||
<script src="vendor/codemirror/lib/codemirror.js"></script>
|
||||
<link rel="stylesheet" href="vendor/codemirror/lib/codemirror.css">
|
||||
<script src="vendor/codemirror/mode/css/css.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="codemirror/addon/dialog/dialog.css">
|
||||
<link rel="stylesheet" href="codemirror/addon/search/matchesonscrollbar.css">
|
||||
<script src="codemirror/addon/scroll/annotatescrollbar.js"></script>
|
||||
<script src="codemirror/addon/search/matchesonscrollbar.js"></script>
|
||||
<script src="codemirror-overwrites/addon/search/match-highlighter.js"></script>
|
||||
<script src="codemirror/addon/dialog/dialog.js"></script>
|
||||
<script src="codemirror/addon/search/searchcursor.js"></script>
|
||||
<script src="codemirror/addon/search/search.js"></script>
|
||||
<script src="codemirror/addon/comment/comment.js"></script>
|
||||
<script src="codemirror/addon/selection/active-line.js"></script>
|
||||
<link rel="stylesheet" href="vendor/codemirror/addon/dialog/dialog.css">
|
||||
<link rel="stylesheet" href="vendor/codemirror/addon/search/matchesonscrollbar.css">
|
||||
<script src="vendor/codemirror/addon/scroll/annotatescrollbar.js"></script>
|
||||
<script src="vendor/codemirror/addon/search/matchesonscrollbar.js"></script>
|
||||
<script src="vendor-overwrites/codemirror/addon/search/match-highlighter.js"></script>
|
||||
<script src="vendor/codemirror/addon/dialog/dialog.js"></script>
|
||||
<script src="vendor/codemirror/addon/search/searchcursor.js"></script>
|
||||
<script src="vendor/codemirror/addon/search/search.js"></script>
|
||||
<script src="vendor/codemirror/addon/comment/comment.js"></script>
|
||||
<script src="vendor/codemirror/addon/selection/active-line.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="codemirror/addon/fold/foldgutter.css" />
|
||||
<script src="codemirror/addon/fold/foldcode.js"></script>
|
||||
<script src="codemirror/addon/fold/foldgutter.js"></script>
|
||||
<script src="codemirror/addon/fold/brace-fold.js"></script>
|
||||
<script src="codemirror/addon/fold/comment-fold.js"></script>
|
||||
<link rel="stylesheet" href="vendor/codemirror/addon/fold/foldgutter.css" />
|
||||
<script src="vendor/codemirror/addon/fold/foldcode.js"></script>
|
||||
<script src="vendor/codemirror/addon/fold/foldgutter.js"></script>
|
||||
<script src="vendor/codemirror/addon/fold/brace-fold.js"></script>
|
||||
<script src="vendor/codemirror/addon/fold/comment-fold.js"></script>
|
||||
|
||||
<script src="codemirror/addon/edit/matchbrackets.js"></script>
|
||||
<script src="vendor/codemirror/addon/edit/matchbrackets.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="codemirror/addon/lint/lint.css" />
|
||||
<script src="csslint/csslint-worker.js"></script>
|
||||
<script src="codemirror/addon/lint/lint.js"></script>
|
||||
<script src="codemirror-overwrites/addon/lint/css-lint.js"></script>
|
||||
<link rel="stylesheet" href="vendor/codemirror/addon/lint/lint.css" />
|
||||
<script src="vendor/csslint/csslint-worker.js"></script>
|
||||
<script src="vendor/codemirror/addon/lint/lint.js"></script>
|
||||
<script src="vendor-overwrites/codemirror/addon/lint/css-lint.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="codemirror/addon/hint/show-hint.css" />
|
||||
<script src="codemirror/addon/hint/show-hint.js"></script>
|
||||
<script src="codemirror/addon/hint/css-hint.js"></script>
|
||||
<link rel="stylesheet" href="vendor/codemirror/addon/hint/show-hint.css" />
|
||||
<script src="vendor/codemirror/addon/hint/show-hint.js"></script>
|
||||
<script src="vendor/codemirror/addon/hint/css-hint.js"></script>
|
||||
|
||||
<script src="codemirror/keymap/sublime.js"></script>
|
||||
<script src="codemirror/keymap/emacs.js"></script>
|
||||
<script src="codemirror/keymap/vim.js"></script>
|
||||
<script src="vendor/codemirror/keymap/sublime.js"></script>
|
||||
<script src="vendor/codemirror/keymap/emacs.js"></script>
|
||||
<script src="vendor/codemirror/keymap/vim.js"></script>
|
||||
<link id="cm-theme" rel="stylesheet">
|
||||
|
||||
<style type="text/css">
|
||||
<template data-id="appliesTo">
|
||||
<li>
|
||||
<select name="applies-type" class="applies-type style-contributor">
|
||||
<option value="url" i18n-text="appliesUrlOption"></option>
|
||||
<option value="url-prefix" i18n-text="appliesUrlPrefixOption"></option>
|
||||
<option value="domain" i18n-text="appliesDomainOption"></option>
|
||||
<option value="regexp" i18n-text="appliesRegexpOption"></option>
|
||||
</select>
|
||||
<input name="applies-value" class="applies-value style-contributor">
|
||||
<button class="remove-applies-to" i18n-text="appliesRemove"></button>
|
||||
<button class="add-applies-to" i18n-text="appliesAdd"></button>
|
||||
</li>
|
||||
</template>
|
||||
<template data-id="appliesToEverything">
|
||||
<li class="applies-to-everything" i18n-html="appliesToEverything">
|
||||
<button class="add-applies-to" i18n-text="appliesSpecify"></button>
|
||||
</li>
|
||||
</template>
|
||||
<template data-id="section">
|
||||
<div>
|
||||
<label i18n-text="sectionCode"></label>
|
||||
<textarea class="code"></textarea>
|
||||
<br>
|
||||
<div class="applies-to">
|
||||
<label i18n-text="appliesLabel">
|
||||
<svg class="svg-icon info applies-to-help"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</label>
|
||||
<ul class="applies-to-list"></ul>
|
||||
</div>
|
||||
<button class="remove-section" i18n-text="sectionRemove"></button>
|
||||
<button class="add-section" i18n-text="sectionAdd"></button>
|
||||
<button class="beautify-section" i18n-text="styleBeautify"></button>
|
||||
<button class="test-regexp" i18n-text="styleRegexpTestButton"></button>
|
||||
</div>
|
||||
</template>
|
||||
<template data-id="find">
|
||||
<span i18n-text="search">: <input type="text" class="CodeMirror-search-field" spellcheck="false">
|
||||
<span class="CodeMirror-search-hint">(<span i18n-text="searchRegexp"></span>)</span>
|
||||
</span>
|
||||
</template>
|
||||
<template data-id="replace">
|
||||
<span i18n-text="replace">: <input type="text" class="CodeMirror-search-field" spellcheck="false">
|
||||
<span class="CodeMirror-search-hint">(<span i18n-text="searchRegexp"></span>)</span>
|
||||
</span>
|
||||
</template>
|
||||
<template data-id="replaceAll">
|
||||
<span i18n-text="replaceAll">: <input type="text" class="CodeMirror-search-field" spellcheck="false">
|
||||
<span class="CodeMirror-search-hint">(<span i18n-text="searchRegexp"></span>)</span>
|
||||
</span>
|
||||
</template>
|
||||
<template data-id="replaceWith">
|
||||
<span i18n-text="replaceWith">: <input type="text" class="CodeMirror-search-field" spellcheck="false">
|
||||
</span>
|
||||
</template>
|
||||
<template data-id="replaceConfirm">
|
||||
<span i18n-text="replace">?
|
||||
<button i18n-text="confirmYes"></button>
|
||||
<button i18n-text="confirmNo"></button>
|
||||
<button i18n-text="confirmStop"></button>
|
||||
</span>
|
||||
</template>
|
||||
<template data-id="jumpToLine">
|
||||
<span i18n-text="editGotoLine">: <input class="CodeMirror-jump-field" type="text"></span>
|
||||
</template>
|
||||
<template data-id="regexpTestPartial">
|
||||
<a target="_blank" href="https://github.com/stylish-userstyles/stylish/wiki/Applying-styles-to-specific-sites#advanced-matching-with-regular-expressions"><svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></a>
|
||||
</template>
|
||||
<template data-id="resizeGrip">
|
||||
<div class="resize-grip" i18n-title="cm_resizeGripHint"></div>
|
||||
</template>
|
||||
</head>
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font: 12px arial,sans-serif;
|
||||
}
|
||||
/************ header ************/
|
||||
#header {
|
||||
width: 280px;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
padding: 15px;
|
||||
border-right: 1px dashed #AAA;
|
||||
-webkit-box-shadow: 0 0 3rem -1.2rem black;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#header h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
#sections {
|
||||
padding-left: 280px;
|
||||
}
|
||||
#sections h2 {
|
||||
margin-top: 1rem;
|
||||
margin-left: 1.7rem;
|
||||
}
|
||||
.aligned {
|
||||
display: table-row;
|
||||
}
|
||||
.aligned > *:not(svg) {
|
||||
display: table-cell;
|
||||
margin-top: 0.1rem;
|
||||
min-height: 1.4rem;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
margin-left: 0.1rem;
|
||||
}
|
||||
/* basic info */
|
||||
#basic-info {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
#name {
|
||||
width: 100%;
|
||||
}
|
||||
#basic-info-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
#url {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
#url:not([href^="http"]) {
|
||||
display: none;
|
||||
}
|
||||
#save-button {
|
||||
opacity: .5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.dirty #save-button {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
.svg-icon {
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
transition: fill .5s;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.svg-icon:not(.dismiss) {
|
||||
margin-left: 0.2rem;
|
||||
}
|
||||
h2 .svg-icon, label .svg-icon {
|
||||
margin-top: -1px;
|
||||
}
|
||||
.svg-icon.info {
|
||||
width: 14px;
|
||||
height: 16px;
|
||||
}
|
||||
.svg-icon:hover,
|
||||
.svg-icon.info {
|
||||
fill: #666;
|
||||
}
|
||||
.svg-icon,
|
||||
.svg-icon.info:hover {
|
||||
fill: #000;
|
||||
}
|
||||
#enabled {
|
||||
margin-left: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#enabled-label {
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* actions */
|
||||
#actions > * {
|
||||
margin-right: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
/* options */
|
||||
#options [type="number"] {
|
||||
max-width: 2.5rem;
|
||||
text-align: right;
|
||||
}
|
||||
#options .option > * {
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
/************ content ***********/
|
||||
#sections > div {
|
||||
margin: 0.7rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
#sections > div:not(:first-of-type) {
|
||||
border-top: 2px solid black;
|
||||
}
|
||||
#sections > div:only-of-type .remove-section {
|
||||
display: none;
|
||||
}
|
||||
#sections > div > button:not(:first-of-type) {
|
||||
margin-left: 0.2rem;
|
||||
}
|
||||
.dirty > label::before {
|
||||
content: "*";
|
||||
font-weight: bold;
|
||||
}
|
||||
#sections {
|
||||
counter-reset: codebox;
|
||||
}
|
||||
#sections > div > label::after {
|
||||
counter-increment: codebox;
|
||||
content: counter(codebox);
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
/* code */
|
||||
.CodeMirror-hint:hover {
|
||||
color: white;
|
||||
background: #08f;
|
||||
}
|
||||
.code {
|
||||
height: 10rem;
|
||||
width: 40rem;
|
||||
}
|
||||
.CodeMirror {
|
||||
border: solid #CCC 1px;
|
||||
}
|
||||
.CodeMirror-scroll {
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 6px; /* resize-grip height */
|
||||
}
|
||||
.CodeMirror-lint-mark-warning {
|
||||
background: none;
|
||||
}
|
||||
.CodeMirror-vscrollbar {
|
||||
margin-bottom: 7px; /* make space for resize-grip */
|
||||
}
|
||||
.CodeMirror-hscrollbar {
|
||||
bottom: 7px; /* make space for resize-grip */
|
||||
}
|
||||
.CodeMirror-scrollbar-filler {
|
||||
bottom: 7px; /* make space for resize-grip */
|
||||
}
|
||||
.CodeMirror-dialog {
|
||||
-webkit-animation: highlight 3s ease-out;
|
||||
}
|
||||
.CodeMirror-focused {
|
||||
outline: -webkit-focus-ring-color auto 5px;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.CodeMirror-search-field {
|
||||
width: 10em;
|
||||
}
|
||||
.CodeMirror-jump-field {
|
||||
width: 5em;
|
||||
}
|
||||
.CodeMirror-search-hint {
|
||||
color: #888;
|
||||
}
|
||||
body[data-match-highlight="token"] .cm-matchhighlight-approved .cm-matchhighlight,
|
||||
body[data-match-highlight="token"] .CodeMirror-selection-highlight-scrollbar {
|
||||
animation: fadein-match-highlighter 1s cubic-bezier(.97,.01,.42,.98);
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
body[data-match-highlight="selection"] .cm-matchhighlight-approved .cm-matchhighlight,
|
||||
body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar {
|
||||
background-color: rgba(1, 151, 193, 0.1);
|
||||
}
|
||||
@-webkit-keyframes highlight {
|
||||
from {
|
||||
background-color: #ff9;
|
||||
}
|
||||
to {
|
||||
background-color: none;
|
||||
}
|
||||
}
|
||||
@keyframes fadein {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes fadein-match-highlighter {
|
||||
from { background-color: transparent; }
|
||||
to { background-color: rgba(1, 151, 193, 0.1); }
|
||||
}
|
||||
.resize-grip {
|
||||
position: absolute;
|
||||
display: block;
|
||||
height: 6px;
|
||||
content: "";
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9;
|
||||
cursor: n-resize;
|
||||
background-color: inherit;
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
border-top-color: inherit;
|
||||
}
|
||||
.resize-grip:after {
|
||||
content: "";
|
||||
bottom: 2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 8px;
|
||||
display: block;
|
||||
position: absolute;
|
||||
border-top-width: 2px;
|
||||
border-top-style: dotted;
|
||||
border-top-color: inherit;
|
||||
}
|
||||
/* applies-to */
|
||||
.applies-to {
|
||||
display: flex;
|
||||
}
|
||||
.applies-to label {
|
||||
flex: auto;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
.applies-to ul {
|
||||
flex: auto;
|
||||
flex-grow: 99;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.applies-to li {
|
||||
display: flex;
|
||||
list-style-type: none;
|
||||
align-items: center;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
.applies-to li > *:not(button) {
|
||||
flex: auto;
|
||||
min-height: 1.4rem;
|
||||
margin-left: 0.35rem;
|
||||
}
|
||||
.applies-to li .add-applies-to {
|
||||
visibility: hidden;
|
||||
text-align: left;
|
||||
}
|
||||
.applies-to li:last-child .add-applies-to {
|
||||
visibility: visible
|
||||
}
|
||||
.applies-to li .add-applies-to:first-child {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.applies-to li .applies-value {
|
||||
flex-grow: 99;
|
||||
padding-left: 0.2rem;
|
||||
}
|
||||
.applies-to img {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
.test-regexp {
|
||||
display: none;
|
||||
}
|
||||
.has-regexp .test-regexp {
|
||||
display: inline-block;
|
||||
}
|
||||
.regexp-report summary, .regexp-report div {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
.regexp-report mark {
|
||||
background-color: rgba(255, 255, 0, .5);
|
||||
}
|
||||
.regexp-report details {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.regexp-report details:not(:last-child) {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.regexp-report summary {
|
||||
font-weight: bold;
|
||||
margin-left: -1rem;
|
||||
margin-bottom: .5rem;
|
||||
outline: none;
|
||||
cursor: default;
|
||||
}
|
||||
.regexp-report details[data-type="full"] {
|
||||
color: darkgreen;
|
||||
}
|
||||
.regexp-report details[data-type="partial"] {
|
||||
color: darkgray;
|
||||
}
|
||||
.regexp-report details[data-type="invalid"] {
|
||||
color: maroon;
|
||||
}
|
||||
.regexp-report details details {
|
||||
margin-left: 2rem;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
.regexp-report .svg-icon {
|
||||
position: absolute;
|
||||
margin-top: -1px;
|
||||
}
|
||||
.regexp-report details div:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration-skip: ink;
|
||||
}
|
||||
.regexp-report details div img {
|
||||
width: 16px;
|
||||
max-height: 16px;
|
||||
position: absolute;
|
||||
margin-left: -20px;
|
||||
margin-top: -1px;
|
||||
animation: fadein 1s cubic-bezier(.03, .67, .08, .94);
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
/************ help popup ************/
|
||||
#help-popup {
|
||||
top: 3rem;
|
||||
right: 3rem;
|
||||
max-width: 50vw;
|
||||
position: fixed;
|
||||
display: none;
|
||||
background-color: white;
|
||||
box-shadow: 3px 3px 30px rgba(0, 0, 0, 0.5);
|
||||
padding: 0.5rem;
|
||||
z-index: 99;
|
||||
}
|
||||
#help-popup.big {
|
||||
max-width: 100%;
|
||||
left: 3rem;
|
||||
}
|
||||
#help-popup.big .CodeMirror {
|
||||
min-height: 2rem;
|
||||
height: 70vh;
|
||||
}
|
||||
#help-popup .title {
|
||||
font-weight: bold;
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
margin: -0.5rem -0.5rem 0.5rem;
|
||||
padding: .5rem 32px .5rem .5rem;
|
||||
}
|
||||
#help-popup .contents {
|
||||
max-height: calc(100vh - 8rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
#help-popup .dismiss {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: .5em;
|
||||
}
|
||||
<body id="stylus-edit">
|
||||
<div id="header">
|
||||
<h1 id="heading"> </h1> <!-- nbsp allocates the actual height which prevents page shift -->
|
||||
<section id="basic-info">
|
||||
<div id="basic-info-name">
|
||||
<input id="name" class="style-contributor" i18n-placeholder="styleMissingName" spellcheck="false">
|
||||
<a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a>
|
||||
</div>
|
||||
<div id="basic-info-enabled">
|
||||
<input type="checkbox" id="enabled" class="style-contributor">
|
||||
<label for="enabled" id="enabled-label" i18n-text="styleEnabledLabel"></label><!--
|
||||
--><svg id="toggle-style-help" class="svg-icon info">
|
||||
<use xlink:href="#svg-icon-help"/>
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
<section id="actions">
|
||||
<div>
|
||||
<button id="save-button" title="Ctrl-S" i18n-text="styleSaveLabel"></button>
|
||||
<button id="beautify" i18n-text="styleBeautify"></button>
|
||||
<a href="manage.html"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
|
||||
</div>
|
||||
<div>
|
||||
<h2 id="mozilla-format-heading" i18n-text="styleMozillaFormatHeading"><svg id="to-mozilla-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></h2>
|
||||
<button id="from-mozilla" i18n-text="importLabel"></button>
|
||||
<button id="to-mozilla" i18n-text="exportLabel"></button>
|
||||
</div>
|
||||
</section>
|
||||
<section id="options">
|
||||
<h2 id="options-heading" i18n-text="optionsHeading"></h2>
|
||||
<div class="option">
|
||||
<input id="editor.lineWrapping" type="checkbox">
|
||||
<label id="lineWrapping-label" for="editor.lineWrapping" i18n-text="cm_lineWrapping"></label>
|
||||
</div>
|
||||
<div class="option">
|
||||
<input id="editor.smartIndent" type="checkbox">
|
||||
<label id="smartIndent-label" for="editor.smartIndent" i18n-text="cm_smartIndent"></label>
|
||||
</div>
|
||||
<div class="option">
|
||||
<input id="editor.indentWithTabs" type="checkbox">
|
||||
<label id="indentWithTabs-label" for="editor.indentWithTabs" i18n-text="cm_indentWithTabs"></label>
|
||||
</div>
|
||||
<div class="option">
|
||||
<input id="editor.autocompleteOnTyping" type="checkbox">
|
||||
<label for="editor.autocompleteOnTyping" i18n-text="cm_autocompleteOnTyping"></label>
|
||||
</div>
|
||||
<div class="option aligned">
|
||||
<label id="tabSize-label" for="editor.tabSize" i18n-text="cm_tabSize"></label>
|
||||
<input id="editor.tabSize" type="number" min="0">
|
||||
</div>
|
||||
<div class="option aligned">
|
||||
<label id="keyMap-label" for="editor.keyMap" i18n-text="cm_keyMap"></label>
|
||||
<select id="editor.keyMap"></select>
|
||||
<svg id="keyMap-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</div>
|
||||
<div class="option aligned">
|
||||
<label id="theme-label" for="editor.theme" i18n-text="cm_theme"></label>
|
||||
<select id="editor.theme"></select>
|
||||
</div>
|
||||
<div class="option aligned">
|
||||
<label id="highlight-label" for="editor.matchHighlight" i18n-text="cm_matchHighlight"></label>
|
||||
<select id="editor.matchHighlight">
|
||||
<option i18n-text="cm_matchHighlightToken" value="token">
|
||||
<option i18n-text="cm_matchHighlightSelection" value="selection">
|
||||
<option i18n-text="genericDisabledLabel" value="">
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
<section id="lint"><h2 i18n-text="issues">: <span id="issue-count"></span><svg id="lint-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></h2><div></div></section>
|
||||
</div>
|
||||
<section id="sections">
|
||||
<h2><span id="sections-heading" i18n-text="styleSectionsTitle"></span><svg id="sections-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></h2>
|
||||
</section>
|
||||
<div id="help-popup">
|
||||
<div class="title"></div><svg id="sections-help" class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg></svg>
|
||||
<div class="contents"></div>
|
||||
</div>
|
||||
|
||||
.keymap-list {
|
||||
font-size: 85%;
|
||||
line-height: 1.0;
|
||||
border-spacing: 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
.keymap-list input {
|
||||
width: 100%;
|
||||
}
|
||||
.keymap-list tr:nth-child(odd) {
|
||||
background-color: rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
.keymap-list td:first-child {
|
||||
white-space: nowrap;
|
||||
font-family: monospace;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none">
|
||||
<symbol id="svg-icon-external-link" height="16" width="16" viewBox="0 0 8 8">
|
||||
<path d="M0 0v8h8v-2h-1v1h-6v-6h1v-1h-2zm4 0l1.5 1.5-2.5 2.5 1 1 2.5-2.5 1.5 1.5v-4h-4z"></path>
|
||||
</symbol>
|
||||
<symbol id="svg-icon-help" height="16" width="14" viewBox="0 0 14 16" i18n-alt="helpAlt">
|
||||
<path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path>
|
||||
</symbol>
|
||||
<symbol id="svg-icon-close" height="16" width="12" viewBox="0 0 12 16">
|
||||
<path fill-rule="evenodd" d="M7.48 8l3.75 3.75-1.48 1.48L6 9.48l-3.75 3.75-1.48-1.48L4.52 8 .77 4.25l1.48-1.48L6 6.52l3.75-3.75 1.48 1.48z"></path>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
#help-popup button[name^="import"] {
|
||||
line-height: 1.5rem;
|
||||
padding: 0 0.5rem;
|
||||
margin: 0.5rem 0 0 0.5rem;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
float: right;
|
||||
}
|
||||
#help-popup.ready button[name^="import"] {
|
||||
pointer-events: all;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
/************ lint ************/
|
||||
#lint {
|
||||
display: none;
|
||||
}
|
||||
#lint > div {
|
||||
overflow-y: auto;
|
||||
}
|
||||
#lint table {
|
||||
font-size: 100%;
|
||||
border-spacing: 0;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.0;
|
||||
}
|
||||
#lint table:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
#lint caption {
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
}
|
||||
#lint tbody {
|
||||
font-size: 85%;
|
||||
cursor: pointer;
|
||||
}
|
||||
#lint tr:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
#lint td[role="severity"] {
|
||||
font-size: 0;
|
||||
width: 16px;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
#lint td[role="line"], #lint td[role="sep"] {
|
||||
text-align: right;
|
||||
padding-right: 0;
|
||||
}
|
||||
#lint td[role="col"] {
|
||||
text-align: left;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
#lint td[role="message"] {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/************ CSS beautifier ************/
|
||||
.beautify-options {
|
||||
white-space: nowrap;
|
||||
font-family: monospace;
|
||||
}
|
||||
.beautify-options div {
|
||||
float: left;
|
||||
}
|
||||
.beautify-options div[newline="true"] + div {
|
||||
clear: left;
|
||||
}
|
||||
.beautify-options div[newline="true"] + div span[indent] {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
.beautify-options:after {
|
||||
clear: both;
|
||||
display: block;
|
||||
content: " ";
|
||||
height: 1rem;
|
||||
}
|
||||
.beautify-options span {
|
||||
font-weight: bold;
|
||||
}
|
||||
.beautify-options select {
|
||||
border: none;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/************ reponsive layouts ************/
|
||||
@media(max-width:737px) {
|
||||
#header {
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: inherit;
|
||||
border-right: none;
|
||||
border-bottom: 1px dashed #AAA;
|
||||
}
|
||||
#header section:not(:last-child) {
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
#header input[type="checkbox"] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
h2 {
|
||||
display: none;
|
||||
}
|
||||
#basic-info {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
#basic-info > * {
|
||||
flex: auto;
|
||||
}
|
||||
#basic-info > *:first-child {
|
||||
flex-grow: 99;
|
||||
display: flex;
|
||||
}
|
||||
#basic-info > *:not(:last-child) {
|
||||
margin-right: 0.8rem;
|
||||
}
|
||||
#basic-info #name {
|
||||
width: auto;
|
||||
flex-grow: 99;
|
||||
}
|
||||
#actions {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
#actions > * {
|
||||
display: inline-block;
|
||||
}
|
||||
#options {
|
||||
-webkit-column-count: 2;
|
||||
}
|
||||
#options .aligned > *:not(svg) {
|
||||
margin: 1px 0 0 0; /* workaround the flowing-padding column bug in webkit */
|
||||
padding-right: 0.4rem;
|
||||
vertical-align: baseline;
|
||||
min-height: 1.4rem;
|
||||
}
|
||||
.option {
|
||||
-webkit-column-break-inside: avoid;
|
||||
}
|
||||
.option label {
|
||||
line-height: 1.25rem;
|
||||
margin: 0;
|
||||
}
|
||||
#options [type="number"] {
|
||||
text-align: left; /* workaround the column flow bug in webkit */
|
||||
padding-left: 0.2rem;
|
||||
}
|
||||
#options #tabSize-label {
|
||||
position: relative;
|
||||
top: 0.2rem;
|
||||
}
|
||||
#lint h2 {
|
||||
display: block;
|
||||
cursor: default;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
#lint > div {
|
||||
max-height: 0;
|
||||
}
|
||||
#lint.collapsed > div {
|
||||
display: none;
|
||||
}
|
||||
#lint:hover > div {
|
||||
margin-top: 1em;
|
||||
max-height: 30vh;
|
||||
}
|
||||
#sections {
|
||||
padding-left: 0;
|
||||
}
|
||||
#sections > div {
|
||||
padding: 0;
|
||||
}
|
||||
#sections > *:not(h2) {
|
||||
padding-left: 0.4rem;
|
||||
}
|
||||
.applies-type {
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
@media(max-width:500px) {
|
||||
#options {
|
||||
-webkit-column-count: 1;
|
||||
}
|
||||
#options #tabSize-label {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<link id="cm-theme" rel="stylesheet">
|
||||
|
||||
<template data-id="appliesTo">
|
||||
<li>
|
||||
<select name="applies-type" class="applies-type style-contributor">
|
||||
<option value="url" i18n-text="appliesUrlOption"></option>
|
||||
<option value="url-prefix" i18n-text="appliesUrlPrefixOption"></option>
|
||||
<option value="domain" i18n-text="appliesDomainOption"></option>
|
||||
<option value="regexp" i18n-text="appliesRegexpOption"></option>
|
||||
</select>
|
||||
<input name="applies-value" class="applies-value style-contributor">
|
||||
<button class="remove-applies-to" i18n-text="appliesRemove"></button>
|
||||
<button class="add-applies-to" i18n-text="appliesAdd"></button>
|
||||
</li>
|
||||
</template>
|
||||
<template data-id="appliesToEverything">
|
||||
<li class="applies-to-everything" i18n-html="appliesToEverything">
|
||||
<button class="add-applies-to" i18n-text="appliesSpecify"></button>
|
||||
</li>
|
||||
</template>
|
||||
<template data-id="section">
|
||||
<div>
|
||||
<label i18n-text="sectionCode"></label>
|
||||
<textarea class="code"></textarea>
|
||||
<br>
|
||||
<div class="applies-to">
|
||||
<label i18n-text="appliesLabel">
|
||||
<svg class="svg-icon info applies-to-help"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</label>
|
||||
<ul class="applies-to-list"></ul>
|
||||
</div>
|
||||
<button class="remove-section" i18n-text="sectionRemove"></button>
|
||||
<button class="add-section" i18n-text="sectionAdd"></button>
|
||||
<button class="beautify-section" i18n-text="styleBeautify"></button>
|
||||
<button class="test-regexp" i18n-text="styleRegexpTestButton"></button>
|
||||
</div>
|
||||
</template>
|
||||
<template data-id="find">
|
||||
<span i18n-text="search">: <input type="text" class="CodeMirror-search-field" spellcheck="false">
|
||||
<span class="CodeMirror-search-hint">(<span i18n-text="searchRegexp"></span>)</span>
|
||||
</span>
|
||||
</template>
|
||||
<template data-id="replace">
|
||||
<span i18n-text="replace">: <input type="text" class="CodeMirror-search-field" spellcheck="false">
|
||||
<span class="CodeMirror-search-hint">(<span i18n-text="searchRegexp"></span>)</span>
|
||||
</span>
|
||||
</template>
|
||||
<template data-id="replaceAll">
|
||||
<span i18n-text="replaceAll">: <input type="text" class="CodeMirror-search-field" spellcheck="false">
|
||||
<span class="CodeMirror-search-hint">(<span i18n-text="searchRegexp"></span>)</span>
|
||||
</span>
|
||||
</template>
|
||||
<template data-id="replaceWith">
|
||||
<span i18n-text="replaceWith">: <input type="text" class="CodeMirror-search-field" spellcheck="false">
|
||||
</span>
|
||||
</template>
|
||||
<template data-id="replaceConfirm">
|
||||
<span i18n-text="replace">?
|
||||
<button i18n-text="confirmYes"></button>
|
||||
<button i18n-text="confirmNo"></button>
|
||||
<button i18n-text="confirmStop"></button>
|
||||
</span>
|
||||
</template>
|
||||
<template data-id="jumpToLine">
|
||||
<span i18n-text="editGotoLine">: <input class="CodeMirror-jump-field" type="text"></span>
|
||||
</template>
|
||||
<template data-id="regexpTestPartial">
|
||||
<a target="_blank" href="https://github.com/stylish-userstyles/stylish/wiki/Applying-styles-to-specific-sites#advanced-matching-with-regular-expressions"><svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></a>
|
||||
</template>
|
||||
<template data-id="resizeGrip">
|
||||
<div class="resize-grip" i18n-title="cm_resizeGripHint"></div>
|
||||
</template>
|
||||
</head>
|
||||
|
||||
<body id="stylus-edit">
|
||||
<div id="header">
|
||||
<h1 id="heading"> </h1> <!-- nbsp allocates the actual height which prevents page shift -->
|
||||
<section id="basic-info">
|
||||
<div id="basic-info-name">
|
||||
<input id="name" class="style-contributor" i18n-placeholder="styleMissingName" spellcheck="false">
|
||||
<a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a>
|
||||
</div>
|
||||
<div id="basic-info-enabled">
|
||||
<input type="checkbox" id="enabled" class="style-contributor">
|
||||
<label for="enabled" id="enabled-label" i18n-text="styleEnabledLabel"></label><!--
|
||||
--><svg id="toggle-style-help" class="svg-icon info">
|
||||
<use xlink:href="#svg-icon-help"/>
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
<section id="actions">
|
||||
<div>
|
||||
<button id="save-button" title="Ctrl-S" i18n-text="styleSaveLabel"></button>
|
||||
<button id="beautify" i18n-text="styleBeautify"></button>
|
||||
<a href="manage.html"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
|
||||
</div>
|
||||
<div>
|
||||
<h2 id="mozilla-format-heading" i18n-text="styleMozillaFormatHeading"><svg id="to-mozilla-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></h2>
|
||||
<button id="from-mozilla" i18n-text="importLabel"></button>
|
||||
<button id="to-mozilla" i18n-text="exportLabel"></button>
|
||||
</div>
|
||||
</section>
|
||||
<section id="options">
|
||||
<h2 id="options-heading" i18n-text="optionsHeading"></h2>
|
||||
<div class="option">
|
||||
<input id="editor.lineWrapping" type="checkbox">
|
||||
<label id="lineWrapping-label" for="editor.lineWrapping" i18n-text="cm_lineWrapping"></label>
|
||||
</div>
|
||||
<div class="option">
|
||||
<input id="editor.smartIndent" type="checkbox">
|
||||
<label id="smartIndent-label" for="editor.smartIndent" i18n-text="cm_smartIndent"></label>
|
||||
</div>
|
||||
<div class="option">
|
||||
<input id="editor.indentWithTabs" type="checkbox">
|
||||
<label id="indentWithTabs-label" for="editor.indentWithTabs" i18n-text="cm_indentWithTabs"></label>
|
||||
</div>
|
||||
<div class="option">
|
||||
<input id="editor.autocompleteOnTyping" type="checkbox">
|
||||
<label for="editor.autocompleteOnTyping" i18n-text="cm_autocompleteOnTyping"></label>
|
||||
</div>
|
||||
<div class="option aligned">
|
||||
<label id="tabSize-label" for="editor.tabSize" i18n-text="cm_tabSize"></label>
|
||||
<input id="editor.tabSize" type="number" min="0">
|
||||
</div>
|
||||
<div class="option aligned">
|
||||
<label id="keyMap-label" for="editor.keyMap" i18n-text="cm_keyMap"></label>
|
||||
<select id="editor.keyMap"></select>
|
||||
<svg id="keyMap-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</div>
|
||||
<div class="option aligned">
|
||||
<label id="theme-label" for="editor.theme" i18n-text="cm_theme"></label>
|
||||
<select id="editor.theme"></select>
|
||||
</div>
|
||||
<div class="option aligned">
|
||||
<label id="highlight-label" for="editor.matchHighlight" i18n-text="cm_matchHighlight"></label>
|
||||
<select id="editor.matchHighlight">
|
||||
<option i18n-text="cm_matchHighlightToken" value="token">
|
||||
<option i18n-text="cm_matchHighlightSelection" value="selection">
|
||||
<option i18n-text="genericDisabledLabel" value="">
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
<section id="lint"><h2 i18n-text="issues">: <span id="issue-count"></span><svg id="lint-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></h2><div></div></section>
|
||||
</div>
|
||||
<section id="sections">
|
||||
<h2><span id="sections-heading" i18n-text="styleSectionsTitle"></span><svg id="sections-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></h2>
|
||||
</section>
|
||||
<div id="help-popup">
|
||||
<div class="title"></div><svg id="sections-help" class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg></svg>
|
||||
<div class="contents"></div>
|
||||
</div>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none">
|
||||
<symbol id="svg-icon-external-link" height="16" width="16" viewBox="0 0 8 8">
|
||||
<path d="M0 0v8h8v-2h-1v1h-6v-6h1v-1h-2zm4 0l1.5 1.5-2.5 2.5 1 1 2.5-2.5 1.5 1.5v-4h-4z"></path>
|
||||
</symbol>
|
||||
<symbol id="svg-icon-help" height="16" width="14" viewBox="0 0 14 16" i18n-alt="helpAlt">
|
||||
<path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path>
|
||||
</symbol>
|
||||
<symbol id="svg-icon-close" height="16" width="12" viewBox="0 0 12 16">
|
||||
<path fill-rule="evenodd" d="M7.48 8l3.75 3.75-1.48 1.48L6 9.48l-3.75 3.75-1.48-1.48L4.52 8 .77 4.25l1.48-1.48L6 6.52l3.75-3.75 1.48 1.48z"></path>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
</body>
|
||||
</body>
|
||||
</html>
|
||||
|
|
576
edit/edit.css
Normal file
576
edit/edit.css
Normal file
|
@ -0,0 +1,576 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font: 12px arial,sans-serif;
|
||||
}
|
||||
/************ header ************/
|
||||
#header {
|
||||
width: 280px;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
padding: 15px;
|
||||
border-right: 1px dashed #AAA;
|
||||
-webkit-box-shadow: 0 0 3rem -1.2rem black;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#header h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
#sections {
|
||||
padding-left: 280px;
|
||||
}
|
||||
#sections h2 {
|
||||
margin-top: 1rem;
|
||||
margin-left: 1.7rem;
|
||||
}
|
||||
.aligned {
|
||||
display: table-row;
|
||||
}
|
||||
.aligned > *:not(svg) {
|
||||
display: table-cell;
|
||||
margin-top: 0.1rem;
|
||||
min-height: 1.4rem;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
margin-left: 0.1rem;
|
||||
}
|
||||
/* basic info */
|
||||
#basic-info {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
#name {
|
||||
width: 100%;
|
||||
}
|
||||
#basic-info-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
#url {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
#url:not([href^="http"]) {
|
||||
display: none;
|
||||
}
|
||||
#save-button {
|
||||
opacity: .5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.dirty #save-button {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
.svg-icon {
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
transition: fill .5s;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.svg-icon:not(.dismiss) {
|
||||
margin-left: 0.2rem;
|
||||
}
|
||||
h2 .svg-icon, label .svg-icon {
|
||||
margin-top: -1px;
|
||||
}
|
||||
.svg-icon.info {
|
||||
width: 14px;
|
||||
height: 16px;
|
||||
}
|
||||
.svg-icon:hover,
|
||||
.svg-icon.info {
|
||||
fill: #666;
|
||||
}
|
||||
.svg-icon,
|
||||
.svg-icon.info:hover {
|
||||
fill: #000;
|
||||
}
|
||||
#enabled {
|
||||
margin-left: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#enabled-label {
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* actions */
|
||||
#actions > * {
|
||||
margin-right: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
/* options */
|
||||
#options [type="number"] {
|
||||
max-width: 2.5rem;
|
||||
text-align: right;
|
||||
}
|
||||
#options .option > * {
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
/************ content ***********/
|
||||
#sections > div {
|
||||
margin: 0.7rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
#sections > div:not(:first-of-type) {
|
||||
border-top: 2px solid black;
|
||||
}
|
||||
#sections > div:only-of-type .remove-section {
|
||||
display: none;
|
||||
}
|
||||
#sections > div > button:not(:first-of-type) {
|
||||
margin-left: 0.2rem;
|
||||
}
|
||||
.dirty > label::before {
|
||||
content: "*";
|
||||
font-weight: bold;
|
||||
}
|
||||
#sections {
|
||||
counter-reset: codebox;
|
||||
}
|
||||
#sections > div > label::after {
|
||||
counter-increment: codebox;
|
||||
content: counter(codebox);
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
/* code */
|
||||
.CodeMirror-hint:hover {
|
||||
color: white;
|
||||
background: #08f;
|
||||
}
|
||||
.code {
|
||||
height: 10rem;
|
||||
width: 40rem;
|
||||
}
|
||||
.CodeMirror {
|
||||
border: solid #CCC 1px;
|
||||
}
|
||||
.CodeMirror-scroll {
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 6px; /* resize-grip height */
|
||||
}
|
||||
.CodeMirror-lint-mark-warning {
|
||||
background: none;
|
||||
}
|
||||
.CodeMirror-vscrollbar {
|
||||
margin-bottom: 7px; /* make space for resize-grip */
|
||||
}
|
||||
.CodeMirror-hscrollbar {
|
||||
bottom: 7px; /* make space for resize-grip */
|
||||
}
|
||||
.CodeMirror-scrollbar-filler {
|
||||
bottom: 7px; /* make space for resize-grip */
|
||||
}
|
||||
.CodeMirror-dialog {
|
||||
-webkit-animation: highlight 3s ease-out;
|
||||
}
|
||||
.CodeMirror-focused {
|
||||
outline: -webkit-focus-ring-color auto 5px;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.CodeMirror-search-field {
|
||||
width: 10em;
|
||||
}
|
||||
.CodeMirror-jump-field {
|
||||
width: 5em;
|
||||
}
|
||||
.CodeMirror-search-hint {
|
||||
color: #888;
|
||||
}
|
||||
body[data-match-highlight="token"] .cm-matchhighlight-approved .cm-matchhighlight,
|
||||
body[data-match-highlight="token"] .CodeMirror-selection-highlight-scrollbar {
|
||||
animation: fadein-match-highlighter 1s cubic-bezier(.97,.01,.42,.98);
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
body[data-match-highlight="selection"] .cm-matchhighlight-approved .cm-matchhighlight,
|
||||
body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar {
|
||||
background-color: rgba(1, 151, 193, 0.1);
|
||||
}
|
||||
@-webkit-keyframes highlight {
|
||||
from {
|
||||
background-color: #ff9;
|
||||
}
|
||||
to {
|
||||
background-color: none;
|
||||
}
|
||||
}
|
||||
@keyframes fadein {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes fadein-match-highlighter {
|
||||
from { background-color: transparent; }
|
||||
to { background-color: rgba(1, 151, 193, 0.1); }
|
||||
}
|
||||
.resize-grip {
|
||||
position: absolute;
|
||||
display: block;
|
||||
height: 6px;
|
||||
content: "";
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9;
|
||||
cursor: n-resize;
|
||||
background-color: inherit;
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
border-top-color: inherit;
|
||||
}
|
||||
.resize-grip:after {
|
||||
content: "";
|
||||
bottom: 2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 8px;
|
||||
display: block;
|
||||
position: absolute;
|
||||
border-top-width: 2px;
|
||||
border-top-style: dotted;
|
||||
border-top-color: inherit;
|
||||
}
|
||||
/* applies-to */
|
||||
.applies-to {
|
||||
display: flex;
|
||||
}
|
||||
.applies-to label {
|
||||
flex: auto;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
.applies-to ul {
|
||||
flex: auto;
|
||||
flex-grow: 99;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.applies-to li {
|
||||
display: flex;
|
||||
list-style-type: none;
|
||||
align-items: center;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
.applies-to li > *:not(button) {
|
||||
flex: auto;
|
||||
min-height: 1.4rem;
|
||||
margin-left: 0.35rem;
|
||||
}
|
||||
.applies-to li .add-applies-to {
|
||||
visibility: hidden;
|
||||
text-align: left;
|
||||
}
|
||||
.applies-to li:last-child .add-applies-to {
|
||||
visibility: visible
|
||||
}
|
||||
.applies-to li .add-applies-to:first-child {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.applies-to li .applies-value {
|
||||
flex-grow: 99;
|
||||
padding-left: 0.2rem;
|
||||
}
|
||||
.applies-to img {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
.test-regexp {
|
||||
display: none;
|
||||
}
|
||||
.has-regexp .test-regexp {
|
||||
display: inline-block;
|
||||
}
|
||||
.regexp-report summary, .regexp-report div {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
.regexp-report mark {
|
||||
background-color: rgba(255, 255, 0, .5);
|
||||
}
|
||||
.regexp-report details {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.regexp-report details:not(:last-child) {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.regexp-report summary {
|
||||
font-weight: bold;
|
||||
margin-left: -1rem;
|
||||
margin-bottom: .5rem;
|
||||
outline: none;
|
||||
cursor: default;
|
||||
}
|
||||
.regexp-report details[data-type="full"] {
|
||||
color: darkgreen;
|
||||
}
|
||||
.regexp-report details[data-type="partial"] {
|
||||
color: darkgray;
|
||||
}
|
||||
.regexp-report details[data-type="invalid"] {
|
||||
color: maroon;
|
||||
}
|
||||
.regexp-report details details {
|
||||
margin-left: 2rem;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
.regexp-report .svg-icon {
|
||||
position: absolute;
|
||||
margin-top: -1px;
|
||||
}
|
||||
.regexp-report details div:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration-skip: ink;
|
||||
}
|
||||
.regexp-report details div img {
|
||||
width: 16px;
|
||||
max-height: 16px;
|
||||
position: absolute;
|
||||
margin-left: -20px;
|
||||
margin-top: -1px;
|
||||
animation: fadein 1s cubic-bezier(.03, .67, .08, .94);
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
/************ help popup ************/
|
||||
#help-popup {
|
||||
top: 3rem;
|
||||
right: 3rem;
|
||||
max-width: 50vw;
|
||||
position: fixed;
|
||||
display: none;
|
||||
background-color: white;
|
||||
box-shadow: 3px 3px 30px rgba(0, 0, 0, 0.5);
|
||||
padding: 0.5rem;
|
||||
z-index: 99;
|
||||
}
|
||||
#help-popup.big {
|
||||
max-width: 100%;
|
||||
left: 3rem;
|
||||
}
|
||||
#help-popup.big .CodeMirror {
|
||||
min-height: 2rem;
|
||||
height: 70vh;
|
||||
}
|
||||
#help-popup .title {
|
||||
font-weight: bold;
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
margin: -0.5rem -0.5rem 0.5rem;
|
||||
padding: .5rem 32px .5rem .5rem;
|
||||
}
|
||||
#help-popup .contents {
|
||||
max-height: calc(100vh - 8rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
#help-popup .dismiss {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: .5em;
|
||||
}
|
||||
|
||||
.keymap-list {
|
||||
font-size: 85%;
|
||||
line-height: 1.0;
|
||||
border-spacing: 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
.keymap-list input {
|
||||
width: 100%;
|
||||
}
|
||||
.keymap-list tr:nth-child(odd) {
|
||||
background-color: rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
.keymap-list td:first-child {
|
||||
white-space: nowrap;
|
||||
font-family: monospace;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
#help-popup button[name^="import"] {
|
||||
line-height: 1.5rem;
|
||||
padding: 0 0.5rem;
|
||||
margin: 0.5rem 0 0 0.5rem;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
float: right;
|
||||
}
|
||||
#help-popup.ready button[name^="import"] {
|
||||
pointer-events: all;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
/************ lint ************/
|
||||
#lint {
|
||||
display: none;
|
||||
}
|
||||
#lint > div {
|
||||
overflow-y: auto;
|
||||
}
|
||||
#lint table {
|
||||
font-size: 100%;
|
||||
border-spacing: 0;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.0;
|
||||
}
|
||||
#lint table:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
#lint caption {
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
}
|
||||
#lint tbody {
|
||||
font-size: 85%;
|
||||
cursor: pointer;
|
||||
}
|
||||
#lint tr:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
#lint td[role="severity"] {
|
||||
font-size: 0;
|
||||
width: 16px;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
#lint td[role="line"], #lint td[role="sep"] {
|
||||
text-align: right;
|
||||
padding-right: 0;
|
||||
}
|
||||
#lint td[role="col"] {
|
||||
text-align: left;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
#lint td[role="message"] {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/************ CSS beautifier ************/
|
||||
.beautify-options {
|
||||
white-space: nowrap;
|
||||
font-family: monospace;
|
||||
}
|
||||
.beautify-options div {
|
||||
float: left;
|
||||
}
|
||||
.beautify-options div[newline="true"] + div {
|
||||
clear: left;
|
||||
}
|
||||
.beautify-options div[newline="true"] + div span[indent] {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
.beautify-options:after {
|
||||
clear: both;
|
||||
display: block;
|
||||
content: " ";
|
||||
height: 1rem;
|
||||
}
|
||||
.beautify-options span {
|
||||
font-weight: bold;
|
||||
}
|
||||
.beautify-options select {
|
||||
border: none;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/************ reponsive layouts ************/
|
||||
@media(max-width:737px) {
|
||||
#header {
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: inherit;
|
||||
border-right: none;
|
||||
border-bottom: 1px dashed #AAA;
|
||||
}
|
||||
#header section:not(:last-child) {
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
#header input[type="checkbox"] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
h2 {
|
||||
display: none;
|
||||
}
|
||||
#basic-info {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
#basic-info > * {
|
||||
flex: auto;
|
||||
}
|
||||
#basic-info > *:first-child {
|
||||
flex-grow: 99;
|
||||
display: flex;
|
||||
}
|
||||
#basic-info > *:not(:last-child) {
|
||||
margin-right: 0.8rem;
|
||||
}
|
||||
#basic-info #name {
|
||||
width: auto;
|
||||
flex-grow: 99;
|
||||
}
|
||||
#actions {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
#actions > * {
|
||||
display: inline-block;
|
||||
}
|
||||
#options {
|
||||
-webkit-column-count: 2;
|
||||
}
|
||||
#options .aligned > *:not(svg) {
|
||||
margin: 1px 0 0 0; /* workaround the flowing-padding column bug in webkit */
|
||||
padding-right: 0.4rem;
|
||||
vertical-align: baseline;
|
||||
min-height: 1.4rem;
|
||||
}
|
||||
.option {
|
||||
-webkit-column-break-inside: avoid;
|
||||
}
|
||||
.option label {
|
||||
line-height: 1.25rem;
|
||||
margin: 0;
|
||||
}
|
||||
#options [type="number"] {
|
||||
text-align: left; /* workaround the column flow bug in webkit */
|
||||
padding-left: 0.2rem;
|
||||
}
|
||||
#options #tabSize-label {
|
||||
position: relative;
|
||||
top: 0.2rem;
|
||||
}
|
||||
#lint h2 {
|
||||
display: block;
|
||||
cursor: default;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
#lint > div {
|
||||
max-height: 0;
|
||||
}
|
||||
#lint.collapsed > div {
|
||||
display: none;
|
||||
}
|
||||
#lint:hover > div {
|
||||
margin-top: 1em;
|
||||
max-height: 30vh;
|
||||
}
|
||||
#sections {
|
||||
padding-left: 0;
|
||||
}
|
||||
#sections > div {
|
||||
padding: 0;
|
||||
}
|
||||
#sections > *:not(h2) {
|
||||
padding-left: 0.4rem;
|
||||
}
|
||||
.applies-type {
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
@media(max-width:500px) {
|
||||
#options {
|
||||
-webkit-column-count: 1;
|
||||
}
|
||||
#options #tabSize-label {
|
||||
position: static;
|
||||
}
|
||||
}
|
2072
edit/edit.js
Normal file
2072
edit/edit.js
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -12,6 +12,7 @@ for (const type of [NodeList, NamedNodeMap, HTMLCollection, HTMLAllCollection])
|
|||
}
|
||||
|
||||
// add favicon in Firefox
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
navigator.userAgent.includes('Firefox') && setTimeout(() => {
|
||||
const iconset = ['', 'light/'][prefs.get('iconset')] || '';
|
||||
for (const size of [38, 32, 19, 16]) {
|
||||
|
@ -26,7 +27,7 @@ navigator.userAgent.includes('Firefox') && setTimeout(() => {
|
|||
|
||||
|
||||
function onDOMready() {
|
||||
if (document.readyState != 'loading') {
|
||||
if (document.readyState !== 'loading') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
|
@ -78,9 +79,9 @@ function enforceInputRange(element) {
|
|||
const max = Number(element.max);
|
||||
const doNotify = () => element.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
const onChange = ({type}) => {
|
||||
if (type == 'input' && element.checkValidity()) {
|
||||
if (type === 'input' && element.checkValidity()) {
|
||||
doNotify();
|
||||
} else if (type == 'change' && !element.checkValidity()) {
|
||||
} else if (type === 'change' && !element.checkValidity()) {
|
||||
element.value = Math.max(min, Math.min(max, Number(element.value)));
|
||||
doNotify();
|
||||
}
|
||||
|
@ -112,7 +113,7 @@ function $element(opt) {
|
|||
? opt.tag.split('#')
|
||||
: [null, opt.tag];
|
||||
const element = ns
|
||||
? document.createElementNS(ns == 'SVG' || ns == 'svg' ? 'http://www.w3.org/2000/svg' : ns, tag)
|
||||
? document.createElementNS(ns === 'SVG' || ns === 'svg' ? 'http://www.w3.org/2000/svg' : ns, tag)
|
||||
: document.createElement(tag || 'div');
|
||||
(opt.appendChild instanceof Array ? opt.appendChild : [opt.appendChild])
|
||||
.forEach(child => child && element.appendChild(child));
|
|
@ -7,7 +7,7 @@ tDocLoader();
|
|||
function t(key, params) {
|
||||
const cache = !params && t.cache[key];
|
||||
const s = cache || chrome.i18n.getMessage(key, params);
|
||||
if (s == '') {
|
||||
if (s === '') {
|
||||
throw `Missing string "${key}"`;
|
||||
}
|
||||
if (!params && !cache) {
|
||||
|
@ -20,7 +20,7 @@ function t(key, params) {
|
|||
function tE(id, key, attr, esc) {
|
||||
if (attr) {
|
||||
document.getElementById(id).setAttribute(attr, t(key));
|
||||
} else if (typeof esc == 'undefined' || esc) {
|
||||
} else if (typeof esc === 'undefined' || esc) {
|
||||
document.getElementById(id).appendChild(document.createTextNode(t(key)));
|
||||
} else {
|
||||
document.getElementById(id).innerHTML = t(key);
|
||||
|
@ -43,10 +43,10 @@ function tNodeList(nodes) {
|
|||
for (let n = nodes.length; --n >= 0;) {
|
||||
const node = nodes[n];
|
||||
// skip non-ELEMENT_NODE
|
||||
if (node.nodeType != 1) {
|
||||
if (node.nodeType !== 1) {
|
||||
continue;
|
||||
}
|
||||
if (node.localName == 'template') {
|
||||
if (node.localName === 'template') {
|
||||
const elements = node.content.querySelectorAll('*');
|
||||
tNodeList(elements);
|
||||
template[node.dataset.id] = elements[0];
|
||||
|
@ -94,7 +94,7 @@ function tDocLoader() {
|
|||
|
||||
// reset L10N cache on UI language change
|
||||
const UIlang = chrome.i18n.getUILanguage();
|
||||
if (t.cache.browserUIlanguage != UIlang) {
|
||||
if (t.cache.browserUIlanguage !== UIlang) {
|
||||
t.cache = {browserUIlanguage: UIlang};
|
||||
localStorage.L10N = JSON.stringify(t.cache);
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ function tDocLoader() {
|
|||
const onLoad = () => {
|
||||
tDocLoader.stop();
|
||||
process(observer.takeRecords());
|
||||
if (cacheLength != Object.keys(t.cache).length) {
|
||||
if (cacheLength !== Object.keys(t.cache).length) {
|
||||
localStorage.L10N = JSON.stringify(t.cache);
|
||||
}
|
||||
};
|
|
@ -1,371 +1,371 @@
|
|||
/* global BG: true, onRuntimeMessage, applyOnMessage, handleUpdate, handleDelete */
|
||||
'use strict';
|
||||
|
||||
// keep message channel open for sendResponse in chrome.runtime.onMessage listener
|
||||
const KEEP_CHANNEL_OPEN = true;
|
||||
|
||||
const FIREFOX = /Firefox/.test(navigator.userAgent);
|
||||
const OPERA = /OPR/.test(navigator.userAgent);
|
||||
|
||||
const URLS = {
|
||||
ownOrigin: chrome.runtime.getURL(''),
|
||||
|
||||
optionsUI: [
|
||||
chrome.runtime.getURL('options/index.html'),
|
||||
'chrome://extensions/?options=' + chrome.runtime.id,
|
||||
],
|
||||
|
||||
configureCommands:
|
||||
OPERA ? 'opera://settings/configureCommands'
|
||||
: 'chrome://extensions/configureCommands',
|
||||
|
||||
// CWS cannot be scripted in chromium, see ChromeExtensionsClient::IsScriptableURL
|
||||
// https://cs.chromium.org/chromium/src/chrome/common/extensions/chrome_extensions_client.cc
|
||||
chromeWebStore: FIREFOX ? 'https://addons.mozilla.org/' : (
|
||||
OPERA ? 'https://addons.opera.com/' : 'https://chrome.google.com/webstore/'
|
||||
),
|
||||
|
||||
supported: new RegExp(
|
||||
'^(file|ftps?|http)://|' +
|
||||
`^https://${FIREFOX ? '(?!addons\\.mozilla\\.org)' : (
|
||||
OPERA ? '(?!addons\\.opera\\.com)' : '(?!chrome\\.google\\.com/webstore)'
|
||||
)}|` +
|
||||
'^' + chrome.runtime.getURL('')),
|
||||
};
|
||||
|
||||
let BG = chrome.extension.getBackgroundPage();
|
||||
|
||||
if (!BG || BG != window) {
|
||||
document.documentElement.classList.toggle('firefox', FIREFOX);
|
||||
document.documentElement.classList.toggle('opera', OPERA);
|
||||
// TODO: remove once our manifest's minimum_chrome_version is 50+
|
||||
// Chrome 49 doesn't report own extension pages in webNavigation apparently
|
||||
if (navigator.userAgent.includes('Chrome/49.')) {
|
||||
getActiveTab().then(BG.updateIcon);
|
||||
}
|
||||
}
|
||||
|
||||
function notifyAllTabs(msg) {
|
||||
const originalMessage = msg;
|
||||
if (msg.method == 'styleUpdated' || msg.method == 'styleAdded') {
|
||||
// apply/popup/manage use only meta for these two methods,
|
||||
// editor may need the full code but can fetch it directly,
|
||||
// so we send just the meta to avoid spamming lots of tabs with huge styles
|
||||
msg = Object.assign({}, msg, {
|
||||
style: getStyleWithNoCode(msg.style)
|
||||
});
|
||||
}
|
||||
const affectsAll = !msg.affects || msg.affects.all;
|
||||
const affectsOwnOriginOnly = !affectsAll && (msg.affects.editor || msg.affects.manager);
|
||||
const affectsTabs = affectsAll || affectsOwnOriginOnly;
|
||||
const affectsIcon = affectsAll || msg.affects.icon;
|
||||
const affectsPopup = affectsAll || msg.affects.popup;
|
||||
const affectsSelf = affectsPopup || msg.prefs;
|
||||
if (affectsTabs || affectsIcon) {
|
||||
const notifyTab = tab => {
|
||||
// own pages will be notified via runtime.sendMessage later
|
||||
if ((affectsTabs || URLS.optionsUI.includes(tab.url))
|
||||
&& !(affectsSelf && tab.url.startsWith(URLS.ownOrigin))
|
||||
// skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF
|
||||
&& (!FIREFOX || tab.width)) {
|
||||
chrome.tabs.sendMessage(tab.id, msg);
|
||||
}
|
||||
if (affectsIcon && BG) {
|
||||
BG.updateIcon(tab);
|
||||
}
|
||||
};
|
||||
// list all tabs including chrome-extension:// which can be ours
|
||||
Promise.all([
|
||||
queryTabs(affectsOwnOriginOnly ? {url: URLS.ownOrigin + '*'} : {}),
|
||||
getActiveTab(),
|
||||
]).then(([tabs, activeTab]) => {
|
||||
const activeTabId = activeTab && activeTab.id;
|
||||
for (const tab of tabs) {
|
||||
invokeOrPostpone(tab.id === activeTabId, notifyTab, tab);
|
||||
}
|
||||
});
|
||||
}
|
||||
// notify self: the message no longer is sent to the origin in new Chrome
|
||||
if (typeof onRuntimeMessage != 'undefined') {
|
||||
onRuntimeMessage(originalMessage);
|
||||
}
|
||||
// notify apply.js on own pages
|
||||
if (typeof applyOnMessage != 'undefined') {
|
||||
applyOnMessage(originalMessage);
|
||||
}
|
||||
// notify background page and all open popups
|
||||
if (affectsSelf) {
|
||||
chrome.runtime.sendMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function queryTabs(options = {}) {
|
||||
return new Promise(resolve =>
|
||||
chrome.tabs.query(options, tabs =>
|
||||
resolve(tabs)));
|
||||
}
|
||||
|
||||
|
||||
function getTab(id) {
|
||||
return new Promise(resolve =>
|
||||
chrome.tabs.get(id, tab =>
|
||||
!chrome.runtime.lastError && resolve(tab)));
|
||||
}
|
||||
|
||||
|
||||
function getOwnTab() {
|
||||
return new Promise(resolve =>
|
||||
chrome.tabs.getCurrent(tab => resolve(tab)));
|
||||
}
|
||||
|
||||
|
||||
function getActiveTab() {
|
||||
return queryTabs({currentWindow: true, active: true})
|
||||
.then(tabs => tabs[0]);
|
||||
}
|
||||
|
||||
|
||||
function getActiveTabRealURL() {
|
||||
return getActiveTab()
|
||||
.then(getTabRealURL);
|
||||
}
|
||||
|
||||
|
||||
function getTabRealURL(tab) {
|
||||
return new Promise(resolve => {
|
||||
if (tab.url != 'chrome://newtab/') {
|
||||
resolve(tab.url);
|
||||
} else {
|
||||
chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, frame => {
|
||||
resolve(frame && frame.url || '');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// opens a tab or activates the already opened one,
|
||||
// reuses the New Tab page if it's focused now
|
||||
function openURL({url, currentWindow = true}) {
|
||||
if (!url.includes('://')) {
|
||||
url = chrome.runtime.getURL(url);
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
// [some] chromium forks don't handle their fake branded protocols
|
||||
url = url.replace(/^(opera|vivaldi)/, 'chrome');
|
||||
// FF doesn't handle moz-extension:// URLs (bug)
|
||||
// API doesn't handle the hash-fragment part
|
||||
const urlQuery = url.startsWith('moz-extension') ? undefined : url.replace(/#.*/, '');
|
||||
queryTabs({url: urlQuery, currentWindow}).then(tabs => {
|
||||
for (const tab of tabs) {
|
||||
if (tab.url == url) {
|
||||
activateTab(tab).then(resolve);
|
||||
return;
|
||||
}
|
||||
}
|
||||
getActiveTab().then(tab => {
|
||||
if (tab && tab.url == 'chrome://newtab/'
|
||||
// prevent redirecting incognito NTP to a chrome URL as it crashes Chrome
|
||||
&& (!url.startsWith('chrome') || !tab.incognito)) {
|
||||
chrome.tabs.update({url}, resolve);
|
||||
} else {
|
||||
chrome.tabs.create(tab && !FIREFOX ? {url, openerTabId: tab.id} : {url}, resolve);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function activateTab(tab) {
|
||||
return Promise.all([
|
||||
new Promise(resolve => {
|
||||
chrome.tabs.update(tab.id, {active: true}, resolve);
|
||||
}),
|
||||
new Promise(resolve => {
|
||||
chrome.windows.update(tab.windowId, {focused: true}, resolve);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
function stringAsRegExp(s, flags) {
|
||||
return new RegExp(s.replace(/[{}()[\]/\\.+?^$:=*!|]/g, '\\$&'), flags);
|
||||
}
|
||||
|
||||
|
||||
function ignoreChromeError() {
|
||||
chrome.runtime.lastError; // eslint-disable-line no-unused-expressions
|
||||
}
|
||||
|
||||
|
||||
function getStyleWithNoCode(style) {
|
||||
const stripped = Object.assign({}, style, {sections: []});
|
||||
for (const section of style.sections) {
|
||||
stripped.sections.push(Object.assign({}, section, {code: null}));
|
||||
}
|
||||
return stripped;
|
||||
}
|
||||
|
||||
|
||||
// js engine can't optimize the entire function if it contains try-catch
|
||||
// so we should keep it isolated from normal code in a minimal wrapper
|
||||
// Update: might get fixed in V8 TurboFan in the future
|
||||
function tryCatch(func, ...args) {
|
||||
try {
|
||||
return func(...args);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
||||
function tryRegExp(regexp) {
|
||||
try {
|
||||
return new RegExp(regexp);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
||||
function tryJSONparse(jsonString) {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
||||
const debounce = Object.assign((fn, delay, ...args) => {
|
||||
clearTimeout(debounce.timers.get(fn));
|
||||
debounce.timers.set(fn, setTimeout(debounce.run, delay, fn, ...args));
|
||||
}, {
|
||||
timers: new Map(),
|
||||
run(fn, ...args) {
|
||||
debounce.timers.delete(fn);
|
||||
fn(...args);
|
||||
},
|
||||
unregister(fn) {
|
||||
clearTimeout(debounce.timers.get(fn));
|
||||
debounce.timers.delete(fn);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
function deepCopy(obj) {
|
||||
return obj !== null && obj !== undefined && typeof obj == 'object'
|
||||
? deepMerge(typeof obj.slice == 'function' ? [] : {}, obj)
|
||||
: obj;
|
||||
}
|
||||
|
||||
|
||||
function deepMerge(target, ...args) {
|
||||
const isArray = typeof target.slice == 'function';
|
||||
for (const obj of args) {
|
||||
if (isArray && obj !== null && obj !== undefined) {
|
||||
for (const element of obj) {
|
||||
target.push(deepCopy(element));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (const k in obj) {
|
||||
const value = obj[k];
|
||||
if (k in target && typeof value == 'object' && value !== null) {
|
||||
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 && BG.getStyles ? Promise.resolve() : new Promise(function ping(resolve) {
|
||||
chrome.runtime.sendMessage({method: 'healthCheck'}, health => {
|
||||
if (health !== undefined) {
|
||||
BG = chrome.extension.getBackgroundPage();
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(ping, 0, resolve);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage
|
||||
function getStylesSafe(options) {
|
||||
return onBackgroundReady()
|
||||
.then(() => BG.getStyles(options));
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function download(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.timeout = 10e3;
|
||||
xhr.onloadend = () => (xhr.status == 200
|
||||
? resolve(xhr.responseText)
|
||||
: reject(xhr.status));
|
||||
const [mainUrl, query] = url.split('?');
|
||||
xhr.open(query ? 'POST' : 'GET', mainUrl, true);
|
||||
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
xhr.send(query);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function doTimeout(ms = 0, ...args) {
|
||||
return ms > 0
|
||||
? () => new Promise(resolve => setTimeout(resolve, ms, ...args))
|
||||
: new Promise(resolve => setTimeout(resolve, 0, ...args));
|
||||
}
|
||||
|
||||
|
||||
function invokeOrPostpone(isInvoke, fn, ...args) {
|
||||
return isInvoke
|
||||
? fn(...args)
|
||||
: setTimeout(invokeOrPostpone, 0, true, fn, ...args);
|
||||
}
|
||||
/* global BG: true, onRuntimeMessage, applyOnMessage, handleUpdate, handleDelete */
|
||||
'use strict';
|
||||
|
||||
// keep message channel open for sendResponse in chrome.runtime.onMessage listener
|
||||
const KEEP_CHANNEL_OPEN = true;
|
||||
|
||||
const FIREFOX = /Firefox/.test(navigator.userAgent);
|
||||
const OPERA = /OPR/.test(navigator.userAgent);
|
||||
|
||||
const URLS = {
|
||||
ownOrigin: chrome.runtime.getURL(''),
|
||||
|
||||
optionsUI: [
|
||||
chrome.runtime.getURL('options.html'),
|
||||
'chrome://extensions/?options=' + chrome.runtime.id,
|
||||
],
|
||||
|
||||
configureCommands:
|
||||
OPERA ? 'opera://settings/configureCommands'
|
||||
: 'chrome://extensions/configureCommands',
|
||||
|
||||
// CWS cannot be scripted in chromium, see ChromeExtensionsClient::IsScriptableURL
|
||||
// https://cs.chromium.org/chromium/src/chrome/common/extensions/chrome_extensions_client.cc
|
||||
chromeWebStore: FIREFOX ? 'https://addons.mozilla.org/' : (
|
||||
OPERA ? 'https://addons.opera.com/' : 'https://chrome.google.com/webstore/'
|
||||
),
|
||||
|
||||
supported: new RegExp(
|
||||
'^(file|ftps?|http)://|' +
|
||||
`^https://${FIREFOX ? '(?!addons\\.mozilla\\.org)' : (
|
||||
OPERA ? '(?!addons\\.opera\\.com)' : '(?!chrome\\.google\\.com/webstore)'
|
||||
)}|` +
|
||||
'^' + chrome.runtime.getURL('')),
|
||||
};
|
||||
|
||||
let BG = chrome.extension.getBackgroundPage();
|
||||
|
||||
if (!BG || BG !== window) {
|
||||
document.documentElement.classList.toggle('firefox', FIREFOX);
|
||||
document.documentElement.classList.toggle('opera', OPERA);
|
||||
// TODO: remove once our manifest's minimum_chrome_version is 50+
|
||||
// Chrome 49 doesn't report own extension pages in webNavigation apparently
|
||||
if (navigator.userAgent.includes('Chrome/49.')) {
|
||||
getActiveTab().then(BG.updateIcon);
|
||||
}
|
||||
}
|
||||
|
||||
function notifyAllTabs(msg) {
|
||||
const originalMessage = msg;
|
||||
if (msg.method === 'styleUpdated' || msg.method === 'styleAdded') {
|
||||
// apply/popup/manage use only meta for these two methods,
|
||||
// editor may need the full code but can fetch it directly,
|
||||
// so we send just the meta to avoid spamming lots of tabs with huge styles
|
||||
msg = Object.assign({}, msg, {
|
||||
style: getStyleWithNoCode(msg.style)
|
||||
});
|
||||
}
|
||||
const affectsAll = !msg.affects || msg.affects.all;
|
||||
const affectsOwnOriginOnly = !affectsAll && (msg.affects.editor || msg.affects.manager);
|
||||
const affectsTabs = affectsAll || affectsOwnOriginOnly;
|
||||
const affectsIcon = affectsAll || msg.affects.icon;
|
||||
const affectsPopup = affectsAll || msg.affects.popup;
|
||||
const affectsSelf = affectsPopup || msg.prefs;
|
||||
if (affectsTabs || affectsIcon) {
|
||||
const notifyTab = tab => {
|
||||
// own pages will be notified via runtime.sendMessage later
|
||||
if ((affectsTabs || URLS.optionsUI.includes(tab.url))
|
||||
&& !(affectsSelf && tab.url.startsWith(URLS.ownOrigin))
|
||||
// skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF
|
||||
&& (!FIREFOX || tab.width)) {
|
||||
chrome.tabs.sendMessage(tab.id, msg);
|
||||
}
|
||||
if (affectsIcon && BG) {
|
||||
BG.updateIcon(tab);
|
||||
}
|
||||
};
|
||||
// list all tabs including chrome-extension:// which can be ours
|
||||
Promise.all([
|
||||
queryTabs(affectsOwnOriginOnly ? {url: URLS.ownOrigin + '*'} : {}),
|
||||
getActiveTab(),
|
||||
]).then(([tabs, activeTab]) => {
|
||||
const activeTabId = activeTab && activeTab.id;
|
||||
for (const tab of tabs) {
|
||||
invokeOrPostpone(tab.id === activeTabId, notifyTab, tab);
|
||||
}
|
||||
});
|
||||
}
|
||||
// notify self: the message no longer is sent to the origin in new Chrome
|
||||
if (typeof onRuntimeMessage !== 'undefined') {
|
||||
onRuntimeMessage(originalMessage);
|
||||
}
|
||||
// notify apply.js on own pages
|
||||
if (typeof applyOnMessage !== 'undefined') {
|
||||
applyOnMessage(originalMessage);
|
||||
}
|
||||
// notify background page and all open popups
|
||||
if (affectsSelf) {
|
||||
chrome.runtime.sendMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function queryTabs(options = {}) {
|
||||
return new Promise(resolve =>
|
||||
chrome.tabs.query(options, tabs =>
|
||||
resolve(tabs)));
|
||||
}
|
||||
|
||||
|
||||
function getTab(id) {
|
||||
return new Promise(resolve =>
|
||||
chrome.tabs.get(id, tab =>
|
||||
!chrome.runtime.lastError && resolve(tab)));
|
||||
}
|
||||
|
||||
|
||||
function getOwnTab() {
|
||||
return new Promise(resolve =>
|
||||
chrome.tabs.getCurrent(tab => resolve(tab)));
|
||||
}
|
||||
|
||||
|
||||
function getActiveTab() {
|
||||
return queryTabs({currentWindow: true, active: true})
|
||||
.then(tabs => tabs[0]);
|
||||
}
|
||||
|
||||
|
||||
function getActiveTabRealURL() {
|
||||
return getActiveTab()
|
||||
.then(getTabRealURL);
|
||||
}
|
||||
|
||||
|
||||
function getTabRealURL(tab) {
|
||||
return new Promise(resolve => {
|
||||
if (tab.url !== 'chrome://newtab/') {
|
||||
resolve(tab.url);
|
||||
} else {
|
||||
chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, frame => {
|
||||
resolve(frame && frame.url || '');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// opens a tab or activates the already opened one,
|
||||
// reuses the New Tab page if it's focused now
|
||||
function openURL({url, currentWindow = true}) {
|
||||
if (!url.includes('://')) {
|
||||
url = chrome.runtime.getURL(url);
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
// [some] chromium forks don't handle their fake branded protocols
|
||||
url = url.replace(/^(opera|vivaldi)/, 'chrome');
|
||||
// FF doesn't handle moz-extension:// URLs (bug)
|
||||
// API doesn't handle the hash-fragment part
|
||||
const urlQuery = url.startsWith('moz-extension') ? undefined : url.replace(/#.*/, '');
|
||||
queryTabs({url: urlQuery, currentWindow}).then(tabs => {
|
||||
for (const tab of tabs) {
|
||||
if (tab.url === url) {
|
||||
activateTab(tab).then(resolve);
|
||||
return;
|
||||
}
|
||||
}
|
||||
getActiveTab().then(tab => {
|
||||
if (tab && tab.url === 'chrome://newtab/'
|
||||
// prevent redirecting incognito NTP to a chrome URL as it crashes Chrome
|
||||
&& (!url.startsWith('chrome') || !tab.incognito)) {
|
||||
chrome.tabs.update({url}, resolve);
|
||||
} else {
|
||||
chrome.tabs.create(tab && !FIREFOX ? {url, openerTabId: tab.id} : {url}, resolve);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function activateTab(tab) {
|
||||
return Promise.all([
|
||||
new Promise(resolve => {
|
||||
chrome.tabs.update(tab.id, {active: true}, resolve);
|
||||
}),
|
||||
new Promise(resolve => {
|
||||
chrome.windows.update(tab.windowId, {focused: true}, resolve);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
function stringAsRegExp(s, flags) {
|
||||
return new RegExp(s.replace(/[{}()[\]/\\.+?^$:=*!|]/g, '\\$&'), flags);
|
||||
}
|
||||
|
||||
|
||||
function ignoreChromeError() {
|
||||
chrome.runtime.lastError; // eslint-disable-line no-unused-expressions
|
||||
}
|
||||
|
||||
|
||||
function getStyleWithNoCode(style) {
|
||||
const stripped = Object.assign({}, style, {sections: []});
|
||||
for (const section of style.sections) {
|
||||
stripped.sections.push(Object.assign({}, section, {code: null}));
|
||||
}
|
||||
return stripped;
|
||||
}
|
||||
|
||||
|
||||
// js engine can't optimize the entire function if it contains try-catch
|
||||
// so we should keep it isolated from normal code in a minimal wrapper
|
||||
// Update: might get fixed in V8 TurboFan in the future
|
||||
function tryCatch(func, ...args) {
|
||||
try {
|
||||
return func(...args);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
||||
function tryRegExp(regexp) {
|
||||
try {
|
||||
return new RegExp(regexp);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
||||
function tryJSONparse(jsonString) {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
||||
const debounce = Object.assign((fn, delay, ...args) => {
|
||||
clearTimeout(debounce.timers.get(fn));
|
||||
debounce.timers.set(fn, setTimeout(debounce.run, delay, fn, ...args));
|
||||
}, {
|
||||
timers: new Map(),
|
||||
run(fn, ...args) {
|
||||
debounce.timers.delete(fn);
|
||||
fn(...args);
|
||||
},
|
||||
unregister(fn) {
|
||||
clearTimeout(debounce.timers.get(fn));
|
||||
debounce.timers.delete(fn);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
function deepCopy(obj) {
|
||||
return obj !== null && obj !== undefined && typeof obj === 'object'
|
||||
? deepMerge(typeof obj.slice === 'function' ? [] : {}, obj)
|
||||
: obj;
|
||||
}
|
||||
|
||||
|
||||
function deepMerge(target, ...args) {
|
||||
const isArray = typeof target.slice === 'function';
|
||||
for (const obj of args) {
|
||||
if (isArray && obj !== null && obj !== undefined) {
|
||||
for (const element of obj) {
|
||||
target.push(deepCopy(element));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (const k in obj) {
|
||||
const value = obj[k];
|
||||
if (k in target && typeof value === 'object' && value !== null) {
|
||||
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 && BG.getStyles ? Promise.resolve() : new Promise(function ping(resolve) {
|
||||
chrome.runtime.sendMessage({method: 'healthCheck'}, health => {
|
||||
if (health !== undefined) {
|
||||
BG = chrome.extension.getBackgroundPage();
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(ping, 0, resolve);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage
|
||||
function getStylesSafe(options) {
|
||||
return onBackgroundReady()
|
||||
.then(() => BG.getStyles(options));
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function download(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.timeout = 10e3;
|
||||
xhr.onloadend = () => (xhr.status === 200
|
||||
? resolve(xhr.responseText)
|
||||
: reject(xhr.status));
|
||||
const [mainUrl, query] = url.split('?');
|
||||
xhr.open(query ? 'POST' : 'GET', mainUrl, true);
|
||||
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
xhr.send(query);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function doTimeout(ms = 0, ...args) {
|
||||
return ms > 0
|
||||
? () => new Promise(resolve => setTimeout(resolve, ms, ...args))
|
||||
: new Promise(resolve => setTimeout(resolve, 0, ...args));
|
||||
}
|
||||
|
||||
|
||||
function invokeOrPostpone(isInvoke, fn, ...args) {
|
||||
return isInvoke
|
||||
? fn(...args)
|
||||
: setTimeout(invokeOrPostpone, 0, true, fn, ...args);
|
||||
}
|
|
@ -115,10 +115,10 @@ var prefs = new function Prefs() {
|
|||
defineReadonlyProperty(this.readOnlyValues, key, value);
|
||||
const hasChanged = !equal(value, oldValue);
|
||||
if (!fromBroadcast) {
|
||||
if (BG && BG != window) {
|
||||
if (BG && BG !== window) {
|
||||
BG.prefs.set(key, BG.deepCopy(value), {broadcast, sync});
|
||||
} else {
|
||||
localStorage[key] = typeof defaults[key] == 'object'
|
||||
localStorage[key] = typeof defaults[key] === 'object'
|
||||
? JSON.stringify(value)
|
||||
: value;
|
||||
if (broadcast && hasChanged) {
|
||||
|
@ -166,7 +166,7 @@ var prefs = new function Prefs() {
|
|||
for (const key in defaults) {
|
||||
const defaultValue = defaults[key];
|
||||
let value = localStorage[key];
|
||||
if (typeof value == 'string') {
|
||||
if (typeof value === 'string') {
|
||||
switch (typeof defaultValue) {
|
||||
case 'boolean':
|
||||
value = value.toLowerCase() === 'true';
|
||||
|
@ -181,7 +181,7 @@ var prefs = new function Prefs() {
|
|||
} else {
|
||||
value = defaultValue;
|
||||
}
|
||||
if (BG == window) {
|
||||
if (BG === window) {
|
||||
// when in bg page, .set() will write to localStorage
|
||||
this.set(key, value, {broadcast: false, sync: false});
|
||||
} else {
|
||||
|
@ -190,13 +190,13 @@ var prefs = new function Prefs() {
|
|||
}
|
||||
}
|
||||
|
||||
if (!BG || BG == window) {
|
||||
if (!BG || BG === window) {
|
||||
affectsIcon.forEach(key => this.broadcast(key, values[key], {sync: false}));
|
||||
|
||||
getSync().get('settings', ({settings: synced} = {}) => {
|
||||
if (synced) {
|
||||
for (const key in defaults) {
|
||||
if (key == 'popupWidth' && synced[key] != values.popupWidth) {
|
||||
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;
|
||||
|
@ -209,7 +209,7 @@ var prefs = new function Prefs() {
|
|||
});
|
||||
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (area == 'sync' && 'settings' in changes) {
|
||||
if (area === 'sync' && 'settings' in changes) {
|
||||
const synced = changes.settings.newValue;
|
||||
if (synced) {
|
||||
for (const key in defaults) {
|
||||
|
@ -283,21 +283,21 @@ var prefs = new function Prefs() {
|
|||
|
||||
function defineReadonlyProperty(obj, key, value) {
|
||||
const copy = deepCopy(value);
|
||||
if (typeof copy == 'object') {
|
||||
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') {
|
||||
if (!a || !b || typeof a !== 'object' || typeof b !== 'object') {
|
||||
return a === b;
|
||||
}
|
||||
if (Object.keys(a).length != Object.keys(b).length) {
|
||||
if (Object.keys(a).length !== Object.keys(b).length) {
|
||||
return false;
|
||||
}
|
||||
for (const k in a) {
|
||||
if (typeof a[k] == 'object') {
|
||||
if (typeof a[k] === 'object') {
|
||||
if (!equal(a[k], b[k])) {
|
||||
return false;
|
||||
}
|
||||
|
@ -315,7 +315,7 @@ var prefs = new function Prefs() {
|
|||
// Chrome and co.
|
||||
/Safari\/[\d.]+$/.test(navigator.userAgent) &&
|
||||
// skip forks with Flash as those are likely to have the menu e.g. CentBrowser
|
||||
!Array.from(navigator.plugins).some(p => p.name == 'Shockwave Flash')
|
||||
!Array.from(navigator.plugins).some(p => p.name === 'Shockwave Flash')
|
||||
);
|
||||
}
|
||||
}();
|
||||
|
@ -330,7 +330,7 @@ function setupLivePrefs(
|
|||
const checkedProps = {};
|
||||
for (const id of IDs) {
|
||||
const element = document.getElementById(id);
|
||||
checkedProps[id] = element.type == 'checkbox' ? 'checked' : 'value';
|
||||
checkedProps[id] = element.type === 'checkbox' ? 'checked' : 'value';
|
||||
updateElement({id, element, force: true});
|
||||
element.addEventListener('change', onChange);
|
||||
}
|
||||
|
@ -338,7 +338,7 @@ function setupLivePrefs(
|
|||
|
||||
function onChange() {
|
||||
const value = this[checkedProps[this.id]];
|
||||
if (prefs.get(this.id) != value) {
|
||||
if (prefs.get(this.id) !== value) {
|
||||
prefs.set(this.id, value);
|
||||
}
|
||||
}
|
||||
|
@ -349,7 +349,7 @@ function setupLivePrefs(
|
|||
force,
|
||||
}) {
|
||||
const prop = checkedProps[id];
|
||||
if (force || element[prop] != value) {
|
||||
if (force || element[prop] !== value) {
|
||||
element[prop] = value;
|
||||
element.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
|
||||
}
|
16
manage.html
16
manage.html
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
||||
<title i18n-text="manageTitle"></title>
|
||||
<link rel="stylesheet" href="manage.css">
|
||||
<link rel="stylesheet" href="manage/manage.css">
|
||||
<link rel="stylesheet" href="msgbox/msgbox.css">
|
||||
|
||||
<style id="style-overrides"></style>
|
||||
|
@ -121,12 +121,12 @@
|
|||
</details>
|
||||
</template>
|
||||
|
||||
<script src="dom.js"></script>
|
||||
<script src="messaging.js"></script>
|
||||
<script src="prefs.js"></script>
|
||||
<script src="apply.js"></script>
|
||||
<script src="localization.js"></script>
|
||||
<script src="manage.js"></script>
|
||||
<script src="js/dom.js"></script>
|
||||
<script src="js/messaging.js"></script>
|
||||
<script src="js/prefs.js"></script>
|
||||
<script src="content/apply.js"></script>
|
||||
<script src="js/localization.js"></script>
|
||||
<script src="manage/manage.js"></script>
|
||||
</head>
|
||||
|
||||
<body id="stylus-manage" i18n-dragndrop-hint="dragDropMessage">
|
||||
|
@ -216,7 +216,7 @@
|
|||
</div>
|
||||
<div id="installed"></div>
|
||||
|
||||
<script src="backup/fileSaveLoad.js"></script>
|
||||
<script src="manage/fileSaveLoad.js"></script>
|
||||
<script src="msgbox/msgbox.js"></script>
|
||||
|
||||
</body>
|
||||
|
|
|
@ -1,395 +1,395 @@
|
|||
/* global messageBox, handleUpdate, applyOnMessage */
|
||||
'use strict';
|
||||
|
||||
const STYLISH_DUMP_FILE_EXT = '.txt';
|
||||
const STYLUS_BACKUP_FILE_EXT = '.json';
|
||||
|
||||
|
||||
function importFromFile({fileTypeFilter, file} = {}) {
|
||||
return new Promise(resolve => {
|
||||
const fileInput = document.createElement('input');
|
||||
if (file) {
|
||||
readFile();
|
||||
return;
|
||||
}
|
||||
fileInput.style.display = 'none';
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = fileTypeFilter || STYLISH_DUMP_FILE_EXT;
|
||||
fileInput.acceptCharset = 'utf-8';
|
||||
|
||||
document.body.appendChild(fileInput);
|
||||
fileInput.initialValue = fileInput.value;
|
||||
fileInput.onchange = readFile;
|
||||
fileInput.click();
|
||||
|
||||
function readFile() {
|
||||
if (file || fileInput.value !== fileInput.initialValue) {
|
||||
file = file || fileInput.files[0];
|
||||
if (file.size > 100e6) {
|
||||
console.warn("100MB backup? I don't believe you.");
|
||||
importFromString('').then(resolve);
|
||||
return;
|
||||
}
|
||||
document.body.style.cursor = 'wait';
|
||||
const fReader = new FileReader();
|
||||
fReader.onloadend = event => {
|
||||
fileInput.remove();
|
||||
importFromString(event.target.result).then(numStyles => {
|
||||
document.body.style.cursor = '';
|
||||
resolve(numStyles);
|
||||
});
|
||||
};
|
||||
fReader.readAsText(file, 'utf-8');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function importFromString(jsonString) {
|
||||
if (!BG) {
|
||||
onBackgroundReady().then(() => importFromString(jsonString));
|
||||
return;
|
||||
}
|
||||
// create objects in background context
|
||||
const json = BG.tryJSONparse(jsonString) || [];
|
||||
if (typeof json.slice != 'function') {
|
||||
json.length = 0;
|
||||
}
|
||||
const oldStyles = json.length && BG.deepCopy(BG.cachedStyles.list || []);
|
||||
const oldStylesByName = json.length && new Map(
|
||||
oldStyles.map(style => [style.name.trim(), style]));
|
||||
|
||||
const stats = {
|
||||
added: {names: [], ids: [], legend: 'importReportLegendAdded'},
|
||||
unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'},
|
||||
metaAndCode: {names: [], ids: [], legend: 'importReportLegendUpdatedBoth'},
|
||||
metaOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedMeta'},
|
||||
codeOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedCode'},
|
||||
invalid: {names: [], legend: 'importReportLegendInvalid'},
|
||||
};
|
||||
|
||||
let index = 0;
|
||||
let lastRenderTime = performance.now();
|
||||
const renderQueue = [];
|
||||
const RENDER_NAP_TIME_MAX = 1000; // ms
|
||||
const RENDER_QUEUE_MAX = 50; // number of styles
|
||||
const SAVE_OPTIONS = {reason: 'import', notify: false};
|
||||
|
||||
return new Promise(proceed);
|
||||
|
||||
function proceed(resolve) {
|
||||
while (index < json.length) {
|
||||
const item = json[index++];
|
||||
const info = analyze(item);
|
||||
if (info) {
|
||||
// using saveStyle directly since json was parsed in background page context
|
||||
return BG.saveStyle(Object.assign(item, SAVE_OPTIONS))
|
||||
.then(style => account({style, info, resolve}));
|
||||
}
|
||||
}
|
||||
renderQueue.forEach(style => handleUpdate(style, {reason: 'import'}));
|
||||
renderQueue.length = 0;
|
||||
done(resolve);
|
||||
}
|
||||
|
||||
function analyze(item) {
|
||||
if (!item || !item.name || !item.name.trim() || typeof item != 'object'
|
||||
|| (item.sections && typeof item.sections.slice != 'function')) {
|
||||
stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`);
|
||||
return;
|
||||
}
|
||||
item.name = item.name.trim();
|
||||
const byId = BG.cachedStyles.byId.get(item.id);
|
||||
const byName = oldStylesByName.get(item.name);
|
||||
oldStylesByName.delete(item.name);
|
||||
let oldStyle;
|
||||
if (byId) {
|
||||
if (sameStyle(byId, item)) {
|
||||
oldStyle = byId;
|
||||
} else {
|
||||
item.id = null;
|
||||
}
|
||||
}
|
||||
if (!oldStyle && byName) {
|
||||
item.id = byName.id;
|
||||
oldStyle = byName;
|
||||
}
|
||||
const oldStyleKeys = oldStyle && Object.keys(oldStyle);
|
||||
const metaEqual = oldStyleKeys &&
|
||||
oldStyleKeys.length == Object.keys(item).length &&
|
||||
oldStyleKeys.every(k => k == 'sections' || oldStyle[k] === item[k]);
|
||||
const codeEqual = oldStyle && BG.styleSectionsEqual(oldStyle, item);
|
||||
if (metaEqual && codeEqual) {
|
||||
stats.unchanged.names.push(oldStyle.name);
|
||||
stats.unchanged.ids.push(oldStyle.id);
|
||||
return;
|
||||
}
|
||||
return {oldStyle, metaEqual, codeEqual};
|
||||
}
|
||||
|
||||
function sameStyle(oldStyle, newStyle) {
|
||||
return oldStyle.name.trim() === newStyle.name.trim() ||
|
||||
['updateUrl', 'originalMd5', 'originalDigest']
|
||||
.some(field => oldStyle[field] && oldStyle[field] == newStyle[field]);
|
||||
}
|
||||
|
||||
function account({style, info, resolve}) {
|
||||
renderQueue.push(style);
|
||||
if (performance.now() - lastRenderTime > RENDER_NAP_TIME_MAX
|
||||
|| renderQueue.length > RENDER_QUEUE_MAX) {
|
||||
renderQueue.forEach(style => handleUpdate(style, {reason: 'import'}));
|
||||
setTimeout(scrollElementIntoView, 0, $('#style-' + renderQueue.pop().id));
|
||||
renderQueue.length = 0;
|
||||
lastRenderTime = performance.now();
|
||||
}
|
||||
setTimeout(proceed, 0, resolve);
|
||||
const {oldStyle, metaEqual, codeEqual} = info;
|
||||
if (!oldStyle) {
|
||||
stats.added.names.push(style.name);
|
||||
stats.added.ids.push(style.id);
|
||||
return;
|
||||
}
|
||||
if (!metaEqual && !codeEqual) {
|
||||
stats.metaAndCode.names.push(reportNameChange(oldStyle, style));
|
||||
stats.metaAndCode.ids.push(style.id);
|
||||
return;
|
||||
}
|
||||
if (!codeEqual) {
|
||||
stats.codeOnly.names.push(style.name);
|
||||
stats.codeOnly.ids.push(style.id);
|
||||
return;
|
||||
}
|
||||
stats.metaOnly.names.push(reportNameChange(oldStyle, style));
|
||||
stats.metaOnly.ids.push(style.id);
|
||||
}
|
||||
|
||||
function done(resolve) {
|
||||
const numChanged = stats.metaAndCode.names.length +
|
||||
stats.metaOnly.names.length +
|
||||
stats.codeOnly.names.length +
|
||||
stats.added.names.length;
|
||||
Promise.resolve(numChanged && refreshAllTabs()).then(() => {
|
||||
const report = Object.keys(stats)
|
||||
.filter(kind => stats[kind].names.length)
|
||||
.map(kind => {
|
||||
const {ids, names, legend} = stats[kind];
|
||||
const listItemsWithId = (name, i) =>
|
||||
$element({dataset: {id: ids[i]}, textContent: name});
|
||||
const listItems = name =>
|
||||
$element({textContent: name});
|
||||
const block =
|
||||
$element({tag: 'details', dataset: {id: kind}, appendChild: [
|
||||
$element({tag: 'summary', appendChild:
|
||||
$element({tag: 'b', textContent: names.length + ' ' + t(legend)})
|
||||
}),
|
||||
$element({tag: 'small', appendChild:
|
||||
names.map(ids ? listItemsWithId : listItems)
|
||||
}),
|
||||
]});
|
||||
return block;
|
||||
});
|
||||
scrollTo(0, 0);
|
||||
messageBox({
|
||||
title: t('importReportTitle'),
|
||||
contents: report.length ? report : t('importReportUnchanged'),
|
||||
buttons: [t('confirmOK'), numChanged && t('undo')],
|
||||
onshow: bindClick,
|
||||
}).then(({button, enter, esc}) => {
|
||||
if (button == 1) {
|
||||
undo();
|
||||
}
|
||||
});
|
||||
resolve(numChanged);
|
||||
});
|
||||
}
|
||||
|
||||
function undo() {
|
||||
const oldStylesById = new Map(oldStyles.map(style => [style.id, style]));
|
||||
const newIds = [
|
||||
...stats.metaAndCode.ids,
|
||||
...stats.metaOnly.ids,
|
||||
...stats.codeOnly.ids,
|
||||
...stats.added.ids,
|
||||
];
|
||||
let resolve;
|
||||
index = 0;
|
||||
return new Promise(resolve_ => {
|
||||
resolve = resolve_;
|
||||
undoNextId();
|
||||
}).then(refreshAllTabs)
|
||||
.then(() => messageBox({
|
||||
title: t('importReportUndoneTitle'),
|
||||
contents: newIds.length + ' ' + t('importReportUndone'),
|
||||
buttons: [t('confirmOK')],
|
||||
}));
|
||||
function undoNextId() {
|
||||
if (index == newIds.length) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const id = newIds[index++];
|
||||
deleteStyleSafe({id, notify: false}).then(id => {
|
||||
const oldStyle = oldStylesById.get(id);
|
||||
if (oldStyle) {
|
||||
saveStyleSafe(Object.assign(oldStyle, SAVE_OPTIONS))
|
||||
.then(undoNextId);
|
||||
} else {
|
||||
undoNextId();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function bindClick(box) {
|
||||
const highlightElement = event => {
|
||||
const styleElement = $('#style-' + event.target.dataset.id);
|
||||
if (styleElement) {
|
||||
scrollElementIntoView(styleElement);
|
||||
animateElement(styleElement);
|
||||
}
|
||||
};
|
||||
for (const block of $$('details')) {
|
||||
if (block.dataset.id != 'invalid') {
|
||||
block.style.cursor = 'pointer';
|
||||
block.onclick = highlightElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function limitString(s, limit = 100) {
|
||||
return s.length <= limit ? s : s.substr(0, limit) + '...';
|
||||
}
|
||||
|
||||
function reportNameChange(oldStyle, newStyle) {
|
||||
return newStyle.name != oldStyle.name
|
||||
? oldStyle.name + ' —> ' + newStyle.name
|
||||
: oldStyle.name;
|
||||
}
|
||||
|
||||
function refreshAllTabs() {
|
||||
return Promise.all([
|
||||
getActiveTab(),
|
||||
getOwnTab(),
|
||||
]).then(([activeTab, ownTab]) => new Promise(resolve => {
|
||||
// list all tabs including chrome-extension:// which can be ours
|
||||
queryTabs().then(tabs => {
|
||||
const lastTab = tabs[tabs.length - 1];
|
||||
for (const tab of tabs) {
|
||||
// skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF
|
||||
if (FIREFOX && !tab.width) {
|
||||
if (tab == lastTab) {
|
||||
resolve();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => {
|
||||
const message = {method: 'styleReplaceAll', styles};
|
||||
if (tab.id == ownTab.id) {
|
||||
applyOnMessage(message);
|
||||
} else {
|
||||
invokeOrPostpone(tab.id == activeTab.id,
|
||||
chrome.tabs.sendMessage, tab.id, message, ignoreChromeError);
|
||||
}
|
||||
setTimeout(BG.updateIcon, 0, tab, styles);
|
||||
if (tab == lastTab) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$('#file-all-styles').onclick = () => {
|
||||
getStylesSafe().then(styles => {
|
||||
const text = JSON.stringify(styles, null, '\t');
|
||||
const url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text);
|
||||
return url;
|
||||
// for long URLs; https://github.com/schomery/stylus/issues/13#issuecomment-284582600
|
||||
}).then(fetch)
|
||||
.then(res => res.blob())
|
||||
.then(blob => {
|
||||
const objectURL = URL.createObjectURL(blob);
|
||||
let link = $element({
|
||||
tag:'a',
|
||||
href: objectURL,
|
||||
type: 'application/json',
|
||||
download: generateFileName(),
|
||||
});
|
||||
// TODO: remove the fallback when FF multi-process bug is fixed
|
||||
if (!FIREFOX) {
|
||||
link.dispatchEvent(new MouseEvent('click'));
|
||||
setTimeout(() => URL.revokeObjectURL(objectURL));
|
||||
} else {
|
||||
const iframe = document.body.appendChild($element({
|
||||
tag: 'iframe',
|
||||
style: 'width: 0; height: 0; position: fixed; opacity: 0;'.replace(/;/g, '!important;'),
|
||||
}));
|
||||
doTimeout().then(() => {
|
||||
link = iframe.contentDocument.importNode(link, true);
|
||||
iframe.contentDocument.body.appendChild(link);
|
||||
})
|
||||
.then(doTimeout)
|
||||
.then(() => link.dispatchEvent(new MouseEvent('click')))
|
||||
.then(doTimeout(1000))
|
||||
.then(() => {
|
||||
URL.revokeObjectURL(objectURL);
|
||||
iframe.remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function generateFileName() {
|
||||
const today = new Date();
|
||||
const dd = ('0' + today.getDate()).substr(-2);
|
||||
const mm = ('0' + (today.getMonth() + 1)).substr(-2);
|
||||
const yyyy = today.getFullYear();
|
||||
return `stylus-${yyyy}-${mm}-${dd}${STYLUS_BACKUP_FILE_EXT}`;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
$('#unfile-all-styles').onclick = () => {
|
||||
importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT});
|
||||
};
|
||||
|
||||
Object.assign(document.body, {
|
||||
ondragover(event) {
|
||||
const hasFiles = event.dataTransfer.types.includes('Files');
|
||||
event.dataTransfer.dropEffect = hasFiles || event.target.type == 'search' ? 'copy' : 'none';
|
||||
this.classList.toggle('dropzone', hasFiles);
|
||||
if (hasFiles) {
|
||||
event.preventDefault();
|
||||
clearTimeout(this.fadeoutTimer);
|
||||
this.classList.remove('fadeout');
|
||||
}
|
||||
},
|
||||
ondragend(event) {
|
||||
animateElement(this, {className: 'fadeout', removeExtraClasses: ['dropzone']}).then(() => {
|
||||
this.style.animationDuration = '';
|
||||
});
|
||||
},
|
||||
ondragleave(event) {
|
||||
try {
|
||||
// in Firefox event.target could be XUL browser and hence there is no permission to access it
|
||||
if (event.target === this) {
|
||||
this.ondragend();
|
||||
}
|
||||
} catch (e) {
|
||||
this.ondragend();
|
||||
}
|
||||
},
|
||||
ondrop(event) {
|
||||
this.ondragend();
|
||||
if (event.dataTransfer.files.length) {
|
||||
event.preventDefault();
|
||||
if ($('#onlyUpdates input').checked) {
|
||||
$('#onlyUpdates input').click();
|
||||
}
|
||||
importFromFile({file: event.dataTransfer.files[0]});
|
||||
}
|
||||
},
|
||||
});
|
||||
/* global messageBox, handleUpdate, applyOnMessage */
|
||||
'use strict';
|
||||
|
||||
const STYLISH_DUMP_FILE_EXT = '.txt';
|
||||
const STYLUS_BACKUP_FILE_EXT = '.json';
|
||||
|
||||
|
||||
function importFromFile({fileTypeFilter, file} = {}) {
|
||||
return new Promise(resolve => {
|
||||
const fileInput = document.createElement('input');
|
||||
if (file) {
|
||||
readFile();
|
||||
return;
|
||||
}
|
||||
fileInput.style.display = 'none';
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = fileTypeFilter || STYLISH_DUMP_FILE_EXT;
|
||||
fileInput.acceptCharset = 'utf-8';
|
||||
|
||||
document.body.appendChild(fileInput);
|
||||
fileInput.initialValue = fileInput.value;
|
||||
fileInput.onchange = readFile;
|
||||
fileInput.click();
|
||||
|
||||
function readFile() {
|
||||
if (file || fileInput.value !== fileInput.initialValue) {
|
||||
file = file || fileInput.files[0];
|
||||
if (file.size > 100e6) {
|
||||
console.warn("100MB backup? I don't believe you.");
|
||||
importFromString('').then(resolve);
|
||||
return;
|
||||
}
|
||||
document.body.style.cursor = 'wait';
|
||||
const fReader = new FileReader();
|
||||
fReader.onloadend = event => {
|
||||
fileInput.remove();
|
||||
importFromString(event.target.result).then(numStyles => {
|
||||
document.body.style.cursor = '';
|
||||
resolve(numStyles);
|
||||
});
|
||||
};
|
||||
fReader.readAsText(file, 'utf-8');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function importFromString(jsonString) {
|
||||
if (!BG) {
|
||||
onBackgroundReady().then(() => importFromString(jsonString));
|
||||
return;
|
||||
}
|
||||
// create objects in background context
|
||||
const json = BG.tryJSONparse(jsonString) || [];
|
||||
if (typeof json.slice !== 'function') {
|
||||
json.length = 0;
|
||||
}
|
||||
const oldStyles = json.length && BG.deepCopy(BG.cachedStyles.list || []);
|
||||
const oldStylesByName = json.length && new Map(
|
||||
oldStyles.map(style => [style.name.trim(), style]));
|
||||
|
||||
const stats = {
|
||||
added: {names: [], ids: [], legend: 'importReportLegendAdded'},
|
||||
unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'},
|
||||
metaAndCode: {names: [], ids: [], legend: 'importReportLegendUpdatedBoth'},
|
||||
metaOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedMeta'},
|
||||
codeOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedCode'},
|
||||
invalid: {names: [], legend: 'importReportLegendInvalid'},
|
||||
};
|
||||
|
||||
let index = 0;
|
||||
let lastRenderTime = performance.now();
|
||||
const renderQueue = [];
|
||||
const RENDER_NAP_TIME_MAX = 1000; // ms
|
||||
const RENDER_QUEUE_MAX = 50; // number of styles
|
||||
const SAVE_OPTIONS = {reason: 'import', notify: false};
|
||||
|
||||
return new Promise(proceed);
|
||||
|
||||
function proceed(resolve) {
|
||||
while (index < json.length) {
|
||||
const item = json[index++];
|
||||
const info = analyze(item);
|
||||
if (info) {
|
||||
// using saveStyle directly since json was parsed in background page context
|
||||
return BG.saveStyle(Object.assign(item, SAVE_OPTIONS))
|
||||
.then(style => account({style, info, resolve}));
|
||||
}
|
||||
}
|
||||
renderQueue.forEach(style => handleUpdate(style, {reason: 'import'}));
|
||||
renderQueue.length = 0;
|
||||
done(resolve);
|
||||
}
|
||||
|
||||
function analyze(item) {
|
||||
if (!item || !item.name || !item.name.trim() || typeof item !== 'object'
|
||||
|| (item.sections && typeof item.sections.slice !== 'function')) {
|
||||
stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`);
|
||||
return;
|
||||
}
|
||||
item.name = item.name.trim();
|
||||
const byId = BG.cachedStyles.byId.get(item.id);
|
||||
const byName = oldStylesByName.get(item.name);
|
||||
oldStylesByName.delete(item.name);
|
||||
let oldStyle;
|
||||
if (byId) {
|
||||
if (sameStyle(byId, item)) {
|
||||
oldStyle = byId;
|
||||
} else {
|
||||
item.id = null;
|
||||
}
|
||||
}
|
||||
if (!oldStyle && byName) {
|
||||
item.id = byName.id;
|
||||
oldStyle = byName;
|
||||
}
|
||||
const oldStyleKeys = oldStyle && Object.keys(oldStyle);
|
||||
const metaEqual = oldStyleKeys &&
|
||||
oldStyleKeys.length === Object.keys(item).length &&
|
||||
oldStyleKeys.every(k => k === 'sections' || oldStyle[k] === item[k]);
|
||||
const codeEqual = oldStyle && BG.styleSectionsEqual(oldStyle, item);
|
||||
if (metaEqual && codeEqual) {
|
||||
stats.unchanged.names.push(oldStyle.name);
|
||||
stats.unchanged.ids.push(oldStyle.id);
|
||||
return;
|
||||
}
|
||||
return {oldStyle, metaEqual, codeEqual};
|
||||
}
|
||||
|
||||
function sameStyle(oldStyle, newStyle) {
|
||||
return oldStyle.name.trim() === newStyle.name.trim() ||
|
||||
['updateUrl', 'originalMd5', 'originalDigest']
|
||||
.some(field => oldStyle[field] && oldStyle[field] === newStyle[field]);
|
||||
}
|
||||
|
||||
function account({style, info, resolve}) {
|
||||
renderQueue.push(style);
|
||||
if (performance.now() - lastRenderTime > RENDER_NAP_TIME_MAX
|
||||
|| renderQueue.length > RENDER_QUEUE_MAX) {
|
||||
renderQueue.forEach(style => handleUpdate(style, {reason: 'import'}));
|
||||
setTimeout(scrollElementIntoView, 0, $('#style-' + renderQueue.pop().id));
|
||||
renderQueue.length = 0;
|
||||
lastRenderTime = performance.now();
|
||||
}
|
||||
setTimeout(proceed, 0, resolve);
|
||||
const {oldStyle, metaEqual, codeEqual} = info;
|
||||
if (!oldStyle) {
|
||||
stats.added.names.push(style.name);
|
||||
stats.added.ids.push(style.id);
|
||||
return;
|
||||
}
|
||||
if (!metaEqual && !codeEqual) {
|
||||
stats.metaAndCode.names.push(reportNameChange(oldStyle, style));
|
||||
stats.metaAndCode.ids.push(style.id);
|
||||
return;
|
||||
}
|
||||
if (!codeEqual) {
|
||||
stats.codeOnly.names.push(style.name);
|
||||
stats.codeOnly.ids.push(style.id);
|
||||
return;
|
||||
}
|
||||
stats.metaOnly.names.push(reportNameChange(oldStyle, style));
|
||||
stats.metaOnly.ids.push(style.id);
|
||||
}
|
||||
|
||||
function done(resolve) {
|
||||
const numChanged = stats.metaAndCode.names.length +
|
||||
stats.metaOnly.names.length +
|
||||
stats.codeOnly.names.length +
|
||||
stats.added.names.length;
|
||||
Promise.resolve(numChanged && refreshAllTabs()).then(() => {
|
||||
const report = Object.keys(stats)
|
||||
.filter(kind => stats[kind].names.length)
|
||||
.map(kind => {
|
||||
const {ids, names, legend} = stats[kind];
|
||||
const listItemsWithId = (name, i) =>
|
||||
$element({dataset: {id: ids[i]}, textContent: name});
|
||||
const listItems = name =>
|
||||
$element({textContent: name});
|
||||
const block =
|
||||
$element({tag: 'details', dataset: {id: kind}, appendChild: [
|
||||
$element({tag: 'summary', appendChild:
|
||||
$element({tag: 'b', textContent: names.length + ' ' + t(legend)})
|
||||
}),
|
||||
$element({tag: 'small', appendChild:
|
||||
names.map(ids ? listItemsWithId : listItems)
|
||||
}),
|
||||
]});
|
||||
return block;
|
||||
});
|
||||
scrollTo(0, 0);
|
||||
messageBox({
|
||||
title: t('importReportTitle'),
|
||||
contents: report.length ? report : t('importReportUnchanged'),
|
||||
buttons: [t('confirmOK'), numChanged && t('undo')],
|
||||
onshow: bindClick,
|
||||
}).then(({button}) => {
|
||||
if (button === 1) {
|
||||
undo();
|
||||
}
|
||||
});
|
||||
resolve(numChanged);
|
||||
});
|
||||
}
|
||||
|
||||
function undo() {
|
||||
const oldStylesById = new Map(oldStyles.map(style => [style.id, style]));
|
||||
const newIds = [
|
||||
...stats.metaAndCode.ids,
|
||||
...stats.metaOnly.ids,
|
||||
...stats.codeOnly.ids,
|
||||
...stats.added.ids,
|
||||
];
|
||||
let resolve;
|
||||
index = 0;
|
||||
return new Promise(resolve_ => {
|
||||
resolve = resolve_;
|
||||
undoNextId();
|
||||
}).then(refreshAllTabs)
|
||||
.then(() => messageBox({
|
||||
title: t('importReportUndoneTitle'),
|
||||
contents: newIds.length + ' ' + t('importReportUndone'),
|
||||
buttons: [t('confirmOK')],
|
||||
}));
|
||||
function undoNextId() {
|
||||
if (index === newIds.length) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const id = newIds[index++];
|
||||
deleteStyleSafe({id, notify: false}).then(id => {
|
||||
const oldStyle = oldStylesById.get(id);
|
||||
if (oldStyle) {
|
||||
saveStyleSafe(Object.assign(oldStyle, SAVE_OPTIONS))
|
||||
.then(undoNextId);
|
||||
} else {
|
||||
undoNextId();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function bindClick() {
|
||||
const highlightElement = event => {
|
||||
const styleElement = $('#style-' + event.target.dataset.id);
|
||||
if (styleElement) {
|
||||
scrollElementIntoView(styleElement);
|
||||
animateElement(styleElement);
|
||||
}
|
||||
};
|
||||
for (const block of $$('details')) {
|
||||
if (block.dataset.id !== 'invalid') {
|
||||
block.style.cursor = 'pointer';
|
||||
block.onclick = highlightElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function limitString(s, limit = 100) {
|
||||
return s.length <= limit ? s : s.substr(0, limit) + '...';
|
||||
}
|
||||
|
||||
function reportNameChange(oldStyle, newStyle) {
|
||||
return newStyle.name !== oldStyle.name
|
||||
? oldStyle.name + ' —> ' + newStyle.name
|
||||
: oldStyle.name;
|
||||
}
|
||||
|
||||
function refreshAllTabs() {
|
||||
return Promise.all([
|
||||
getActiveTab(),
|
||||
getOwnTab(),
|
||||
]).then(([activeTab, ownTab]) => new Promise(resolve => {
|
||||
// list all tabs including chrome-extension:// which can be ours
|
||||
queryTabs().then(tabs => {
|
||||
const lastTab = tabs[tabs.length - 1];
|
||||
for (const tab of tabs) {
|
||||
// skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF
|
||||
if (FIREFOX && !tab.width) {
|
||||
if (tab === lastTab) {
|
||||
resolve();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => {
|
||||
const message = {method: 'styleReplaceAll', styles};
|
||||
if (tab.id === ownTab.id) {
|
||||
applyOnMessage(message);
|
||||
} else {
|
||||
invokeOrPostpone(tab.id === activeTab.id,
|
||||
chrome.tabs.sendMessage, tab.id, message, ignoreChromeError);
|
||||
}
|
||||
setTimeout(BG.updateIcon, 0, tab, styles);
|
||||
if (tab === lastTab) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$('#file-all-styles').onclick = () => {
|
||||
getStylesSafe().then(styles => {
|
||||
const text = JSON.stringify(styles, null, '\t');
|
||||
const url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text);
|
||||
return url;
|
||||
// for long URLs; https://github.com/schomery/stylus/issues/13#issuecomment-284582600
|
||||
}).then(fetch)
|
||||
.then(res => res.blob())
|
||||
.then(blob => {
|
||||
const objectURL = URL.createObjectURL(blob);
|
||||
let link = $element({
|
||||
tag:'a',
|
||||
href: objectURL,
|
||||
type: 'application/json',
|
||||
download: generateFileName(),
|
||||
});
|
||||
// TODO: remove the fallback when FF multi-process bug is fixed
|
||||
if (!FIREFOX) {
|
||||
link.dispatchEvent(new MouseEvent('click'));
|
||||
setTimeout(() => URL.revokeObjectURL(objectURL));
|
||||
} else {
|
||||
const iframe = document.body.appendChild($element({
|
||||
tag: 'iframe',
|
||||
style: 'width: 0; height: 0; position: fixed; opacity: 0;'.replace(/;/g, '!important;'),
|
||||
}));
|
||||
doTimeout().then(() => {
|
||||
link = iframe.contentDocument.importNode(link, true);
|
||||
iframe.contentDocument.body.appendChild(link);
|
||||
})
|
||||
.then(doTimeout)
|
||||
.then(() => link.dispatchEvent(new MouseEvent('click')))
|
||||
.then(doTimeout(1000))
|
||||
.then(() => {
|
||||
URL.revokeObjectURL(objectURL);
|
||||
iframe.remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function generateFileName() {
|
||||
const today = new Date();
|
||||
const dd = ('0' + today.getDate()).substr(-2);
|
||||
const mm = ('0' + (today.getMonth() + 1)).substr(-2);
|
||||
const yyyy = today.getFullYear();
|
||||
return `stylus-${yyyy}-${mm}-${dd}${STYLUS_BACKUP_FILE_EXT}`;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
$('#unfile-all-styles').onclick = () => {
|
||||
importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT});
|
||||
};
|
||||
|
||||
Object.assign(document.body, {
|
||||
ondragover(event) {
|
||||
const hasFiles = event.dataTransfer.types.includes('Files');
|
||||
event.dataTransfer.dropEffect = hasFiles || event.target.type === 'search' ? 'copy' : 'none';
|
||||
this.classList.toggle('dropzone', hasFiles);
|
||||
if (hasFiles) {
|
||||
event.preventDefault();
|
||||
clearTimeout(this.fadeoutTimer);
|
||||
this.classList.remove('fadeout');
|
||||
}
|
||||
},
|
||||
ondragend() {
|
||||
animateElement(this, {className: 'fadeout', removeExtraClasses: ['dropzone']}).then(() => {
|
||||
this.style.animationDuration = '';
|
||||
});
|
||||
},
|
||||
ondragleave(event) {
|
||||
try {
|
||||
// in Firefox event.target could be XUL browser and hence there is no permission to access it
|
||||
if (event.target === this) {
|
||||
this.ondragend();
|
||||
}
|
||||
} catch (e) {
|
||||
this.ondragend();
|
||||
}
|
||||
},
|
||||
ondrop(event) {
|
||||
this.ondragend();
|
||||
if (event.dataTransfer.files.length) {
|
||||
event.preventDefault();
|
||||
if ($('#onlyUpdates input').checked) {
|
||||
$('#onlyUpdates input').click();
|
||||
}
|
||||
importFromFile({file: event.dataTransfer.files[0]});
|
||||
}
|
||||
},
|
||||
});
|
|
@ -73,7 +73,7 @@ function initGlobalEvents() {
|
|||
|
||||
// focus search field on / key
|
||||
document.onkeypress = event => {
|
||||
if ((event.keyCode || event.which) == 47
|
||||
if ((event.keyCode || event.which) === 47
|
||||
&& !event.altKey && !event.shiftKey && !event.ctrlKey && !event.metaKey
|
||||
&& !event.target.matches('[type="text"], [type="search"]')) {
|
||||
event.preventDefault();
|
||||
|
@ -114,7 +114,7 @@ function initGlobalEvents() {
|
|||
function showStyles(styles = []) {
|
||||
const sorted = styles
|
||||
.map(style => ({name: style.name.toLocaleLowerCase(), style}))
|
||||
.sort((a, b) => (a.name < b.name ? -1 : a.name == b.name ? 0 : 1));
|
||||
.sort((a, b) => (a.name < b.name ? -1 : a.name === b.name ? 0 : 1));
|
||||
let index = 0;
|
||||
const scrollY = (history.state || {}).scrollY;
|
||||
const shouldRenderAll = scrollY > window.innerHeight || sessionStorage.justEditedStyleId;
|
||||
|
@ -128,8 +128,11 @@ function showStyles(styles = []) {
|
|||
function renderStyles() {
|
||||
const t0 = performance.now();
|
||||
let rendered = 0;
|
||||
while (index < sorted.length
|
||||
&& (shouldRenderAll || ++rendered < 10 || performance.now() - t0 < 10)) {
|
||||
while (
|
||||
index < sorted.length &&
|
||||
// eslint-disable-next-line no-unmodified-loop-condition
|
||||
(shouldRenderAll || ++rendered < 10 || performance.now() - t0 < 10)
|
||||
) {
|
||||
renderBin.appendChild(createStyleElement(sorted[index++]));
|
||||
}
|
||||
filterAndAppend({container: renderBin});
|
||||
|
@ -225,18 +228,18 @@ function createStyleTargetsElement({entry, style, postponeFavicons}) {
|
|||
displayed.add(targetValue);
|
||||
const element = template.appliesToTarget.cloneNode(true);
|
||||
if (!newUI.enabled) {
|
||||
if (numTargets == 10) {
|
||||
if (numTargets === 10) {
|
||||
container = container.appendChild(template.extraAppliesTo.cloneNode(true));
|
||||
} else if (numTargets > 1) {
|
||||
container.appendChild(template.appliesToSeparator.cloneNode(true));
|
||||
}
|
||||
} else if (newUI.favicons) {
|
||||
let favicon = '';
|
||||
if (type == 'domains') {
|
||||
if (type === 'domains') {
|
||||
favicon = GET_FAVICON_URL + targetValue;
|
||||
} else if (targetValue.startsWith('chrome-extension:')) {
|
||||
favicon = OWN_ICON;
|
||||
} else if (type != 'regexps') {
|
||||
} else if (type !== 'regexps') {
|
||||
favicon = targetValue.includes('://') && targetValue.match(/^.*?:\/\/([^/]+)/);
|
||||
favicon = favicon ? GET_FAVICON_URL + favicon[1] : '';
|
||||
}
|
||||
|
@ -289,7 +292,7 @@ Object.assign(handleEvent, {
|
|||
const target = event.target;
|
||||
const entry = target.closest('.entry');
|
||||
for (const selector in handleEvent.ENTRY_ROUTES) {
|
||||
for (let el = target; el && el != entry; el = el.parentElement) {
|
||||
for (let el = target; el && el !== entry; el = el.parentElement) {
|
||||
if (el.matches(selector)) {
|
||||
const handler = handleEvent.ENTRY_ROUTES[selector];
|
||||
return handleEvent[handler].call(el, event, entry);
|
||||
|
@ -304,8 +307,8 @@ Object.assign(handleEvent, {
|
|||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const left = event.button == 0;
|
||||
const middle = event.button == 1;
|
||||
const left = event.button === 0;
|
||||
const middle = event.button === 1;
|
||||
const shift = event.shiftKey;
|
||||
const ctrl = event.ctrlKey;
|
||||
const openWindow = left && shift && !ctrl;
|
||||
|
@ -357,8 +360,8 @@ Object.assign(handleEvent, {
|
|||
className: 'danger center',
|
||||
buttons: [t('confirmDelete'), t('confirmCancel')],
|
||||
})
|
||||
.then(({button, enter, esc}) => {
|
||||
if (button == 0 || enter) {
|
||||
.then(({button, enter}) => {
|
||||
if (button === 0 || enter) {
|
||||
deleteStyleSafe({id});
|
||||
}
|
||||
});
|
||||
|
@ -383,10 +386,10 @@ Object.assign(handleEvent, {
|
|||
},
|
||||
|
||||
filterOnChange({target: el, forceRefilter}) {
|
||||
const getValue = el => (el.type == 'checkbox' ? el.checked : el.value.trim());
|
||||
const getValue = el => (el.type === 'checkbox' ? el.checked : el.value.trim());
|
||||
if (!forceRefilter) {
|
||||
const value = getValue(el);
|
||||
if (value == el.lastValue) {
|
||||
if (value === el.lastValue) {
|
||||
return;
|
||||
}
|
||||
el.lastValue = value;
|
||||
|
@ -412,22 +415,22 @@ Object.assign(handleEvent, {
|
|||
function handleUpdate(style, {reason, method} = {}) {
|
||||
let entry;
|
||||
let oldEntry = $(ENTRY_ID_PREFIX + style.id);
|
||||
if (oldEntry && method == 'styleUpdated') {
|
||||
if (oldEntry && method === 'styleUpdated') {
|
||||
handleToggledOrCodeOnly();
|
||||
}
|
||||
entry = entry || createStyleElement({style});
|
||||
if (oldEntry) {
|
||||
if (oldEntry.styleNameLowerCase == entry.styleNameLowerCase) {
|
||||
if (oldEntry.styleNameLowerCase === entry.styleNameLowerCase) {
|
||||
installed.replaceChild(entry, oldEntry);
|
||||
} else {
|
||||
oldEntry.remove();
|
||||
}
|
||||
}
|
||||
if (reason == 'update' && entry.matches('.updatable')) {
|
||||
if (reason === 'update' && entry.matches('.updatable')) {
|
||||
handleUpdateInstalled();
|
||||
}
|
||||
filterAndAppend({entry});
|
||||
if (!entry.matches('.hidden') && reason != 'import') {
|
||||
if (!entry.matches('.hidden') && reason !== 'import') {
|
||||
animateElement(entry);
|
||||
scrollElementIntoView(entry);
|
||||
}
|
||||
|
@ -435,12 +438,12 @@ function handleUpdate(style, {reason, method} = {}) {
|
|||
function handleToggledOrCodeOnly() {
|
||||
const newStyleMeta = getStyleWithNoCode(style);
|
||||
const diff = objectDiff(oldEntry.styleMeta, newStyleMeta);
|
||||
if (diff.length == 0) {
|
||||
if (diff.length === 0) {
|
||||
// only code was modified
|
||||
entry = oldEntry;
|
||||
oldEntry = null;
|
||||
}
|
||||
if (diff.length == 1 && diff[0].key == 'enabled') {
|
||||
if (diff.length === 1 && diff[0].key === 'enabled') {
|
||||
oldEntry.classList.toggle('enabled', style.enabled);
|
||||
oldEntry.classList.toggle('disabled', !style.enabled);
|
||||
$$('.checker', oldEntry).forEach(el => (el.checked = style.enabled));
|
||||
|
@ -478,8 +481,8 @@ function switchUI({styleOnly} = {}) {
|
|||
// ensure the global option is processed first
|
||||
for (const el of [$('#manage.newUI'), ...$$('[id^="manage.newUI."]')]) {
|
||||
const id = el.id.replace(/^manage\.newUI\.?/, '') || 'enabled';
|
||||
const value = el.type == 'checkbox' ? el.checked : Number(el.value);
|
||||
const valueChanged = value !== newUI[id] && (id == 'enabled' || current.enabled);
|
||||
const value = el.type === 'checkbox' ? el.checked : Number(el.value);
|
||||
const valueChanged = value !== newUI[id] && (id === 'enabled' || current.enabled);
|
||||
current[id] = value;
|
||||
changed[id] = valueChanged;
|
||||
someChanged |= valueChanged;
|
||||
|
@ -565,7 +568,7 @@ function checkUpdateAll() {
|
|||
$('#apply-all-updates').classList.add('hidden');
|
||||
$('#update-all-no-updates').classList.add('hidden');
|
||||
|
||||
const ignoreDigest = this && this.id == 'check-all-updates-force';
|
||||
const ignoreDigest = this && this.id === 'check-all-updates-force';
|
||||
$$('.updatable:not(.can-update)' + (ignoreDigest ? '' : ':not(.update-problem)'))
|
||||
.map(el => checkUpdate(el, {single: false}));
|
||||
|
||||
|
@ -582,7 +585,7 @@ function checkUpdateAll() {
|
|||
total = value;
|
||||
break;
|
||||
case BG.updater.UPDATED:
|
||||
if (++updated == 1) {
|
||||
if (++updated === 1) {
|
||||
$('#apply-all-updates').disabled = true;
|
||||
$('#apply-all-updates').classList.remove('hidden');
|
||||
}
|
||||
|
@ -590,7 +593,7 @@ function checkUpdateAll() {
|
|||
// fallthrough
|
||||
case BG.updater.SKIPPED:
|
||||
checked++;
|
||||
if (details == BG.updater.EDITED || details == BG.updater.MAYBE_EDITED) {
|
||||
if (details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED) {
|
||||
skippedEdited++;
|
||||
}
|
||||
reportUpdateState(state, value, details);
|
||||
|
@ -603,13 +606,13 @@ function checkUpdateAll() {
|
|||
|
||||
function done() {
|
||||
document.body.classList.remove('update-in-progress');
|
||||
$('#check-all-updates').disabled = total == 0;
|
||||
$('#check-all-updates').disabled = total === 0;
|
||||
$('#apply-all-updates').disabled = false;
|
||||
renderUpdatesOnlyFilter({check: updated + skippedEdited > 0});
|
||||
if (!updated) {
|
||||
$('#update-all-no-updates').dataset.skippedEdited = skippedEdited > 0;
|
||||
$('#update-all-no-updates').classList.remove('hidden');
|
||||
$('#check-all-updates-force').classList.toggle('hidden', skippedEdited == 0);
|
||||
$('#check-all-updates-force').classList.toggle('hidden', skippedEdited === 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -645,16 +648,16 @@ function reportUpdateState(state, style, details) {
|
|||
if (entry.classList.contains('can-update')) {
|
||||
break;
|
||||
}
|
||||
const same = details == BG.updater.SAME_MD5 || details == BG.updater.SAME_CODE;
|
||||
const edited = details == BG.updater.EDITED || details == BG.updater.MAYBE_EDITED;
|
||||
const same = details === BG.updater.SAME_MD5 || details === BG.updater.SAME_CODE;
|
||||
const edited = details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED;
|
||||
entry.dataset.details = details;
|
||||
if (!details) {
|
||||
details = t('updateCheckFailServerUnreachable');
|
||||
} else if (typeof details == 'number') {
|
||||
} else if (typeof details === 'number') {
|
||||
details = t('updateCheckFailBadResponseCode', [details]);
|
||||
} else if (details == BG.updater.EDITED) {
|
||||
} else if (details === BG.updater.EDITED) {
|
||||
details = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
|
||||
} else if (details == BG.updater.MAYBE_EDITED) {
|
||||
} else if (details === BG.updater.MAYBE_EDITED) {
|
||||
details = t('updateCheckSkippedMaybeLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
|
||||
}
|
||||
const message = same ? t('updateCheckSucceededNoUpdate') : details;
|
||||
|
@ -716,7 +719,7 @@ function searchStyles({immediately, container}) {
|
|||
const searchElement = $('#search');
|
||||
const query = searchElement.value.toLocaleLowerCase();
|
||||
const queryPrev = searchElement.lastValue || '';
|
||||
if (query == queryPrev && !immediately && !container) {
|
||||
if (query === queryPrev && !immediately && !container) {
|
||||
return;
|
||||
}
|
||||
if (!immediately) {
|
||||
|
@ -738,7 +741,7 @@ function searchStyles({immediately, container}) {
|
|||
style.url && isMatchingText(style.url) ||
|
||||
isMatchingStyle(style)));
|
||||
}
|
||||
if (entry.classList.contains('not-matching') != !isMatching) {
|
||||
if (entry.classList.contains('not-matching') !== !isMatching) {
|
||||
entry.classList.toggle('not-matching', !isMatching);
|
||||
needsRefilter = true;
|
||||
}
|
||||
|
@ -847,7 +850,7 @@ function reapplyFilter(container = installed) {
|
|||
shuffle(false);
|
||||
setTimeout(shuffle, 0, true);
|
||||
// single-element job from handleEvent(): add the last wraith
|
||||
if (toHide.length == 1 && toHide[0].parentElement != installed) {
|
||||
if (toHide.length === 1 && toHide[0].parentElement !== installed) {
|
||||
installed.appendChild(toHide[0]);
|
||||
}
|
||||
return;
|
||||
|
@ -882,7 +885,7 @@ function reapplyFilter(container = installed) {
|
|||
const skipGroup = state => {
|
||||
const start = i;
|
||||
const first = entry;
|
||||
while (entry && entry.classList.contains('hidden') == state) {
|
||||
while (entry && entry.classList.contains('hidden') === state) {
|
||||
entry = entry.nextElementSibling;
|
||||
i++;
|
||||
}
|
||||
|
@ -900,7 +903,7 @@ function reapplyFilter(container = installed) {
|
|||
// 3. move the shortest group; repeat 2-3
|
||||
if (hidden.len < visible.len && (fullPass || hidden.len % 2)) {
|
||||
// 3a. move hidden under the horizon
|
||||
for (let j = 0; j < (fullPass ? hidden.len : 1); j++) {
|
||||
for (let j = 0; j < (fullPass ? hidden.len : 1); j++) {
|
||||
const entry = entries[hidden.start];
|
||||
installed.insertBefore(entry, horizon);
|
||||
horizon = entry;
|
||||
|
@ -975,14 +978,19 @@ function objectDiff(first, second, path = '') {
|
|||
diff.push({path, key, values: [a], type: 'removed'});
|
||||
continue;
|
||||
}
|
||||
if (a && typeof a.filter == 'function' && b && typeof b.filter == 'function') {
|
||||
if (a.length != b.length
|
||||
|| a.some((el, i) => !el || typeof el != 'object' ? el != b[i]
|
||||
: objectDiff(el, b[i], path + key + '[' + i + '].').length)
|
||||
if (a && typeof a.filter === 'function' && b && typeof b.filter === 'function') {
|
||||
if (
|
||||
a.length !== b.length ||
|
||||
a.some((el, i) => {
|
||||
const result = !el || typeof el !== 'object'
|
||||
? el !== b[i]
|
||||
: objectDiff(el, b[i], path + key + '[' + i + '].').length;
|
||||
return result;
|
||||
})
|
||||
) {
|
||||
diff.push({path, key, values: [a, b], type: 'changed'});
|
||||
}
|
||||
} else if (typeof a == 'object' && typeof b == 'object') {
|
||||
} else if (typeof a === 'object' && typeof b === 'object') {
|
||||
diff.push(...objectDiff(a, b, path + key + '.'));
|
||||
} else {
|
||||
diff.push({path, key, values: [a, b], type: 'changed'});
|
|
@ -19,7 +19,13 @@
|
|||
"<all_urls>"
|
||||
],
|
||||
"background": {
|
||||
"scripts": ["messaging.js", "storage.js", "prefs.js", "background.js", "update.js"]
|
||||
"scripts": [
|
||||
"js/messaging.js",
|
||||
"background/storage.js",
|
||||
"js/prefs.js",
|
||||
"background/background.js",
|
||||
"background/update.js"
|
||||
]
|
||||
},
|
||||
"commands": {
|
||||
"openManage": {
|
||||
|
@ -35,13 +41,13 @@
|
|||
"run_at": "document_start",
|
||||
"all_frames": true,
|
||||
"match_about_blank": true,
|
||||
"js": ["apply.js"]
|
||||
"js": ["content/apply.js"]
|
||||
},
|
||||
{
|
||||
"matches": ["http://userstyles.org/*", "https://userstyles.org/*"],
|
||||
"run_at": "document_start",
|
||||
"all_frames": false,
|
||||
"js": ["install.js"]
|
||||
"js": ["content/install.js"]
|
||||
}
|
||||
],
|
||||
"browser_action": {
|
||||
|
@ -56,7 +62,7 @@
|
|||
},
|
||||
"default_locale": "en",
|
||||
"options_ui": {
|
||||
"page": "options/index.html",
|
||||
"page": "options.html",
|
||||
"chrome_style": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,9 +30,9 @@ function messageBox({
|
|||
key(event) {
|
||||
const keyCode = event.keyCode || event.which;
|
||||
if (!event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey
|
||||
&& (keyCode == 13 || keyCode == 27)) {
|
||||
&& (keyCode === 13 || keyCode === 27)) {
|
||||
event.preventDefault();
|
||||
resolveWith(keyCode == 13 ? {enter: true} : {esc: true});
|
||||
resolveWith(keyCode === 13 ? {enter: true} : {esc: true});
|
||||
}
|
||||
},
|
||||
scroll() {
|
||||
|
@ -52,7 +52,7 @@ function messageBox({
|
|||
unbindAndRemoveSelf();
|
||||
}
|
||||
const id = 'message-box';
|
||||
const putAs = typeof contents == 'string' ? 'innerHTML' : 'appendChild';
|
||||
const putAs = typeof contents === 'string' ? 'innerHTML' : 'appendChild';
|
||||
messageBox.element = $element({id, className, appendChild: [
|
||||
$element({appendChild: [
|
||||
$element({id: `${id}-title`, innerHTML: title}),
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
<html id="stylus">
|
||||
<head>
|
||||
<title i18n-text-append="optionsHeading">Stylus </title>
|
||||
<link rel="stylesheet" href="index.css">
|
||||
<script src="/dom.js"></script>
|
||||
<script src="/messaging.js"></script>
|
||||
<script src="/localization.js"></script>
|
||||
<script src="/prefs.js"></script>
|
||||
<script src="/apply.js"></script>
|
||||
<link rel="stylesheet" href="options/index.css">
|
||||
<script src="js/dom.js"></script>
|
||||
<script src="js/messaging.js"></script>
|
||||
<script src="js/localization.js"></script>
|
||||
<script src="js/prefs.js"></script>
|
||||
<script src="content/apply.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -128,6 +128,6 @@
|
|||
</ol>
|
||||
</div>
|
||||
|
||||
<script src="index.js"></script>
|
||||
<script src="options/index.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -81,6 +81,7 @@ label:not([disabled]) > :first-child {
|
|||
|
||||
label:not([disabled]):hover > :first-child {
|
||||
text-shadow: 0 0 0.01px rgba(0, 0, 0, .25);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button,
|
||||
|
|
|
@ -15,7 +15,7 @@ document.onclick = e => {
|
|||
|
||||
switch (target.dataset.cmd) {
|
||||
case 'open-manage':
|
||||
openURL({url: '/manage.html'});
|
||||
openURL({url: 'manage.html'});
|
||||
break;
|
||||
|
||||
case 'check-updates':
|
||||
|
@ -65,9 +65,9 @@ function checkUpdates() {
|
|||
|
||||
function setupRadioButtons() {
|
||||
const sets = {};
|
||||
const onChange = function() {
|
||||
const onChange = function () {
|
||||
const newValue = sets[this.name].indexOf(this);
|
||||
if (newValue >= 0 && prefs.get(this.name) != newValue) {
|
||||
if (newValue >= 0 && prefs.get(this.name) !== newValue) {
|
||||
prefs.set(this.name, newValue);
|
||||
}
|
||||
};
|
||||
|
|
14
popup.html
14
popup.html
|
@ -2,7 +2,7 @@
|
|||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
||||
<link rel="stylesheet" href="popup.css">
|
||||
<link rel="stylesheet" href="popup/popup.css">
|
||||
|
||||
<!-- Notes:
|
||||
* Chrome doesn't garbage-collect (or even leaks) SVG <symbol> referenced via <use> so we'll embed the code directly
|
||||
|
@ -55,12 +55,12 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="dom.js"></script>
|
||||
<script src="messaging.js"></script>
|
||||
<script src="localization.js"></script>
|
||||
<script src="prefs.js"></script>
|
||||
<script src="apply.js"></script>
|
||||
<script src="popup.js"></script>
|
||||
<script src="js/dom.js"></script>
|
||||
<script src="js/messaging.js"></script>
|
||||
<script src="js/localization.js"></script>
|
||||
<script src="js/prefs.js"></script>
|
||||
<script src="content/apply.js"></script>
|
||||
<script src="popup/popup.js"></script>
|
||||
</head>
|
||||
|
||||
<body id="stylus-popup">
|
||||
|
|
|
@ -79,7 +79,7 @@ function initPopup(url) {
|
|||
}
|
||||
|
||||
// action buttons
|
||||
$('#disableAll').onchange = function() {
|
||||
$('#disableAll').onchange = function () {
|
||||
installed.classList.toggle('disabled', this.checked);
|
||||
};
|
||||
setupLivePrefs();
|
||||
|
@ -278,7 +278,7 @@ Object.assign(handleEvent, {
|
|||
toggle(event) {
|
||||
saveStyleSafe({
|
||||
id: handleEvent.getClickedStyleId(event),
|
||||
enabled: this.type == 'checkbox' ? this.checked : this.matches('.enable'),
|
||||
enabled: this.type === 'checkbox' ? this.checked : this.matches('.enable'),
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -293,9 +293,9 @@ Object.assign(handleEvent, {
|
|||
window.onkeydown = event => {
|
||||
const keyCode = event.keyCode || event.which;
|
||||
if (!event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey
|
||||
&& (keyCode == 13 || keyCode == 27)) {
|
||||
&& (keyCode === 13 || keyCode === 27)) {
|
||||
event.preventDefault();
|
||||
confirm(keyCode == 13);
|
||||
confirm(keyCode === 13);
|
||||
}
|
||||
};
|
||||
function confirm(ok) {
|
||||
|
@ -342,9 +342,9 @@ Object.assign(handleEvent, {
|
|||
|
||||
maybeEdit(event) {
|
||||
if (!(
|
||||
event.button == 0 && (event.ctrlKey || event.metaKey) ||
|
||||
event.button == 1 ||
|
||||
event.button == 2)) {
|
||||
event.button === 0 && (event.ctrlKey || event.metaKey) ||
|
||||
event.button === 1 ||
|
||||
event.button === 2)) {
|
||||
return;
|
||||
}
|
||||
// open an editor on middleclick
|
||||
|
@ -401,10 +401,10 @@ function detectSloppyRegexps({entry, style}) {
|
|||
for (const section of style.sections) {
|
||||
for (const regexp of section.regexps) {
|
||||
for (let pass = 1; pass <= 2; pass++) {
|
||||
const cacheKey = pass == 1 ? regexp : BG.SLOPPY_REGEXP_PREFIX + regexp;
|
||||
const cacheKey = pass === 1 ? regexp : BG.SLOPPY_REGEXP_PREFIX + regexp;
|
||||
if (!rxCache.has(cacheKey)) {
|
||||
// according to CSS4 @document specification the entire URL must match
|
||||
const anchored = pass == 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$';
|
||||
const anchored = pass === 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$';
|
||||
const rx = tryRegExp(anchored);
|
||||
rxCache.set(cacheKey, rx || false);
|
||||
}
|
|
@ -22,7 +22,7 @@ project = transifex.project(project_slug)
|
|||
project.languages.each do |language|
|
||||
code = language.language_code
|
||||
puts "Getting locale #{code}"
|
||||
dir_name = "_locales/#{code}"
|
||||
dir_name = "../_locales/#{code}"
|
||||
Dir.mkdir(dir_name) if !Dir.exist?(dir_name)
|
||||
has_content = false
|
||||
project.resources.each do |resource|
|
0
pull_locales.sh → tools/pull_locales.sh
Executable file → Normal file
0
pull_locales.sh → tools/pull_locales.sh
Executable file → Normal file
|
@ -2,19 +2,19 @@
|
|||
import io, os, json, re
|
||||
from collections import OrderedDict
|
||||
|
||||
with io.open('_locales/en/messages.json', 'r', encoding='utf-8') as f:
|
||||
with io.open('../_locales/en/messages.json', 'r', encoding='utf-8') as f:
|
||||
items = json.load(f).items()
|
||||
english = [(k, v['message']) for k, v in items if 'message' in v]
|
||||
english_placeholders = [(k, v['placeholders']) for k,v in items
|
||||
if 'placeholders' in v]
|
||||
|
||||
for locale_name in os.listdir('_locales'):
|
||||
for locale_name in os.listdir('../_locales'):
|
||||
if locale_name == 'en':
|
||||
continue
|
||||
if not re.match(r'^\w{2}(_\w{2,3})?$', locale_name):
|
||||
print('Skipped %s: not a locale dir' % locale_name)
|
||||
continue
|
||||
loc_path = '_locales/' + locale_name + '/messages.json'
|
||||
loc_path = '../_locales/' + locale_name + '/messages.json'
|
||||
with io.open(loc_path, 'r+', encoding='utf-8') as f:
|
||||
loc = json.load(f, object_pairs_hook=OrderedDict)
|
||||
|
|
@ -364,7 +364,8 @@
|
|||
}
|
||||
}
|
||||
outputPosCol = 0;
|
||||
let i = output.length, token;
|
||||
let i = output.length;
|
||||
let token;
|
||||
while (--i >= 0 && (token = output[i]) != '\n') {
|
||||
outputPosCol += token.length;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user