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 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:
parent
7fa4d10fd6
commit
420733b93a
|
@ -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]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ function createChromeStorageDB() {
|
|||
}
|
||||
}
|
||||
return output;
|
||||
})
|
||||
}),
|
||||
};
|
||||
|
||||
return {exec};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -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());
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
140
background/style-via-webrequest.js
Normal file
140
background/style-via-webrequest.js
Normal 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);
|
||||
}
|
||||
})();
|
|
@ -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);
|
||||
}
|
||||
})();
|
|
@ -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}`);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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}));
|
||||
|
|
|
@ -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(',');
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -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) {}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'})),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
];
|
||||
|
|
70
edit/edit.js
70
edit/edit.js
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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};
|
||||
})();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -75,7 +75,7 @@ function MozSectionFinder(cm) {
|
|||
/** @param {MozSection} [section] */
|
||||
updatePositions(section) {
|
||||
(section ? [section] : getState().sections).forEach(setPositionFromMark);
|
||||
}
|
||||
},
|
||||
};
|
||||
return MozSectionFinder;
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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')},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -214,6 +214,6 @@ function createHotkeyInput(prefId, onDone = () => {}) {
|
|||
},
|
||||
onpaste(event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -142,6 +142,7 @@ select {
|
|||
transition: color .5s;
|
||||
}
|
||||
|
||||
.select-wrapper,
|
||||
.select-resizer {
|
||||
display: inline-flex!important;
|
||||
cursor: default;
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
))
|
||||
)),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ function createCache({size = 1000, onDeleted} = {}) {
|
|||
},
|
||||
get size() {
|
||||
return map.size;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function get(id) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 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
|
||||
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;
|
||||
}) || '';
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -67,7 +67,7 @@ const workerUtil = {
|
|||
message: err.message,
|
||||
lineNumber: err.lineNumber,
|
||||
columnNumber: err.columnNumber,
|
||||
fileName: err.fileName
|
||||
fileName: err.fileName,
|
||||
}, err);
|
||||
},
|
||||
|
||||
|
|
35
manage.html
35
manage.html
|
@ -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">
|
||||
|
||||
<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">
|
||||
|
||||
<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>
|
||||
|
|
|
@ -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,
|
||||
]));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
277
manage/manage.js
277
manage/manage.js
|
@ -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('');
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
|
|
56
options.html
56
options.html
|
@ -52,7 +52,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="block">
|
||||
<h1 i18n-text="optionsCustomizeIcon"></h1>
|
||||
<div class="items">
|
||||
|
@ -76,7 +76,7 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="block">
|
||||
<h1 i18n-text="optionsCustomizeBadge"></h1>
|
||||
<div class="items">
|
||||
|
@ -97,7 +97,7 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="block">
|
||||
<h1 i18n-text="optionsCustomizePopup"></h1>
|
||||
<div class="items">
|
||||
|
@ -143,7 +143,7 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="block">
|
||||
<h1 i18n-text="openManage"></h1>
|
||||
<div class="items">
|
||||
|
@ -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>
|
||||
|
@ -182,17 +179,14 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="block" id="updates">
|
||||
<h1 i18n-text="optionsCustomizeUpdate"></h1>
|
||||
<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>
|
||||
|
@ -200,7 +194,7 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="block sync-options">
|
||||
<h1 i18n-text="optionsCustomizeSync"></h1>
|
||||
<div class="items">
|
||||
|
@ -224,7 +218,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="block" id="advanced">
|
||||
<div class="collapsible-resizer">
|
||||
<h1 i18n-text="optionsAdvanced">
|
||||
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -16,7 +16,7 @@ function createZip({isFirefox} = {}) {
|
|||
'package-lock.json',
|
||||
'yarn.lock',
|
||||
'*.zip',
|
||||
'*.map'
|
||||
'*.map',
|
||||
];
|
||||
|
||||
const file = fs.createWriteStream(fileName);
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue
Block a user