PatchCSP + tweaks/fixes/features (#1107)

* add Patch CSP option
* show style version, size, and update age in manager
* add scope selector to style search in manager
* keep scroll position and selections in tab's session
* directly install usercss from raw github links
* ditch localStorage, use on-demand SessionStore proxy
* simplify localization
* allow <code> tag in i18n-html
* keep &nbsp; nodes in HTML templates
* API.getAllStyles is actually faster with code untouched
* fix fitToContent when applies-to is taller than window
* dedupe linter.enableForEditor calls
* prioritize visible CMs in refreshOnViewListener
* don't scroll to last style on editing a new one
* delay colorview for invisible CMs
* eslint comma-dangle error + autofix files
* styleViaXhr: also toggle for disableAll pref
* styleViaXhr: allow cookies for sandbox CSP
* simplify notes in options
* simplify getStylesViaXhr
* oldUI fixups:
  * remove separator before 1st applies-to
  * center name bubbles
* fix updateToc focus on a newly added section
* fix fitToContent when cloning section
* remove CSS `contain` as it makes no difference
* replace overrides with declarative CSS + code cosmetics
* simplify adjustWidth and make it work in FF
This commit is contained in:
tophf 2020-11-18 14:17:15 +03:00 committed by GitHub
parent 7fa4d10fd6
commit 420733b93a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 1273 additions and 1010 deletions

View File

@ -19,7 +19,7 @@ rules:
brace-style: [2, 1tbs, {allowSingleLine: false}] brace-style: [2, 1tbs, {allowSingleLine: false}]
camelcase: [2, {properties: never}] camelcase: [2, {properties: never}]
class-methods-use-this: [2] class-methods-use-this: [2]
comma-dangle: [0] comma-dangle: [2, {arrays: always-multiline, objects: always-multiline}]
comma-spacing: [2, {before: false, after: true}] comma-spacing: [2, {before: false, after: true}]
comma-style: [2, last] comma-style: [2, last]
complexity: [0] complexity: [0]

View File

@ -260,6 +260,42 @@
"message": "Stop using customized name, switch to the style's own name", "message": "Stop using customized name, switch to the style's own name",
"description": "Tooltip of 'x' button shown in editor when changing the name input of a) styles updated from a URL i.e. not locally created, b) UserCSS styles" "description": "Tooltip of 'x' button shown in editor when changing the name input of a) styles updated from a URL i.e. not locally created, b) UserCSS styles"
}, },
"dateAbbrDay": {
"message": "$value$d",
"description": "Day suffix in a short relative date, for example: 8d",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"dateAbbrHour": {
"message": "$value$h",
"description": "Hour suffix in a short relative date, for example: 8h",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"dateAbbrMonth": {
"message": "$value$m",
"description": "Month suffix in a short relative date, for example: 8m",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"dateAbbrYear": {
"message": "$value$y",
"description": "Year suffix in a short relative date, for example: 8y",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"dateInstalled": { "dateInstalled": {
"message": "Date installed", "message": "Date installed",
"description": "Option text for the user to sort the style by install date" "description": "Option text for the user to sort the style by install date"
@ -976,6 +1012,12 @@
"optionsAdvancedNewStyleAsUsercss": { "optionsAdvancedNewStyleAsUsercss": {
"message": "Write new style as usercss" "message": "Write new style as usercss"
}, },
"optionsAdvancedPatchCsp": {
"message": "Patch <code>CSP</code> to allow style assets"
},
"optionsAdvancedPatchCspNote": {
"message": "Enable this if styles contain images or fonts which fail to load on sites with a strict <code>CSP</code> (<code>Content-Security-Policy</code>).\n\nEnabling this setting will relax <code>CSP</code> restrictions, allowing essential style content to load. This option is only intended for advanced users who understand the potential security implications, and accept responsibility for monitoring the content which they're allowing. Read about CSS-based attacks for more information.\n\nAlso be aware, this particular setting is not guaranteed to take effect if another installed extension modifies the network response first."
},
"optionsAdvancedStyleViaXhr": { "optionsAdvancedStyleViaXhr": {
"message": "Instant inject mode" "message": "Instant inject mode"
}, },
@ -1243,14 +1285,30 @@
"message": "Weekly installs", "message": "Weekly installs",
"description": "Text for label that shows the number of times a search result was installed during last week" "description": "Text for label that shows the number of times a search result was installed during last week"
}, },
"searchStyles": { "searchStylesAll": {
"message": "Search contents", "message": "All",
"description": "Label for the search filter textbox on the Manage styles page" "description": "Option for `find styles` scope selector in the manager."
},
"searchStylesCode": {
"message": "CSS code",
"description": "Option for `find styles` scope selector in the manager."
}, },
"searchStylesHelp": { "searchStylesHelp": {
"message": "</> key focuses the search field.\nPlain text: search within the name, code, homepage URL and sites it is applied to. Words with less than 3 letters are ignored.\nStyles matching a full URL: prefix the search with <url:>, e.g. <url:https://github.com/openstyles/stylus>\nRegular expressions: include slashes and flags, e.g. </body.*?\\ba\\b/simguy>\nExact words: wrap the query in double quotes, e.g. <\".header ~ div\">", "message": "</> or <Ctrl-F> key focuses the search field.\nDefault mode is plain text search for all space-separated terms in any order.\nExact words: wrap the query in double quotes, e.g. <\".header ~ div\">\nRegular expressions: include slashes and flags, e.g. </body.*?\\ba\\b/i>\n\"By URL\" in scope selector: finds styles that apply to a fully specified URL e.g. https://www.example.org/\n\"Metadata\" in scope selector: searches in names, \"applies to\" specifiers, installation URL, update URL, and the entire metadata block for usercss styles.",
"description": "Text in the minihelp displayed when clicking (i) icon to the right of the search input field on the Manage styles page" "description": "Text in the minihelp displayed when clicking (i) icon to the right of the search input field on the Manage styles page"
}, },
"searchStylesMatchUrl": {
"message": "By URL",
"description": "Option for `find styles` scope selector in the manager. See searchMatchUrlHint for more info."
},
"searchStylesMeta": {
"message": "Metadata",
"description": "Option for `find styles` scope selector in the manager."
},
"searchStylesName": {
"message": "Name",
"description": "Option for `find styles` scope selector in the manager."
},
"sectionAdd": { "sectionAdd": {
"message": "Add another section", "message": "Add another section",
"description": "Label for the button to add a section" "description": "Label for the button to add a section"

View File

@ -25,7 +25,7 @@ workerUtil.createAPI({
'/js/meta-parser.js' '/js/meta-parser.js'
); );
return metaParser.nullifyInvalidVars(vars); return metaParser.nullifyInvalidVars(vars);
} },
}); });
function compileUsercss(preprocessor, code, vars) { function compileUsercss(preprocessor, code, vars) {
@ -55,7 +55,7 @@ function compileUsercss(preprocessor, code, vars) {
const va = vars[key]; const va = vars[key];
output[key] = Object.assign({}, va, { output[key] = Object.assign({}, va, {
value: va.value === null || va.value === undefined ? value: va.value === null || va.value === undefined ?
getVarValue(va, 'default') : getVarValue(va, 'value') getVarValue(va, 'default') : getVarValue(va, 'value'),
}); });
return output; return output;
}, {}); }, {});
@ -86,7 +86,7 @@ function getUsercssCompiler(preprocessor) {
section.code = varDef + section.code; section.code = varDef + section.code;
} }
} }
} },
}, },
stylus: { stylus: {
preprocess(source, vars) { preprocess(source, vars) {
@ -96,7 +96,7 @@ function getUsercssCompiler(preprocessor) {
new self.StylusRenderer(varDef + source) new self.StylusRenderer(varDef + source)
.render((err, output) => err ? reject(err) : resolve(output)); .render((err, output) => err ? reject(err) : resolve(output));
}); });
} },
}, },
less: { less: {
preprocess(source, vars) { preprocess(source, vars) {
@ -110,7 +110,7 @@ function getUsercssCompiler(preprocessor) {
const varDefs = Object.keys(vars).map(key => `@${key}:${vars[key].value};\n`).join(''); const varDefs = Object.keys(vars).map(key => `@${key}:${vars[key].value};\n`).join('');
return self.less.render(varDefs + source) return self.less.render(varDefs + source)
.then(({css}) => css); .then(({css}) => css);
} },
}, },
uso: { uso: {
preprocess(source, vars) { preprocess(source, vars) {
@ -162,8 +162,8 @@ function getUsercssCompiler(preprocessor) {
return pool.get(name); return pool.get(name);
}); });
} }
} },
} },
}; };
if (preprocessor) { if (preprocessor) {

View File

@ -8,7 +8,7 @@
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
var backgroundWorker = workerUtil.createWorker({ var backgroundWorker = workerUtil.createWorker({
url: '/background/background-worker.js' url: '/background/background-worker.js',
}); });
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
@ -99,7 +99,7 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
getSyncStatus: sync.getStatus, getSyncStatus: sync.getStatus,
syncLogin: sync.login, syncLogin: sync.login,
openManage openManage,
}); });
// ************************************************************************* // *************************************************************************
@ -119,7 +119,7 @@ if (FIREFOX) {
navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, { navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, {
url: [ url: [
{urlEquals: 'about:blank'}, {urlEquals: 'about:blank'},
] ],
}); });
} }
@ -135,24 +135,13 @@ if (chrome.commands) {
// ************************************************************************* // *************************************************************************
chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => { chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
// save install type: "admin", "development", "normal", "sideload" or "other"
// "normal" = addon installed from webstore
chrome.management.getSelf(info => {
localStorage.installType = info.installType;
if (reason === 'install' && info.installType === 'development' && chrome.contextMenus) {
createContextMenus(['reload']);
}
});
if (reason !== 'update') return; if (reason !== 'update') return;
// translations may change
localStorage.L10N = JSON.stringify({
browserUIlanguage: chrome.i18n.getUILanguage(),
});
// themes may change
delete localStorage.codeMirrorThemes;
// inline search cache for USO is not needed anymore, TODO: remove this by the middle of 2021
if (semverCompare(previousVersion, '1.5.13') <= 0) { if (semverCompare(previousVersion, '1.5.13') <= 0) {
// Removing unused stuff
// TODO: delete this entire block by the middle of 2021
try {
localStorage.clear();
} catch (e) {}
setTimeout(async () => { setTimeout(async () => {
const del = Object.keys(await chromeLocal.get()) const del = Object.keys(await chromeLocal.get())
.filter(key => key.startsWith('usoSearchCache')); .filter(key => key.startsWith('usoSearchCache'));
@ -181,7 +170,7 @@ contextMenus = {
click: browserCommands.openOptions, click: browserCommands.openOptions,
}, },
'reload': { 'reload': {
presentIf: () => localStorage.installType === 'development', presentIf: async () => (await browser.management.getSelf()).installType === 'development',
title: 'reload', title: 'reload',
click: browserCommands.reload, click: browserCommands.reload,
}, },
@ -195,13 +184,13 @@ contextMenus = {
msg.sendTab(tab.id, {method: 'editDeleteText'}, undefined, 'extension') msg.sendTab(tab.id, {method: 'editDeleteText'}, undefined, 'extension')
.catch(msg.ignoreError); .catch(msg.ignoreError);
}, },
} },
}; };
function createContextMenus(ids) { async function createContextMenus(ids) {
for (const id of ids) { for (const id of ids) {
let item = contextMenus[id]; let item = contextMenus[id];
if (item.presentIf && !item.presentIf()) { if (item.presentIf && !await item.presentIf()) {
continue; continue;
} }
item = Object.assign({id}, item); item = Object.assign({id}, item);
@ -320,33 +309,29 @@ function openEditor(params) {
}); });
} }
function openManage({options = false, search} = {}) { async function openManage({options = false, search, searchMode} = {}) {
let url = chrome.runtime.getURL('manage.html'); let url = chrome.runtime.getURL('manage.html');
if (search) { if (search) {
url += `?search=${encodeURIComponent(search)}`; url += `?search=${encodeURIComponent(search)}&searchMode=${searchMode}`;
} }
if (options) { if (options) {
url += '#stylus-options'; url += '#stylus-options';
} }
return findExistingTab({ let tab = await findExistingTab({
url, url,
currentWindow: null, currentWindow: null,
ignoreHash: true, ignoreHash: true,
ignoreSearch: true ignoreSearch: true,
}) });
.then(tab => { if (tab) {
if (tab) { await activateTab(tab);
return Promise.all([ if (url !== (tab.pendingUrl || tab.url)) {
activateTab(tab), await msg.sendTab(tab.id, {method: 'pushState', url}).catch(console.error);
(tab.pendingUrl || tab.url) !== url && msg.sendTab(tab.id, {method: 'pushState', url}) }
.catch(console.error) return tab;
]); }
} tab = await getActiveTab();
return getActiveTab().then(tab => { return isTabReplaceable(tab, url)
if (isTabReplaceable(tab, url)) { ? activateTab(tab, {url})
return activateTab(tab, {url}); : browser.tabs.create({url});
}
return browser.tabs.create({url});
});
});
} }

View File

@ -29,7 +29,7 @@ const contentScripts = (() => {
url: [ url: [
{hostEquals: 'greasyfork.org', urlMatches}, {hostEquals: 'greasyfork.org', urlMatches},
{hostEquals: 'sleazyfork.org', urlMatches}, {hostEquals: 'sleazyfork.org', urlMatches},
] ],
}); });
return {injectToTab, injectToAllTabs}; return {injectToTab, injectToAllTabs};
@ -57,7 +57,7 @@ const contentScripts = (() => {
const options = { const options = {
runAt: script.run_at, runAt: script.run_at,
allFrames: script.all_frames, allFrames: script.all_frames,
matchAboutBlank: script.match_about_blank matchAboutBlank: script.match_about_blank,
}; };
if (frameId !== null) { if (frameId !== null) {
options.allFrames = false; options.allFrames = false;
@ -80,7 +80,7 @@ const contentScripts = (() => {
} else { } else {
injectToTab({ injectToTab({
url: tab.pendingUrl || tab.url, url: tab.pendingUrl || tab.url,
tabId: tab.id tabId: tab.id,
}); });
} }
} }

View File

@ -32,7 +32,7 @@ function createChromeStorageDB() {
} }
} }
return output; return output;
}) }),
}; };
return {exec}; return {exec};

View File

@ -24,14 +24,11 @@ const db = (() => {
async function tryUsingIndexedDB() { async function tryUsingIndexedDB() {
// we use chrome.storage.local fallback if IndexedDB doesn't save data, // we use chrome.storage.local fallback if IndexedDB doesn't save data,
// which, once detected on the first run, is remembered in chrome.storage.local // which, once detected on the first run, is remembered in chrome.storage.local
// for reliablility and in localStorage for fast synchronous access // note that accessing indexedDB may throw, https://github.com/openstyles/stylus/issues/615
// (FF may block localStorage depending on its privacy options)
// note that it may throw when accessing the variable
// https://github.com/openstyles/stylus/issues/615
if (typeof indexedDB === 'undefined') { if (typeof indexedDB === 'undefined') {
throw new Error('indexedDB is undefined'); throw new Error('indexedDB is undefined');
} }
switch (await getFallback()) { switch (await chromeLocal.getValue(FALLBACK)) {
case true: throw null; case true: throw null;
case false: break; case false: break;
default: await testDB(); default: await testDB();
@ -39,12 +36,6 @@ const db = (() => {
return useIndexedDB(); return useIndexedDB();
} }
async function getFallback() {
return localStorage[FALLBACK] === 'true' ? true :
localStorage[FALLBACK] === 'false' ? false :
chromeLocal.getValue(FALLBACK);
}
async function testDB() { async function testDB() {
let e = await dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1); let e = await dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1);
// throws if result is null // throws if result is null
@ -62,13 +53,11 @@ const db = (() => {
chromeLocal.setValue(FALLBACK + 'Reason', workerUtil.cloneError(err)); chromeLocal.setValue(FALLBACK + 'Reason', workerUtil.cloneError(err));
console.warn('Failed to access indexedDB. Switched to storage API.', err); console.warn('Failed to access indexedDB. Switched to storage API.', err);
} }
localStorage[FALLBACK] = 'true';
return createChromeStorageDB().exec; return createChromeStorageDB().exec;
} }
function useIndexedDB() { function useIndexedDB() {
chromeLocal.setValue(FALLBACK, false); chromeLocal.setValue(FALLBACK, false);
localStorage[FALLBACK] = 'false';
return dbExecIndexedDB; return dbExecIndexedDB;
} }

View File

@ -13,7 +13,7 @@ const iconManager = (() => {
], () => debounce(refreshIconBadgeColor)); ], () => debounce(refreshIconBadgeColor));
prefs.subscribe([ prefs.subscribe([
'show-badge' 'show-badge',
], () => debounce(refreshAllIconsBadgeText)); ], () => debounce(refreshAllIconsBadgeText));
prefs.subscribe([ prefs.subscribe([
@ -79,7 +79,7 @@ const iconManager = (() => {
tabManager.set(tabId, 'icon', newIcon); tabManager.set(tabId, 'icon', newIcon);
iconUtil.setIcon({ iconUtil.setIcon({
path: getIconPath(newIcon), path: getIconPath(newIcon),
tabId tabId,
}); });
} }
@ -103,14 +103,14 @@ const iconManager = (() => {
function refreshGlobalIcon() { function refreshGlobalIcon() {
iconUtil.setIcon({ iconUtil.setIcon({
path: getIconPath(getIconName()) path: getIconPath(getIconName()),
}); });
} }
function refreshIconBadgeColor() { function refreshIconBadgeColor() {
const color = prefs.get(prefs.get('disableAll') ? 'badgeDisabled' : 'badgeNormal'); const color = prefs.get(prefs.get('disableAll') ? 'badgeDisabled' : 'badgeNormal');
iconUtil.setBadgeBackgroundColor({ iconUtil.setBadgeBackgroundColor({
color color,
}); });
} }

View File

@ -19,7 +19,7 @@ const iconUtil = (() => {
Cache imageData for paths Cache imageData for paths
*/ */
setIcon, setIcon,
setBadgeText setBadgeText,
}); });
function loadImage(url) { function loadImage(url) {
@ -85,7 +85,7 @@ const iconUtil = (() => {
return target[prop]; return target[prop];
} }
return chrome.browserAction[prop].bind(chrome.browserAction); return chrome.browserAction[prop].bind(chrome.browserAction);
} },
}); });
} }
})(); })();

View File

@ -4,7 +4,7 @@
const navigatorUtil = (() => { const navigatorUtil = (() => {
const handler = { const handler = {
urlChange: null urlChange: null,
}; };
return extendNative({onUrlChange}); return extendNative({onUrlChange});
@ -69,7 +69,7 @@ const navigatorUtil = (() => {
return target[prop]; return target[prop];
} }
return chrome.webNavigation[prop].addListener.bind(chrome.webNavigation[prop]); return chrome.webNavigation[prop].addListener.bind(chrome.webNavigation[prop]);
} },
}); });
} }
})(); })();

View File

@ -31,11 +31,11 @@
return fetch(api, { return fetch(api, {
method: 'POST', method: 'POST',
headers: new Headers({ headers: new Headers({
'Content-Type': 'application/json' 'Content-Type': 'application/json',
}), }),
body: query({ body: query({
id id,
}) }),
}) })
.then(res => res.json()); .then(res => res.json());
}; };

View File

