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}]
camelcase: [2, {properties: never}]
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-style: [2, last]
complexity: [0]

View File

@ -260,6 +260,42 @@
"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"
},
"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": {
"message": "Date installed",
"description": "Option text for the user to sort the style by install date"
@ -976,6 +1012,12 @@
"optionsAdvancedNewStyleAsUsercss": {
"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": {
"message": "Instant inject mode"
},
@ -1243,14 +1285,30 @@
"message": "Weekly installs",
"description": "Text for label that shows the number of times a search result was installed during last week"
},
"searchStyles": {
"message": "Search contents",
"description": "Label for the search filter textbox on the Manage styles page"
"searchStylesAll": {
"message": "All",
"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": {
"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"
},
"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": {
"message": "Add another section",
"description": "Label for the button to add a section"

View File

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

View File

@ -8,7 +8,7 @@
// eslint-disable-next-line no-var
var backgroundWorker = workerUtil.createWorker({
url: '/background/background-worker.js'
url: '/background/background-worker.js',
});
// eslint-disable-next-line no-var
@ -99,7 +99,7 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
getSyncStatus: sync.getStatus,
syncLogin: sync.login,
openManage
openManage,
});
// *************************************************************************
@ -119,7 +119,7 @@ if (FIREFOX) {
navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, {
url: [
{urlEquals: 'about:blank'},
]
],
});
}
@ -135,24 +135,13 @@ if (chrome.commands) {
// *************************************************************************
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;
// 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) {
// Removing unused stuff
// TODO: delete this entire block by the middle of 2021
try {
localStorage.clear();
} catch (e) {}
setTimeout(async () => {
const del = Object.keys(await chromeLocal.get())
.filter(key => key.startsWith('usoSearchCache'));
@ -181,7 +170,7 @@ contextMenus = {
click: browserCommands.openOptions,
},
'reload': {
presentIf: () => localStorage.installType === 'development',
presentIf: async () => (await browser.management.getSelf()).installType === 'development',
title: 'reload',
click: browserCommands.reload,
},
@ -195,13 +184,13 @@ contextMenus = {
msg.sendTab(tab.id, {method: 'editDeleteText'}, undefined, 'extension')
.catch(msg.ignoreError);
},
}
},
};
function createContextMenus(ids) {
async function createContextMenus(ids) {
for (const id of ids) {
let item = contextMenus[id];
if (item.presentIf && !item.presentIf()) {
if (item.presentIf && !await item.presentIf()) {
continue;
}
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');
if (search) {
url += `?search=${encodeURIComponent(search)}`;
url += `?search=${encodeURIComponent(search)}&searchMode=${searchMode}`;
}
if (options) {
url += '#stylus-options';
}
return findExistingTab({
let tab = await findExistingTab({
url,
currentWindow: null,
ignoreHash: true,
ignoreSearch: true
})
.then(tab => {
if (tab) {
return Promise.all([
activateTab(tab),
(tab.pendingUrl || tab.url) !== url && msg.sendTab(tab.id, {method: 'pushState', url})
.catch(console.error)
]);
}
return getActiveTab().then(tab => {
if (isTabReplaceable(tab, url)) {
return activateTab(tab, {url});
}
return browser.tabs.create({url});
});
});
ignoreSearch: true,
});
if (tab) {
await activateTab(tab);
if (url !== (tab.pendingUrl || tab.url)) {
await msg.sendTab(tab.id, {method: 'pushState', url}).catch(console.error);
}
return tab;
}
tab = await getActiveTab();
return isTabReplaceable(tab, url)
? activateTab(tab, {url})
: browser.tabs.create({url});
}

View File

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

View File

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

View File

@ -24,14 +24,11 @@ const db = (() => {
async function tryUsingIndexedDB() {
// 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
// for reliablility and in localStorage for fast synchronous access
// (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
// note that accessing indexedDB may throw, https://github.com/openstyles/stylus/issues/615
if (typeof indexedDB === 'undefined') {
throw new Error('indexedDB is undefined');
}
switch (await getFallback()) {
switch (await chromeLocal.getValue(FALLBACK)) {
case true: throw null;
case false: break;
default: await testDB();
@ -39,12 +36,6 @@ const db = (() => {
return useIndexedDB();
}
async function getFallback() {
return localStorage[FALLBACK] === 'true' ? true :
localStorage[FALLBACK] === 'false' ? false :
chromeLocal.getValue(FALLBACK);
}
async function testDB() {
let e = await dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1);
// throws if result is null
@ -62,13 +53,11 @@ const db = (() => {
chromeLocal.setValue(FALLBACK + 'Reason', workerUtil.cloneError(err));
console.warn('Failed to access indexedDB. Switched to storage API.', err);
}
localStorage[FALLBACK] = 'true';
return createChromeStorageDB().exec;
}
function useIndexedDB() {
chromeLocal.setValue(FALLBACK, false);
localStorage[FALLBACK] = 'false';
return dbExecIndexedDB;
}

View File

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

View File

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

View File

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

View File

@ -31,11 +31,11 @@
return fetch(api, {
method: 'POST',
headers: new Headers({
'Content-Type': 'application/json'
'Content-Type': 'application/json',
}),
body: query({
id
})
id,
}),
})
.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';
(() => {
// toLocaleLowerCase cache, autocleared after 1 minute
const cache = new Map();
// top-level style properties to be searched
const PARTS = {
name: searchText,
url: searchText,
sourceCode: searchText,
sections: searchSections,
};
const METAKEYS = ['customName', 'name', 'url', 'installationUrl', 'updateUrl'];
const extractMeta = style =>
style.usercssData
? (style.sourceCode.match(usercss.RX_META) || [''])[0]
: 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 {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
* @returns {number[]} - array of matched styles ids
*/
API_METHODS.searchDB = ({query, ids}) => {
let rx, words, icase, matchUrl;
query = query.trim();
if (/^url:/i.test(query)) {
matchUrl = query.slice(query.indexOf(':') + 1).trim();
if (matchUrl) {
return styleManager.getStylesByUrl(matchUrl)
.then(results => results.map(r => r.data.id));
}
}
if (query.startsWith('/') && /^\/(.+?)\/([gimsuy]*)$/.test(query)) {
rx = tryRegExp(RegExp.$1, RegExp.$2);
}
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;
}
}
}
API_METHODS.searchDB = async ({query, mode = 'all', ids}) => {
let res = [];
if (mode === 'url' && query) {
res = (await styleManager.getStylesByUrl(query)).map(r => r.data.id);
} else if (mode in MODES) {
const modeHandler = MODES[mode];
const m = /^\/(.+?)\/([gimsuy]*)$/.exec(query);
const rx = m && tryRegExp(m[1], m[2]);
const test = rx ? rx.test.bind(rx) : makeTester(query);
res = (await styleManager.getAllStyles())
.filter(style =>
(!ids || ids.includes(style.id)) &&
(!query || modeHandler(style, test)))
.map(style => style.id);
if (cache.size) debounce(clearCache, 60e3);
return results;
});
}
return res;
};
function searchText(text, rx, words, icase) {
if (rx) return rx.test(text);
for (let pass = 1; pass <= (icase ? 2 : 1); pass++) {
if (words.every(w => text.includes(w))) return true;
text = lower(text);
}
function makeTester(query) {
const flags = `u${lower(query) === query ? 'i' : ''}`;
const words = query
.split(/(".*?")|\s+/)
.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 prop in section) {
const value = section[prop];
if (typeof value === 'string') {
if (searchText(value, rx, words, icase)) return true;
} else if (Array.isArray(value)) {
if (value.some(str => searchText(str, rx, words, icase))) return true;
if (inCode && prop === 'code' && test(value) ||
inFuncs && Array.isArray(value) && value.some(str => test(str))) {
return true;
}
}
}
@ -92,9 +99,7 @@
function lower(text) {
let result = cache.get(text);
if (result) return result;
result = text.toLocaleLowerCase();
cache.set(text, result);
if (!result) cache.set(text, result = text.toLocaleLowerCase());
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`
to cleanup the temporary code. See /edit/live-preview.js.
*/
/** @type {styleManager} */
const styleManager = (() => {
const preparing = prepare();
@ -38,7 +40,7 @@ const styleManager = (() => {
style.appliesTo.delete(url);
}
}
}
},
});
const BAD_MATCHER = {test: () => false};
@ -58,16 +60,16 @@ const styleManager = (() => {
protocol: '',
search: '',
searchParams: new URLSearchParams(),
username: ''
username: '',
};
const DELETE_IF_NULL = ['id', 'customName'];
handleLivePreviewConnections();
return Object.assign({
compareRevision
}, ensurePrepared({
return Object.assign(/** @namespace styleManager */{
compareRevision,
}, ensurePrepared(/** @namespace styleManager */{
get,
getByUUID,
getSectionsByUrl,
@ -86,7 +88,7 @@ const styleManager = (() => {
addExclusion,
removeExclusion,
addInclusion,
removeInclusion
removeInclusion,
}));
function handleLivePreviewConnections() {
@ -135,9 +137,8 @@ const styleManager = (() => {
}
}
function getAllStyles(noCode = false) {
const datas = [...styles.values()].map(s => s.data);
return noCode ? datas.map(getStyleWithNoCode) : datas;
function getAllStyles() {
return [...styles.values()].map(s => s.data);
}
function compareRevision(rev1, rev2) {
@ -316,7 +317,7 @@ const styleManager = (() => {
uuidIndex.delete(style.data._id);
return msg.broadcast({
method: 'styleDeleted',
style: {id}
style: {id},
});
})
.then(() => id);
@ -347,7 +348,7 @@ const styleManager = (() => {
md5Url: null,
url: null,
originalMd5: null,
installDate: Date.now()
installDate: Date.now(),
};
}
@ -368,7 +369,7 @@ const styleManager = (() => {
updated.add(url);
cache.sections[data.id] = {
id: data.id,
code
code,
};
}
}
@ -378,10 +379,10 @@ const styleManager = (() => {
style: {
id: data.id,
md5Url: data.md5Url,
enabled: data.enabled
enabled: data.enabled,
},
reason,
codeIsUpdated
codeIsUpdated,
});
}
@ -424,7 +425,7 @@ const styleManager = (() => {
if (!style) {
styles.set(data.id, {
appliesTo: new Set(),
data
data,
});
method = 'styleAdded';
} else {
@ -469,11 +470,7 @@ const styleManager = (() => {
}
}
if (sectionMatched) {
result.push({
data: getStyleWithNoCode(data),
excluded,
sloppy
});
result.push({data, excluded, sloppy});
}
}
return result;
@ -484,7 +481,7 @@ const styleManager = (() => {
if (!cache) {
cache = {
sections: {},
maybeMatch: new Set()
maybeMatch: new Set(),
};
buildCache(styles.values());
cachedStyleForUrl.set(url, cache);
@ -510,7 +507,7 @@ const styleManager = (() => {
if (code) {
cache.sections[data.id] = {
id: data.id,
code
code,
};
appliesTo.add(url);
}
@ -535,7 +532,7 @@ const styleManager = (() => {
const ADD_MISSING_PROPS = {
name: style => `ID: ${style.id}`,
_id: () => uuidv4(),
_rev: () => Date.now()
_rev: () => Date.now(),
};
return db.exec('getAll')
@ -559,7 +556,7 @@ const styleManager = (() => {
fixUsoMd5Issue(style);
styles.set(style.id, {
appliesTo: new Set(),
data: style
data: style,
});
uuidIndex.set(style._id, style.id);
}
@ -705,7 +702,7 @@ const styleManager = (() => {
domain = u.hostname;
}
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,
currentDriveName: null,
errorMessage: null,
login: false
login: false,
};
let currentDrive;
const ctrl = dbToCloud.dbToCloud({
@ -43,7 +43,7 @@ const sync = (() => {
setState(drive, state) {
const key = `sync/state/${drive.name}`;
return chromeLocal.setValue(key, state);
}
},
});
const initializing = prefs.initializing.then(() => {
@ -58,7 +58,7 @@ const sync = (() => {
});
return Object.assign({
getStatus: () => status
getStatus: () => status,
}, ensurePrepared({
start,
stop,
@ -73,7 +73,7 @@ const sync = (() => {
return ctrl.delete(...args);
},
syncNow,
login
login,
}));
function ensurePrepared(obj) {
@ -99,7 +99,7 @@ const sync = (() => {
function schedule(delay = SYNC_DELAY) {
chrome.alarms.create('syncNow', {
delayInMinutes: delay,
periodInMinutes: SYNC_INTERVAL
periodInMinutes: SYNC_INTERVAL,
});
}
@ -206,7 +206,7 @@ const sync = (() => {
function getDrive(name) {
if (name === 'dropbox' || name === 'google' || name === 'onedrive') {
return dbToCloud.drive[name]({
getAccessToken: () => tokenManager.getToken(name)
getAccessToken: () => tokenManager.getToken(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', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
}
})
'Authorization': `Bearer ${token}`,
},
}),
},
google: {
flow: 'code',
@ -27,14 +27,14 @@ const tokenManager = (() => {
// tokens for multiple machines.
// https://stackoverflow.com/q/18519185
access_type: 'offline',
prompt: 'consent'
prompt: 'consent',
},
tokenURL: 'https://oauth2.googleapis.com/token',
scopes: ['https://www.googleapis.com/auth/drive.appdata'],
revoke: token => {
const params = {token};
return postQuery(`https://accounts.google.com/o/oauth2/revoke?${new URLSearchParams(params)}`);
}
},
},
onedrive: {
flow: 'code',
@ -45,8 +45,8 @@ const tokenManager = (() => {
redirect_uri: FIREFOX ?
'https://clngdbkpkpeebahjckkjfobafhncgmne.chromiumapp.org/' :
'https://' + location.hostname + '.chromiumapp.org/',
scopes: ['Files.ReadWrite.AppFolder', 'offline_access']
}
scopes: ['Files.ReadWrite.AppFolder', 'offline_access'],
},
};
const NETWORK_LATENCY = 30; // seconds
@ -114,7 +114,7 @@ const tokenManager = (() => {
client_id: provider.clientId,
refresh_token: obj[k.REFRESH],
grant_type: 'refresh_token',
scope: provider.scopes.join(' ')
scope: provider.scopes.join(' '),
};
if (provider.clientSecret) {
body.client_secret = provider.clientSecret;
@ -136,7 +136,7 @@ const tokenManager = (() => {
response_type: provider.flow,
client_id: provider.clientId,
redirect_uri: provider.redirect_uri || chrome.identity.getRedirectURL(),
state
state,
};
if (provider.scopes) {
query.scope = provider.scopes.join(' ');
@ -148,7 +148,7 @@ const tokenManager = (() => {
return webextLaunchWebAuthFlow({
url,
interactive,
redirect_uri: query.redirect_uri
redirect_uri: query.redirect_uri,
})
.then(url => {
const params = new URLSearchParams(
@ -171,7 +171,7 @@ const tokenManager = (() => {
code,
grant_type: 'authorization_code',
client_id: provider.clientId,
redirect_uri: query.redirect_uri
redirect_uri: query.redirect_uri,
};
if (provider.clientSecret) {
body.client_secret = provider.clientSecret;
@ -185,7 +185,7 @@ const tokenManager = (() => {
return chromeLocal.set({
[k.TOKEN]: result.access_token,
[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);
}
@ -194,7 +194,7 @@ const tokenManager = (() => {
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
'Content-Type': 'application/x-www-form-urlencoded',
},
body: body ? new URLSearchParams(body) : null,
};

View File

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

View File

@ -61,7 +61,7 @@ const usercssHelper = (() => {
find(styleId ? {id: styleId} : style) : Promise.resolve();
return Promise.all([
metaOnly ? style : doBuild(style, findDup),
findDup
findDup,
]);
})
.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';
(() => {
@ -27,16 +33,39 @@
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
chrome.webRequest.onBeforeSendHeaders.addListener(({tabId, url}) => {
openInstallerPage(tabId, url, {});
// Silently suppressing navigation like it never happened
return {redirectUrl: 'javascript:void 0'}; // eslint-disable-line no-script-url
const u = new URL(url);
const m = maybeDistro[u.hostname];
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.usoArchiveRaw + 'usercss/*.user.css',
'*://greasyfork.org/scripts/*/code/*.user.css',
'*://sleazyfork.org/scripts/*/code/*.user.css',
...[].concat(
...Object.entries(maybeDistro)
.map(([host, {glob}]) => makeUsercssGlobs(host, glob))),
],
types: ['main_frame'],
}, ['blocking']);
@ -46,7 +75,7 @@
const h = responseHeaders.find(h => h.name.toLowerCase() === 'content-type');
tabManager.set(tabId, isContentTypeText.name, h && isContentTypeText(h.value) || undefined);
}, {
urls: '%css,%css?*,%styl,%styl?*'.replace(/%/g, '*://*/*.user.').split(','),
urls: makeUsercssGlobs('*', '/*'),
types: ['main_frame'],
}, ['responseHeaders']);
@ -57,7 +86,7 @@
!oldUrl.startsWith(URLS.installUsercss)) {
const inTab = url.startsWith('file:') && Boolean(fileLoader);
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});
}
}
@ -80,4 +109,8 @@
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) {
await API.styleViaAPI({method: 'styleApply'});
} else {
const blobId = chrome.app && getXhrBlobId();
const styles = blobId && getStylesViaXhr(blobId) ||
const styles = chrome.app && !chrome.tabs && getStylesViaXhr() ||
await API.getSectionsByUrl(getMatchUrl(), null, true);
if (styles.disableAll) {
delete styles.disableAll;
@ -70,27 +69,16 @@ self.INJECTED !== 1 && (() => {
}
}
function getXhrBlobId() {
function getStylesViaXhr() {
try {
const {cookie} = document; // may throw in sandboxed frames
return new RegExp(`(^|\\s|;)${chrome.runtime.id}=\\s*([-\\w]+)\\s*(;|$)`).exec(cookie)[2];
} catch (e) {}
}
function getStylesViaXhr(data) {
try {
const disableAll = data[0] === '1';
const url = 'blob:' + chrome.runtime.getURL(data.slice(1));
const blobId = document.cookie.split(chrome.runtime.id + '=')[1].split(';')[0];
const url = 'blob:' + chrome.runtime.getURL(blobId);
document.cookie = `${chrome.runtime.id}=1; max-age=0`; // remove our cookie
let res;
if (!disableAll) { // when disabled, will get the styles asynchronously, no rush
const xhr = new XMLHttpRequest();
xhr.open('GET', url, false); // synchronous
xhr.send();
res = JSON.parse(xhr.response);
}
const xhr = new XMLHttpRequest();
xhr.open('GET', url, false); // synchronous
xhr.send();
URL.revokeObjectURL(url);
return res;
return JSON.parse(xhr.response);
} catch (e) {}
}

View File

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

View File

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

View File

@ -162,7 +162,7 @@ function beautify(scope, ui = true) {
$create('SVG:path', {
'fill-rule': 'evenodd',
'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', {
type: 'checkbox',
dataset: {option: optionName},
checked: options[optionName] !== false
checked: options[optionName] !== false,
}),
$create('SVG:svg.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';
@ -82,7 +88,7 @@
[
{from: 'Ctrl-', to: ['Alt-', 'Ctrl-Alt-']},
// 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 => {
const oldKey = remap.from + char;
Object.keys(CodeMirror.keyMap).forEach(keyMapName => {
@ -134,7 +140,7 @@
let filled;
this.eachLine(({text}) => (filled = text && /\S/.test(text)));
return !filled;
}
},
});
// editor commands
@ -183,7 +189,7 @@
// setTimeout(() => {
// $('.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*$/);
if (m) {
cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);

View File

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

View File

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

View File

@ -22,11 +22,10 @@
prefs
rerouteHotkeys
SectionsEditor
sessionStorageHash
sessionStore
setupLivePrefs
SourceEditor
t
tHTML
tryCatch
tryJSONparse
*/
@ -56,13 +55,23 @@ lazyInit();
.then(initTheme),
onDOMready(),
]);
const scrollInfo = style.id && tryJSONparse(sessionStore['editorScrollInfo' + style.id]);
/** @namespace EditorBase */
Object.assign(editor, {
style,
dirty,
scrollInfo,
updateName,
updateToc,
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.keyMap', showHotkeyInTooltip);
@ -78,17 +87,21 @@ lazyInit();
$('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle');
$('#preview-label').classList.toggle('hidden', !style.id);
const toc = [];
const elToc = $('#toc');
elToc.onclick = e => editor.jumpToEditor([...elToc.children].indexOf(e.target));
(editor.isUsercss ? SourceEditor : SectionsEditor)();
if (editor.isUsercss) {
SourceEditor();
} else {
SectionsEditor();
}
prefs.subscribe('editor.toc.expanded', (k, val) => val && editor.updateToc(), {now: true});
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
$('#name').required = !editor.isUsercss;
$('#save-button').onclick = editor.save;
@ -100,7 +113,7 @@ lazyInit();
// switching the mode here to show the correct page ASAP, usually before DOMContentLoaded
editor.isUsercss = Boolean(style.usercssData || !style.id && prefs.get('newStyleAsUsercss'));
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
if (!style.id) history.replaceState({}, '', location.pathname);
updateTitle(false);
@ -290,16 +303,9 @@ lazyInit();
function updateToc(added = editor.sections) {
const {sections} = editor;
const first = sections.indexOf(added[0]);
let el = elToc.children[first];
if (added.focus) {
const cls = 'current';
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 elFirst = elToc.children[first];
if (first >= 0 && (!added.focus || !elFirst)) {
for (let el = elFirst, i = first; i < sections.length; i++) {
const entry = sections[i].tocEntry;
if (!deepEqual(entry, toc[i])) {
if (!el) el = elToc.appendChild($create('li', {tabIndex: 0}));
@ -318,6 +324,13 @@ lazyInit();
elToc.lastElementChild.remove();
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) {
ownTabId = tab.id;
// 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();
$('#cancel-button').onclick = event => {
event.stopPropagation();
@ -346,8 +359,8 @@ function lazyInit() {
}
/** resize on 'undo close' */
function restoreWindowSize() {
const pos = tryJSONparse(sessionStorage.windowPos);
delete sessionStorage.windowPos;
const pos = tryJSONparse(sessionStore.windowPos);
delete sessionStore.windowPos;
if (pos && pos.left != null && chrome.windows) {
chrome.windows.update(chrome.windows.WINDOW_ID_CURRENT, pos);
}
@ -408,7 +421,16 @@ function onRuntimeMessage(request) {
}
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;
if (activeElement) {
// blurring triggers 'change' or 'input' event if needed
@ -429,7 +451,7 @@ function showHelp(title = '', body) {
const contents = $('.contents', div);
contents.textContent = '';
if (body) {
contents.appendChild(typeof body === 'string' ? tHTML(body) : body);
contents.appendChild(typeof body === 'string' ? t.HTML(body) : body);
}
$('.title', div).textContent = title;
@ -492,7 +514,7 @@ function showCodeMirrorPopup(title, html, options) {
matchBrackets: true,
styleActiveLine: true,
theme: prefs.get('editor.theme'),
keyMap: prefs.get('editor.keyMap')
keyMap: prefs.get('editor.keyMap'),
}, options));
cm.focus();
rerouteHotkeys(false);

View File

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

View File

@ -1,5 +1,18 @@
/* global CodeMirror focusAccessibility colorMimicry editor chromeLocal
onDOMready $ $$ $create t debounce tryRegExp stringAsRegExp template */
/* global
$
$$
$create
chromeLocal
CodeMirror
colorMimicry
debounce
editor
focusAccessibility
onDOMready
stringAsRegExp
t
tryRegExp
*/
'use strict';
onDOMready().then(() => {
@ -100,7 +113,7 @@ onDOMready().then(() => {
state.lastFind = '';
toggleDataset(this, 'enabled', !state.icase);
doSearch({canAdvance: false});
}
},
},
};
@ -136,7 +149,7 @@ onDOMready().then(() => {
trimUndoHistory();
enableUndoButton(state.undoHistory.length);
if (state.find) doSearch({canAdvance: false});
}
},
};
const DIALOG_PROPS = {
@ -152,7 +165,7 @@ onDOMready().then(() => {
state.replace = this.value;
adjustTextareaSize(this);
debounce(writeStorage, STORAGE_UPDATE_DELAY);
}
},
},
};
@ -169,7 +182,7 @@ onDOMready().then(() => {
replace(cm) {
state.reverse = false;
focusDialog('replace', cm);
}
},
};
COMMANDS.replaceAll = COMMANDS.replace;
@ -563,14 +576,14 @@ onDOMready().then(() => {
state.originalFocus = document.activeElement;
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);
dialog.addEventListener('focusout', EVENTS.onfocusout);
dialog.dataset.type = type;
dialog.style.pointerEvents = 'auto';
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(1, 'input2', state.replace);
@ -633,7 +646,7 @@ onDOMready().then(() => {
input.value = value;
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;
}

View File

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

View File

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

View File

@ -7,7 +7,7 @@
storageName: chromeSync.LZ_KEY.csslint,
lint: csslint,
validMode: mode => mode === 'css',
getConfig: config => Object.assign({}, LINTER_DEFAULTS.CSSLINT, config)
getConfig: config => Object.assign({}, LINTER_DEFAULTS.CSSLINT, config),
},
stylelint: {
storageName: chromeSync.LZ_KEY.stylelint,
@ -15,9 +15,9 @@
validMode: () => true,
getConfig: config => ({
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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,6 @@
prefs
regExpTester
t
template
tryCatch
*/
'use strict';
@ -55,7 +54,7 @@ function MozSectionWidget(
$create('ul' + C_LIST),
]),
listItem:
template.appliesTo.cloneNode(true),
t.template.appliesTo.cloneNode(true),
appliesToEverything:
$create('li.applies-to-everything', t('appliesToEverything')),
};
@ -74,7 +73,7 @@ function MozSectionWidget(
if (funcs.length < 2) {
messageBox({
contents: t('appliesRemoveError'),
buttons: [t('confirmClose')]
buttons: [t('confirmClose')],
});
return;
}
@ -125,7 +124,7 @@ function MozSectionWidget(
return;
}
}
}
},
};
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 */
'use strict';
@ -86,7 +94,7 @@ const regExpTester = (() => {
full: {data: [], label: t('styleRegexpTestFull')},
partial: {data: [], label: [
t('styleRegexpTestPartial'),
template.regexpTestPartial.cloneNode(true),
t.template.regexpTestPartial.cloneNode(true),
]},
none: {data: [], label: t('styleRegexpTestNone')},
invalid: {data: [], label: t('styleRegexpTestInvalid')},

View File

@ -9,7 +9,6 @@
prefs
regExpTester
t
template
trimCommentLabel
tryRegExp
*/
@ -17,20 +16,28 @@
/* 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 sectionId = genId();
const el = template.section.cloneNode(true);
const el = t.template.section.cloneNode(true);
const elLabel = $('.code-label', el);
const cm = cmFactory.create(wrapper => {
// 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);
}, {
value: originalSection.code,
});
el.CodeMirror = cm; // used by getAssociatedEditor
editor.applyScrollInfo(cm, si);
const changeListeners = new Set();
@ -259,8 +266,8 @@ function createSection(originalSection, genId) {
function createApply({type = 'url', value, all = false}) {
const applyId = genId();
const dirtyPrefix = `section.${sectionId}.apply.${applyId}`;
const el = all ? template.appliesToEverything.cloneNode(true) :
template.appliesTo.cloneNode(true);
const el = all ? t.template.appliesToEverything.cloneNode(true) :
t.template.appliesTo.cloneNode(true);
const selectEl = !all && $('.applies-type', el);
if (selectEl) {
@ -353,7 +360,7 @@ function createSection(originalSection, genId) {
function createResizeGrip(cm) {
const wrapper = cm.display.wrapper;
wrapper.classList.add('resize-grip-enabled');
const resizeGrip = template.resizeGrip.cloneNode(true);
const resizeGrip = t.template.resizeGrip.cloneNode(true);
wrapper.appendChild(resizeGrip);
let lastClickTime = 0;
let initHeight;

View File

@ -15,6 +15,7 @@
messageBox
prefs
sectionsToMozFormat
sessionStore
showCodeMirrorPopup
showHelp
t
@ -117,7 +118,7 @@ function SectionsEditor() {
}
newStyle = await API.editSave(newStyle);
destroyRemovedSections();
sessionStorage.justEditedStyleId = newStyle.id;
sessionStore.justEditedStyleId = newStyle.id;
editor.replaceStyle(newStyle, false);
},
@ -141,7 +142,7 @@ function SectionsEditor() {
/** @param {EditorSection} section */
function fitToContent(section) {
const {el, cm, cm: {display: {wrapper, sizer}}} = section;
const {cm, cm: {display: {wrapper, sizer}}} = section;
if (cm.display.renderedView) {
resize();
} else {
@ -154,12 +155,13 @@ function SectionsEditor() {
return;
}
if (headerOffset == null) {
headerOffset = el.getBoundingClientRect().top;
headerOffset = container.getBoundingClientRect().top;
}
contentHeight += 9; // border & resize grip
cm.off('update', resize);
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);
if (Math.abs(fit - cmHeight) > 1) {
cm.setSize(null, fit);
@ -434,7 +436,7 @@ function SectionsEditor() {
/** @returns {Style} */
function getModel() {
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());
}
function initSections(originalSections, {
function initSections(src, {
focusOn = 0,
replace = false,
pristine = false,
@ -495,27 +497,35 @@ function SectionsEditor() {
container.textContent = '';
}
let done;
const total = originalSections.length;
originalSections = originalSections.slice();
let index = 0;
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 => {
done = resolve;
chunk(true);
chunk(!si);
});
function chunk(forceRefresh) {
const t0 = performance.now();
while (originalSections.length && performance.now() - t0 < 100) {
insertSectionAfter(originalSections.shift(), undefined, forceRefresh);
while (index < total && performance.now() - t0 < 100) {
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 (focusOn !== false && sections[focusOn]) {
sections[focusOn].cm.focus();
focusOn = false;
}
if (index === focusOn && !si) sections[index].cm.focus();
index++;
}
setGlobalProgress(total - originalSections.length, total);
if (!originalSections.length) {
setGlobalProgress(index, total);
if (index === total) {
setGlobalProgress();
requestAnimationFrame(fitToAvailableSpace);
sections.forEach(({cm}) => setTimeout(linter.enableForEditor, 0, cm));
if (!si) requestAnimationFrame(fitToAvailableSpace);
container.style.removeProperty('height');
done();
} else {
setTimeout(chunk);
@ -564,24 +574,26 @@ function SectionsEditor() {
* @param {StyleSection} [init]
* @param {EditorSection} [base]
* @param {boolean} [forceRefresh]
* @param {EditorScrollInfo} [si]
*/
function insertSectionAfter(init, base, forceRefresh) {
function insertSectionAfter(init, base, forceRefresh, si) {
if (!init) {
init = {code: '', urlPrefixes: ['http://example.com']};
}
const section = createSection(init, genId);
const section = createSection(init, genId, si);
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);
refreshOnView(cm, forceRefresh);
refreshOnView(cm, base || forceRefresh);
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
fitToContent(section);
}
if (base) {
cm.focus();
setTimeout(editor.scrollToEditor, 0, cm);
editor.scrollToEditor(cm);
linter.enableForEditor(cm);
}
updateSectionOrder();
@ -646,11 +658,18 @@ function SectionsEditor() {
xo.observe(cm.display.wrapper);
}
/** @param {IntersectionObserverEntry[]} entries */
function refreshOnViewListener(entries) {
for (const {isIntersecting, target} of entries) {
if (isIntersecting) {
target.CodeMirror.refresh();
xo.unobserve(target);
for (const e of entries) {
const r = e.isIntersecting && e.intersectionRect;
if (r) {
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
prefs stringAsRegExp */
/* global
$
$$
$create
CodeMirror
onDOMready
prefs
showHelp
stringAsRegExp
t
*/
'use strict';
onDOMready().then(() => {
@ -11,7 +20,7 @@ function showKeyMapHelp() {
const keyMapSorted = Object.keys(keyMap)
.map(key => ({key, cmd: keyMap[key]}))
.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 row = tBody.rows[0];
const cellA = row.children[0];

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@
if (theme !== 'default') {
document.head.appendChild($create('link', {
rel: 'stylesheet',
href: `vendor/codemirror/theme/${theme}.css`
href: `vendor/codemirror/theme/${theme}.css`,
}));
}
window.addEventListener('resize', adjustCodeHeight);
@ -111,7 +111,7 @@
frag.appendChild($createLink(url,
$create('SVG:svg.svg-icon', {viewBox: '0 0 20 20'},
$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',
$createLink(...args)
)
))
)),
]));
}
}

View File

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

View File

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

View File

@ -1,149 +1,16 @@
/* global tryCatch */
/* exported tHTML formatDate */
'use strict';
const template = {};
tDocLoader();
function t(key, params) {
const cache = !params && t.cache[key];
const s = cache || chrome.i18n.getMessage(key, params);
if (s === '') {
throw `Missing string "${key}"`;
}
if (!params && !cache) {
t.cache[key] = s;
}
const s = chrome.i18n.getMessage(key, params);
if (!s) throw `Missing string "${key}"`;
return s;
}
function tHTML(html, tag) {
// body is a text node without HTML tags
if (typeof html === 'string' && !tag && /<\w+/.test(html) === false) {
return document.createTextNode(html);
}
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([
Object.assign(t, {
template: {},
DOMParser: new DOMParser(),
ALLOWED_TAGS: 'a,b,code,i,sub,sup,wbr'.split(','),
RX_WORD_BREAK: new RegExp([
'(',
/[\d\w\u007B-\uFFFF]{10}/,
'|',
@ -152,73 +19,171 @@ function tDocLoader() {
/((?!\s)\W){10}/,
')',
/(?!\b|\s|$)/,
].map(rx => rx.source || rx).join(''), 'g');
].map(rx => rx.source || rx).join(''), 'g'),
// reset L10N cache on UI language change
const UIlang = chrome.i18n.getUILanguage();
if (t.cache.browserUIlanguage !== UIlang) {
t.cache = {browserUIlanguage: UIlang};
localStorage.L10N = JSON.stringify(t.cache);
}
const cacheLength = Object.keys(t.cache).length;
HTML(html) {
return typeof html !== 'string'
? html
: /<\w+/.test(html) // check for html tags
? t.createHtml(html.replace(/>\n\s*</g, '><').trim())
: document.createTextNode(html);
},
Object.assign(tDocLoader, {
observer: new MutationObserver(process),
start() {
if (!tDocLoader.observing) {
tDocLoader.observing = true;
tDocLoader.observer.observe(document, {subtree: true, childList: true});
NodeList(nodes) {
const PREFIX = 'i18n-';
for (let n = nodes.length; --n >= 0;) {
const node = nodes[n];
if (node.nodeType !== Node.ELEMENT_NODE) {
continue;
}
},
stop() {
tDocLoader.pause();
document.removeEventListener('DOMContentLoaded', onLoad);
},
pause() {
if (tDocLoader.observing) {
tDocLoader.observing = false;
tDocLoader.observer.disconnect();
if (node.localName === 'template') {
t.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 = 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('*'));
tDocLoader.start();
document.addEventListener('DOMContentLoaded', onLoad);
t.NodeList(document.getElementsByTagName('*'));
start();
function process(mutations) {
for (const mutation of mutations) {
tNodeList(mutation.addedNodes);
}
tDocLoader.start();
mutations.forEach(m => t.NodeList(m.addedNodes));
start();
}
function onLoad() {
document.removeEventListener('DOMContentLoaded', onLoad);
process(tDocLoader.observer.takeRecords());
tDocLoader.stop();
if (cacheLength !== Object.keys(t.cache).length) {
localStorage.L10N = JSON.stringify(t.cache);
function start() {
if (!observing) {
observing = true;
observer.observe(document, {subtree: true, childList: true});
}
}
}
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
getStyleWithNoCode tryRegExp sessionStorageHash download deepEqual
closeCurrentTab capitalize CHROME_HAS_BORDER_BUG */
/* exported
capitalize
CHROME_HAS_BORDER_BUG
closeCurrentTab
deepEqual
download
getActiveTab
getStyleWithNoCode
getTab
ignoreChromeError
onTabReady
openURL
sessionStore
stringAsRegExp
tryCatch
tryRegExp
*/
'use strict';
const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(\d+)|$/)[1]);
@ -112,7 +126,7 @@ function urlToMatchPattern(url, ignoreSearch) {
if (ignoreSearch) {
return [
`${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?
@ -206,7 +220,7 @@ function activateTab(tab, {url, index, openerTabId} = {}) {
return Promise.all([
browser.tabs.update(tab.id, options),
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);
}
@ -316,24 +330,28 @@ function deepEqual(a, b, ignoredKeys) {
return true;
}
function sessionStorageHash(name) {
return {
name,
value: tryCatch(JSON.parse, sessionStorage[name]) || {},
set(k, v) {
this.value[k] = v;
this.updateStorage();
},
unset(k) {
delete this.value[k];
this.updateStorage();
},
updateStorage() {
sessionStorage[this.name] = JSON.stringify(this.value);
/* A simple polyfill in case DOM storage is disabled in the browser */
const sessionStore = new Proxy({}, {
get(target, name) {
try {
return sessionStorage[name];
} catch (e) {
Object.defineProperty(window, 'sessionStorage', {value: target});
}
};
}
},
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

View File

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

View File

@ -66,15 +66,6 @@ self.INJECTED !== 1 && (() => {
//#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')) {
// TODO: remove when minimum_chrome_version >= 61
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
'newStyleAsUsercss': false, // create new style in usercss format
'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
'config.autosave': true,

View File

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

View File

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

View File

@ -11,8 +11,6 @@
<link rel="stylesheet" href="options/onoffswitch.css">
<link rel="stylesheet" href="vendor-overwrites/colorpicker/colorpicker.css">
<style id="style-overrides"></style>
<style id="firefox-transitions-bug-suppressor">
/* restrict to FF */
@supports (-moz-appearance:none) {
@ -32,7 +30,10 @@
<template data-id="style">
<div class="entry">
<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>
</h2>
<p class="applies-to">
@ -54,14 +55,17 @@
</div>
</template>
<template data-id="styleCompact">
<template data-id="styleNewUI">
<div class="entry">
<h2 class="style-name">
<div class="checkmate">
<input class="checker" type="checkbox" i18n-title="toggleStyle">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</div>
<a class="style-name-link"></a>
<div class="checkmate">
<input class="checker" type="checkbox" i18n-title="toggleStyle">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</div>
<a class="style-name-link">
&nbsp;
<span class="style-info" data-type="version"></span>
</a>
</h2>
<p class="actions">
<a target="_blank" class="homepage" tabindex="0"></a>
@ -72,6 +76,7 @@
</svg>
</a>
</p>
<p class="style-info" data-type="age"></p>
<div class="applies-to">
<div class="targets"></div>
<a href="#" class="expander" tabindex="0">...</a>
@ -260,9 +265,19 @@
</label>
<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-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">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>

View File

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

View File

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

View File

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

View File

@ -1,20 +1,39 @@
/*
global messageBox getStyleWithNoCode
filterAndAppend showFiltersStats
checkUpdate handleUpdateInstalled
objectDiff
/* global
$
$$
$create
animateElement
API
checkUpdate
CHROME
configDialog
sorter msg prefs API $ $$ $create template setupLivePrefs
t tWordBreak formatDate
getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce
scrollElementIntoView CHROME VIVALDI router
debounce
filterAndAppend
getOwnTab
getStyleWithNoCode
handleUpdateInstalled
messageBox
msg
objectDiff
openURL
prefs
router
scrollElementIntoView
sessionStore
setupLivePrefs
showFiltersStats
sorter
t
VIVALDI
*/
'use strict';
/** @type {HTMLElement} */
let installed;
const ENTRY_ID_PREFIX_RAW = 'style-';
const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW;
const REVEAL_DATES_FOR = 'h2.style-name, [data-type=age]';
const BULK_THROTTLE_MS = 100;
const bulkChangeQueue = [];
@ -43,24 +62,31 @@ newUI.renderClass();
const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps'];
const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain=';
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 = {};
Promise.all([
API.getAllStyles(true),
// FIXME: integrate this into filter.js
router.getSearch('search') && API.searchDB({query: router.getSearch('search')}),
waitForSelector('#installed'), // needed to avoid flicker due to an extra frame and layout shift
prefs.initializing
]).then(([styles, ids, el]) => {
(async () => {
const query = router.getSearch('search');
const [styles, ids, el] = await Promise.all([
API.getAllStyles(),
query && API.searchDB({query, mode: router.getSearch('searchMode')}),
waitForSelector('#installed'), // needed to avoid flicker due to an extra frame and layout shift
prefs.initializing,
]);
installed = el;
installed.onclick = handleEvent.entryClicked;
$('#manage-options-button').onclick = () => router.updateHash('#stylus-options');
$('#sync-styles').onclick = () => router.updateHash('#stylus-options');
$$('#header a[href^="http"]').forEach(a => (a.onclick = handleEvent.external));
// show date installed & last update on hover
installed.addEventListener('mouseover', handleEvent.lazyAddEntryTitle);
installed.addEventListener('mouseout', handleEvent.lazyAddEntryTitle);
installed.addEventListener('mouseover', handleEvent.lazyAddEntryTitle, {passive: true});
installed.addEventListener('mouseout', handleEvent.lazyAddEntryTitle, {passive: true});
document.addEventListener('visibilitychange', onVisibilityChange);
// N.B. triggers existing onchange listeners
setupLivePrefs();
@ -68,19 +94,15 @@ Promise.all([
prefs.subscribe(newUI.ids.map(newUI.prefKeyForId), () => switchUI());
switchUI({styleOnly: true});
// translate CSS manually
document.head.appendChild($create('style', `
.disabled h2::after {
content: "${t('genericDisabledLabel')}";
}
#update-all-no-updates[data-skipped-edited="true"]::after {
content: " ${t('updateAllCheckSucceededSomeEdited')}";
}
body.all-styles-hidden-by-filters::after {
content: "${t('filteredStylesAllHidden')}";
}
`));
document.styleSheets[0].insertRule(
`:root {${[
'genericDisabledLabel',
'updateAllCheckSucceededSomeEdited',
'filteredStylesAllHidden',
].map(id => `--${id}:"${CSS.escape(t(id))}";`).join('')
}}`);
if (!VIVALDI) {
$$('#header select').forEach(el => el.adjustWidth());
$$('.filter-selection select').forEach(el => el.adjustWidth());
}
if (CHROME >= 80 && CHROME <= 88) {
// Wrong checkboxes are randomly checked after going back in history, https://crbug.com/1138598
@ -89,9 +111,11 @@ Promise.all([
});
}
showStyles(styles, ids);
});
})();
msg.onExtension(onRuntimeMessage);
router.watch({hash: '#stylus-options'}, state => (state ? embedOptions : unembedOptions)());
window.addEventListener('closeOptions', () => router.updateHash(''));
function onRuntimeMessage(msg) {
switch (msg.method) {
@ -129,7 +153,7 @@ function showStyles(styles = [], matchUrlIds) {
let firstRun = true;
installed.dataset.total = styles.length;
const scrollY = (history.state || {}).scrollY;
const shouldRenderAll = scrollY > window.innerHeight || sessionStorage.justEditedStyleId;
const shouldRenderAll = scrollY > window.innerHeight || sessionStore.justEditedStyleId;
const renderBin = document.createDocumentFragment();
if (scrollY) {
renderStyles();
@ -155,7 +179,7 @@ function showStyles(styles = [], matchUrlIds) {
return;
}
setTimeout(getFaviconImgSrc);
if (sessionStorage.justEditedStyleId) {
if (sessionStore.justEditedStyleId) {
highlightEditedStyle();
} else if ('scrollY' in (history.state || {})) {
setTimeout(window.scrollTo, 0, 0, history.state.scrollY);
@ -167,7 +191,7 @@ function showStyles(styles = [], matchUrlIds) {
function createStyleElement({style, name: nameLC}) {
// query the sub-elements just once, then reuse the references
if ((createStyleElement.parts || {}).newUI !== newUI.enabled) {
const entry = template[`style${newUI.enabled ? 'Compact' : ''}`];
const entry = t.template[newUI.enabled ? 'styleNewUI' : 'style'];
createStyleElement.parts = {
newUI: newUI.enabled,
entry,
@ -177,7 +201,9 @@ function createStyleElement({style, name: nameLC}) {
editLink: $('.style-edit-link', entry) || {},
editHrefBase: 'edit.html?id=',
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),
targets: $('.targets', entry),
expander: $('.expander', entry),
@ -192,13 +218,18 @@ function createStyleElement({style, name: nameLC}) {
};
}
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;
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.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.oldCheckUpdate.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 + ' ' +
(style.enabled ? 'enabled' : 'disabled') +
(style.updateUrl ? ' updatable' : '') +
(style.usercssData ? ' usercss' : '');
(ud ? ' usercss' : '');
if (style.url) {
$('.homepage', entry).appendChild(parts.homepageIcon.cloneNode(true));
}
if (style.updateUrl && newUI.enabled) {
$('.actions', entry).appendChild(template.updaterIcons.cloneNode(true));
$('.actions', entry).appendChild(t.template.updaterIcons.cloneNode(true));
}
if (configurable && newUI.enabled) {
$('.actions', entry).appendChild(template.configureIcon.cloneNode(true));
$('.actions', entry).appendChild(t.template.configureIcon.cloneNode(true));
}
createStyleTargetsElement({entry, style});
@ -267,12 +298,12 @@ function createStyleTargetsElement({entry, expanded, style = entry.styleMeta}) {
el = next;
continue;
}
const element = template.appliesToTarget.cloneNode(true);
const element = t.template.appliesToTarget.cloneNode(true);
if (!newUI.enabled) {
if (numTargets === maxTargets) {
container = container.appendChild(template.extraAppliesTo.cloneNode(true));
} else if (numTargets > 0) {
container.appendChild(template.appliesToSeparator.cloneNode(true));
container = container.appendChild(t.template.extraAppliesTo.cloneNode(true));
} else if (numTargets > 1) {
container.appendChild(t.template.appliesToSeparator.cloneNode(true));
}
}
element.dataset.type = type;
@ -291,13 +322,37 @@ function createStyleTargetsElement({entry, expanded, style = entry.styleMeta}) {
if (entryTargets.firstElementChild) {
entryTargets.textContent = '';
}
entryTargets.appendChild(template.appliesToEverything.cloneNode(true));
entryTargets.appendChild(t.template.appliesToEverything.cloneNode(true));
}
entry.classList.toggle('global', !numTargets);
entry._allTargetsRendered = allTargetsRendered;
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) {
if (!newUI.enabled || !newUI.favicons) return;
@ -350,7 +405,7 @@ Object.assign(handleEvent, {
'.update': 'update',
'.delete': 'delete',
'.applies-to .expander': 'expandTargets',
'.configure-usercss': 'config'
'.configure-usercss': 'config',
},
entryClicked(event) {
@ -366,42 +421,29 @@ Object.assign(handleEvent, {
}
},
name(event) {
if (newUI.enabled) handleEvent.edit(event);
name(event, entry) {
if (newUI.enabled) handleEvent.edit(event, entry);
},
edit(event) {
async edit(event, entry) {
if (event.altKey) {
return;
}
event.preventDefault();
event.stopPropagation();
const left = event.button === 0;
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 key = `${event.shiftKey ? 's' : ''}${event.ctrlKey ? 'c' : ''}${'LMR'[event.button]}`;
const url = $('[href]', entry).href;
if (openWindow || openBackgroundTab || openForegroundTab) {
if (chrome.windows && openWindow) {
API.openEditor({id: entry.styleId});
} else {
getOwnTab().then(({index}) => {
openURL({
url,
index: index + 1,
active: openForegroundTab
});
});
}
const ownTab = await getOwnTab();
if (key === 'L') {
sessionStore['manageStylesHistory' + ownTab.id] = url;
location.href = url;
} else if (chrome.windows && key === 'sL') {
API.openEditor({id: entry.styleId});
} else {
onVisibilityChange();
getActiveTab().then(tab => {
sessionStorageHash('manageStylesHistory').set(tab.id, url);
location.href = url;
openURL({
url,
index: ownTab.index + 1,
active: key === 'sM' || key === 'scL',
});
}
},
@ -501,9 +543,9 @@ Object.assign(handleEvent, {
},
lazyAddEntryTitle({type, target}) {
const cell = target.closest('h2.style-name');
const cell = target.closest(REVEAL_DATES_FOR);
if (cell) {
const link = $('.style-name-link', cell);
const link = $('.style-name-link', cell) || cell;
if (type === 'mouseover' && !link.title) {
debounce(handleEvent.addEntryTitle, 50, link);
} else {
@ -518,8 +560,8 @@ Object.assign(handleEvent, {
{prop: 'installDate', name: 'dateInstalled'},
{prop: 'updateDate', name: 'dateUpdated'},
].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) {
return API.getStyle(id, true).then(style => {
return API.getStyle(id).then(style => {
handleUpdate(style, opts);
bulkChangeQueue.time = performance.now();
});
@ -620,43 +662,9 @@ function switchUI({styleOnly} = {}) {
Object.assign(newUI, current);
newUI.renderClass();
installed.classList.toggle('has-favicons', newUI.favicons);
$('#style-overrides').textContent = `
.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;
}
` : '');
installed.classList.toggle('has-favicons', newUI.enabled && newUI.favicons);
installed.classList.toggle('favicons-grayed', newUI.enabled && newUI.faviconsGray);
if (changed.targets) installed.style.setProperty('--num-targets', newUI.targets);
if (styleOnly) {
return;
@ -666,7 +674,7 @@ function switchUI({styleOnly} = {}) {
let iconsMissing = iconsEnabled && !$('.applies-to img');
if (changed.enabled || (iconsMissing && !createStyleElement.parts)) {
installed.textContent = '';
API.getAllStyles(true).then(showStyles);
API.getAllStyles().then(showStyles);
return;
}
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
// assuming other changes aren't important enough to justify making a complicated DOM sync
case 'visible': {
const id = sessionStorage.justEditedStyleId;
const id = sessionStore.justEditedStyleId;
if (id) {
handleUpdateForId(Number(id), {method: 'styleUpdated'});
delete sessionStorage.justEditedStyleId;
delete sessionStore.justEditedStyleId;
}
break;
}
@ -707,9 +715,9 @@ function onVisibilityChange() {
function highlightEditedStyle() {
if (!sessionStorage.justEditedStyleId) return;
const entry = $(ENTRY_ID_PREFIX + sessionStorage.justEditedStyleId);
delete sessionStorage.justEditedStyleId;
if (!sessionStore.justEditedStyleId) return;
const entry = $(ENTRY_ID_PREFIX + sessionStore.justEditedStyleId);
delete sessionStore.justEditedStyleId;
if (entry) {
animateElement(entry);
requestAnimationFrame(() => scrollElementIntoView(entry));
@ -732,13 +740,11 @@ function waitForSelector(selector) {
}
function embedOptions() {
let options = $('#stylus-embedded-options');
if (!options) {
options = document.createElement('iframe');
options.id = 'stylus-embedded-options';
options.src = '/options.html';
document.documentElement.appendChild(options);
}
const options = $('#stylus-embedded-options') ||
document.documentElement.appendChild($create('iframe', {
id: 'stylus-embedded-options',
src: '/options.html',
}));
options.focus();
}
@ -746,20 +752,7 @@ async function unembedOptions() {
const options = $('#stylus-embedded-options');
if (options) {
options.contentWindow.document.body.classList.add('scaleout');
options.classList.add('fadeout');
await animateElement(options, 'fadeout');
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: {
text: t('genericTitle'),
parse: ({name}) => name,
sorter: sorterType.alpha
sorter: sorterType.alpha,
},
usercss: {
text: 'Usercss',
parse: ({style}) => style.usercssData ? 0 : 1,
sorter: sorterType.number
sorter: sorterType.number,
},
disabled: {
text: '', // added as either "enabled" or "disabled" by the addOptions function
parse: ({style}) => style.enabled ? 1 : 0,
sorter: sorterType.number
sorter: sorterType.number,
},
dateInstalled: {
text: t('dateInstalled'),
parse: ({style}) => style.installDate,
sorter: sorterType.number
sorter: sorterType.number,
},
dateUpdated: {
text: t('dateUpdated'),
parse: ({style}) => style.updateDate || style.installDate,
sorter: sorterType.number
}
sorter: sorterType.number,
},
};
// Adding (assumed) most commonly used ('title,asc' should always be first)
@ -56,7 +56,7 @@ const sorter = (() => {
'usercss,asc, title,desc',
'usercss,desc, title,desc',
'disabled,desc, title,desc',
'disabled,desc, usercss,asc, title,desc'
'disabled,desc, usercss,asc, title,desc',
];
const splitRegex = /\s*,\s*/;
@ -76,7 +76,7 @@ const sorter = (() => {
dateNew: ` (${t('sortDateNewestFirst')})`,
dateOld: ` (${t('sortDateOldestFirst')})`,
groupAsc: t('sortLabelTitleAsc'),
groupDesc: t('sortLabelTitleDesc')
groupDesc: t('sortLabelTitleDesc'),
};
const optgroupRegex = /\{\w+\}/;
selectOptions.forEach(sort => {
@ -132,7 +132,7 @@ const sorter = (() => {
entry,
name: entry.styleNameLowerCase,
style: entry.styleMeta,
}))
})),
});
if (current.some((entry, index) => entry !== sorted[index].entry)) {
const renderBin = document.createDocumentFragment();

View File

@ -55,7 +55,7 @@
"background/usercss-helper.js",
"background/usercss-install-helper.js",
"background/style-via-api.js",
"background/style-via-xhr.js",
"background/style-via-webrequest.js",
"background/search-db.js",
"background/update.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';
/**
* @param {Object} params
* @param {String} params.title
* @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()
* @param {String} [params.className]
* CSS class name of the message box element
@ -87,7 +94,7 @@ function messageBox({
},
scroll() {
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,' +
'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`,
buttons.map((content, buttonIndex) => content &&
$create('button', Object.assign({
@ -160,7 +167,7 @@ messageBox.alert = (contents, className, title) =>
title,
contents,
className: `center ${className || ''}`,
buttons: [t('confirmClose')]
buttons: [t('confirmClose')],
});
/**
@ -174,5 +181,5 @@ messageBox.confirm = (contents, className, title) =>
title,
contents,
className: `center ${className || ''}`,
buttons: [t('confirmYes'), t('confirmNo')]
buttons: [t('confirmYes'), t('confirmNo')],
}).then(result => result.button === 0 || result.enter);

View File

@ -156,11 +156,8 @@
</label>
<label>
<span i18n-text="manageFavicons">
<a data-cmd="note"
i18n-title="manageFaviconsHelp"
href="#"
class="svg-inline-wrapper"
tabindex="0">
<a i18n-title="manageFaviconsHelp"
data-cmd="note" href="#" class="svg-inline-wrapper">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</span>
@ -188,11 +185,8 @@
<div class="items">
<label>
<span i18n-text="optionsUpdateInterval">
<a data-cmd="note"
i18n-title="optionsUpdateImportNote"
href="#"
class="svg-inline-wrapper"
tabindex="0">
<a i18n-title="optionsUpdateImportNote"
data-cmd="note" href="#" class="svg-inline-wrapper">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</span>
@ -243,11 +237,8 @@
<div class="items">
<label class="chromium-only">
<span i18n-text="optionsAdvancedStyleViaXhr">
<a data-cmd="note"
i18n-title="optionsAdvancedStyleViaXhrNote"
href="#"
class="svg-inline-wrapper"
tabindex="0">
<a i18n-title="optionsAdvancedStyleViaXhrNote"
data-cmd="note" href="#" class="svg-inline-wrapper">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</span>
@ -256,14 +247,23 @@
<span></span>
</span>
</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>
<span i18n-text="optionsAdvancedExposeIframes">
<a data-cmd="note"
i18n-data-title="optionsAdvancedExposeIframesNote"
i18n-title="optionsAdvancedExposeIframesNote"
href="#"
class="svg-inline-wrapper"
tabindex="0">
<a i18n-title="optionsAdvancedExposeIframesNote"
data-cmd="note" href="#" class="svg-inline-wrapper">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</span>

View File

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

View File

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

View File

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

View File

@ -32,37 +32,37 @@ const files = {
'mode/css',
'mode/javascript',
'mode/stylus',
'theme/*'
'theme/*',
],
'jsonlint': [
'lib/jsonlint.js → jsonlint.js',
'README.md → LICENSE'
'README.md → LICENSE',
],
'less-bundle': [
'dist/less.min.js → less.min.js'
'dist/less.min.js → less.min.js',
],
'lz-string-unsafe': [
'lz-string-unsafe.min.js'
'lz-string-unsafe.min.js',
],
'semver-bundle': [
'dist/semver.js → semver.js'
'dist/semver.js → semver.js',
],
'stylelint-bundle': [
'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': [
'dist/stylus-renderer.min.js → stylus-renderer.min.js'
'dist/stylus-renderer.min.js → stylus-renderer.min.js',
],
'usercss-meta': [
'dist/usercss-meta.min.js → usercss-meta.min.js'
'dist/usercss-meta.min.js → usercss-meta.min.js',
],
'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': [
'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);
@ -87,12 +87,15 @@ async function generateThemeList() {
.map(name => name.replace('.css', ''))
.sort();
return endent`
/* exported CODEMIRROR_THEMES */
// this file is generated by update-codemirror-themes.js
/* Do not edit. This file is auto-generated by build-vendor.js */
'use strict';
const CODEMIRROR_THEMES = ${JSON.stringify(themes, null, 2)};
`.replace(/"/g, "'") + '\n';
/* exported CODEMIRROR_THEMES */
const CODEMIRROR_THEMES = [
${
themes.map(t => ` '${t.replace(/'/g, '\\$&')}',\n`).join('')
}];
` + '\n';
}
async function copyLicense(pkg) {

View File

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

View File

@ -12,7 +12,7 @@
{hex: '#00ffff', start: .50},
{hex: '#0000ff', start: .67},
{hex: '#ff00ff', start: .83},
{hex: '#ff0000', start: 1}
{hex: '#ff0000', start: 1},
];
const MIN_HEIGHT = 220;
const MARGIN = 8;
@ -119,7 +119,7 @@
$inputGroups.hex = $(['input-group', 'hex'], [
$(['input-field', 'hex'], [
$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', [
$hexLettercase.true = $('title-action', {onclick: onHexLettercaseClicked}, 'HEX'),
@ -186,7 +186,7 @@
Object.defineProperty($inputs.hsl, 'color', {get: inputsToHSL});
Object.defineProperty($inputs, 'color', {get: () => $inputs[currentFormat].color});
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)));

View File

@ -73,6 +73,11 @@
const height = cm.display.lastWrapHeight;
if (!height || !textHeight) return;
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);
},
mousedown(cm, event) {
@ -164,12 +169,14 @@
function colorizeAll(state) {
const {cm} = state;
const {viewFrom, viewTo} = cm.display;
if (!viewTo) {
state.colorizeOnUpdate = true;
return;
}
const {curOp} = cm;
if (!curOp) cm.startOperation();
const viewFrom = cm.display.viewFrom;
const viewTo = (cm.display.viewTo || maxRenderChunkSize - 1) + 1;
state.line = viewFrom;
state.inComment = null;
state.now = performance.now();

View File

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