@ -1,90 +1,97 @@
/* global API_METHODS styleManager tryRegExp debounce */ /* global
API_METHODS
debounce
stringAsRegExp
styleManager
tryRegExp
usercss
*/
'use strict'; 'use strict';
(() => { (() => {
// toLocaleLowerCase cache, autocleared after 1 minute // toLocaleLowerCase cache, autocleared after 1 minute
const cache = new Map(); const cache = new Map();
// top-level style properties to be searched const METAKEYS = ['customName', 'name', 'url', 'installationUrl', 'updateUrl'];
const PARTS = {
name: searchText, const extractMeta = style =>
url: searchText, style.usercssData
sourceCode: searchText, ? (style.sourceCode.match(usercss.RX_META) || [''])[0]
sections: searchSections, : null;
};
const stripMeta = style =>
style.usercssData
? style.sourceCode.replace(usercss.RX_META, '')
: null;
const MODES = Object.assign(Object.create(null), {
code: (style, test) =>
style.usercssData
? test(stripMeta(style))
: searchSections(style, test, 'code'),
meta: (style, test, part) =>
METAKEYS.some(key => test(style[key])) ||
test(part === 'all' ? style.sourceCode : extractMeta(style)) ||
searchSections(style, test, 'funcs'),
name: (style, test) =>
test(style.customName) ||
test(style.name),
all: (style, test) =>
MODES.meta(style, test, 'all') ||
!style.usercssData && MODES.code(style, test),
});
/** /**
* @param params * @param params
* @param {string} params.query - 1. url:someurl 2. text (may contain quoted parts like "qUot Ed") * @param {string} params.query - 1. url:someurl 2. text (may contain quoted parts like "qUot Ed")
* @param {'name'|'meta'|'code'|'all'|'url'} [params.mode=all]
* @param {number[]} [params.ids] - if not specified, all styles are searched * @param {number[]} [params.ids] - if not specified, all styles are searched
* @returns {number[]} - array of matched styles ids * @returns {number[]} - array of matched styles ids
*/ */
API_METHODS.searchDB = ({query, ids}) => { API_METHODS.searchDB = async ({query, mode = 'all', ids}) => {
let rx, words, icase, matchUrl; let res = [];
query = query.trim(); if (mode === 'url' && query) {
res = (await styleManager.getStylesByUrl(query)).map(r => r.data.id);
if (/^url:/i.test(query)) { } else if (mode in MODES) {
matchUrl = query.slice(query.indexOf(':') + 1).trim(); const modeHandler = MODES[mode];
if (matchUrl) { const m = /^\/(.+?)\/([gimsuy]*)$/.exec(query);
return styleManager.getStylesByUrl(matchUrl) const rx = m && tryRegExp(m[1], m[2]);
.then(results => results.map(r => r.data.id)); const test = rx ? rx.test.bind(rx) : makeTester(query);
} res = (await styleManager.getAllStyles())
} .filter(style =>
if (query.startsWith('/') && /^\/(.+?)\/([gimsuy]*)$/.test(query)) { (!ids || ids.includes(style.id)) &&
rx = tryRegExp(RegExp.$1, RegExp.$2); (!query || modeHandler(style, test)))
} .map(style => style.id);
if (!rx) {
words = query
.split(/(".*?")|\s+/)
.filter(Boolean)
.map(w => w.startsWith('"') && w.endsWith('"')
? w.slice(1, -1)
: w)
.filter(w => w.length > 1);
words = words.length ? words : [query];
icase = words.some(w => w === lower(w));
}
return styleManager.getAllStyles().then(styles => {
if (ids) {
const idSet = new Set(ids);
styles = styles.filter(s => idSet.has(s.id));
}
const results = [];
for (const style of styles) {
const id = style.id;
if (!query || words && !words.length) {
results.push(id);
continue;
}
for (const part in PARTS) {
const text = part === 'name' ? style.customName || style.name : style[part];
if (text && PARTS[part](text, rx, words, icase)) {
results.push(id);
break;
}
}
}
if (cache.size) debounce(clearCache, 60e3); if (cache.size) debounce(clearCache, 60e3);
return results; }
}); return res;
}; };
function searchText(text, rx, words, icase) { function makeTester(query) {
if (rx) return rx.test(text); const flags = `u${lower(query) === query ? 'i' : ''}`;
for (let pass = 1; pass <= (icase ? 2 : 1); pass++) { const words = query
if (words.every(w => text.includes(w))) return true; .split(/(".*?")|\s+/)
text = lower(text); .filter(Boolean)
} .map(w => w.startsWith('"') && w.endsWith('"')
? w.slice(1, -1)
: w)
.filter(w => w.length > 1);
const rxs = (words.length ? words : [query])
.map(w => stringAsRegExp(w, flags));
return text => rxs.every(rx => rx.test(text));
} }
function searchSections(sections, rx, words, icase) { function searchSections({sections}, test, part) {
const inCode = part === 'code' || part === 'all';
const inFuncs = part === 'funcs' || part === 'all';
for (const section of sections) { for (const section of sections) {
for (const prop in section) { for (const prop in section) {
const value = section[prop]; const value = section[prop];
if (typeof value === 'string') { if (inCode && prop === 'code' && test(value) ||
if (searchText(value, rx, words, icase)) return true; inFuncs && Array.isArray(value) && value.some(str => test(str))) {
} else if (Array.isArray(value)) { return true;
if (value.some(str => searchText(str, rx, words, icase))) return true;
} }
} }
} }
@ -92,9 +99,7 @@
function lower(text) { function lower(text) {
let result = cache.get(text); let result = cache.get(text);
if (result) return result; if (!result) cache.set(text, result = text.toLocaleLowerCase());
result = text.toLocaleLowerCase();
cache.set(text, result);
return result; return result;
} }

View File

@ -12,6 +12,8 @@ script would try to fetch the new code.
The live preview feature relies on `runtime.connect` and `port.onDisconnect` The live preview feature relies on `runtime.connect` and `port.onDisconnect`
to cleanup the temporary code. See /edit/live-preview.js. to cleanup the temporary code. See /edit/live-preview.js.
*/ */
/** @type {styleManager} */
const styleManager = (() => { const styleManager = (() => {
const preparing = prepare(); const preparing = prepare();
@ -38,7 +40,7 @@ const styleManager = (() => {
style.appliesTo.delete(url); style.appliesTo.delete(url);
} }
} }
} },
}); });
const BAD_MATCHER = {test: () => false}; const BAD_MATCHER = {test: () => false};
@ -58,16 +60,16 @@ const styleManager = (() => {
protocol: '', protocol: '',
search: '', search: '',
searchParams: new URLSearchParams(), searchParams: new URLSearchParams(),
username: '' username: '',
}; };
const DELETE_IF_NULL = ['id', 'customName']; const DELETE_IF_NULL = ['id', 'customName'];
handleLivePreviewConnections(); handleLivePreviewConnections();
return Object.assign({ return Object.assign(/** @namespace styleManager */{
compareRevision compareRevision,
}, ensurePrepared({ }, ensurePrepared(/** @namespace styleManager */{
get, get,
getByUUID, getByUUID,
getSectionsByUrl, getSectionsByUrl,
@ -86,7 +88,7 @@ const styleManager = (() => {
addExclusion, addExclusion,
removeExclusion, removeExclusion,
addInclusion, addInclusion,
removeInclusion removeInclusion,
})); }));
function handleLivePreviewConnections() { function handleLivePreviewConnections() {
@ -135,9 +137,8 @@ const styleManager = (() => {
} }
} }
function getAllStyles(noCode = false) { function getAllStyles() {
const datas = [...styles.values()].map(s => s.data); return [...styles.values()].map(s => s.data);
return noCode ? datas.map(getStyleWithNoCode) : datas;
} }
function compareRevision(rev1, rev2) { function compareRevision(rev1, rev2) {
@ -316,7 +317,7 @@ const styleManager = (() => {
uuidIndex.delete(style.data._id); uuidIndex.delete(style.data._id);
return msg.broadcast({ return msg.broadcast({
method: 'styleDeleted', method: 'styleDeleted',
style: {id} style: {id},
}); });
}) })
.then(() => id); .then(() => id);
@ -347,7 +348,7 @@ const styleManager = (() => {
md5Url: null, md5Url: null,
url: null, url: null,
originalMd5: null, originalMd5: null,
installDate: Date.now() installDate: Date.now(),
}; };
} }
@ -368,7 +369,7 @@ const styleManager = (() => {
updated.add(url); updated.add(url);
cache.sections[data.id] = { cache.sections[data.id] = {
id: data.id, id: data.id,
code code,
}; };
} }
} }
@ -378,10 +379,10 @@ const styleManager = (() => {
style: { style: {
id: data.id, id: data.id,
md5Url: data.md5Url, md5Url: data.md5Url,
enabled: data.enabled enabled: data.enabled,
}, },
reason, reason,
codeIsUpdated codeIsUpdated,
}); });
} }
@ -424,7 +425,7 @@ const styleManager = (() => {
if (!style) { if (!style) {
styles.set(data.id, { styles.set(data.id, {
appliesTo: new Set(), appliesTo: new Set(),
data data,
}); });
method = 'styleAdded'; method = 'styleAdded';
} else { } else {
@ -469,11 +470,7 @@ const styleManager = (() => {
} }
} }
if (sectionMatched) { if (sectionMatched) {
result.push({ result.push({data, excluded, sloppy});
data: getStyleWithNoCode(data),
excluded,
sloppy
});
} }
} }
return result; return result;
@ -484,7 +481,7 @@ const styleManager = (() => {
if (!cache) { if (!cache) {
cache = { cache = {
sections: {}, sections: {},
maybeMatch: new Set() maybeMatch: new Set(),
}; };
buildCache(styles.values()); buildCache(styles.values());
cachedStyleForUrl.set(url, cache); cachedStyleForUrl.set(url, cache);
@ -510,7 +507,7 @@ const styleManager = (() => {
if (code) { if (code) {
cache.sections[data.id] = { cache.sections[data.id] = {
id: data.id, id: data.id,
code code,
}; };
appliesTo.add(url); appliesTo.add(url);
} }
@ -535,7 +532,7 @@ const styleManager = (() => {
const ADD_MISSING_PROPS = { const ADD_MISSING_PROPS = {
name: style => `ID: ${style.id}`, name: style => `ID: ${style.id}`,
_id: () => uuidv4(), _id: () => uuidv4(),
_rev: () => Date.now() _rev: () => Date.now(),
}; };
return db.exec('getAll') return db.exec('getAll')
@ -559,7 +556,7 @@ const styleManager = (() => {
fixUsoMd5Issue(style); fixUsoMd5Issue(style);
styles.set(style.id, { styles.set(style.id, {
appliesTo: new Set(), appliesTo: new Set(),
data: style data: style,
}); });
uuidIndex.set(style._id, style.id); uuidIndex.set(style._id, style.id);
} }
@ -705,7 +702,7 @@ const styleManager = (() => {
domain = u.hostname; domain = u.hostname;
} }
return domain; return domain;
} },
}; };
} }

View File

@ -0,0 +1,140 @@
/* global API CHROME prefs */
'use strict';
// eslint-disable-next-line no-unused-expressions
CHROME && (async () => {
const idCSP = 'patchCsp';
const idOFF = 'disableAll';
const idXHR = 'styleViaXhr';
const rxHOST = /^('none'|(https?:\/\/)?[^']+?[^:'])$/; // strips CSP sources covered by *
const blobUrlPrefix = 'blob:' + chrome.runtime.getURL('/');
const stylesToPass = {};
const enabled = {};
await prefs.initializing;
prefs.subscribe([idXHR, idOFF, idCSP], toggle, {now: true});
function toggle() {
const csp = prefs.get(idCSP) && !prefs.get(idOFF);
const xhr = prefs.get(idXHR) && !prefs.get(idOFF) && Boolean(chrome.declarativeContent);
if (xhr === enabled.xhr && csp === enabled.csp) {
return;
}
// Need to unregister first so that the optional EXTRA_HEADERS is properly registered
chrome.webRequest.onBeforeRequest.removeListener(prepareStyles);
chrome.webRequest.onHeadersReceived.removeListener(modifyHeaders);
if (xhr || csp) {
const reqFilter = {
urls: ['<all_urls>'],
types: ['main_frame', 'sub_frame'],
};
chrome.webRequest.onBeforeRequest.addListener(prepareStyles, reqFilter);
chrome.webRequest.onHeadersReceived.addListener(modifyHeaders, reqFilter, [
'blocking',
'responseHeaders',
xhr && chrome.webRequest.OnHeadersReceivedOptions.EXTRA_HEADERS,
].filter(Boolean));
}
if (enabled.xhr !== xhr) {
enabled.xhr = xhr;
toggleEarlyInjection();
}
enabled.csp = csp;
}
/** Runs content scripts earlier than document_start */
function toggleEarlyInjection() {
const api = chrome.declarativeContent;
if (!api) return;
api.onPageChanged.removeRules([idXHR], async () => {
if (enabled.xhr) {
api.onPageChanged.addRules([{
id: idXHR,
conditions: [
new api.PageStateMatcher({
pageUrl: {urlContains: '://'},
}),
],
actions: [
new api.RequestContentScript({
js: chrome.runtime.getManifest().content_scripts[0].js,
allFrames: true,
}),
],
}]);
}
});
}
/** @param {chrome.webRequest.WebRequestBodyDetails} req */
function prepareStyles(req) {
API.getSectionsByUrl(req.url).then(sections => {
if (Object.keys(sections).length) {
stylesToPass[req.requestId] = !enabled.xhr ? true :
URL.createObjectURL(new Blob([JSON.stringify(sections)])).slice(blobUrlPrefix.length);
setTimeout(cleanUp, 600e3, req.requestId);
}
});
}
/** @param {chrome.webRequest.WebResponseHeadersDetails} req */
function modifyHeaders(req) {
const {responseHeaders} = req;
const id = stylesToPass[req.requestId];
if (!id) {
return;
}
if (enabled.xhr) {
responseHeaders.push({
name: 'Set-Cookie',
value: `${chrome.runtime.id}=${id}`,
});
}
const csp = enabled.csp &&
responseHeaders.find(h => h.name.toLowerCase() === 'content-security-policy');
if (csp) {
patchCsp(csp);
}
if (enabled.xhr || csp) {
return {responseHeaders};
}
}
/** @param {chrome.webRequest.HttpHeader} csp */
function patchCsp(csp) {
const src = {};
for (let p of csp.value.split(';')) {
p = p.trim().split(/\s+/);
src[p[0]] = p.slice(1);
}
// Allow style assets
patchCspSrc(src, 'img-src', 'data:', '*');
patchCspSrc(src, 'font-src', 'data:', '*');
// Allow our DOM styles
patchCspSrc(src, 'style-src', '\'unsafe-inline\'');
// Allow our XHR cookies in CSP sandbox (known case: raw github urls)
if (src.sandbox && !src.sandbox.includes('allow-same-origin')) {
src.sandbox.push('allow-same-origin');
}
csp.value = Object.entries(src).map(([k, v]) =>
`${k}${v.length ? ' ' : ''}${v.join(' ')}`).join('; ');
}
function patchCspSrc(src, name, ...values) {
let def = src['default-src'];
let list = src[name];
if (def || list) {
if (!def) def = [];
if (!list) list = [...def];
if (values.includes('*')) list = src[name] = list.filter(v => !rxHOST.test(v));
list.push(...values.filter(v => !list.includes(v) && !def.includes(v)));
if (!list.length) delete src[name];
}
}
function cleanUp(key) {
const blobId = stylesToPass[key];
delete stylesToPass[key];
if (blobId) URL.revokeObjectURL(blobUrlPrefix + blobId);
}
})();

View File

@ -1,85 +0,0 @@
/* global API CHROME prefs */
'use strict';
// eslint-disable-next-line no-unused-expressions
CHROME && (async () => {
const prefId = 'styleViaXhr';
const blobUrlPrefix = 'blob:' + chrome.runtime.getURL('/');
const stylesToPass = {};
await prefs.initializing;
toggle(prefId, prefs.get(prefId));
prefs.subscribe([prefId], toggle);
function toggle(key, value) {
if (!chrome.declarativeContent) { // not yet granted in options page
value = false;
}
if (value) {
const reqFilter = {
urls: ['<all_urls>'],
types: ['main_frame', 'sub_frame'],
};
chrome.webRequest.onBeforeRequest.addListener(prepareStyles, reqFilter);
chrome.webRequest.onHeadersReceived.addListener(passStyles, reqFilter, [
'blocking',
'responseHeaders',
chrome.webRequest.OnHeadersReceivedOptions.EXTRA_HEADERS,
].filter(Boolean));
} else {
chrome.webRequest.onBeforeRequest.removeListener(prepareStyles);
chrome.webRequest.onHeadersReceived.removeListener(passStyles);
}
if (!chrome.declarativeContent) {
return;
}
chrome.declarativeContent.onPageChanged.removeRules([prefId], async () => {
if (!value) return;
chrome.declarativeContent.onPageChanged.addRules([{
id: prefId,
conditions: [
new chrome.declarativeContent.PageStateMatcher({
pageUrl: {urlContains: ':'},
}),
],
actions: [
new chrome.declarativeContent.RequestContentScript({
allFrames: true,
// This runs earlier than document_start
js: chrome.runtime.getManifest().content_scripts[0].js,
}),
],
}]);
});
}
/** @param {chrome.webRequest.WebRequestBodyDetails} req */
function prepareStyles(req) {
API.getSectionsByUrl(req.url).then(sections => {
const str = JSON.stringify(sections);
if (str !== '{}') {
stylesToPass[req.requestId] = URL.createObjectURL(new Blob([str])).slice(blobUrlPrefix.length);
setTimeout(cleanUp, 600e3, req.requestId);
}
});
}
/** @param {chrome.webRequest.WebResponseHeadersDetails} req */
function passStyles(req) {
const blobId = stylesToPass[req.requestId];
if (blobId) {
const {responseHeaders} = req;
responseHeaders.push({
name: 'Set-Cookie',
value: `${chrome.runtime.id}=${prefs.get('disableAll') ? 1 : 0}${blobId}`,
});
return {responseHeaders};
}
}
function cleanUp(key) {
const blobId = stylesToPass[key];
delete stylesToPass[key];
if (blobId) URL.revokeObjectURL(blobUrlPrefix + blobId);
}
})();

View File

@ -13,7 +13,7 @@ const sync = (() => {
progress: null, progress: null,
currentDriveName: null, currentDriveName: null,
errorMessage: null, errorMessage: null,
login: false login: false,
}; };
let currentDrive; let currentDrive;
const ctrl = dbToCloud.dbToCloud({ const ctrl = dbToCloud.dbToCloud({
@ -43,7 +43,7 @@ const sync = (() => {
setState(drive, state) { setState(drive, state) {
const key = `sync/state/${drive.name}`; const key = `sync/state/${drive.name}`;
return chromeLocal.setValue(key, state); return chromeLocal.setValue(key, state);
} },
}); });
const initializing = prefs.initializing.then(() => { const initializing = prefs.initializing.then(() => {
@ -58,7 +58,7 @@ const sync = (() => {
}); });
return Object.assign({ return Object.assign({
getStatus: () => status getStatus: () => status,
}, ensurePrepared({ }, ensurePrepared({
start, start,
stop, stop,
@ -73,7 +73,7 @@ const sync = (() => {
return ctrl.delete(...args); return ctrl.delete(...args);
}, },
syncNow, syncNow,
login login,
})); }));
function ensurePrepared(obj) { function ensurePrepared(obj) {
@ -99,7 +99,7 @@ const sync = (() => {
function schedule(delay = SYNC_DELAY) { function schedule(delay = SYNC_DELAY) {
chrome.alarms.create('syncNow', { chrome.alarms.create('syncNow', {
delayInMinutes: delay, delayInMinutes: delay,
periodInMinutes: SYNC_INTERVAL periodInMinutes: SYNC_INTERVAL,
}); });
} }
@ -206,7 +206,7 @@ const sync = (() => {
function getDrive(name) { function getDrive(name) {
if (name === 'dropbox' || name === 'google' || name === 'onedrive') { if (name === 'dropbox' || name === 'google' || name === 'onedrive') {
return dbToCloud.drive[name]({ return dbToCloud.drive[name]({
getAccessToken: () => tokenManager.getToken(name) getAccessToken: () => tokenManager.getToken(name),
}); });
} }
throw new Error(`unknown cloud name: ${name}`); throw new Error(`unknown cloud name: ${name}`);

View File

@ -13,9 +13,9 @@ const tokenManager = (() => {
fetch('https://api.dropboxapi.com/2/auth/token/revoke', { fetch('https://api.dropboxapi.com/2/auth/token/revoke', {
method: 'POST', method: 'POST',
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`,
} },
}) }),
}, },
google: { google: {
flow: 'code', flow: 'code',
@ -27,14 +27,14 @@ const tokenManager = (() => {
// tokens for multiple machines. // tokens for multiple machines.
// https://stackoverflow.com/q/18519185 // https://stackoverflow.com/q/18519185
access_type: 'offline', access_type: 'offline',
prompt: 'consent' prompt: 'consent',
}, },
tokenURL: 'https://oauth2.googleapis.com/token', tokenURL: 'https://oauth2.googleapis.com/token',
scopes: ['https://www.googleapis.com/auth/drive.appdata'], scopes: ['https://www.googleapis.com/auth/drive.appdata'],
revoke: token => { revoke: token => {
const params = {token}; const params = {token};
return postQuery(`https://accounts.google.com/o/oauth2/revoke?${new URLSearchParams(params)}`); return postQuery(`https://accounts.google.com/o/oauth2/revoke?${new URLSearchParams(params)}`);
} },
}, },
onedrive: { onedrive: {
flow: 'code', flow: 'code',
@ -45,8 +45,8 @@ const tokenManager = (() => {
redirect_uri: FIREFOX ? redirect_uri: FIREFOX ?
'https://clngdbkpkpeebahjckkjfobafhncgmne.chromiumapp.org/' : 'https://clngdbkpkpeebahjckkjfobafhncgmne.chromiumapp.org/' :
'https://' + location.hostname + '.chromiumapp.org/', 'https://' + location.hostname + '.chromiumapp.org/',
scopes: ['Files.ReadWrite.AppFolder', 'offline_access'] scopes: ['Files.ReadWrite.AppFolder', 'offline_access'],
} },
}; };
const NETWORK_LATENCY = 30; // seconds const NETWORK_LATENCY = 30; // seconds
@ -114,7 +114,7 @@ const tokenManager = (() => {
client_id: provider.clientId, client_id: provider.clientId,
refresh_token: obj[k.REFRESH], refresh_token: obj[k.REFRESH],
grant_type: 'refresh_token', grant_type: 'refresh_token',
scope: provider.scopes.join(' ') scope: provider.scopes.join(' '),
}; };
if (provider.clientSecret) { if (provider.clientSecret) {
body.client_secret = provider.clientSecret; body.client_secret = provider.clientSecret;
@ -136,7 +136,7 @@ const tokenManager = (() => {
response_type: provider.flow, response_type: provider.flow,
client_id: provider.clientId, client_id: provider.clientId,
redirect_uri: provider.redirect_uri || chrome.identity.getRedirectURL(), redirect_uri: provider.redirect_uri || chrome.identity.getRedirectURL(),
state state,
}; };
if (provider.scopes) { if (provider.scopes) {
query.scope = provider.scopes.join(' '); query.scope = provider.scopes.join(' ');
@ -148,7 +148,7 @@ const tokenManager = (() => {
return webextLaunchWebAuthFlow({ return webextLaunchWebAuthFlow({
url, url,
interactive, interactive,
redirect_uri: query.redirect_uri redirect_uri: query.redirect_uri,
}) })
.then(url => { .then(url => {
const params = new URLSearchParams( const params = new URLSearchParams(
@ -171,7 +171,7 @@ const tokenManager = (() => {
code, code,
grant_type: 'authorization_code', grant_type: 'authorization_code',
client_id: provider.clientId, client_id: provider.clientId,
redirect_uri: query.redirect_uri redirect_uri: query.redirect_uri,
}; };
if (provider.clientSecret) { if (provider.clientSecret) {
body.client_secret = provider.clientSecret; body.client_secret = provider.clientSecret;
@ -185,7 +185,7 @@ const tokenManager = (() => {
return chromeLocal.set({ return chromeLocal.set({
[k.TOKEN]: result.access_token, [k.TOKEN]: result.access_token,
[k.EXPIRE]: result.expires_in ? Date.now() + (Number(result.expires_in) - NETWORK_LATENCY) * 1000 : undefined, [k.EXPIRE]: result.expires_in ? Date.now() + (Number(result.expires_in) - NETWORK_LATENCY) * 1000 : undefined,
[k.REFRESH]: result.refresh_token [k.REFRESH]: result.refresh_token,
}) })
.then(() => result.access_token); .then(() => result.access_token);
} }
@ -194,7 +194,7 @@ const tokenManager = (() => {
const options = { const options = {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded' 'Content-Type': 'application/x-www-form-urlencoded',
}, },
body: body ? new URLSearchParams(body) : null, body: body ? new URLSearchParams(body) : null,
}; };

View File

@ -35,7 +35,7 @@
const ALARM_NAME = 'scheduledUpdate'; const ALARM_NAME = 'scheduledUpdate';
const MIN_INTERVAL_MS = 60e3; const MIN_INTERVAL_MS = 60e3;
let lastUpdateTime = parseInt(localStorage.lastUpdateTime) || Date.now(); let lastUpdateTime;
let checkingAll = false; let checkingAll = false;
let logQueue = []; let logQueue = [];
let logLastWriteTime = 0; let logLastWriteTime = 0;
@ -46,9 +46,11 @@
API_METHODS.updateCheck = checkStyle; API_METHODS.updateCheck = checkStyle;
API_METHODS.getUpdaterStates = () => STATES; API_METHODS.getUpdaterStates = () => STATES;
prefs.subscribe(['updateInterval'], schedule); chromeLocal.getValue('lastUpdateTime').then(val => {
schedule(); lastUpdateTime = val || Date.now();
chrome.alarms.onAlarm.addListener(onAlarm); prefs.subscribe('updateInterval', schedule, {now: true});
chrome.alarms.onAlarm.addListener(onAlarm);
});
return {checkAllStyles, checkStyle, STATES}; return {checkAllStyles, checkStyle, STATES};
@ -255,7 +257,7 @@
} }
function resetInterval() { function resetInterval() {
localStorage.lastUpdateTime = lastUpdateTime = Date.now(); chromeLocal.setValue('lastUpdateTime', lastUpdateTime = Date.now());
schedule(); schedule();
} }

View File

@ -61,7 +61,7 @@ const usercssHelper = (() => {
find(styleId ? {id: styleId} : style) : Promise.resolve(); find(styleId ? {id: styleId} : style) : Promise.resolve();
return Promise.all([ return Promise.all([
metaOnly ? style : doBuild(style, findDup), metaOnly ? style : doBuild(style, findDup),
findDup findDup,
]); ]);
}) })
.then(([style, dup]) => ({style, dup})); .then(([style, dup]) => ({style, dup}));

View File

@ -1,4 +1,10 @@
/* global API_METHODS openURL download URLS tabManager */ /* global
API_METHODS
download
openURL
tabManager
URLS
*/
'use strict'; 'use strict';
(() => { (() => {
@ -27,16 +33,39 @@
return code; return code;
}; };
// `glob`: pathname match pattern for webRequest
// `rx`: pathname regex to verify the URL really looks like a raw usercss
const maybeDistro = {
// https://github.com/StylishThemes/GitHub-Dark/raw/master/github-dark.user.css
'github.com': {
glob: '/*/raw/*',
rx: /^\/[^/]+\/[^/]+\/raw\/[^/]+\/[^/]+?\.user\.(css|styl)$/,
},
// https://raw.githubusercontent.com/StylishThemes/GitHub-Dark/master/github-dark.user.css
'raw.githubusercontent.com': {
glob: '/*',
rx: /^(\/[^/]+?){4}\.user\.(css|styl)$/,
},
};
// Faster installation on known distribution sites to avoid flicker of css text // Faster installation on known distribution sites to avoid flicker of css text
chrome.webRequest.onBeforeSendHeaders.addListener(({tabId, url}) => { chrome.webRequest.onBeforeSendHeaders.addListener(({tabId, url}) => {
openInstallerPage(tabId, url, {}); const u = new URL(url);
// Silently suppressing navigation like it never happened const m = maybeDistro[u.hostname];
return {redirectUrl: 'javascript:void 0'}; // eslint-disable-line no-script-url if (!m || m.rx.test(u.pathname)) {
openInstallerPage(tabId, url, {});
// Silently suppress navigation.
// Don't redirect to the install URL as it'll flash the text!
return {redirectUrl: 'javascript:void 0'}; // eslint-disable-line no-script-url
}
}, { }, {
urls: [ urls: [
URLS.usoArchiveRaw + 'usercss/*.user.css', URLS.usoArchiveRaw + 'usercss/*.user.css',
'*://greasyfork.org/scripts/*/code/*.user.css', '*://greasyfork.org/scripts/*/code/*.user.css',
'*://sleazyfork.org/scripts/*/code/*.user.css', '*://sleazyfork.org/scripts/*/code/*.user.css',
...[].concat(
...Object.entries(maybeDistro)
.map(([host, {glob}]) => makeUsercssGlobs(host, glob))),
], ],
types: ['main_frame'], types: ['main_frame'],
}, ['blocking']); }, ['blocking']);
@ -46,7 +75,7 @@
const h = responseHeaders.find(h => h.name.toLowerCase() === 'content-type'); const h = responseHeaders.find(h => h.name.toLowerCase() === 'content-type');
tabManager.set(tabId, isContentTypeText.name, h && isContentTypeText(h.value) || undefined); tabManager.set(tabId, isContentTypeText.name, h && isContentTypeText(h.value) || undefined);
}, { }, {
urls: '%css,%css?*,%styl,%styl?*'.replace(/%/g, '*://*/*.user.').split(','), urls: makeUsercssGlobs('*', '/*'),
types: ['main_frame'], types: ['main_frame'],
}, ['responseHeaders']); }, ['responseHeaders']);
@ -57,7 +86,7 @@
!oldUrl.startsWith(URLS.installUsercss)) { !oldUrl.startsWith(URLS.installUsercss)) {
const inTab = url.startsWith('file:') && Boolean(fileLoader); const inTab = url.startsWith('file:') && Boolean(fileLoader);
const code = await (inTab ? fileLoader : urlLoader)(tabId, url); const code = await (inTab ? fileLoader : urlLoader)(tabId, url);
if (/==userstyle==/i.test(code)) { if (/==userstyle==/i.test(code) && !/^\s*</.test(code)) {
openInstallerPage(tabId, url, {code, inTab}); openInstallerPage(tabId, url, {code, inTab});
} }
} }
@ -80,4 +109,8 @@
chrome.tabs.update(tabId, {url: newUrl}); chrome.tabs.update(tabId, {url: newUrl});
} }
} }
function makeUsercssGlobs(host, path) {
return '%css,%css?*,%styl,%styl?*'.replace(/%/g, `*://${host}${path}.user.`).split(',');
}
})(); })();

View File

@ -59,8 +59,7 @@ self.INJECTED !== 1 && (() => {
if (STYLE_VIA_API) { if (STYLE_VIA_API) {
await API.styleViaAPI({method: 'styleApply'}); await API.styleViaAPI({method: 'styleApply'});
} else { } else {
const blobId = chrome.app && getXhrBlobId(); const styles = chrome.app && !chrome.tabs && getStylesViaXhr() ||
const styles = blobId && getStylesViaXhr(blobId) ||
await API.getSectionsByUrl(getMatchUrl(), null, true); await API.getSectionsByUrl(getMatchUrl(), null, true);
if (styles.disableAll) { if (styles.disableAll) {
delete styles.disableAll; delete styles.disableAll;
@ -70,27 +69,16 @@ self.INJECTED !== 1 && (() => {
} }
} }
function getXhrBlobId() { function getStylesViaXhr() {
try { try {
const {cookie} = document; // may throw in sandboxed frames const blobId = document.cookie.split(chrome.runtime.id + '=')[1].split(';')[0];
return new RegExp(`(^|\\s|;)${chrome.runtime.id}=\\s*([-\\w]+)\\s*(;|$)`).exec(cookie)[2]; const url = 'blob:' + chrome.runtime.getURL(blobId);
} catch (e) {}
}
function getStylesViaXhr(data) {
try {
const disableAll = data[0] === '1';
const url = 'blob:' + chrome.runtime.getURL(data.slice(1));
document.cookie = `${chrome.runtime.id}=1; max-age=0`; // remove our cookie document.cookie = `${chrome.runtime.id}=1; max-age=0`; // remove our cookie
let res; const xhr = new XMLHttpRequest();
if (!disableAll) { // when disabled, will get the styles asynchronously, no rush xhr.open('GET', url, false); // synchronous
const xhr = new XMLHttpRequest(); xhr.send();
xhr.open('GET', url, false); // synchronous
xhr.send();
res = JSON.parse(xhr.response);
}
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
return res; return JSON.parse(xhr.response);
} catch (e) {} } catch (e) {}
} }

View File

@ -5,7 +5,7 @@
const manifest = chrome.runtime.getManifest(); const manifest = chrome.runtime.getManifest();
const allowedOrigins = [ const allowedOrigins = [
'https://openusercss.org', 'https://openusercss.org',
'https://openusercss.com' 'https://openusercss.com',
]; ];
const sendPostMessage = message => { const sendPostMessage = message => {
@ -17,7 +17,7 @@
const askHandshake = () => { const askHandshake = () => {
// Tell the page that we exist and that it should send the handshake // Tell the page that we exist and that it should send the handshake
sendPostMessage({ sendPostMessage({
type: 'ouc-begin-handshake' type: 'ouc-begin-handshake',
}); });
}; };
@ -25,7 +25,7 @@
const sendInstalledCallback = styleData => { const sendInstalledCallback = styleData => {
sendPostMessage({ sendPostMessage({
type: 'ouc-is-installed-response', type: 'ouc-is-installed-response',
style: styleData style: styleData,
}); });
}; };
@ -36,14 +36,14 @@
) { ) {
API.findUsercss({ API.findUsercss({
name: event.data.name, name: event.data.name,
namespace: event.data.namespace namespace: event.data.namespace,
}).then(style => { }).then(style => {
const data = {event}; const data = {event};
const callbackObject = { const callbackObject = {
installed: Boolean(style), installed: Boolean(style),
enabled: style.enabled, enabled: style.enabled,
name: data.name, name: data.name,
namespace: data.namespace namespace: data.namespace,
}; };
sendInstalledCallback(callbackObject); sendInstalledCallback(callbackObject);
@ -71,7 +71,7 @@
'update-auto', 'update-auto',
'export-json-backups', 'export-json-backups',
'import-json-backups', 'import-json-backups',
'manage-local' 'manage-local',
]; ];
const reportedFeatures = []; const reportedFeatures = [];
@ -96,8 +96,8 @@
key: event.data.key, key: event.data.key,
extension: { extension: {
name: manifest.name, name: manifest.name,
capabilities: reportedFeatures capabilities: reportedFeatures,
} },
}); });
}; };
@ -120,7 +120,7 @@
// we were able to install the theme and it may display a success message // we were able to install the theme and it may display a success message
sendPostMessage({ sendPostMessage({
type: 'ouc-install-callback', type: 'ouc-install-callback',
key: data.key key: data.key,
}); });
}; };
@ -135,7 +135,7 @@
}).then(style => { }).then(style => {
sendInstallCallback({ sendInstallCallback({
enabled: style.enabled, enabled: style.enabled,
key: event.data.key key: event.data.key,
}); });
}); });
} }

View File

@ -85,7 +85,7 @@
const observer = new MutationObserver(check); const observer = new MutationObserver(check);
observer.observe(document.documentElement, { observer.observe(document.documentElement, {
childList: true, childList: true,
subtree: true subtree: true,
}); });
check(); check();
@ -105,7 +105,7 @@
? 'styleCanBeUpdatedChrome' ? 'styleCanBeUpdatedChrome'
: 'styleAlreadyInstalledChrome', : 'styleAlreadyInstalledChrome',
detail: { detail: {
updateUrl: installedStyle.updateUrl updateUrl: installedStyle.updateUrl,
}, },
}); });
}); });
@ -155,7 +155,7 @@
function doInstall() { function doInstall() {
let oldStyle; let oldStyle;
return API.findStyle({ return API.findStyle({
md5Url: getMeta('stylish-md5-url') || location.href md5Url: getMeta('stylish-md5-url') || location.href,
}, true) }, true)
.then(_oldStyle => { .then(_oldStyle => {
oldStyle = _oldStyle; oldStyle = _oldStyle;

View File

@ -162,7 +162,7 @@ function beautify(scope, ui = true) {
$create('SVG:path', { $create('SVG:path', {
'fill-rule': 'evenodd', 'fill-rule': 'evenodd',
'd': 'M1408 704q0 26-19 45l-448 448q-19 19-45 ' + 'd': 'M1408 704q0 26-19 45l-448 448q-19 19-45 ' +
'19t-45-19l-448-448q-19-19-19-45t19-45 45-19h896q26 0 45 19t19 45z' '19t-45-19l-448-448q-19-19-19-45t19-45 45-19h896q26 0 45 19t19 45z',
}), }),
]), ]),
]), ]),
@ -176,7 +176,7 @@ function beautify(scope, ui = true) {
$create('input', { $create('input', {
type: 'checkbox', type: 'checkbox',
dataset: {option: optionName}, dataset: {option: optionName},
checked: options[optionName] !== false checked: options[optionName] !== false,
}), }),
$create('SVG:svg.svg-icon.checked', $create('SVG:svg.svg-icon.checked',
$create('SVG:use', {'xlink:href': '#svg-icon-checked'})), $create('SVG:use', {'xlink:href': '#svg-icon-checked'})),

View File

@ -1,4 +1,10 @@
/* global CodeMirror prefs editor $ template */ /* global
$
CodeMirror
editor
prefs
t
*/
'use strict'; 'use strict';
@ -82,7 +88,7 @@
[ [
{from: 'Ctrl-', to: ['Alt-', 'Ctrl-Alt-']}, {from: 'Ctrl-', to: ['Alt-', 'Ctrl-Alt-']},
// Note: modifier order in CodeMirror is S-C-A // Note: modifier order in CodeMirror is S-C-A
{from: 'Shift-Ctrl-', to: ['Ctrl-Alt-', 'Shift-Ctrl-Alt-']} {from: 'Shift-Ctrl-', to: ['Ctrl-Alt-', 'Shift-Ctrl-Alt-']},
].forEach(remap => { ].forEach(remap => {
const oldKey = remap.from + char; const oldKey = remap.from + char;
Object.keys(CodeMirror.keyMap).forEach(keyMapName => { Object.keys(CodeMirror.keyMap).forEach(keyMapName => {
@ -134,7 +140,7 @@
let filled; let filled;
this.eachLine(({text}) => (filled = text && /\S/.test(text))); this.eachLine(({text}) => (filled = text && /\S/.test(text)));
return !filled; return !filled;
} },
}); });
// editor commands // editor commands
@ -183,7 +189,7 @@
// setTimeout(() => { // setTimeout(() => {
// $('.CodeMirror-dialog', section).focus(); // $('.CodeMirror-dialog', section).focus();
// }); // });
cm.openDialog(template.jumpToLine.cloneNode(true), str => { cm.openDialog(t.template.jumpToLine.cloneNode(true), str => {
const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/); const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/);
if (m) { if (m) {
cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch); cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);

View File

@ -33,13 +33,13 @@ const cmFactory = (() => {
cm.setOption('highlightSelectionMatches', { cm.setOption('highlightSelectionMatches', {
showToken: /[#.\-\w]/, showToken: /[#.\-\w]/,
annotateScrollbar: true, annotateScrollbar: true,
onUpdate: updateMatchHighlightCount onUpdate: updateMatchHighlightCount,
}); });
} else if (value === 'selection') { } else if (value === 'selection') {
cm.setOption('highlightSelectionMatches', { cm.setOption('highlightSelectionMatches', {
showToken: false, showToken: false,
annotateScrollbar: true, annotateScrollbar: true,
onUpdate: updateMatchHighlightCount onUpdate: updateMatchHighlightCount,
}); });
} else { } else {
cm.setOption('highlightSelectionMatches', null); cm.setOption('highlightSelectionMatches', null);

View File

@ -1,7 +1,7 @@
/* exported CODEMIRROR_THEMES */ /* Do not edit. This file is auto-generated by build-vendor.js */
// this file is generated by update-codemirror-themes.js
'use strict'; 'use strict';
/* exported CODEMIRROR_THEMES */
const CODEMIRROR_THEMES = [ const CODEMIRROR_THEMES = [
'3024-day', '3024-day',
'3024-night', '3024-night',
@ -65,5 +65,5 @@ const CODEMIRROR_THEMES = [
'xq-light', 'xq-light',
'yeti', 'yeti',
'yonce', 'yonce',
'zenburn' 'zenburn',
]; ];

View File

@ -22,11 +22,10 @@
prefs prefs
rerouteHotkeys rerouteHotkeys
SectionsEditor SectionsEditor
sessionStorageHash sessionStore
setupLivePrefs setupLivePrefs
SourceEditor SourceEditor
t t
tHTML
tryCatch tryCatch
tryJSONparse tryJSONparse
*/ */
@ -56,13 +55,23 @@ lazyInit();
.then(initTheme), .then(initTheme),
onDOMready(), onDOMready(),
]); ]);
const scrollInfo = style.id && tryJSONparse(sessionStore['editorScrollInfo' + style.id]);
/** @namespace EditorBase */ /** @namespace EditorBase */
Object.assign(editor, { Object.assign(editor, {
style, style,
dirty, dirty,
scrollInfo,
updateName, updateName,
updateToc, updateToc,
toggleStyle, toggleStyle,
applyScrollInfo(cm, si = ((scrollInfo || {}).cms || [])[0]) {
if (si && si.sel) {
cm.operation(() => {
cm.setSelections(...si.sel, {scroll: false});
cm.scrollIntoView(cm.getCursor(), si.parentHeight / 2);
});
}
},
}); });
prefs.subscribe('editor.linter', updateLinter); prefs.subscribe('editor.linter', updateLinter);
prefs.subscribe('editor.keyMap', showHotkeyInTooltip); prefs.subscribe('editor.keyMap', showHotkeyInTooltip);
@ -78,17 +87,21 @@ lazyInit();
$('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle'); $('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle');
$('#preview-label').classList.toggle('hidden', !style.id); $('#preview-label').classList.toggle('hidden', !style.id);
const toc = []; const toc = [];
const elToc = $('#toc'); const elToc = $('#toc');
elToc.onclick = e => editor.jumpToEditor([...elToc.children].indexOf(e.target)); elToc.onclick = e => editor.jumpToEditor([...elToc.children].indexOf(e.target));
if (editor.isUsercss) {
(editor.isUsercss ? SourceEditor : SectionsEditor)(); SourceEditor();
} else {
SectionsEditor();
}
prefs.subscribe('editor.toc.expanded', (k, val) => val && editor.updateToc(), {now: true}); prefs.subscribe('editor.toc.expanded', (k, val) => val && editor.updateToc(), {now: true});
dirty.onChange(updateDirty); dirty.onChange(updateDirty);
await editor.ready;
await editor.ready;
editor.ready = true;
setTimeout(() => editor.getEditors().forEach(linter.enableForEditor));
// enabling after init to prevent flash of validation failure on an empty name // enabling after init to prevent flash of validation failure on an empty name
$('#name').required = !editor.isUsercss; $('#name').required = !editor.isUsercss;
$('#save-button').onclick = editor.save; $('#save-button').onclick = editor.save;
@ -100,7 +113,7 @@ lazyInit();
// switching the mode here to show the correct page ASAP, usually before DOMContentLoaded // switching the mode here to show the correct page ASAP, usually before DOMContentLoaded
editor.isUsercss = Boolean(style.usercssData || !style.id && prefs.get('newStyleAsUsercss')); editor.isUsercss = Boolean(style.usercssData || !style.id && prefs.get('newStyleAsUsercss'));
document.documentElement.classList.toggle('usercss', editor.isUsercss); document.documentElement.classList.toggle('usercss', editor.isUsercss);
sessionStorage.justEditedStyleId = style.id || ''; sessionStore.justEditedStyleId = style.id || '';
// no such style so let's clear the invalid URL parameters // no such style so let's clear the invalid URL parameters
if (!style.id) history.replaceState({}, '', location.pathname); if (!style.id) history.replaceState({}, '', location.pathname);
updateTitle(false); updateTitle(false);
@ -290,16 +303,9 @@ lazyInit();
function updateToc(added = editor.sections) { function updateToc(added = editor.sections) {
const {sections} = editor; const {sections} = editor;
const first = sections.indexOf(added[0]); const first = sections.indexOf(added[0]);
let el = elToc.children[first]; const elFirst = elToc.children[first];
if (added.focus) { if (first >= 0 && (!added.focus || !elFirst)) {
const cls = 'current'; for (let el = elFirst, i = first; i < sections.length; i++) {
const old = $('.' + cls, elToc);
if (old && old !== el) old.classList.remove(cls);
el.classList.add(cls);
return;
}
if (first >= 0) {
for (let i = first; i < sections.length; i++) {
const entry = sections[i].tocEntry; const entry = sections[i].tocEntry;
if (!deepEqual(entry, toc[i])) { if (!deepEqual(entry, toc[i])) {
if (!el) el = elToc.appendChild($create('li', {tabIndex: 0})); if (!el) el = elToc.appendChild($create('li', {tabIndex: 0}));
@ -318,6 +324,13 @@ lazyInit();
elToc.lastElementChild.remove(); elToc.lastElementChild.remove();
toc.length--; toc.length--;
} }
if (added.focus) {
const cls = 'current';
const old = $('.' + cls, elToc);
const el = elFirst || elToc.children[first];
if (old && old !== el) old.classList.remove(cls);
el.classList.add(cls);
}
} }
})(); })();
@ -335,7 +348,7 @@ function lazyInit() {
async function patchHistoryBack(tab) { async function patchHistoryBack(tab) {
ownTabId = tab.id; ownTabId = tab.id;
// use browser history back when 'back to manage' is clicked // use browser history back when 'back to manage' is clicked
if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) { if (sessionStore['manageStylesHistory' + ownTabId] === location.href) {
await onDOMready(); await onDOMready();
$('#cancel-button').onclick = event => { $('#cancel-button').onclick = event => {
event.stopPropagation(); event.stopPropagation();
@ -346,8 +359,8 @@ function lazyInit() {
} }
/** resize on 'undo close' */ /** resize on 'undo close' */
function restoreWindowSize() { function restoreWindowSize() {
const pos = tryJSONparse(sessionStorage.windowPos); const pos = tryJSONparse(sessionStore.windowPos);
delete sessionStorage.windowPos; delete sessionStore.windowPos;
if (pos && pos.left != null && chrome.windows) { if (pos && pos.left != null && chrome.windows) {
chrome.windows.update(chrome.windows.WINDOW_ID_CURRENT, pos); chrome.windows.update(chrome.windows.WINDOW_ID_CURRENT, pos);
} }
@ -408,7 +421,16 @@ function onRuntimeMessage(request) {
} }
function beforeUnload(e) { function beforeUnload(e) {
sessionStorage.windowPos = JSON.stringify(canSaveWindowPos() && prefs.get('windowPosition')); sessionStore.windowPos = JSON.stringify(canSaveWindowPos() && prefs.get('windowPosition'));
sessionStore['editorScrollInfo' + editor.style.id] = JSON.stringify({
scrollY: window.scrollY,
cms: editor.getEditors().map(cm => /** @namespace EditorScrollInfo */({
focus: cm.hasFocus(),
height: cm.display.wrapper.style.height.replace('100vh', ''),
parentHeight: cm.display.wrapper.parentElement.offsetHeight,
sel: cm.isClean() && [cm.doc.sel.ranges, cm.doc.sel.primIndex],
})),
});
const activeElement = document.activeElement; const activeElement = document.activeElement;
if (activeElement) { if (activeElement) {
// blurring triggers 'change' or 'input' event if needed // blurring triggers 'change' or 'input' event if needed
@ -429,7 +451,7 @@ function showHelp(title = '', body) {
const contents = $('.contents', div); const contents = $('.contents', div);
contents.textContent = ''; contents.textContent = '';
if (body) { if (body) {
contents.appendChild(typeof body === 'string' ? tHTML(body) : body); contents.appendChild(typeof body === 'string' ? t.HTML(body) : body);
} }
$('.title', div).textContent = title; $('.title', div).textContent = title;
@ -492,7 +514,7 @@ function showCodeMirrorPopup(title, html, options) {
matchBrackets: true, matchBrackets: true,
styleActiveLine: true, styleActiveLine: true,
theme: prefs.get('editor.theme'), theme: prefs.get('editor.theme'),
keyMap: prefs.get('editor.keyMap') keyMap: prefs.get('editor.keyMap'),
}, options)); }, options));
cm.focus(); cm.focus();
rerouteHotkeys(false); rerouteHotkeys(false);

View File

@ -29,13 +29,13 @@ workerUtil.createAPI({
code: err.code, code: err.code,
args: err.args, args: err.args,
message: err.message, message: err.message,
index: err.index index: err.index,
}) })
); );
return result; return result;
}, },
getStylelintRules, getStylelintRules,
getCsslintRules getCsslintRules,
}); });
function getCsslintRules() { function getCsslintRules() {

View File

@ -1,5 +1,18 @@
/* global CodeMirror focusAccessibility colorMimicry editor chromeLocal /* global
onDOMready $ $$ $create t debounce tryRegExp stringAsRegExp template */ $
$$
$create
chromeLocal
CodeMirror
colorMimicry
debounce
editor
focusAccessibility
onDOMready
stringAsRegExp
t
tryRegExp
*/
'use strict'; 'use strict';
onDOMready().then(() => { onDOMready().then(() => {
@ -100,7 +113,7 @@ onDOMready().then(() => {
state.lastFind = ''; state.lastFind = '';
toggleDataset(this, 'enabled', !state.icase); toggleDataset(this, 'enabled', !state.icase);
doSearch({canAdvance: false}); doSearch({canAdvance: false});
} },
}, },
}; };
@ -136,7 +149,7 @@ onDOMready().then(() => {
trimUndoHistory(); trimUndoHistory();
enableUndoButton(state.undoHistory.length); enableUndoButton(state.undoHistory.length);
if (state.find) doSearch({canAdvance: false}); if (state.find) doSearch({canAdvance: false});
} },
}; };
const DIALOG_PROPS = { const DIALOG_PROPS = {
@ -152,7 +165,7 @@ onDOMready().then(() => {
state.replace = this.value; state.replace = this.value;
adjustTextareaSize(this); adjustTextareaSize(this);
debounce(writeStorage, STORAGE_UPDATE_DELAY); debounce(writeStorage, STORAGE_UPDATE_DELAY);
} },
}, },
}; };
@ -169,7 +182,7 @@ onDOMready().then(() => {
replace(cm) { replace(cm) {
state.reverse = false; state.reverse = false;
focusDialog('replace', cm); focusDialog('replace', cm);
} },
}; };
COMMANDS.replaceAll = COMMANDS.replace; COMMANDS.replaceAll = COMMANDS.replace;
@ -563,14 +576,14 @@ onDOMready().then(() => {
state.originalFocus = document.activeElement; state.originalFocus = document.activeElement;
state.firstRun = true; state.firstRun = true;
const dialog = state.dialog = template.searchReplaceDialog.cloneNode(true); const dialog = state.dialog = t.template.searchReplaceDialog.cloneNode(true);
Object.assign(dialog, DIALOG_PROPS.dialog); Object.assign(dialog, DIALOG_PROPS.dialog);
dialog.addEventListener('focusout', EVENTS.onfocusout); dialog.addEventListener('focusout', EVENTS.onfocusout);
dialog.dataset.type = type; dialog.dataset.type = type;
dialog.style.pointerEvents = 'auto'; dialog.style.pointerEvents = 'auto';
const content = $('[data-type="content"]', dialog); const content = $('[data-type="content"]', dialog);
content.parentNode.replaceChild(template[type].cloneNode(true), content); content.parentNode.replaceChild(t.template[type].cloneNode(true), content);
createInput(0, 'input', state.find); createInput(0, 'input', state.find);
createInput(1, 'input2', state.replace); createInput(1, 'input2', state.replace);
@ -633,7 +646,7 @@ onDOMready().then(() => {
input.value = value; input.value = value;
Object.assign(input, DIALOG_PROPS[name]); Object.assign(input, DIALOG_PROPS[name]);
input.parentElement.appendChild(template.clearSearch.cloneNode(true)); input.parentElement.appendChild(t.template.clearSearch.cloneNode(true));
$('[data-action]', input.parentElement)._input = input; $('[data-action]', input.parentElement)._input = input;
} }

View File

@ -61,7 +61,7 @@
loadScript([ loadScript([
'/vendor/codemirror/mode/javascript/javascript.js', '/vendor/codemirror/mode/javascript/javascript.js',
'/vendor/codemirror/addon/lint/json-lint.js', '/vendor/codemirror/addon/lint/json-lint.js',
'/vendor/jsonlint/jsonlint.js' '/vendor/jsonlint/jsonlint.js',
]).then(() => { ]).then(() => {
cm.setOption('mode', 'application/json'); cm.setOption('mode', 'application/json');
cm.setOption('lint', true); cm.setOption('lint', true);

View File

@ -12,13 +12,13 @@ const LINTER_DEFAULTS = (() => {
rules: { rules: {
'at-rule-no-unknown': [true, { 'at-rule-no-unknown': [true, {
'ignoreAtRules': ['extend', 'extends', 'css', 'block'], 'ignoreAtRules': ['extend', 'extends', 'css', 'block'],
'severity': 'warning' 'severity': 'warning',
}], }],
'block-no-empty': [true, SEVERITY], 'block-no-empty': [true, SEVERITY],
'color-no-invalid-hex': [true, SEVERITY], 'color-no-invalid-hex': [true, SEVERITY],
'declaration-block-no-duplicate-properties': [true, { 'declaration-block-no-duplicate-properties': [true, {
'ignore': ['consecutive-duplicates-with-different-values'], 'ignore': ['consecutive-duplicates-with-different-values'],
'severity': 'warning' 'severity': 'warning',
}], }],
'declaration-block-no-shorthand-property-overrides': [true, SEVERITY], 'declaration-block-no-shorthand-property-overrides': [true, SEVERITY],
'font-family-no-duplicate-names': [true, SEVERITY], 'font-family-no-duplicate-names': [true, SEVERITY],
@ -172,7 +172,7 @@ const LINTER_DEFAULTS = (() => {
'value-list-comma-space-before': 'never', 'value-list-comma-space-before': 'never',
'value-list-max-empty-lines': 0 'value-list-max-empty-lines': 0
*/ */
} },
}; };
const CSSLINT = { const CSSLINT = {
// Default warnings // Default warnings
@ -216,7 +216,7 @@ const LINTER_DEFAULTS = (() => {
'universal-selector': 0, 'universal-selector': 0,
'unqualified-attributes': 0, 'unqualified-attributes': 0,
'vendor-prefix': 0, 'vendor-prefix': 0,
'zero-units': 0 'zero-units': 0,
}; };
return {STYLELINT, CSSLINT, SEVERITY}; return {STYLELINT, CSSLINT, SEVERITY};
})(); })();

View File

@ -7,7 +7,7 @@
storageName: chromeSync.LZ_KEY.csslint, storageName: chromeSync.LZ_KEY.csslint,
lint: csslint, lint: csslint,
validMode: mode => mode === 'css', validMode: mode => mode === 'css',
getConfig: config => Object.assign({}, LINTER_DEFAULTS.CSSLINT, config) getConfig: config => Object.assign({}, LINTER_DEFAULTS.CSSLINT, config),
}, },
stylelint: { stylelint: {
storageName: chromeSync.LZ_KEY.stylelint, storageName: chromeSync.LZ_KEY.stylelint,
@ -15,9 +15,9 @@
validMode: () => true, validMode: () => true,
getConfig: config => ({ getConfig: config => ({
syntax: 'sugarss', syntax: 'sugarss',
rules: Object.assign({}, LINTER_DEFAULTS.STYLELINT.rules, config && config.rules) rules: Object.assign({}, LINTER_DEFAULTS.STYLELINT.rules, config && config.rules),
}) }),
} },
}); });
async function stylelint(text, config, mode) { async function stylelint(text, config, mode) {

View File

@ -33,7 +33,7 @@ function createMetaCompiler(cm, onUpdated) {
to: cm.posFromIndex((err.index || 0) + match.index), to: cm.posFromIndex((err.index || 0) + match.index),
message: err.code && chrome.i18n.getMessage(`meta_${err.code}`, err.args) || err.message, message: err.code && chrome.i18n.getMessage(`meta_${err.code}`, err.args) || err.message,
severity: err.code === 'unknownMeta' ? 'warning' : 'error', severity: err.code === 'unknownMeta' ? 'warning' : 'error',
rule: err.code rule: err.code,
}) })
); );
meta = match[0]; meta = match[0];

View File

@ -77,7 +77,7 @@ Object.assign(linter, (() => {
element: table, element: table,
trs, trs,
updateAnnotations, updateAnnotations,
updateCaption updateCaption,
}; };
function updateCaption() { function updateCaption() {
@ -124,18 +124,18 @@ Object.assign(linter, (() => {
const message = $create('td', {attributes: {role: 'message'}}); const message = $create('td', {attributes: {role: 'message'}});
const trElement = $create('tr', { const trElement = $create('tr', {
onclick: () => gotoLintIssue(cm, anno) onclick: () => gotoLintIssue(cm, anno),
}, [ }, [
severity, severity,
line, line,
$create('td', {attributes: {role: 'sep'}}, ':'), $create('td', {attributes: {role: 'sep'}}, ':'),
col, col,
message message,
]); ]);
return { return {
element: trElement, element: trElement,
update, update,
getAnnotation: () => anno getAnnotation: () => anno,
}; };
function update(_anno) { function update(_anno) {

View File

@ -3,7 +3,7 @@
/* exported editorWorker */ /* exported editorWorker */
const editorWorker = workerUtil.createWorker({ const editorWorker = workerUtil.createWorker({
url: '/edit/editor-worker.js' url: '/edit/editor-worker.js',
}); });
/* exported linter */ /* exported linter */
@ -19,7 +19,7 @@ const linter = (() => {
enableForEditor, enableForEditor,
disableForEditor, disableForEditor,
onLintingUpdated, onLintingUpdated,
onUnhook onUnhook,
}; };
function onUnhook(cb) { function onUnhook(cb) {

View File

@ -40,7 +40,7 @@ function createLivePreview(preprocess, shouldShow) {
function createPreviewer() { function createPreviewer() {
const port = chrome.runtime.connect({ const port = chrome.runtime.connect({
name: 'livePreview' name: 'livePreview',
}); });
port.onDisconnect.addListener(err => { port.onDisconnect.addListener(err => {
throw err; throw err;

View File

@ -75,7 +75,7 @@ function MozSectionFinder(cm) {
/** @param {MozSection} [section] */ /** @param {MozSection} [section] */
updatePositions(section) { updatePositions(section) {
(section ? [section] : getState().sections).forEach(setPositionFromMark); (section ? [section] : getState().sections).forEach(setPositionFromMark);
} },
}; };
return MozSectionFinder; return MozSectionFinder;

View File

@ -9,7 +9,6 @@
prefs prefs
regExpTester regExpTester
t t
template
tryCatch tryCatch
*/ */
'use strict'; 'use strict';
@ -55,7 +54,7 @@ function MozSectionWidget(
$create('ul' + C_LIST), $create('ul' + C_LIST),
]), ]),
listItem: listItem:
template.appliesTo.cloneNode(true), t.template.appliesTo.cloneNode(true),
appliesToEverything: appliesToEverything:
$create('li.applies-to-everything', t('appliesToEverything')), $create('li.applies-to-everything', t('appliesToEverything')),
}; };
@ -74,7 +73,7 @@ function MozSectionWidget(
if (funcs.length < 2) { if (funcs.length < 2) {
messageBox({ messageBox({
contents: t('appliesRemoveError'), contents: t('appliesRemoveError'),
buttons: [t('confirmClose')] buttons: [t('confirmClose')],
}); });
return; return;
} }
@ -125,7 +124,7 @@ function MozSectionWidget(
return; return;
} }
} }
} },
}; };
actualStyle = $create('style'); actualStyle = $create('style');

View File

@ -1,4 +1,12 @@
/* global showHelp $ $create tryRegExp URLS t template openURL */ /* global
$
$create
openURL
showHelp
t
tryRegExp
URLS
*/
/* exported regExpTester */ /* exported regExpTester */
'use strict'; 'use strict';
@ -86,7 +94,7 @@ const regExpTester = (() => {
full: {data: [], label: t('styleRegexpTestFull')}, full: {data: [], label: t('styleRegexpTestFull')},
partial: {data: [], label: [ partial: {data: [], label: [
t('styleRegexpTestPartial'), t('styleRegexpTestPartial'),
template.regexpTestPartial.cloneNode(true), t.template.regexpTestPartial.cloneNode(true),
]}, ]},
none: {data: [], label: t('styleRegexpTestNone')}, none: {data: [], label: t('styleRegexpTestNone')},
invalid: {data: [], label: t('styleRegexpTestInvalid')}, invalid: {data: [], label: t('styleRegexpTestInvalid')},

View File

@ -9,7 +9,6 @@
prefs prefs
regExpTester regExpTester
t t
template
trimCommentLabel trimCommentLabel
tryRegExp tryRegExp
*/ */
@ -17,20 +16,28 @@
/* exported createSection */ /* exported createSection */
/** @returns {EditorSection} */ /**
function createSection(originalSection, genId) { * @param {StyleSection} originalSection
* @param {function():number} genId
* @param {EditorScrollInfo} [si]
* @returns {EditorSection}
*/
function createSection(originalSection, genId, si) {
const {dirty} = editor; const {dirty} = editor;
const sectionId = genId(); const sectionId = genId();
const el = template.section.cloneNode(true); const el = t.template.section.cloneNode(true);
const elLabel = $('.code-label', el); const elLabel = $('.code-label', el);
const cm = cmFactory.create(wrapper => { const cm = cmFactory.create(wrapper => {
// making it tall during initial load so IntersectionObserver sees only one adjacent CM // making it tall during initial load so IntersectionObserver sees only one adjacent CM
wrapper.style.height = '100vh'; if (editor.ready !== true) {
wrapper.style.height = si ? si.height : '100vh';
}
elLabel.after(wrapper); elLabel.after(wrapper);
}, { }, {
value: originalSection.code, value: originalSection.code,
}); });
el.CodeMirror = cm; // used by getAssociatedEditor el.CodeMirror = cm; // used by getAssociatedEditor
editor.applyScrollInfo(cm, si);
const changeListeners = new Set(); const changeListeners = new Set();
@ -259,8 +266,8 @@ function createSection(originalSection, genId) {
function createApply({type = 'url', value, all = false}) { function createApply({type = 'url', value, all = false}) {
const applyId = genId(); const applyId = genId();
const dirtyPrefix = `section.${sectionId}.apply.${applyId}`; const dirtyPrefix = `section.${sectionId}.apply.${applyId}`;
const el = all ? template.appliesToEverything.cloneNode(true) : const el = all ? t.template.appliesToEverything.cloneNode(true) :
template.appliesTo.cloneNode(true); t.template.appliesTo.cloneNode(true);
const selectEl = !all && $('.applies-type', el); const selectEl = !all && $('.applies-type', el);
if (selectEl) { if (selectEl) {
@ -353,7 +360,7 @@ function createSection(originalSection, genId) {
function createResizeGrip(cm) { function createResizeGrip(cm) {
const wrapper = cm.display.wrapper; const wrapper = cm.display.wrapper;
wrapper.classList.add('resize-grip-enabled'); wrapper.classList.add('resize-grip-enabled');
const resizeGrip = template.resizeGrip.cloneNode(true); const resizeGrip = t.template.resizeGrip.cloneNode(true);
wrapper.appendChild(resizeGrip); wrapper.appendChild(resizeGrip);
let lastClickTime = 0; let lastClickTime = 0;
let initHeight; let initHeight;

View File

@ -15,6 +15,7 @@
messageBox messageBox
prefs prefs
sectionsToMozFormat sectionsToMozFormat
sessionStore
showCodeMirrorPopup showCodeMirrorPopup
showHelp showHelp
t t
@ -117,7 +118,7 @@ function SectionsEditor() {
} }
newStyle = await API.editSave(newStyle); newStyle = await API.editSave(newStyle);
destroyRemovedSections(); destroyRemovedSections();
sessionStorage.justEditedStyleId = newStyle.id; sessionStore.justEditedStyleId = newStyle.id;
editor.replaceStyle(newStyle, false); editor.replaceStyle(newStyle, false);
}, },
@ -141,7 +142,7 @@ function SectionsEditor() {
/** @param {EditorSection} section */ /** @param {EditorSection} section */
function fitToContent(section) { function fitToContent(section) {
const {el, cm, cm: {display: {wrapper, sizer}}} = section; const {cm, cm: {display: {wrapper, sizer}}} = section;
if (cm.display.renderedView) { if (cm.display.renderedView) {
resize(); resize();
} else { } else {
@ -154,12 +155,13 @@ function SectionsEditor() {
return; return;
} }
if (headerOffset == null) { if (headerOffset == null) {
headerOffset = el.getBoundingClientRect().top; headerOffset = container.getBoundingClientRect().top;
} }
contentHeight += 9; // border & resize grip contentHeight += 9; // border & resize grip
cm.off('update', resize); cm.off('update', resize);
const cmHeight = wrapper.offsetHeight; const cmHeight = wrapper.offsetHeight;
const maxHeight = (window.innerHeight - headerOffset) - (section.el.offsetHeight - cmHeight); const appliesToHeight = Math.min(section.el.offsetHeight - cmHeight, window.innerHeight / 2);
const maxHeight = (window.innerHeight - headerOffset) - appliesToHeight;
const fit = Math.min(contentHeight, maxHeight); const fit = Math.min(contentHeight, maxHeight);
if (Math.abs(fit - cmHeight) > 1) { if (Math.abs(fit - cmHeight) > 1) {
cm.setSize(null, fit); cm.setSize(null, fit);
@ -434,7 +436,7 @@ function SectionsEditor() {
/** @returns {Style} */ /** @returns {Style} */
function getModel() { function getModel() {
return Object.assign({}, style, { return Object.assign({}, style, {
sections: sections.filter(s => !s.removed).map(s => s.getModel()) sections: sections.filter(s => !s.removed).map(s => s.getModel()),
}); });
} }
@ -484,7 +486,7 @@ function SectionsEditor() {
livePreview.update(getModel()); livePreview.update(getModel());
} }
function initSections(originalSections, { function initSections(src, {
focusOn = 0, focusOn = 0,
replace = false, replace = false,
pristine = false, pristine = false,
@ -495,27 +497,35 @@ function SectionsEditor() {
container.textContent = ''; container.textContent = '';
} }
let done; let done;
const total = originalSections.length; let index = 0;
originalSections = originalSections.slice(); let y = 0;
const total = src.length;
let si = editor.scrollInfo;
if (si && si.cms && si.cms.length === src.length) {
si.scrollY2 = si.scrollY + window.innerHeight;
container.style.height = si.scrollY2 + 'px';
scrollTo(0, si.scrollY);
} else {
si = null;
}
return new Promise(resolve => { return new Promise(resolve => {
done = resolve; done = resolve;
chunk(true); chunk(!si);
}); });
function chunk(forceRefresh) { function chunk(forceRefresh) {
const t0 = performance.now(); const t0 = performance.now();
while (originalSections.length && performance.now() - t0 < 100) { while (index < total && performance.now() - t0 < 100) {
insertSectionAfter(originalSections.shift(), undefined, forceRefresh); if (si) forceRefresh = y < si.scrollY2 && (y += si.cms[index].parentHeight) > si.scrollY;
insertSectionAfter(src[index], undefined, forceRefresh, si && si.cms[index]);
if (pristine) dirty.clear(); if (pristine) dirty.clear();
if (focusOn !== false && sections[focusOn]) { if (index === focusOn && !si) sections[index].cm.focus();
sections[focusOn].cm.focus(); index++;
focusOn = false;
}
} }
setGlobalProgress(total - originalSections.length, total); setGlobalProgress(index, total);
if (!originalSections.length) { if (index === total) {
setGlobalProgress(); setGlobalProgress();
requestAnimationFrame(fitToAvailableSpace); if (!si) requestAnimationFrame(fitToAvailableSpace);
sections.forEach(({cm}) => setTimeout(linter.enableForEditor, 0, cm)); container.style.removeProperty('height');
done(); done();
} else { } else {
setTimeout(chunk); setTimeout(chunk);
@ -564,24 +574,26 @@ function SectionsEditor() {
* @param {StyleSection} [init] * @param {StyleSection} [init]
* @param {EditorSection} [base] * @param {EditorSection} [base]
* @param {boolean} [forceRefresh] * @param {boolean} [forceRefresh]
* @param {EditorScrollInfo} [si]
*/ */
function insertSectionAfter(init, base, forceRefresh) { function insertSectionAfter(init, base, forceRefresh, si) {
if (!init) { if (!init) {
init = {code: '', urlPrefixes: ['http://example.com']}; init = {code: '', urlPrefixes: ['http://example.com']};
} }
const section = createSection(init, genId); const section = createSection(init, genId, si);
const {cm} = section; const {cm} = section;
sections.splice(base ? sections.indexOf(base) + 1 : sections.length, 0, section); const index = base ? sections.indexOf(base) + 1 : sections.length;
sections.splice(index, 0, section);
container.insertBefore(section.el, base ? base.el.nextSibling : null); container.insertBefore(section.el, base ? base.el.nextSibling : null);
refreshOnView(cm, forceRefresh); refreshOnView(cm, base || forceRefresh);
registerEvents(section); registerEvents(section);
if (!base || init.code) { if ((!si || !si.height) && (!base || init.code)) {
// Fit a) during startup or b) when the clone button is clicked on a section with some code // Fit a) during startup or b) when the clone button is clicked on a section with some code
fitToContent(section); fitToContent(section);
} }
if (base) { if (base) {
cm.focus(); cm.focus();
setTimeout(editor.scrollToEditor, 0, cm); editor.scrollToEditor(cm);
linter.enableForEditor(cm); linter.enableForEditor(cm);
} }
updateSectionOrder(); updateSectionOrder();
@ -646,11 +658,18 @@ function SectionsEditor() {
xo.observe(cm.display.wrapper); xo.observe(cm.display.wrapper);
} }
/** @param {IntersectionObserverEntry[]} entries */
function refreshOnViewListener(entries) { function refreshOnViewListener(entries) {
for (const {isIntersecting, target} of entries) { for (const e of entries) {
if (isIntersecting) { const r = e.isIntersecting && e.intersectionRect;
target.CodeMirror.refresh(); if (r) {
xo.unobserve(target); xo.unobserve(e.target);
const cm = e.target.CodeMirror;
if (r.bottom > 0 && r.top < window.innerHeight) {
cm.refresh();
} else {
setTimeout(() => cm.refresh());
}
} }
} }
} }

View File

@ -1,5 +1,14 @@
/* global CodeMirror showHelp onDOMready $ $$ $create template t /* global
prefs stringAsRegExp */ $
$$
$create
CodeMirror
onDOMready
prefs
showHelp
stringAsRegExp
t
*/
'use strict'; 'use strict';
onDOMready().then(() => { onDOMready().then(() => {
@ -11,7 +20,7 @@ function showKeyMapHelp() {
const keyMapSorted = Object.keys(keyMap) const keyMapSorted = Object.keys(keyMap)
.map(key => ({key, cmd: keyMap[key]})) .map(key => ({key, cmd: keyMap[key]}))
.sort((a, b) => (a.cmd < b.cmd || (a.cmd === b.cmd && a.key < b.key) ? -1 : 1)); .sort((a, b) => (a.cmd < b.cmd || (a.cmd === b.cmd && a.key < b.key) ? -1 : 1));
const table = template.keymapHelp.cloneNode(true); const table = t.template.keymapHelp.cloneNode(true);
const tBody = table.tBodies[0]; const tBody = table.tBodies[0];
const row = tBody.rows[0]; const row = tBody.rows[0];
const cellA = row.children[0]; const cellA = row.children[0];

View File

@ -16,6 +16,7 @@
MozSectionWidget MozSectionWidget
prefs prefs
sectionsToMozFormat sectionsToMozFormat
sessionStore
t t
*/ */
@ -74,6 +75,7 @@ function SourceEditor() {
'editor.appliesToLineWidget': (k, val) => sectionWidget.toggle(val), 'editor.appliesToLineWidget': (k, val) => sectionWidget.toggle(val),
'editor.toc.expanded': (k, val) => sectionFinder.onOff(editor.updateToc, val), 'editor.toc.expanded': (k, val) => sectionFinder.onOff(editor.updateToc, val),
}, {now: true}); }, {now: true});
editor.applyScrollInfo(cm);
cm.clearHistory(); cm.clearHistory();
cm.markClean(); cm.markClean();
savedGeneration = cm.changeGeneration(); savedGeneration = cm.changeGeneration();
@ -89,7 +91,6 @@ function SourceEditor() {
linter.run(); linter.run();
updateLinterSwitch(); updateLinterSwitch();
}); });
debounce(linter.enableForEditor, 0, cm);
if (!$.isTextInput(document.activeElement)) { if (!$.isTextInput(document.activeElement)) {
cm.focus(); cm.focus();
} }
@ -98,7 +99,7 @@ function SourceEditor() {
return API.buildUsercss({ return API.buildUsercss({
styleId: style.id, styleId: style.id,
sourceCode: style.sourceCode, sourceCode: style.sourceCode,
assignVars: true assignVars: true,
}) })
.then(({style: newStyle}) => { .then(({style: newStyle}) => {
delete newStyle.enabled; delete newStyle.enabled;
@ -217,7 +218,7 @@ function SourceEditor() {
if (style.id !== newStyle.id) { if (style.id !== newStyle.id) {
history.replaceState({}, '', `?id=${newStyle.id}`); history.replaceState({}, '', `?id=${newStyle.id}`);
} }
sessionStorage.justEditedStyleId = newStyle.id; sessionStore.justEditedStyleId = newStyle.id;
Object.assign(style, newStyle); Object.assign(style, newStyle);
$('#preview-label').classList.remove('hidden'); $('#preview-label').classList.remove('hidden');
updateMeta(); updateMeta();

View File

@ -214,6 +214,6 @@ function createHotkeyInput(prefId, onDone = () => {}) {
}, },
onpaste(event) { onpaste(event) {
event.preventDefault(); event.preventDefault();
} },
}); });
} }

View File

@ -142,6 +142,7 @@ select {
transition: color .5s; transition: color .5s;
} }
.select-wrapper,
.select-resizer { .select-resizer {
display: inline-flex!important; display: inline-flex!important;
cursor: default; cursor: default;

View File

@ -22,7 +22,7 @@
if (theme !== 'default') { if (theme !== 'default') {
document.head.appendChild($create('link', { document.head.appendChild($create('link', {
rel: 'stylesheet', rel: 'stylesheet',
href: `vendor/codemirror/theme/${theme}.css` href: `vendor/codemirror/theme/${theme}.css`,
})); }));
} }
window.addEventListener('resize', adjustCodeHeight); window.addEventListener('resize', adjustCodeHeight);
@ -111,7 +111,7 @@
frag.appendChild($createLink(url, frag.appendChild($createLink(url,
$create('SVG:svg.svg-icon', {viewBox: '0 0 20 20'}, $create('SVG:svg.svg-icon', {viewBox: '0 0 20 20'},
$create('SVG:path', { $create('SVG:path', {
d: 'M4,4h5v2H6v8h8v-3h2v5H4V4z M11,3h6v6l-2-2l-4,4L9,9l4-4L11,3z' d: 'M4,4h5v2H6v8h8v-3h2v5H4V4z M11,3h6v6l-2-2l-4,4L9,9l4-4L11,3z',
})) }))
)); ));
} }
@ -130,7 +130,7 @@
$create('li', $create('li',
$createLink(...args) $createLink(...args)
) )
)) )),
])); ]));
} }
} }

View File

@ -25,7 +25,7 @@ function createCache({size = 1000, onDeleted} = {}) {
}, },
get size() { get size() {
return map.size; return map.size;
} },
}; };
function get(id) { function get(id) {

View File

@ -296,7 +296,7 @@ function $createLink(href = '', content) {
const opt = { const opt = {
tag: 'a', tag: 'a',
target: '_blank', target: '_blank',
rel: 'noopener' rel: 'noopener',
}; };
if (typeof href === 'object') { if (typeof href === 'object') {
Object.assign(opt, href); Object.assign(opt, href);

View File

@ -1,149 +1,16 @@
/* global tryCatch */
/* exported tHTML formatDate */
'use strict'; 'use strict';
const template = {};
tDocLoader();
function t(key, params) { function t(key, params) {
const cache = !params && t.cache[key]; const s = chrome.i18n.getMessage(key, params);
const s = cache || chrome.i18n.getMessage(key, params); if (!s) throw `Missing string "${key}"`;
if (s === '') {
throw `Missing string "${key}"`;
}
if (!params && !cache) {
t.cache[key] = s;
}
return s; return s;
} }
Object.assign(t, {
function tHTML(html, tag) { template: {},
// body is a text node without HTML tags DOMParser: new DOMParser(),
if (typeof html === 'string' && !tag && /<\w+/.test(html) === false) { ALLOWED_TAGS: 'a,b,code,i,sub,sup,wbr'.split(','),
return document.createTextNode(html); RX_WORD_BREAK: new RegExp([
}
if (typeof html === 'string') {
// spaces are removed; use &nbsp; for an explicit space
html = html.replace(/>\s+</g, '><').trim();
if (tag) {
html = `<${tag}>${html}</${tag}>`;
}
const body = t.DOMParser.parseFromString(html, 'text/html').body;
if (html.includes('i18n-')) {
tNodeList(body.getElementsByTagName('*'));
}
// the html string may contain more than one top-level node
if (!body.childNodes[1]) {
return body.firstChild;
}
const fragment = document.createDocumentFragment();
while (body.firstChild) {
fragment.appendChild(body.firstChild);
}
return fragment;
}
return html;
}
function tNodeList(nodes) {
const PREFIX = 'i18n-';
for (let n = nodes.length; --n >= 0;) {
const node = nodes[n];
if (node.nodeType !== Node.ELEMENT_NODE) {
continue;
}
if (node.localName === 'template') {
createTemplate(node);
continue;
}
for (let a = node.attributes.length; --a >= 0;) {
const attr = node.attributes[a];
const name = attr.nodeName;
if (!name.startsWith(PREFIX)) {
continue;
}
const type = name.substr(PREFIX.length);
const value = t(attr.value);
let toInsert, before;
switch (type) {
case 'word-break':
// we already know that: hasWordBreak
break;
case 'text':
before = node.firstChild;
// fallthrough to text-append
case 'text-append':
toInsert = createText(value);
break;
case 'html': {
toInsert = createHtml(value);
break;
}
default:
node.setAttribute(type, value);
}
tDocLoader.pause();
if (toInsert) {
node.insertBefore(toInsert, before || null);
}
node.removeAttribute(name);
}
}
function createTemplate(node) {
const elements = node.content.querySelectorAll('*');
tNodeList(elements);
template[node.dataset.id] = elements[0];
// compress inter-tag whitespace to reduce number of DOM nodes by 25%
const walker = document.createTreeWalker(elements[0], NodeFilter.SHOW_TEXT);
const toRemove = [];
while (walker.nextNode()) {
const textNode = walker.currentNode;
if (!textNode.nodeValue.trim()) {
toRemove.push(textNode);
}
}
tDocLoader.pause();
toRemove.forEach(el => el.remove());
}
function createText(str) {
return document.createTextNode(tWordBreak(str));
}
function createHtml(value) {
// <a href=foo>bar</a> are the only recognizable HTML elements
const rx = /(?:<a\s([^>]*)>([^<]*)<\/a>)?([^<]*)/gi;
const bin = document.createDocumentFragment();
for (let m; (m = rx.exec(value)) && m[0];) {
const [, linkParams, linkText, nextText] = m;
if (linkText) {
const href = /\bhref\s*=\s*(\S+)/.exec(linkParams);
const a = bin.appendChild(document.createElement('a'));
a.href = href && href[1].replace(/^(["'])(.*)\1$/, '$2') || '';
a.appendChild(createText(linkText));
}
if (nextText) {
bin.appendChild(createText(nextText));
}
}
return bin;
}
}
function tDocLoader() {
t.DOMParser = new DOMParser();
t.cache = (() => {
try {
return JSON.parse(localStorage.L10N);
} catch (e) {}
})() || {};
t.RX_WORD_BREAK = new RegExp([
'(', '(',
/[\d\w\u007B-\uFFFF]{10}/, /[\d\w\u007B-\uFFFF]{10}/,
'|', '|',
@ -152,73 +19,171 @@ function tDocLoader() {
/((?!\s)\W){10}/, /((?!\s)\W){10}/,
')', ')',
/(?!\b|\s|$)/, /(?!\b|\s|$)/,
].map(rx => rx.source || rx).join(''), 'g'); ].map(rx => rx.source || rx).join(''), 'g'),
// reset L10N cache on UI language change HTML(html) {
const UIlang = chrome.i18n.getUILanguage(); return typeof html !== 'string'
if (t.cache.browserUIlanguage !== UIlang) { ? html
t.cache = {browserUIlanguage: UIlang}; : /<\w+/.test(html) // check for html tags
localStorage.L10N = JSON.stringify(t.cache); ? t.createHtml(html.replace(/>\n\s*</g, '><').trim())
} : document.createTextNode(html);
const cacheLength = Object.keys(t.cache).length; },
Object.assign(tDocLoader, { NodeList(nodes) {
observer: new MutationObserver(process), const PREFIX = 'i18n-';
start() { for (let n = nodes.length; --n >= 0;) {
if (!tDocLoader.observing) { const node = nodes[n];
tDocLoader.observing = true; if (node.nodeType !== Node.ELEMENT_NODE) {
tDocLoader.observer.observe(document, {subtree: true, childList: true}); continue;
} }
}, if (node.localName === 'template') {
stop() { t.createTemplate(node);
tDocLoader.pause(); continue;
document.removeEventListener('DOMContentLoaded', onLoad); }
}, for (let a = node.attributes.length; --a >= 0;) {
pause() { const attr = node.attributes[a];
if (tDocLoader.observing) { const name = attr.nodeName;
tDocLoader.observing = false; if (!name.startsWith(PREFIX)) {
tDocLoader.observer.disconnect(); continue;
}
const type = name.substr(PREFIX.length);
const value = t(attr.value);
let toInsert, before;
switch (type) {
case 'word-break':
// we already know that: hasWordBreak
break;
case 'text':
before = node.firstChild;
// fallthrough to text-append
case 'text-append':
toInsert = t.createText(value);
break;
case 'html': {
toInsert = t.createHtml(value);
break;
}
default:
node.setAttribute(type, value);
}
t.stopObserver();
if (toInsert) {
node.insertBefore(toInsert, before || null);
}
node.removeAttribute(name);
}
}
},
/** Adds soft hyphens every 10 characters to ensure the long words break before breaking the layout */
breakWord(text) {
return text.length <= 10 ? text :
text.replace(t.RX_WORD_BREAK, '$&\u00AD');
},
createTemplate(node) {
const elements = node.content.querySelectorAll('*');
t.NodeList(elements);
t.template[node.dataset.id] = elements[0];
// compress inter-tag whitespace to reduce number of DOM nodes by 25%
const walker = document.createTreeWalker(elements[0], NodeFilter.SHOW_TEXT);
const toRemove = [];
while (walker.nextNode()) {
const textNode = walker.currentNode;
if (!/[\xA0\S]/.test(textNode.nodeValue)) { // allow \xA0 to keep &nbsp;
toRemove.push(textNode);
}
}
t.stopObserver();
toRemove.forEach(el => el.remove());
},
createText(str) {
return document.createTextNode(t.breakWord(str));
},
createHtml(str, trusted) {
const root = t.DOMParser.parseFromString(str, 'text/html').body;
if (!trusted) {
t.sanitizeHtml(root);
} else if (str.includes('i18n-')) {
t.NodeList(root.getElementsByTagName('*'));
}
const bin = document.createDocumentFragment();
while (root.firstChild) {
bin.appendChild(root.firstChild);
}
return bin;
},
sanitizeHtml(root) {
const toRemove = [];
const walker = document.createTreeWalker(root);
for (let n; (n = walker.nextNode());) {
if (n.nodeType === Node.TEXT_NODE) {
n.nodeValue = t.breakWord(n.nodeValue);
} else if (t.ALLOWED_TAGS.includes(n.localName)) {
for (const attr of n.attributes) {
if (n.localName !== 'a' || attr.localName !== 'href' || !/^https?:/.test(n.href)) {
n.removeAttribute(attr.name);
}
}
} else {
toRemove.push(n);
}
}
for (const n of toRemove) {
const parent = n.parentNode;
if (parent) parent.removeChild(n); // not using .remove() as there may be a non-element
}
},
formatDate(date) {
if (!date) {
return '';
}
try {
const newDate = new Date(Number(date) || date);
const string = newDate.toLocaleDateString([chrome.i18n.getUILanguage(), 'en'], {
day: '2-digit',
month: 'short',
year: newDate.getYear() === new Date().getYear() ? undefined : '2-digit',
});
return string === 'Invalid Date' ? '' : string;
} catch (e) {
return '';
}
},
});
(() => {
const observer = new MutationObserver(process);
let observing = false;
Object.assign(t, {
stopObserver() {
if (observing) {
observing = false;
observer.disconnect();
} }
}, },
}); });
document.addEventListener('DOMContentLoaded', () => {
process(observer.takeRecords());
t.stopObserver();
}, {once: true});
tNodeList(document.getElementsByTagName('*')); t.NodeList(document.getElementsByTagName('*'));
tDocLoader.start(); start();
document.addEventListener('DOMContentLoaded', onLoad);
function process(mutations) { function process(mutations) {
for (const mutation of mutations) { mutations.forEach(m => t.NodeList(m.addedNodes));
tNodeList(mutation.addedNodes); start();
}
tDocLoader.start();
} }
function onLoad() { function start() {
document.removeEventListener('DOMContentLoaded', onLoad); if (!observing) {
process(tDocLoader.observer.takeRecords()); observing = true;
tDocLoader.stop(); observer.observe(document, {subtree: true, childList: true});
if (cacheLength !== Object.keys(t.cache).length) {
localStorage.L10N = JSON.stringify(t.cache);
} }
} }
} })();
function tWordBreak(text) {
// adds soft hyphens every 10 characters to ensure the long words break before breaking the layout
return text.length <= 10 ? text :
text.replace(t.RX_WORD_BREAK, '$&\u00AD');
}
function formatDate(date) {
return !date ? '' : tryCatch(() => {
const newDate = new Date(Number(date) || date);
const string = newDate.toLocaleDateString([t.cache.browserUIlanguage, 'en'], {
day: '2-digit',
month: 'short',
year: newDate.getYear() === new Date().getYear() ? undefined : '2-digit',
});
return string === 'Invalid Date' ? '' : string;
}) || '';
}

View File

@ -1,6 +1,20 @@
/* exported getTab getActiveTab onTabReady stringAsRegExp openURL ignoreChromeError /* exported
getStyleWithNoCode tryRegExp sessionStorageHash download deepEqual capitalize
closeCurrentTab capitalize CHROME_HAS_BORDER_BUG */ CHROME_HAS_BORDER_BUG
closeCurrentTab
deepEqual
download
getActiveTab
getStyleWithNoCode
getTab
ignoreChromeError
onTabReady
openURL
sessionStore
stringAsRegExp
tryCatch
tryRegExp
*/
'use strict'; 'use strict';
const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(\d+)|$/)[1]); const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(\d+)|$/)[1]);
@ -112,7 +126,7 @@ function urlToMatchPattern(url, ignoreSearch) {
if (ignoreSearch) { if (ignoreSearch) {
return [ return [
`${url.protocol}//${url.hostname}/${url.pathname}`, `${url.protocol}//${url.hostname}/${url.pathname}`,
`${url.protocol}//${url.hostname}/${url.pathname}?*` `${url.protocol}//${url.hostname}/${url.pathname}?*`,
]; ];
} }
// FIXME: is %2f allowed in pathname and search? // FIXME: is %2f allowed in pathname and search?
@ -206,7 +220,7 @@ function activateTab(tab, {url, index, openerTabId} = {}) {
return Promise.all([ return Promise.all([
browser.tabs.update(tab.id, options), browser.tabs.update(tab.id, options),
browser.windows && browser.windows.update(tab.windowId, {focused: true}), browser.windows && browser.windows.update(tab.windowId, {focused: true}),
index != null && browser.tabs.move(tab.id, {index}) index != null && browser.tabs.move(tab.id, {index}),
]) ])
.then(() => tab); .then(() => tab);
} }
@ -316,24 +330,28 @@ function deepEqual(a, b, ignoredKeys) {
return true; return true;
} }
/* A simple polyfill in case DOM storage is disabled in the browser */
function sessionStorageHash(name) { const sessionStore = new Proxy({}, {
return { get(target, name) {
name, try {
value: tryCatch(JSON.parse, sessionStorage[name]) || {}, return sessionStorage[name];
set(k, v) { } catch (e) {
this.value[k] = v; Object.defineProperty(window, 'sessionStorage', {value: target});
this.updateStorage();
},
unset(k) {
delete this.value[k];
this.updateStorage();
},
updateStorage() {
sessionStorage[this.name] = JSON.stringify(this.value);
} }
}; },
} set(target, name, value, proxy) {
try {
sessionStorage[name] = `${value}`;
} catch (e) {
proxy[name]; // eslint-disable-line no-unused-expressions
target[name] = `${value}`;
}
return true;
},
deleteProperty(target, name) {
return delete target[name];
},
});
/** /**
* @param {String} url * @param {String} url

View File

@ -12,17 +12,17 @@ const metaParser = (() => {
throw new ParseError({ throw new ParseError({
code: 'unknownPreprocessor', code: 'unknownPreprocessor',
args: [state.value], args: [state.value],
index: state.valueIndex index: state.valueIndex,
}); });
} }
} },
}, },
validateVar: { validateVar: {
select: state => { select: state => {
if (state.varResult.options.every(o => o.name !== state.value)) { if (state.varResult.options.every(o => o.name !== state.value)) {
throw new ParseError({ throw new ParseError({
code: 'invalidSelectValueMismatch', code: 'invalidSelectValueMismatch',
index: state.valueIndex index: state.valueIndex,
}); });
} }
}, },
@ -32,19 +32,19 @@ const metaParser = (() => {
throw new ParseError({ throw new ParseError({
code: 'invalidColor', code: 'invalidColor',
args: [state.value], args: [state.value],
index: state.valueIndex index: state.valueIndex,
}); });
} }
state.value = colorConverter.format(color, 'rgb'); state.value = colorConverter.format(color, 'rgb');
} },
} },
}; };
const parser = createParser(options); const parser = createParser(options);
const looseParser = createParser(Object.assign({}, options, {allowErrors: true, unknownKey: 'throw'})); const looseParser = createParser(Object.assign({}, options, {allowErrors: true, unknownKey: 'throw'}));
return { return {
parse, parse,
lint, lint,
nullifyInvalidVars nullifyInvalidVars,
}; };
function parse(text, indexOffset) { function parse(text, indexOffset) {

View File

@ -66,15 +66,6 @@ self.INJECTED !== 1 && (() => {
//#region for our extension pages //#region for our extension pages
for (const storage of ['localStorage', 'sessionStorage']) {
try {
window[storage]._access_check = 1;
delete window[storage]._access_check;
} catch (err) {
Object.defineProperty(window, storage, {value: {}});
}
}
if (!(new URLSearchParams({foo: 1})).get('foo')) { if (!(new URLSearchParams({foo: 1})).get('foo')) {
// TODO: remove when minimum_chrome_version >= 61 // TODO: remove when minimum_chrome_version >= 61
window.URLSearchParams = class extends URLSearchParams { window.URLSearchParams = class extends URLSearchParams {

View File

@ -15,6 +15,7 @@ window.INJECTED !== 1 && (() => {
'exposeIframes': false, // Add 'stylus-iframe' attribute to HTML element in all iframes 'exposeIframes': false, // Add 'stylus-iframe' attribute to HTML element in all iframes
'newStyleAsUsercss': false, // create new style in usercss format 'newStyleAsUsercss': false, // create new style in usercss format
'styleViaXhr': false, // early style injection to avoid FOUC 'styleViaXhr': false, // early style injection to avoid FOUC
'patchCsp': false, // add data: and popular image hosting sites to strict CSP
// checkbox in style config dialog // checkbox in style config dialog
'config.autosave': true, 'config.autosave': true,

View File

@ -12,7 +12,12 @@ const usercss = (() => {
}; };
const RX_META = /\/\*!?\s*==userstyle==[\s\S]*?==\/userstyle==\s*\*\//i; const RX_META = /\/\*!?\s*==userstyle==[\s\S]*?==\/userstyle==\s*\*\//i;
const ERR_ARGS_IS_LIST = new Set(['missingMandatory', 'missingChar']); const ERR_ARGS_IS_LIST = new Set(['missingMandatory', 'missingChar']);
return {buildMeta, buildCode, assignVars}; return {
RX_META,
buildMeta,
buildCode,
assignVars,
};
function buildMeta(sourceCode) { function buildMeta(sourceCode) {
sourceCode = sourceCode.replace(/\r\n?/g, '\n'); sourceCode = sourceCode.replace(/\r\n?/g, '\n');
@ -20,7 +25,7 @@ const usercss = (() => {
const style = { const style = {
enabled: true, enabled: true,
sourceCode, sourceCode,
sections: [] sections: [],
}; };
const match = sourceCode.match(RX_META); const match = sourceCode.match(RX_META);

View File

@ -67,7 +67,7 @@ const workerUtil = {
message: err.message, message: err.message,
lineNumber: err.lineNumber, lineNumber: err.lineNumber,
columnNumber: err.columnNumber, columnNumber: err.columnNumber,
fileName: err.fileName fileName: err.fileName,
}, err); }, err);
}, },

View File

@ -11,8 +11,6 @@
<link rel="stylesheet" href="options/onoffswitch.css"> <link rel="stylesheet" href="options/onoffswitch.css">
<link rel="stylesheet" href="vendor-overwrites/colorpicker/colorpicker.css"> <link rel="stylesheet" href="vendor-overwrites/colorpicker/colorpicker.css">
<style id="style-overrides"></style>
<style id="firefox-transitions-bug-suppressor"> <style id="firefox-transitions-bug-suppressor">
/* restrict to FF */ /* restrict to FF */
@supports (-moz-appearance:none) { @supports (-moz-appearance:none) {
@ -32,7 +30,10 @@
<template data-id="style"> <template data-id="style">
<div class="entry"> <div class="entry">
<h2 class="style-name"> <h2 class="style-name">
<a class="style-name-link"></a> <a class="style-name-link">
&nbsp;
<span class="style-info" data-type="version"></span>
</a>
<a target="_blank" class="homepage"></a> <a target="_blank" class="homepage"></a>
</h2> </h2>
<p class="applies-to"> <p class="applies-to">
@ -54,14 +55,17 @@
</div> </div>
</template> </template>
<template data-id="styleCompact"> <template data-id="styleNewUI">
<div class="entry"> <div class="entry">
<h2 class="style-name"> <h2 class="style-name">
<div class="checkmate"> <div class="checkmate">
<input class="checker" type="checkbox" i18n-title="toggleStyle"> <input class="checker" type="checkbox" i18n-title="toggleStyle">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg> <svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</div> </div>
<a class="style-name-link"></a> <a class="style-name-link">
&nbsp;
<span class="style-info" data-type="version"></span>
</a>
</h2> </h2>
<p class="actions"> <p class="actions">
<a target="_blank" class="homepage" tabindex="0"></a> <a target="_blank" class="homepage" tabindex="0"></a>
@ -72,6 +76,7 @@
</svg> </svg>
</a> </a>
</p> </p>
<p class="style-info" data-type="age"></p>
<div class="applies-to"> <div class="applies-to">
<div class="targets"></div> <div class="targets"></div>
<a href="#" class="expander" tabindex="0">...</a> <a href="#" class="expander" tabindex="0">...</a>
@ -260,9 +265,19 @@
</label> </label>
<div id="search-wrapper"> <div id="search-wrapper">
<input id="search" type="search" i18n-placeholder="searchStyles" spellcheck="false" <input id="search" type="search" i18n-placeholder="search" spellcheck="false"
data-filter=":not(.not-matching)" data-filter=":not(.not-matching)"
data-filter-hide=".not-matching"> data-filter-hide=".not-matching">
<div class="select-wrapper">
<select id="searchMode">
<option i18n-text="searchStylesName" value="name"></option>
<option i18n-text="searchStylesMeta" value="meta" selected></option>
<option i18n-text="searchStylesCode" value="code"></option>
<option i18n-text="searchStylesMatchUrl" value="url"></option>
<option i18n-text="searchStylesAll" value="all"></option>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
<a href="#" id="search-help" tabindex="0"> <a href="#" id="search-help" tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg> <svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a> </a>

View File

@ -1,5 +1,15 @@
/* global messageBox deepCopy $create $createLink $ t tWordBreak /* global
prefs setupLivePrefs debounce API */ $
$create
$createLink
API
debounce
deepCopy
messageBox
prefs
setupLivePrefs
t
*/
/* exported configDialog */ /* exported configDialog */
'use strict'; 'use strict';
@ -28,7 +38,7 @@ function configDialog(style) {
contents: [ contents: [
$create('.config-heading', data.supportURL && $create('.config-heading', data.supportURL &&
$createLink({className: '.external-support', href: data.supportURL}, t('externalFeedback'))), $createLink({className: '.external-support', href: data.supportURL}, t('externalFeedback'))),
$create('.config-body', elements) $create('.config-body', elements),
], ],
buttons: [{ buttons: [{
textContent: t('confirmSave'), textContent: t('confirmSave'),
@ -210,8 +220,8 @@ function configDialog(style) {
$create('SVG:polygon', { $create('SVG:polygon', {
points: '16.2,5.5 14.5,3.8 10,8.3 5.5,3.8 3.8,5.5 8.3,10 3.8,14.5 ' + points: '16.2,5.5 14.5,3.8 10,8.3 5.5,3.8 3.8,5.5 8.3,10 3.8,14.5 ' +
'5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10', '5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10',
}) }),
]) ]),
]); ]);
for (const va of vars) { for (const va of vars) {
let children; let children;
@ -222,7 +232,7 @@ function configDialog(style) {
va.input = $create('a.color-swatch', { va.input = $create('a.color-swatch', {
va, va,
href: '#', href: '#',
onclick: showColorpicker onclick: showColorpicker,
}), }),
]), ]),
]; ];
@ -268,7 +278,7 @@ function configDialog(style) {
onblur: va.type === 'number' ? updateVarOnBlur : null, onblur: va.type === 'number' ? updateVarOnBlur : null,
onchange: updateVarOnChange, onchange: updateVarOnChange,
oninput: updateVarOnInput, oninput: updateVarOnInput,
required: true required: true,
}; };
if (typeof va.min === 'number') { if (typeof va.min === 'number') {
options.min = va.min; options.min = va.min;
@ -281,7 +291,7 @@ function configDialog(style) {
} }
children = [ children = [
va.type === 'range' && $create('span.current-value'), va.type === 'range' && $create('span.current-value'),
va.input = $create('input.config-value', options) va.input = $create('input.config-value', options),
]; ];
break; break;
} }
@ -305,7 +315,7 @@ function configDialog(style) {
elements.push( elements.push(
$create(`label.config-${va.type}`, [ $create(`label.config-${va.type}`, [
$create('span.config-name', tWordBreak(va.label)), $create('span.config-name', t.breakWord(va.label)),
...children, ...children,
resetter, resetter,
])); ]));

View File

@ -11,8 +11,9 @@ const filtersSelector = {
let initialized = false; let initialized = false;
router.watch({search: ['search']}, ([search]) => { router.watch({search: ['search', 'searchMode']}, ([search, mode]) => {
$('#search').value = search || ''; $('#search').value = search || '';
if (mode) $('#searchMode').value = mode;
if (!initialized) { if (!initialized) {
initFilters(); initFilters();
initialized = true; initialized = true;
@ -22,30 +23,29 @@ router.watch({search: ['search']}, ([search]) => {
}); });
HTMLSelectElement.prototype.adjustWidth = function () { HTMLSelectElement.prototype.adjustWidth = function () {
const option0 = this.selectedOptions[0]; const sel = this.selectedOptions[0];
if (!option0) return; if (!sel) return;
const parent = this.parentNode; const wOld = parseFloat(this.style.width);
const singleSelect = this.cloneNode(false); const opts = [...this];
singleSelect.style.width = ''; opts.forEach(opt => opt !== sel && opt.remove());
singleSelect.appendChild(option0.cloneNode(true)); this.style.width = '';
parent.replaceChild(singleSelect, this); requestAnimationFrame(() => {
const w = singleSelect.offsetWidth; const w = this.offsetWidth;
if (w && this.style.width !== w + 'px') { if (w && wOld !== w) this.style.width = w + 'px';
this.style.width = w + 'px'; this.append(...opts);
} });
parent.replaceChild(this, singleSelect);
}; };
function initFilters() { function initFilters() {
$('#search').oninput = e => { $('#search').oninput = $('#searchMode').oninput = function (e) {
router.updateSearch('search', e.target.value); router.updateSearch(this.id, e.target.value);
}; };
$('#search-help').onclick = event => { $('#search-help').onclick = event => {
event.preventDefault(); event.preventDefault();
messageBox({ messageBox({
className: 'help-text', className: 'help-text',
title: t('searchStyles'), title: t('search'),
contents: contents:
$create('ul', $create('ul',
t('searchStylesHelp').split('\n').map(line => t('searchStylesHelp').split('\n').map(line =>
@ -133,7 +133,7 @@ function initFilters() {
prefs.subscribe(['manage.filters.expanded'], () => { prefs.subscribe(['manage.filters.expanded'], () => {
const el = $('#filters'); const el = $('#filters');
if (el.open) { if (el.open) {
$$('select', el).forEach(select => select.adjustWidth()); $$('.filter-selection select', el).forEach(select => select.adjustWidth());
} }
}); });
@ -141,7 +141,7 @@ function initFilters() {
} }
function filterOnChange({target: el, forceRefilter}) { function filterOnChange({target: el, forceRefilter, alreadySearched}) {
const getValue = el => (el.type === 'checkbox' ? el.checked : el.value.trim()); const getValue = el => (el.type === 'checkbox' ? el.checked : el.value.trim());
if (!forceRefilter) { if (!forceRefilter) {
const value = getValue(el); const value = getValue(el);
@ -157,14 +157,14 @@ function filterOnChange({target: el, forceRefilter}) {
el.dataset[hide ? 'filterHide' : 'filter'] el.dataset[hide ? 'filterHide' : 'filter']
.split(/,\s*/) .split(/,\s*/)
.map(s => (hide ? '.entry:not(.hidden)' : '') + s) .map(s => (hide ? '.entry:not(.hidden)' : '') + s)
.join(',')) .join(',')),
].join(hide ? ',' : ''); ].join(hide ? ',' : '');
Object.assign(filtersSelector, { Object.assign(filtersSelector, {
hide: buildFilter(true), hide: buildFilter(true),
unhide: buildFilter(false), unhide: buildFilter(false),
}); });
if (installed) { if (installed) {
reapplyFilter().then(sorter.updateStripes); reapplyFilter(installed, alreadySearched).then(sorter.updateStripes);
} }
} }
@ -278,10 +278,12 @@ function showFiltersStats() {
} }
function searchStyles({immediately, container} = {}) { async function searchStyles({immediately, container} = {}) {
const el = $('#search'); const el = $('#search');
const elMode = $('#searchMode');
const query = el.value.trim(); const query = el.value.trim();
if (query === el.lastValue && !immediately && !container) { const mode = elMode.value;
if (query === el.lastValue && mode === elMode.lastValue && !immediately && !container) {
return; return;
} }
if (!immediately) { if (!immediately) {
@ -289,24 +291,24 @@ function searchStyles({immediately, container} = {}) {
return; return;
} }
el.lastValue = query; el.lastValue = query;
elMode.lastValue = mode;
const entries = container && container.children || container || installed.children; const all = installed.children;
return API.searchDB({ const entries = container && container.children || container || all;
query, const idsToSearch = entries !== all && [...entries].map(el => el.styleId);
ids: [...entries].map(el => el.styleId), const ids = entries[0]
}).then(ids => { ? await API.searchDB({query, mode, ids: idsToSearch})
ids = new Set(ids); : [];
let needsRefilter = false; let needsRefilter = false;
for (const entry of entries) { for (const entry of entries) {
const isMatching = ids.has(entry.styleId); const isMatching = ids.includes(entry.styleId);
if (entry.classList.contains('not-matching') !== !isMatching) { if (entry.classList.contains('not-matching') !== !isMatching) {
entry.classList.toggle('not-matching', !isMatching); entry.classList.toggle('not-matching', !isMatching);
needsRefilter = true; needsRefilter = true;
}
} }
if (needsRefilter && !container) { }
filterOnChange({forceRefilter: true}); if (needsRefilter && !container) {
} filterOnChange({forceRefilter: true, alreadySearched: true});
return container; }
}); return container;
} }

View File

@ -18,8 +18,8 @@ body {
font-weight: normal; font-weight: normal;
} }
body.all-styles-hidden-by-filters:before, body.all-styles-hidden-by-filters::before,
body.all-styles-hidden-by-filters:after { body.all-styles-hidden-by-filters::after {
position: absolute; position: absolute;
left: calc(3rem + var(--header-width)); left: calc(3rem + var(--header-width));
color: hsla(180, 40%, 45%, .3); color: hsla(180, 40%, 45%, .3);
@ -32,7 +32,8 @@ body.all-styles-hidden-by-filters:before {
top: 3.5rem; top: 3.5rem;
} }
body.all-styles-hidden-by-filters:after { body.all-styles-hidden-by-filters::after {
content: var(--filteredStylesAllHidden);
font-size: 1.5rem; font-size: 1.5rem;
position: absolute; position: absolute;
top: 3rem; top: 3rem;
@ -177,6 +178,20 @@ a:hover {
text-decoration: none; text-decoration: none;
} }
.style-info {
text-align: right;
padding: 0 .25em;
font-weight: normal;
color: #999;
}
.style-info[data-type=version] {
color: #666;
padding-left: .5em;
}
.newUI .style-info[data-type=version][data-value="1.0.0"] {
display: none;
}
.applies-to { .applies-to {
overflow-wrap: break-word; overflow-wrap: break-word;
} }
@ -233,7 +248,11 @@ a:hover {
vertical-align: text-top; vertical-align: text-top;
} }
.disabled h2::after, .entry.usercss .style-name-link::after { .disabled h2::after {
content: var(--genericDisabledLabel);
}
.disabled h2::after,
.entry.usercss .style-name-link::after {
font-weight: normal; font-weight: normal;
font-size: 11px; font-size: 11px;
text-transform: lowercase; text-transform: lowercase;
@ -243,6 +262,10 @@ a:hover {
margin-left: 1ex; margin-left: 1ex;
white-space: nowrap; white-space: nowrap;
position: relative; position: relative;
top: -2px;
}
.newUI .disabled h2::after,
.newUI .entry.usercss .style-name-link::after {
top: 2px; top: 2px;
} }
@ -353,6 +376,10 @@ a:hover {
vertical-align: middle; vertical-align: middle;
} }
.newUI .entry > .style-info {
padding-right: 1em;
}
.newUI .entry .actions { .newUI .entry .actions {
position: relative; position: relative;
} }
@ -614,6 +641,7 @@ a:hover {
.newUI .targets { .newUI .targets {
overflow: hidden; overflow: hidden;
max-height: calc(var(--num-targets) * 18px);
} }
.newUI .applies-to.expanded .targets { .newUI .applies-to.expanded .targets {
@ -625,7 +653,7 @@ a:hover {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
max-width: calc(100vw - var(--header-width) - var(--actions-width) - var(--name-padding-left) - 25vw - var(--name-padding-right)); max-width: calc(75vw - var(--header-width) - var(--actions-width) - var(--name-padding-left) - var(--name-padding-right) - 6rem);
box-sizing: border-box; box-sizing: border-box;
padding-right: 1rem; padding-right: 1rem;
line-height: 18px; line-height: 18px;
@ -659,13 +687,16 @@ a:hover {
vertical-align: middle; vertical-align: middle;
margin: -1px 4px 0 -20px; margin: -1px 4px 0 -20px;
transition: opacity .5s, filter .5s; transition: opacity .5s, filter .5s;
filter: grayscale(1);
/* workaround for the buggy CSS filter: images in the hidden overflow are shown on Mac */ /* workaround for the buggy CSS filter: images in the hidden overflow are shown on Mac */
backface-visibility: hidden; backface-visibility: hidden;
opacity: .25;
display: none; display: none;
} }
.newUI .favicons-grayed .target img {
filter: grayscale(1);
opacity: .25;
}
.newUI .has-favicons .target { .newUI .has-favicons .target {
padding-left: 20px; padding-left: 20px;
} }
@ -745,6 +776,7 @@ a:hover {
} }
#update-all-no-updates[data-skipped-edited="true"]::after { #update-all-no-updates[data-skipped-edited="true"]::after {
content: " " var(--updateAllCheckSucceededSomeEdited);
font-weight: normal; font-weight: normal;
display: block; display: block;
} }
@ -787,10 +819,6 @@ a:hover {
background-color: hsla(0, 0%, 50%, .4); background-color: hsla(0, 0%, 50%, .4);
} }
#filters {
border: 1px solid transparent;
}
.active #filters-stats { .active #filters-stats {
background-color: darkcyan; background-color: darkcyan;
border-color: darkcyan; border-color: darkcyan;
@ -836,10 +864,11 @@ a:hover {
#search-wrapper, #sort-wrapper { #search-wrapper, #sort-wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap;
margin-bottom: .5rem; margin-bottom: .5rem;
} }
#searchMode {
margin-left: -1px;
}
#search-wrapper { #search-wrapper {
margin-top: .35rem; margin-top: .35rem;
} }
@ -848,18 +877,14 @@ a:hover {
display: inline-flex; display: inline-flex;
flex-grow: 1; flex-grow: 1;
position: relative; position: relative;
max-width: calc(100% - 30px);
} }
#manage\.newUI\.sort { #manage\.newUI\.sort {
max-width: 100%; max-width: 100%;
} }
#search {
max-width: calc(100% - 30px);
}
#search, #manage\.newUI\.sort { #search, #manage\.newUI\.sort {
min-width: 4em; /* reduces the big default width */
flex-grow: 1; flex-grow: 1;
background: #fff; background: #fff;
height: 20px; height: 20px;
@ -1022,6 +1047,12 @@ a:hover {
} }
} }
@media (max-width: 1000px) {
.newUI .entry > .style-info {
display: none;
}
}
@media (max-width: 850px) { @media (max-width: 850px) {
body { body {
display: table; display: table;
@ -1164,10 +1195,6 @@ a:hover {
padding-left: 0; padding-left: 0;
} }
#reset-filters {
margin-top: 4px;
}
#filters summary h2 { #filters summary h2 {
margin-left: -2px; margin-left: -2px;
} }

View File

@ -1,20 +1,39 @@
/* /* global
global messageBox getStyleWithNoCode $
filterAndAppend showFiltersStats $$
checkUpdate handleUpdateInstalled $create
objectDiff animateElement
API
checkUpdate
CHROME
configDialog configDialog
sorter msg prefs API $ $$ $create template setupLivePrefs debounce
t tWordBreak formatDate filterAndAppend
getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce getOwnTab
scrollElementIntoView CHROME VIVALDI router getStyleWithNoCode
handleUpdateInstalled
messageBox
msg
objectDiff
openURL
prefs
router
scrollElementIntoView
sessionStore
setupLivePrefs
showFiltersStats
sorter
t
VIVALDI
*/ */
'use strict'; 'use strict';
/** @type {HTMLElement} */
let installed; let installed;
const ENTRY_ID_PREFIX_RAW = 'style-'; const ENTRY_ID_PREFIX_RAW = 'style-';
const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW; const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW;
const REVEAL_DATES_FOR = 'h2.style-name, [data-type=age]';
const BULK_THROTTLE_MS = 100; const BULK_THROTTLE_MS = 100;
const bulkChangeQueue = []; const bulkChangeQueue = [];
@ -43,24 +62,31 @@ newUI.renderClass();
const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps']; const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps'];
const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain='; const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain=';
const OWN_ICON = chrome.runtime.getManifest().icons['16']; const OWN_ICON = chrome.runtime.getManifest().icons['16'];
const AGES = [
[24, 'h', t('dateAbbrHour', '\x01')],
[30, 'd', t('dateAbbrDay', '\x01')],
[12, 'm', t('dateAbbrMonth', '\x01')],
[Infinity, 'y', t('dateAbbrYear', '\x01')],
];
const handleEvent = {}; const handleEvent = {};
Promise.all([ (async () => {
API.getAllStyles(true), const query = router.getSearch('search');
// FIXME: integrate this into filter.js const [styles, ids, el] = await Promise.all([
router.getSearch('search') && API.searchDB({query: router.getSearch('search')}), API.getAllStyles(),
waitForSelector('#installed'), // needed to avoid flicker due to an extra frame and layout shift query && API.searchDB({query, mode: router.getSearch('searchMode')}),
prefs.initializing waitForSelector('#installed'), // needed to avoid flicker due to an extra frame and layout shift
]).then(([styles, ids, el]) => { prefs.initializing,
]);
installed = el; installed = el;
installed.onclick = handleEvent.entryClicked; installed.onclick = handleEvent.entryClicked;
$('#manage-options-button').onclick = () => router.updateHash('#stylus-options'); $('#manage-options-button').onclick = () => router.updateHash('#stylus-options');
$('#sync-styles').onclick = () => router.updateHash('#stylus-options'); $('#sync-styles').onclick = () => router.updateHash('#stylus-options');
$$('#header a[href^="http"]').forEach(a => (a.onclick = handleEvent.external)); $$('#header a[href^="http"]').forEach(a => (a.onclick = handleEvent.external));
// show date installed & last update on hover // show date installed & last update on hover
installed.addEventListener('mouseover', handleEvent.lazyAddEntryTitle); installed.addEventListener('mouseover', handleEvent.lazyAddEntryTitle, {passive: true});
installed.addEventListener('mouseout', handleEvent.lazyAddEntryTitle); installed.addEventListener('mouseout', handleEvent.lazyAddEntryTitle, {passive: true});
document.addEventListener('visibilitychange', onVisibilityChange); document.addEventListener('visibilitychange', onVisibilityChange);
// N.B. triggers existing onchange listeners // N.B. triggers existing onchange listeners
setupLivePrefs(); setupLivePrefs();
@ -68,19 +94,15 @@ Promise.all([
prefs.subscribe(newUI.ids.map(newUI.prefKeyForId), () => switchUI()); prefs.subscribe(newUI.ids.map(newUI.prefKeyForId), () => switchUI());
switchUI({styleOnly: true}); switchUI({styleOnly: true});
// translate CSS manually // translate CSS manually
document.head.appendChild($create('style', ` document.styleSheets[0].insertRule(
.disabled h2::after { `:root {${[
content: "${t('genericDisabledLabel')}"; 'genericDisabledLabel',
} 'updateAllCheckSucceededSomeEdited',
#update-all-no-updates[data-skipped-edited="true"]::after { 'filteredStylesAllHidden',
content: " ${t('updateAllCheckSucceededSomeEdited')}"; ].map(id => `--${id}:"${CSS.escape(t(id))}";`).join('')
} }}`);
body.all-styles-hidden-by-filters::after {
content: "${t('filteredStylesAllHidden')}";
}
`));
if (!VIVALDI) { if (!VIVALDI) {
$$('#header select').forEach(el => el.adjustWidth()); $$('.filter-selection select').forEach(el => el.adjustWidth());
} }
if (CHROME >= 80 && CHROME <= 88) { if (CHROME >= 80 && CHROME <= 88) {
// Wrong checkboxes are randomly checked after going back in history, https://crbug.com/1138598 // Wrong checkboxes are randomly checked after going back in history, https://crbug.com/1138598
@ -89,9 +111,11 @@ Promise.all([
}); });
} }
showStyles(styles, ids); showStyles(styles, ids);
}); })();
msg.onExtension(onRuntimeMessage); msg.onExtension(onRuntimeMessage);
router.watch({hash: '#stylus-options'}, state => (state ? embedOptions : unembedOptions)());
window.addEventListener('closeOptions', () => router.updateHash(''));
function onRuntimeMessage(msg) { function onRuntimeMessage(msg) {
switch (msg.method) { switch (msg.method) {
@ -129,7 +153,7 @@ function showStyles(styles = [], matchUrlIds) {
let firstRun = true; let firstRun = true;
installed.dataset.total = styles.length; installed.dataset.total = styles.length;
const scrollY = (history.state || {}).scrollY; const scrollY = (history.state || {}).scrollY;
const shouldRenderAll = scrollY > window.innerHeight || sessionStorage.justEditedStyleId; const shouldRenderAll = scrollY > window.innerHeight || sessionStore.justEditedStyleId;
const renderBin = document.createDocumentFragment(); const renderBin = document.createDocumentFragment();
if (scrollY) { if (scrollY) {
renderStyles(); renderStyles();
@ -155,7 +179,7 @@ function showStyles(styles = [], matchUrlIds) {
return; return;
} }
setTimeout(getFaviconImgSrc); setTimeout(getFaviconImgSrc);
if (sessionStorage.justEditedStyleId) { if (sessionStore.justEditedStyleId) {
highlightEditedStyle(); highlightEditedStyle();
} else if ('scrollY' in (history.state || {})) { } else if ('scrollY' in (history.state || {})) {
setTimeout(window.scrollTo, 0, 0, history.state.scrollY); setTimeout(window.scrollTo, 0, 0, history.state.scrollY);
@ -167,7 +191,7 @@ function showStyles(styles = [], matchUrlIds) {
function createStyleElement({style, name: nameLC}) { function createStyleElement({style, name: nameLC}) {
// query the sub-elements just once, then reuse the references // query the sub-elements just once, then reuse the references
if ((createStyleElement.parts || {}).newUI !== newUI.enabled) { if ((createStyleElement.parts || {}).newUI !== newUI.enabled) {
const entry = template[`style${newUI.enabled ? 'Compact' : ''}`]; const entry = t.template[newUI.enabled ? 'styleNewUI' : 'style'];
createStyleElement.parts = { createStyleElement.parts = {
newUI: newUI.enabled, newUI: newUI.enabled,
entry, entry,
@ -177,7 +201,9 @@ function createStyleElement({style, name: nameLC}) {
editLink: $('.style-edit-link', entry) || {}, editLink: $('.style-edit-link', entry) || {},
editHrefBase: 'edit.html?id=', editHrefBase: 'edit.html?id=',
homepage: $('.homepage', entry), homepage: $('.homepage', entry),
homepageIcon: template[`homepageIcon${newUI.enabled ? 'Small' : 'Big'}`], homepageIcon: t.template[`homepageIcon${newUI.enabled ? 'Small' : 'Big'}`],
infoAge: $('[data-type=age]', entry),
infoVer: $('[data-type=version]', entry),
appliesTo: $('.applies-to', entry), appliesTo: $('.applies-to', entry),
targets: $('.targets', entry), targets: $('.targets', entry),
expander: $('.expander', entry), expander: $('.expander', entry),
@ -192,13 +218,18 @@ function createStyleElement({style, name: nameLC}) {
}; };
} }
const parts = createStyleElement.parts; const parts = createStyleElement.parts;
const configurable = style.usercssData && style.usercssData.vars && Object.keys(style.usercssData.vars).length > 0; const ud = style.usercssData;
const configurable = ud && ud.vars && Object.keys(ud.vars).length > 0;
const name = style.customName || style.name; const name = style.customName || style.name;
parts.checker.checked = style.enabled; parts.checker.checked = style.enabled;
parts.nameLink.textContent = tWordBreak(name); parts.nameLink.firstChild.textContent = t.breakWord(name);
parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id; parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id;
parts.homepage.href = parts.homepage.title = style.url || ''; parts.homepage.href = parts.homepage.title = style.url || '';
if (!newUI.enabled) { parts.infoVer.textContent = ud ? ud.version : '';
parts.infoVer.dataset.value = ud ? ud.version : '';
if (newUI.enabled) {
createAgeText(parts.infoAge, style);
} else {
parts.oldConfigure.classList.toggle('hidden', !configurable); parts.oldConfigure.classList.toggle('hidden', !configurable);
parts.oldCheckUpdate.classList.toggle('hidden', !style.updateUrl); parts.oldCheckUpdate.classList.toggle('hidden', !style.updateUrl);
parts.oldUpdate.classList.toggle('hidden', !style.updateUrl); parts.oldUpdate.classList.toggle('hidden', !style.updateUrl);
@ -217,16 +248,16 @@ function createStyleElement({style, name: nameLC}) {
entry.className = parts.entryClassBase + ' ' + entry.className = parts.entryClassBase + ' ' +
(style.enabled ? 'enabled' : 'disabled') + (style.enabled ? 'enabled' : 'disabled') +
(style.updateUrl ? ' updatable' : '') + (style.updateUrl ? ' updatable' : '') +
(style.usercssData ? ' usercss' : ''); (ud ? ' usercss' : '');
if (style.url) { if (style.url) {
$('.homepage', entry).appendChild(parts.homepageIcon.cloneNode(true)); $('.homepage', entry).appendChild(parts.homepageIcon.cloneNode(true));
} }
if (style.updateUrl && newUI.enabled) { if (style.updateUrl && newUI.enabled) {
$('.actions', entry).appendChild(template.updaterIcons.cloneNode(true)); $('.actions', entry).appendChild(t.template.updaterIcons.cloneNode(true));
} }
if (configurable && newUI.enabled) { if (configurable && newUI.enabled) {
$('.actions', entry).appendChild(template.configureIcon.cloneNode(true)); $('.actions', entry).appendChild(t.template.configureIcon.cloneNode(true));
} }
createStyleTargetsElement({entry, style}); createStyleTargetsElement({entry, style});
@ -267,12 +298,12 @@ function createStyleTargetsElement({entry, expanded, style = entry.styleMeta}) {
el = next; el = next;
continue; continue;
} }
const element = template.appliesToTarget.cloneNode(true); const element = t.template.appliesToTarget.cloneNode(true);
if (!newUI.enabled) { if (!newUI.enabled) {
if (numTargets === maxTargets) { if (numTargets === maxTargets) {
container = container.appendChild(template.extraAppliesTo.cloneNode(true)); container = container.appendChild(t.template.extraAppliesTo.cloneNode(true));
} else if (numTargets > 0) { } else if (numTargets > 1) {
container.appendChild(template.appliesToSeparator.cloneNode(true)); container.appendChild(t.template.appliesToSeparator.cloneNode(true));
} }
} }
element.dataset.type = type; element.dataset.type = type;
@ -291,13 +322,37 @@ function createStyleTargetsElement({entry, expanded, style = entry.styleMeta}) {
if (entryTargets.firstElementChild) { if (entryTargets.firstElementChild) {
entryTargets.textContent = ''; entryTargets.textContent = '';
} }
entryTargets.appendChild(template.appliesToEverything.cloneNode(true)); entryTargets.appendChild(t.template.appliesToEverything.cloneNode(true));
} }
entry.classList.toggle('global', !numTargets); entry.classList.toggle('global', !numTargets);
entry._allTargetsRendered = allTargetsRendered; entry._allTargetsRendered = allTargetsRendered;
entry._numTargets = numTargets; entry._numTargets = numTargets;
} }
function createAgeText(el, style) {
let val = style.updateDate || style.installDate;
if (val) {
val = (Date.now() - val) / 3600e3; // age in hours
for (const [max, unit, text] of AGES) {
const rounded = Math.round(val);
if (rounded < max) {
el.textContent = text.replace('\x01', rounded);
el.dataset.value = padLeft(Math.round(rounded), 2) + unit;
break;
}
val /= max;
}
} else if (el.firstChild) {
el.textContent = '';
delete el.dataset.value;
}
}
/** Adding spaces so CSS can detect "bigness" of a value via amount of spaces at the beginning */
function padLeft(val, width) {
val = `${val}`;
return ' '.repeat(Math.max(0, width - val.length)) + val;
}
function getFaviconImgSrc(container = installed) { function getFaviconImgSrc(container = installed) {
if (!newUI.enabled || !newUI.favicons) return; if (!newUI.enabled || !newUI.favicons) return;
@ -350,7 +405,7 @@ Object.assign(handleEvent, {
'.update': 'update', '.update': 'update',
'.delete': 'delete', '.delete': 'delete',
'.applies-to .expander': 'expandTargets', '.applies-to .expander': 'expandTargets',
'.configure-usercss': 'config' '.configure-usercss': 'config',
}, },
entryClicked(event) { entryClicked(event) {
@ -366,42 +421,29 @@ Object.assign(handleEvent, {
} }
}, },
name(event) { name(event, entry) {
if (newUI.enabled) handleEvent.edit(event); if (newUI.enabled) handleEvent.edit(event, entry);
}, },
edit(event) { async edit(event, entry) {
if (event.altKey) { if (event.altKey) {
return; return;
} }
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
const left = event.button === 0; const key = `${event.shiftKey ? 's' : ''}${event.ctrlKey ? 'c' : ''}${'LMR'[event.button]}`;
const middle = event.button === 1;
const shift = event.shiftKey;
const ctrl = event.ctrlKey;
const openWindow = left && shift && !ctrl;
const openBackgroundTab = (middle && !shift) || (left && ctrl && !shift);
const openForegroundTab = (middle && shift) || (left && ctrl && shift);
const entry = event.target.closest('.entry');
const url = $('[href]', entry).href; const url = $('[href]', entry).href;
if (openWindow || openBackgroundTab || openForegroundTab) { const ownTab = await getOwnTab();
if (chrome.windows && openWindow) { if (key === 'L') {
API.openEditor({id: entry.styleId}); sessionStore['manageStylesHistory' + ownTab.id] = url;
} else { location.href = url;
getOwnTab().then(({index}) => { } else if (chrome.windows && key === 'sL') {
openURL({ API.openEditor({id: entry.styleId});
url,
index: index + 1,
active: openForegroundTab
});
});
}
} else { } else {
onVisibilityChange(); openURL({
getActiveTab().then(tab => { url,
sessionStorageHash('manageStylesHistory').set(tab.id, url); index: ownTab.index + 1,
location.href = url; active: key === 'sM' || key === 'scL',
}); });
} }
}, },
@ -501,9 +543,9 @@ Object.assign(handleEvent, {
}, },
lazyAddEntryTitle({type, target}) { lazyAddEntryTitle({type, target}) {
const cell = target.closest('h2.style-name'); const cell = target.closest(REVEAL_DATES_FOR);
if (cell) { if (cell) {
const link = $('.style-name-link', cell); const link = $('.style-name-link', cell) || cell;
if (type === 'mouseover' && !link.title) { if (type === 'mouseover' && !link.title) {
debounce(handleEvent.addEntryTitle, 50, link); debounce(handleEvent.addEntryTitle, 50, link);
} else { } else {
@ -518,8 +560,8 @@ Object.assign(handleEvent, {
{prop: 'installDate', name: 'dateInstalled'}, {prop: 'installDate', name: 'dateInstalled'},
{prop: 'updateDate', name: 'dateUpdated'}, {prop: 'updateDate', name: 'dateUpdated'},
].map(({prop, name}) => ].map(({prop, name}) =>
t(name) + ': ' + (formatDate(entry.styleMeta[prop]) || '—')).join('\n'); t(name) + ': ' + (t.formatDate(entry.styleMeta[prop]) || '—')).join('\n');
} },
}); });
@ -537,7 +579,7 @@ function handleBulkChange() {
} }
function handleUpdateForId(id, opts) { function handleUpdateForId(id, opts) {
return API.getStyle(id, true).then(style => { return API.getStyle(id).then(style => {
handleUpdate(style, opts); handleUpdate(style, opts);
bulkChangeQueue.time = performance.now(); bulkChangeQueue.time = performance.now();
}); });
@ -620,43 +662,9 @@ function switchUI({styleOnly} = {}) {
Object.assign(newUI, current); Object.assign(newUI, current);
newUI.renderClass(); newUI.renderClass();
installed.classList.toggle('has-favicons', newUI.favicons); installed.classList.toggle('has-favicons', newUI.enabled && newUI.favicons);
$('#style-overrides').textContent = ` installed.classList.toggle('favicons-grayed', newUI.enabled && newUI.faviconsGray);
.newUI .targets { if (changed.targets) installed.style.setProperty('--num-targets', newUI.targets);
max-height: ${newUI.targets * 18}px;
}
` + (newUI.faviconsGray ? `
.newUI .target img {
filter: grayscale(1);
opacity: .25;
}
` : `
.newUI .target img {
filter: none;
opacity: 1;
}
`) + (CHROME >= 58 ? `
.newUI .entry {
contain: strict;
}
.newUI .entry > * {
contain: content;
}
.newUI .entry .actions {
contain: none;
}
.newUI .target {
contain: layout style;
}
.newUI .target img {
contain: layout style size;
}
.newUI .entry.can-update,
.newUI .entry.update-problem,
.newUI .entry.update-done {
contain: none;
}
` : '');
if (styleOnly) { if (styleOnly) {
return; return;
@ -666,7 +674,7 @@ function switchUI({styleOnly} = {}) {
let iconsMissing = iconsEnabled && !$('.applies-to img'); let iconsMissing = iconsEnabled && !$('.applies-to img');
if (changed.enabled || (iconsMissing && !createStyleElement.parts)) { if (changed.enabled || (iconsMissing && !createStyleElement.parts)) {
installed.textContent = ''; installed.textContent = '';
API.getAllStyles(true).then(showStyles); API.getAllStyles().then(showStyles);
return; return;
} }
if (changed.targets) { if (changed.targets) {
@ -691,10 +699,10 @@ function onVisibilityChange() {
// the catch here is that DOM may be outdated so we'll at least refresh the just edited style // the catch here is that DOM may be outdated so we'll at least refresh the just edited style
// assuming other changes aren't important enough to justify making a complicated DOM sync // assuming other changes aren't important enough to justify making a complicated DOM sync
case 'visible': { case 'visible': {
const id = sessionStorage.justEditedStyleId; const id = sessionStore.justEditedStyleId;
if (id) { if (id) {
handleUpdateForId(Number(id), {method: 'styleUpdated'}); handleUpdateForId(Number(id), {method: 'styleUpdated'});
delete sessionStorage.justEditedStyleId; delete sessionStore.justEditedStyleId;
} }
break; break;
} }
@ -707,9 +715,9 @@ function onVisibilityChange() {
function highlightEditedStyle() { function highlightEditedStyle() {
if (!sessionStorage.justEditedStyleId) return; if (!sessionStore.justEditedStyleId) return;
const entry = $(ENTRY_ID_PREFIX + sessionStorage.justEditedStyleId); const entry = $(ENTRY_ID_PREFIX + sessionStore.justEditedStyleId);
delete sessionStorage.justEditedStyleId; delete sessionStore.justEditedStyleId;
if (entry) { if (entry) {
animateElement(entry); animateElement(entry);
requestAnimationFrame(() => scrollElementIntoView(entry)); requestAnimationFrame(() => scrollElementIntoView(entry));
@ -732,13 +740,11 @@ function waitForSelector(selector) {
} }
function embedOptions() { function embedOptions() {
let options = $('#stylus-embedded-options'); const options = $('#stylus-embedded-options') ||
if (!options) { document.documentElement.appendChild($create('iframe', {
options = document.createElement('iframe'); id: 'stylus-embedded-options',
options.id = 'stylus-embedded-options'; src: '/options.html',
options.src = '/options.html'; }));
document.documentElement.appendChild(options);
}
options.focus(); options.focus();
} }
@ -746,20 +752,7 @@ async function unembedOptions() {
const options = $('#stylus-embedded-options'); const options = $('#stylus-embedded-options');
if (options) { if (options) {
options.contentWindow.document.body.classList.add('scaleout'); options.contentWindow.document.body.classList.add('scaleout');
options.classList.add('fadeout');
await animateElement(options, 'fadeout'); await animateElement(options, 'fadeout');
options.remove(); options.remove();
} }
} }
router.watch({hash: '#stylus-options'}, state => {
if (state) {
embedOptions();
} else {
unembedOptions();
}
});
window.addEventListener('closeOptions', () => {
router.updateHash('');
});

View File

@ -13,28 +13,28 @@ const sorter = (() => {
title: { title: {
text: t('genericTitle'), text: t('genericTitle'),
parse: ({name}) => name, parse: ({name}) => name,
sorter: sorterType.alpha sorter: sorterType.alpha,
}, },
usercss: { usercss: {
text: 'Usercss', text: 'Usercss',
parse: ({style}) => style.usercssData ? 0 : 1, parse: ({style}) => style.usercssData ? 0 : 1,
sorter: sorterType.number sorter: sorterType.number,
}, },
disabled: { disabled: {
text: '', // added as either "enabled" or "disabled" by the addOptions function text: '', // added as either "enabled" or "disabled" by the addOptions function
parse: ({style}) => style.enabled ? 1 : 0, parse: ({style}) => style.enabled ? 1 : 0,
sorter: sorterType.number sorter: sorterType.number,
}, },
dateInstalled: { dateInstalled: {
text: t('dateInstalled'), text: t('dateInstalled'),
parse: ({style}) => style.installDate, parse: ({style}) => style.installDate,
sorter: sorterType.number sorter: sorterType.number,
}, },
dateUpdated: { dateUpdated: {
text: t('dateUpdated'), text: t('dateUpdated'),
parse: ({style}) => style.updateDate || style.installDate, parse: ({style}) => style.updateDate || style.installDate,
sorter: sorterType.number sorter: sorterType.number,
} },
}; };
// Adding (assumed) most commonly used ('title,asc' should always be first) // Adding (assumed) most commonly used ('title,asc' should always be first)
@ -56,7 +56,7 @@ const sorter = (() => {
'usercss,asc, title,desc', 'usercss,asc, title,desc',
'usercss,desc, title,desc', 'usercss,desc, title,desc',
'disabled,desc, title,desc', 'disabled,desc, title,desc',
'disabled,desc, usercss,asc, title,desc' 'disabled,desc, usercss,asc, title,desc',
]; ];
const splitRegex = /\s*,\s*/; const splitRegex = /\s*,\s*/;
@ -76,7 +76,7 @@ const sorter = (() => {
dateNew: ` (${t('sortDateNewestFirst')})`, dateNew: ` (${t('sortDateNewestFirst')})`,
dateOld: ` (${t('sortDateOldestFirst')})`, dateOld: ` (${t('sortDateOldestFirst')})`,
groupAsc: t('sortLabelTitleAsc'), groupAsc: t('sortLabelTitleAsc'),
groupDesc: t('sortLabelTitleDesc') groupDesc: t('sortLabelTitleDesc'),
}; };
const optgroupRegex = /\{\w+\}/; const optgroupRegex = /\{\w+\}/;
selectOptions.forEach(sort => { selectOptions.forEach(sort => {
@ -132,7 +132,7 @@ const sorter = (() => {
entry, entry,
name: entry.styleNameLowerCase, name: entry.styleNameLowerCase,
style: entry.styleMeta, style: entry.styleMeta,
})) })),
}); });
if (current.some((entry, index) => entry !== sorted[index].entry)) { if (current.some((entry, index) => entry !== sorted[index].entry)) {
const renderBin = document.createDocumentFragment(); const renderBin = document.createDocumentFragment();

View File

@ -55,7 +55,7 @@
"background/usercss-helper.js", "background/usercss-helper.js",
"background/usercss-install-helper.js", "background/usercss-install-helper.js",
"background/style-via-api.js", "background/style-via-api.js",
"background/style-via-xhr.js", "background/style-via-webrequest.js",
"background/search-db.js", "background/search-db.js",
"background/update.js", "background/update.js",
"background/openusercss-api.js" "background/openusercss-api.js"

View File

@ -1,11 +1,18 @@
/* global focusAccessibility moveFocus $ $create t tHTML animateElement */ /* global
$
$create
animateElement
focusAccessibility
moveFocus
t
*/
'use strict'; 'use strict';
/** /**
* @param {Object} params * @param {Object} params
* @param {String} params.title * @param {String} params.title
* @param {String|Node|Object|Array<String|Node|Object>} params.contents * @param {String|Node|Object|Array<String|Node|Object>} params.contents
* a string gets parsed via tHTML, * a string gets parsed via t.HTML,
* a non-string is passed as is to $create() * a non-string is passed as is to $create()
* @param {String} [params.className] * @param {String} [params.className]
* CSS class name of the message box element * CSS class name of the message box element
@ -87,7 +94,7 @@ function messageBox({
}, },
scroll() { scroll() {
scrollTo(blockScroll.x, blockScroll.y); scrollTo(blockScroll.x, blockScroll.y);
} },
}; };
} }
@ -116,7 +123,7 @@ function messageBox({
$create('SVG:path', {d: 'M11.69,10l4.55,4.55-1.69,1.69L10,11.69,' + $create('SVG:path', {d: 'M11.69,10l4.55,4.55-1.69,1.69L10,11.69,' +
'5.45,16.23,3.77,14.55,8.31,10,3.77,5.45,5.45,3.77,10,8.31l4.55-4.55,1.69,1.69Z', '5.45,16.23,3.77,14.55,8.31,10,3.77,5.45,5.45,3.77,10,8.31l4.55-4.55,1.69,1.69Z',
}))), }))),
$create(`#${id}-contents`, tHTML(contents)), $create(`#${id}-contents`, t.HTML(contents)),
$create(`#${id}-buttons`, $create(`#${id}-buttons`,
buttons.map((content, buttonIndex) => content && buttons.map((content, buttonIndex) => content &&
$create('button', Object.assign({ $create('button', Object.assign({
@ -160,7 +167,7 @@ messageBox.alert = (contents, className, title) =>
title, title,
contents, contents,
className: `center ${className || ''}`, className: `center ${className || ''}`,
buttons: [t('confirmClose')] buttons: [t('confirmClose')],
}); });
/** /**
@ -174,5 +181,5 @@ messageBox.confirm = (contents, className, title) =>
title, title,
contents, contents,
className: `center ${className || ''}`, className: `center ${className || ''}`,
buttons: [t('confirmYes'), t('confirmNo')] buttons: [t('confirmYes'), t('confirmNo')],
}).then(result => result.button === 0 || result.enter); }).then(result => result.button === 0 || result.enter);

View File

@ -52,7 +52,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="block"> <div class="block">
<h1 i18n-text="optionsCustomizeIcon"></h1> <h1 i18n-text="optionsCustomizeIcon"></h1>
<div class="items"> <div class="items">
@ -76,7 +76,7 @@
</label> </label>
</div> </div>
</div> </div>
<div class="block"> <div class="block">
<h1 i18n-text="optionsCustomizeBadge"></h1> <h1 i18n-text="optionsCustomizeBadge"></h1>
<div class="items"> <div class="items">
@ -97,7 +97,7 @@
</label> </label>
</div> </div>
</div> </div>
<div class="block"> <div class="block">
<h1 i18n-text="optionsCustomizePopup"></h1> <h1 i18n-text="optionsCustomizePopup"></h1>
<div class="items"> <div class="items">
@ -143,7 +143,7 @@
</label> </label>
</div> </div>
</div> </div>
<div class="block"> <div class="block">
<h1 i18n-text="openManage"></h1> <h1 i18n-text="openManage"></h1>
<div class="items"> <div class="items">
@ -156,11 +156,8 @@
</label> </label>
<label> <label>
<span i18n-text="manageFavicons"> <span i18n-text="manageFavicons">
<a data-cmd="note" <a i18n-title="manageFaviconsHelp"
i18n-title="manageFaviconsHelp" data-cmd="note" href="#" class="svg-inline-wrapper">
href="#"
class="svg-inline-wrapper"
tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg> <svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a> </a>
</span> </span>
@ -182,17 +179,14 @@
</label> </label>
</div> </div>
</div> </div>
<div class="block" id="updates"> <div class="block" id="updates">
<h1 i18n-text="optionsCustomizeUpdate"></h1> <h1 i18n-text="optionsCustomizeUpdate"></h1>
<div class="items"> <div class="items">
<label> <label>
<span i18n-text="optionsUpdateInterval"> <span i18n-text="optionsUpdateInterval">
<a data-cmd="note" <a i18n-title="optionsUpdateImportNote"
i18n-title="optionsUpdateImportNote" data-cmd="note" href="#" class="svg-inline-wrapper">
href="#"
class="svg-inline-wrapper"
tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg> <svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a> </a>
</span> </span>
@ -200,7 +194,7 @@
</label> </label>
</div> </div>
</div> </div>
<div class="block sync-options"> <div class="block sync-options">
<h1 i18n-text="optionsCustomizeSync"></h1> <h1 i18n-text="optionsCustomizeSync"></h1>
<div class="items"> <div class="items">
@ -224,7 +218,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="block" id="advanced"> <div class="block" id="advanced">
<div class="collapsible-resizer"> <div class="collapsible-resizer">
<h1 i18n-text="optionsAdvanced"> <h1 i18n-text="optionsAdvanced">
@ -243,11 +237,8 @@
<div class="items"> <div class="items">
<label class="chromium-only"> <label class="chromium-only">
<span i18n-text="optionsAdvancedStyleViaXhr"> <span i18n-text="optionsAdvancedStyleViaXhr">
<a data-cmd="note" <a i18n-title="optionsAdvancedStyleViaXhrNote"
i18n-title="optionsAdvancedStyleViaXhrNote" data-cmd="note" href="#" class="svg-inline-wrapper">
href="#"
class="svg-inline-wrapper"
tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg> <svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a> </a>
</span> </span>
@ -256,14 +247,23 @@
<span></span> <span></span>
</span> </span>
</label> </label>
<label>
<span>
<span i18n-html="optionsAdvancedPatchCsp"></span>
<a i18n-title="optionsAdvancedPatchCspNote"
data-cmd="note" href="#" class="svg-inline-wrapper">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</span>
<span class="onoffswitch">
<input type="checkbox" id="patchCsp" class="slider">
<span></span>
</span>
</label>
<label> <label>
<span i18n-text="optionsAdvancedExposeIframes"> <span i18n-text="optionsAdvancedExposeIframes">
<a data-cmd="note" <a i18n-title="optionsAdvancedExposeIframesNote"
i18n-data-title="optionsAdvancedExposeIframesNote" data-cmd="note" href="#" class="svg-inline-wrapper">
i18n-title="optionsAdvancedExposeIframesNote"
href="#"
class="svg-inline-wrapper"
tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg> <svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a> </a>
</span> </span>

View File

@ -238,6 +238,7 @@ function setupRadioButtons() {
function splitLongTooltips() { function splitLongTooltips() {
for (const el of $$('[title]')) { for (const el of $$('[title]')) {
el.dataset.title = el.title; el.dataset.title = el.title;
el.title = el.title.replace(/<\/?\w+>/g, ''); // strip html tags
if (el.title.length < 50) { if (el.title.length < 50) {
continue; continue;
} }

View File

@ -1,7 +1,23 @@
/* global configDialog hotkeys msg /* global
getActiveTab CHROME FIREFOX URLS API onDOMready $ $$ prefs $
setupLivePrefs template t $create animateElement $$
tryJSONparse CHROME_HAS_BORDER_BUG */ $create
animateElement
API
CHROME
CHROME_HAS_BORDER_BUG
configDialog
FIREFOX
getActiveTab
hotkeys
msg
onDOMready
prefs
setupLivePrefs
t
tryJSONparse
URLS
*/
'use strict'; 'use strict';
@ -121,7 +137,6 @@ function initPopup(frames) {
Object.assign($('#popup-manage-button'), { Object.assign($('#popup-manage-button'), {
onclick: handleEvent.openManager, onclick: handleEvent.openManager,
onmouseup: handleEvent.openManager,
oncontextmenu: handleEvent.openManager, oncontextmenu: handleEvent.openManager,
}); });
@ -165,7 +180,7 @@ function initPopup(frames) {
setTimeout(ping, 100, tab, --retryCountdown); setTimeout(ping, 100, tab, --retryCountdown);
return; return;
} }
const info = template.unreachableInfo; const info = t.template.unreachableInfo;
if (!FIREFOX) { if (!FIREFOX) {
// Chrome "Allow access to file URLs" in chrome://extensions message // Chrome "Allow access to file URLs" in chrome://extensions message
info.appendChild($create('p', t('unreachableFileHint'))); info.appendChild($create('p', t('unreachableFileHint')));
@ -204,7 +219,7 @@ function createWriterElement(frame) {
const targets = $create('span'); const targets = $create('span');
// For this URL // For this URL
const urlLink = template.writeStyle.cloneNode(true); const urlLink = t.template.writeStyle.cloneNode(true);
const isAboutBlank = url === ABOUT_BLANK; const isAboutBlank = url === ABOUT_BLANK;
Object.assign(urlLink, { Object.assign(urlLink, {
href: 'edit.html?url-prefix=' + encodeURIComponent(url), href: 'edit.html?url-prefix=' + encodeURIComponent(url),
@ -233,7 +248,7 @@ function createWriterElement(frame) {
if (domains.length > 1 && numParts === 1) { if (domains.length > 1 && numParts === 1) {
continue; continue;
} }
const domainLink = template.writeStyle.cloneNode(true); const domainLink = t.template.writeStyle.cloneNode(true);
Object.assign(domainLink, { Object.assign(domainLink, {
href: 'edit.html?domain=' + encodeURIComponent(domain), href: 'edit.html?domain=' + encodeURIComponent(domain),
textContent: numParts > 2 ? domain.split('.')[0] : domain, textContent: numParts > 2 ? domain.split('.')[0] : domain,
@ -322,7 +337,7 @@ function showStyles(frameResults) {
if (entries.size) { if (entries.size) {
resortEntries([...entries.values()]); resortEntries([...entries.values()]);
} else { } else {
installed.appendChild(template.noStyles); installed.appendChild(t.template.noStyles);
} }
window.dispatchEvent(new Event('showStyles:done')); window.dispatchEvent(new Event('showStyles:done'));
} }
@ -337,14 +352,14 @@ function resortEntries(entries) {
function createStyleElement(style) { function createStyleElement(style) {
let entry = $.entry(style); let entry = $.entry(style);
if (!entry) { if (!entry) {
entry = template.style.cloneNode(true); entry = t.template.style.cloneNode(true);
entry.setAttribute('style-id', style.id); entry.setAttribute('style-id', style.id);
Object.assign(entry, { Object.assign(entry, {
id: ENTRY_ID_PREFIX_RAW + style.id, id: ENTRY_ID_PREFIX_RAW + style.id,
styleId: style.id, styleId: style.id,
styleIsUsercss: Boolean(style.usercssData), styleIsUsercss: Boolean(style.usercssData),
onmousedown: handleEvent.maybeEdit, onmousedown: handleEvent.maybeEdit,
styleMeta: style styleMeta: style,
}); });
const checkbox = $('.checker', entry); const checkbox = $('.checker', entry);
Object.assign(checkbox, { Object.assign(checkbox, {
@ -384,7 +399,7 @@ function createStyleElement(style) {
$('.delete', entry).onclick = handleEvent.delete; $('.delete', entry).onclick = handleEvent.delete;
const indicator = template.regexpProblemIndicator.cloneNode(true); const indicator = t.template.regexpProblemIndicator.cloneNode(true);
indicator.appendChild(document.createTextNode('!')); indicator.appendChild(document.createTextNode('!'));
indicator.onclick = handleEvent.indicator; indicator.onclick = handleEvent.indicator;
$('.main-controls', entry).appendChild(indicator); $('.main-controls', entry).appendChild(indicator);
@ -587,7 +602,7 @@ Object.assign(handleEvent, {
indicator(event) { indicator(event) {
const entry = handleEvent.getClickedStyleElement(event); const entry = handleEvent.getClickedStyleElement(event);
const info = template.regexpProblemExplanation.cloneNode(true); const info = t.template.regexpProblemExplanation.cloneNode(true);
$.remove('#' + info.id); $.remove('#' + info.id);
$$('a', info).forEach(el => (el.onclick = handleEvent.openURLandHide)); $$('a', info).forEach(el => (el.onclick = handleEvent.openURLandHide));
$$('button', info).forEach(el => (el.onclick = handleEvent.closeExplanation)); $$('button', info).forEach(el => (el.onclick = handleEvent.closeExplanation));
@ -638,17 +653,10 @@ Object.assign(handleEvent, {
}, },
openManager(event) { openManager(event) {
if (event.button === 2 && !tabURL) return;
event.preventDefault(); event.preventDefault();
if (!this.eventHandled) { const isSearch = tabURL && (event.shiftKey || event.button === 2);
// FIXME: this only works if popup is closed API.openManage(isSearch ? {search: tabURL, searchMode: 'url'} : {});
this.eventHandled = true; window.close();
API.openManage({
search: tabURL && (event.shiftKey || event.button === 2) ?
`url:${tabURL}` : null
});
window.close();
}
}, },
copyContent(event) { copyContent(event) {
@ -684,7 +692,7 @@ function handleDelete(id) {
const el = $.entry(id); const el = $.entry(id);
if (el) { if (el) {
el.remove(); el.remove();
if (!$('.entry')) installed.appendChild(template.noStyles); if (!$('.entry')) installed.appendChild(t.template.noStyles);
} }
} }
@ -714,9 +722,9 @@ async function getStyleDataMerged(url, id) {
function blockPopup(isBlocked = true) { function blockPopup(isBlocked = true) {
document.body.classList.toggle('blocked', isBlocked); document.body.classList.toggle('blocked', isBlocked);
if (isBlocked) { if (isBlocked) {
document.body.prepend(template.unavailableInfo); document.body.prepend(t.template.unavailableInfo);
} else { } else {
template.unavailableInfo.remove(); t.template.unavailableInfo.remove();
template.noStyles.remove(); t.template.noStyles.remove();
} }
} }

View File

@ -1,5 +1,18 @@
/* global URLS tabURL handleEvent $ $$ prefs template FIREFOX debounce /* global
$create t API tWordBreak formatDate tryCatch download */ $
$$
$create
API
debounce
download
FIREFOX
handleEvent
prefs
t
tabURL
tryCatch
URLS
*/
'use strict'; 'use strict';
window.addEventListener('showStyles:done', () => { window.addEventListener('showStyles:done', () => {
@ -103,7 +116,7 @@ window.addEventListener('showStyles:done', () => {
const navOnClick = {prev, next}; const navOnClick = {prev, next};
for (const place of ['top', 'bottom']) { for (const place of ['top', 'bottom']) {
const nav = $(`.search-results-nav[data-type="${place}"]`); const nav = $(`.search-results-nav[data-type="${place}"]`);
nav.appendChild(template.searchNav.cloneNode(true)); nav.appendChild(t.template.searchNav.cloneNode(true));
dom.nav[place] = nav; dom.nav[place] = nav;
for (const child of $$('[data-type]', nav)) { for (const child of $$('[data-type]', nav)) {
const type = child.dataset.type; const type = child.dataset.type;
@ -181,7 +194,7 @@ window.addEventListener('showStyles:done', () => {
results = await search({retry}); results = await search({retry});
} }
if (results.length) { if (results.length) {
const installedStyles = await API.getAllStyles(true); const installedStyles = await API.getAllStyles();
const allUsoIds = new Set(installedStyles.map(calcUsoId)); const allUsoIds = new Set(installedStyles.map(calcUsoId));
results = results.filter(r => !allUsoIds.has(r.i)); results = results.filter(r => !allUsoIds.has(r.i));
} }
@ -257,7 +270,7 @@ window.addEventListener('showStyles:done', () => {
* @returns {Node} * @returns {Node}
*/ */
function createSearchResultNode(result) { function createSearchResultNode(result) {
const entry = template.searchResult.cloneNode(true); const entry = t.template.searchResult.cloneNode(true);
const { const {
i: id, i: id,
n: name, n: name,
@ -273,10 +286,10 @@ window.addEventListener('showStyles:done', () => {
// title // title
Object.assign($('.search-result-title', entry), { Object.assign($('.search-result-title', entry), {
onclick: handleEvent.openURLandHide, onclick: handleEvent.openURLandHide,
href: URLS.usoArchive + `?category=${category}&style=${id}` href: URLS.usoArchive + `?category=${category}&style=${id}`,
}); });
$('.search-result-title span', entry).textContent = $('.search-result-title span', entry).textContent =
tWordBreak(name.length < 300 ? name : name.slice(0, 300) + '...'); t.breakWord(name.length < 300 ? name : name.slice(0, 300) + '...');
// screenshot // screenshot
const auto = URLS.uso + `auto_style_screenshots/${id}${USO_AUTO_PIC_SUFFIX}`; const auto = URLS.uso + `auto_style_screenshots/${id}${USO_AUTO_PIC_SUFFIX}`;
Object.assign($('.search-result-screenshot', entry), { Object.assign($('.search-result-screenshot', entry), {
@ -303,7 +316,7 @@ window.addEventListener('showStyles:done', () => {
// time // time
Object.assign($('[data-type="updated"] time', entry), { Object.assign($('[data-type="updated"] time', entry), {
dateTime: updateTime * 1000, dateTime: updateTime * 1000,
textContent: formatDate(updateTime * 1000) textContent: t.formatDate(updateTime * 1000),
}); });
// totals // totals
$('[data-type="weekly"] dd', entry).textContent = formatNumber(weeklyInstalls); $('[data-type="weekly"] dd', entry).textContent = formatNumber(weeklyInstalls);

View File

@ -32,37 +32,37 @@ const files = {
'mode/css', 'mode/css',
'mode/javascript', 'mode/javascript',
'mode/stylus', 'mode/stylus',
'theme/*' 'theme/*',
], ],
'jsonlint': [ 'jsonlint': [
'lib/jsonlint.js → jsonlint.js', 'lib/jsonlint.js → jsonlint.js',
'README.md → LICENSE' 'README.md → LICENSE',
], ],
'less-bundle': [ 'less-bundle': [
'dist/less.min.js → less.min.js' 'dist/less.min.js → less.min.js',
], ],
'lz-string-unsafe': [ 'lz-string-unsafe': [
'lz-string-unsafe.min.js' 'lz-string-unsafe.min.js',
], ],
'semver-bundle': [ 'semver-bundle': [
'dist/semver.js → semver.js' 'dist/semver.js → semver.js',
], ],
'stylelint-bundle': [ 'stylelint-bundle': [
'stylelint-bundle.min.js', 'stylelint-bundle.min.js',
'https://github.com/stylelint/stylelint/raw/{VERSION}/LICENSE → LICENSE' 'https://github.com/stylelint/stylelint/raw/{VERSION}/LICENSE → LICENSE',
], ],
'stylus-lang-bundle': [ 'stylus-lang-bundle': [
'dist/stylus-renderer.min.js → stylus-renderer.min.js' 'dist/stylus-renderer.min.js → stylus-renderer.min.js',
], ],
'usercss-meta': [ 'usercss-meta': [
'dist/usercss-meta.min.js → usercss-meta.min.js' 'dist/usercss-meta.min.js → usercss-meta.min.js',
], ],
'db-to-cloud': [ 'db-to-cloud': [
'dist/db-to-cloud.min.js → db-to-cloud.min.js' 'dist/db-to-cloud.min.js → db-to-cloud.min.js',
], ],
'webext-launch-web-auth-flow': [ 'webext-launch-web-auth-flow': [
'dist/webext-launch-web-auth-flow.min.js → webext-launch-web-auth-flow.min.js' 'dist/webext-launch-web-auth-flow.min.js → webext-launch-web-auth-flow.min.js',
] ],
}; };
main().catch(console.error); main().catch(console.error);
@ -87,12 +87,15 @@ async function generateThemeList() {
.map(name => name.replace('.css', '')) .map(name => name.replace('.css', ''))
.sort(); .sort();
return endent` return endent`
/* exported CODEMIRROR_THEMES */ /* Do not edit. This file is auto-generated by build-vendor.js */
// this file is generated by update-codemirror-themes.js
'use strict'; 'use strict';
const CODEMIRROR_THEMES = ${JSON.stringify(themes, null, 2)}; /* exported CODEMIRROR_THEMES */
`.replace(/"/g, "'") + '\n'; const CODEMIRROR_THEMES = [
${
themes.map(t => ` '${t.replace(/'/g, '\\$&')}',\n`).join('')
}];
` + '\n';
} }
async function copyLicense(pkg) { async function copyLicense(pkg) {

View File

@ -16,7 +16,7 @@ function createZip({isFirefox} = {}) {
'package-lock.json', 'package-lock.json',
'yarn.lock', 'yarn.lock',
'*.zip', '*.zip',
'*.map' '*.map',
]; ];
const file = fs.createWriteStream(fileName); const file = fs.createWriteStream(fileName);

View File

@ -12,7 +12,7 @@
{hex: '#00ffff', start: .50}, {hex: '#00ffff', start: .50},
{hex: '#0000ff', start: .67}, {hex: '#0000ff', start: .67},
{hex: '#ff00ff', start: .83}, {hex: '#ff00ff', start: .83},
{hex: '#ff0000', start: 1} {hex: '#ff0000', start: 1},
]; ];
const MIN_HEIGHT = 220; const MIN_HEIGHT = 220;
const MARGIN = 8; const MARGIN = 8;
@ -119,7 +119,7 @@
$inputGroups.hex = $(['input-group', 'hex'], [ $inputGroups.hex = $(['input-group', 'hex'], [
$(['input-field', 'hex'], [ $(['input-field', 'hex'], [
$hexCode = $('input', {tag: 'input', type: 'text', spellcheck: false, $hexCode = $('input', {tag: 'input', type: 'text', spellcheck: false,
pattern: /^\s*#([a-fA-F\d]{3}([a-fA-F\d]([a-fA-F\d]{2}([a-fA-F\d]{2})?)?)?)\s*$/.source pattern: /^\s*#([a-fA-F\d]{3}([a-fA-F\d]([a-fA-F\d]{2}([a-fA-F\d]{2})?)?)?)\s*$/.source,
}), }),
$('title', [ $('title', [
$hexLettercase.true = $('title-action', {onclick: onHexLettercaseClicked}, 'HEX'), $hexLettercase.true = $('title-action', {onclick: onHexLettercaseClicked}, 'HEX'),
@ -186,7 +186,7 @@
Object.defineProperty($inputs.hsl, 'color', {get: inputsToHSL}); Object.defineProperty($inputs.hsl, 'color', {get: inputsToHSL});
Object.defineProperty($inputs, 'color', {get: () => $inputs[currentFormat].color}); Object.defineProperty($inputs, 'color', {get: () => $inputs[currentFormat].color});
Object.defineProperty($inputs, 'colorString', { Object.defineProperty($inputs, 'colorString', {
get: () => currentFormat && colorConverter.format($inputs[currentFormat].color) get: () => currentFormat && colorConverter.format($inputs[currentFormat].color),
}); });
HUE_COLORS.forEach(color => Object.assign(color, colorConverter.parse(color.hex))); HUE_COLORS.forEach(color => Object.assign(color, colorConverter.parse(color.hex)));

View File

@ -73,6 +73,11 @@
const height = cm.display.lastWrapHeight; const height = cm.display.lastWrapHeight;
if (!height || !textHeight) return; if (!height || !textHeight) return;
maxRenderChunkSize = Math.max(20, Math.ceil(height / textHeight)); maxRenderChunkSize = Math.max(20, Math.ceil(height / textHeight));
const state = cm.state.colorpicker;
if (state.colorizeOnUpdate) {
state.colorizeOnUpdate = false;
colorizeAll(state);
}
cm.off('update', CM_EVENTS.update); cm.off('update', CM_EVENTS.update);
}, },
mousedown(cm, event) { mousedown(cm, event) {
@ -164,12 +169,14 @@
function colorizeAll(state) { function colorizeAll(state) {
const {cm} = state; const {cm} = state;
const {viewFrom, viewTo} = cm.display;
if (!viewTo) {
state.colorizeOnUpdate = true;
return;
}
const {curOp} = cm; const {curOp} = cm;
if (!curOp) cm.startOperation(); if (!curOp) cm.startOperation();
const viewFrom = cm.display.viewFrom;
const viewTo = (cm.display.viewTo || maxRenderChunkSize - 1) + 1;
state.line = viewFrom; state.line = viewFrom;
state.inComment = null; state.inComment = null;
state.now = performance.now(); state.now = performance.now();

View File

@ -2057,7 +2057,7 @@ self.parserlib = (() => {
return m.toString(p); return m.toString(p);
}).join(required === false ? ' || ' : ' && '); }).join(required === false ? ' || ' : ' && ');
return prec > p ? `[ ${s} ]` : s; return prec > p ? `[ ${s} ]` : s;
} },
}; };
Matcher.parseGrammar = (() => { Matcher.parseGrammar = (() => {
@ -5300,7 +5300,7 @@ self.parserlib = (() => {
_readDeclarations({ _readDeclarations({
checkStart = true, checkStart = true,
readMargins = false, readMargins = false,
stopAfterBrace = false stopAfterBrace = false,
} = {}) { } = {}) {
const stream = this._tokenStream; const stream = this._tokenStream;
if (checkStart) stream.mustMatch(Tokens.LBRACE); if (checkStart) stream.mustMatch(Tokens.LBRACE);