Merge branch 'master' into dev-color-scheme

This commit is contained in:
eight04 2020-10-23 07:10:24 +08:00
commit 5f46a008c5
110 changed files with 2438 additions and 2761 deletions

View File

@ -1,7 +1,7 @@
# https://github.com/eslint/eslint/blob/master/docs/rules/README.md
parserOptions:
ecmaVersion: 2015
ecmaVersion: 2017
env:
browser: true

View File

@ -217,10 +217,6 @@
}
}
},
"editorStylesButton": {
"message": "Стилове за редактора",
"description": "Find styles for the editor"
},
"enableStyleLabel": {
"message": "Включване",
"description": "Label for the button to enable a style"
@ -741,4 +737,4 @@
"message": "този адрес",
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
}
}
}

View File

@ -305,10 +305,6 @@
}
}
},
"editorStylesButton": {
"message": "Najít styly pro editor",
"description": "Find styles for the editor"
},
"enableStyleLabel": {
"message": "Povolit",
"description": "Label for the button to enable a style"
@ -1306,4 +1302,4 @@
"message": "Nahrávání souboru…",
"description": ""
}
}
}

View File

@ -309,10 +309,6 @@
}
}
},
"editorStylesButton": {
"message": "Editor Styles finden",
"description": "Find styles for the editor"
},
"enableStyleLabel": {
"message": "Aktivieren",
"description": "Label for the button to enable a style"
@ -1600,4 +1596,4 @@
"message": "Lade Styles hoch...",
"description": ""
}
}
}

View File

@ -250,6 +250,13 @@
"message": "Copy to clipboard",
"description": "Tooltip for elements which can be copied"
},
"customNameHint": {
"message": "Enter a custom name here to rename the style in UI without breaking its updates"
},
"customNameResetHint": {
"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"
},
"dateInstalled": {
"message": "Date installed",
"description": "Option text for the user to sort the style by install date"
@ -319,10 +326,6 @@
},
"description": "Title of the page for editing styles"
},
"editorStylesButton": {
"message": "Find editor styles",
"description": "Find styles for the editor"
},
"enableStyleLabel": {
"message": "Enable",
"description": "Label for the button to enable a style"
@ -984,6 +987,12 @@
"optionsAdvancedAutoSwitchSchemeByTime": {
"message": "By night time:"
},
"optionsAdvancedStyleViaXhr": {
"message": "Instant inject mode"
},
"optionsAdvancedStyleViaXhrNote": {
"message": "Enable this if you encounter flashing of unstyled content (FOUC) when browsing, which is especially noticeable with dark themes.\n\nThe technical reason is that Chrome/Chromium postpones asynchronous communication of extensions, in a usually meaningless attempt to improve page load speed, potentially causing styles to be late to apply. To circumvent this, since web extensions are not provided a synchronous API, Stylus provides this option to utilize the \"deprecated\" synchronous XMLHttpRequest web API to fetch applicable styles. There shouldn't be any detrimental effects, since the request is fulfilled within a few milliseconds while the page is still being downloaded from the server.\n\nNevertheless, Chromium will print a warning in devtools' console. Right-clicking a warning, and hiding them, will prevent future warnings from being shown."
},
"optionsBadgeDisabled": {
"message": "Background color when disabled"
},
@ -1036,6 +1045,9 @@
"optionsResetButton": {
"message": "Reset options"
},
"optionsStylusThemes": {
"message": "Find a Stylus UI theme"
},
"optionsSubheading": {
"message": "More Options",
"description": "Subheading for options section on manage page."
@ -1147,6 +1159,10 @@
"message": "Action menu",
"description": "Tooltip for menu button in popup."
},
"popupOpenEditInPopup": {
"message": "Use a simple window (no omnibox)",
"description": "Label for the checkbox controlling 'edit' action behavior in the popup."
},
"popupOpenEditInWindow": {
"message": "Open editor in a new window",
"description": "Label for the checkbox controlling 'edit' action behavior in the popup."
@ -1198,6 +1214,10 @@
"message": "Case-sensitive",
"description": "Tooltip for the 'Aa' icon that enables case-sensitive search in the editor shown on Ctrl-F"
},
"searchGlobalStyles": {
"message": "Also search global styles",
"description": "Checkbox label in the popup's inline style search, shown when the text to search is entered"
},
"searchNumberOfResults": {
"message": "Number of matches",
"description": "Tooltip for the number of found search results in the editor shown on Ctrl-F"
@ -1206,6 +1226,10 @@
"message": "Number of matches in code and applies-to values",
"description": "Tooltip for the number of found search results in the editor shown on Ctrl-F"
},
"searchStyleQueryHint": {
"message": "Search style names case-insensitively:\nsome words - all words in any order\n\"some phrase\" - this exact phrase without quotes\n2020 - a year like this also shows styles updated in 2020",
"description": "Tooltip shown for the text input in the popup's inline style finder"
},
"searchRegexp": {
"message": "Use /re/ syntax for regexp search",
"description": "Label after the search input field in the editor shown on Ctrl-F"

View File

@ -313,10 +313,6 @@
}
}
},
"editorStylesButton": {
"message": "Buscar estilos del editor",
"description": "Find styles for the editor"
},
"enableStyleLabel": {
"message": "Activar",
"description": "Label for the button to enable a style"
@ -1568,4 +1564,4 @@
"message": "Subiendo el archivo....",
"description": ""
}
}
}

View File

@ -313,10 +313,6 @@
}
}
},
"editorStylesButton": {
"message": "Leia redaktori stiile",
"description": "Find styles for the editor"
},
"enableStyleLabel": {
"message": "Luba",
"description": "Label for the button to enable a style"
@ -1486,4 +1482,4 @@
"message": "Faili üleslaadimine...",
"description": ""
}
}
}

View File

@ -321,10 +321,6 @@
}
}
},
"editorStylesButton": {
"message": "Trouver des styles pour léditeur",
"description": "Find styles for the editor"
},
"enableStyleLabel": {
"message": "Activer",
"description": "Label for the button to enable a style"
@ -1616,4 +1612,4 @@
"message": "Envoi du fichier…",
"description": ""
}
}
}

View File

@ -321,10 +321,6 @@
}
}
},
"editorStylesButton": {
"message": "מצא עיצובים לעורך",
"description": "Find styles for the editor"
},
"enableStyleLabel": {
"message": "אפשר",
"description": "Label for the button to enable a style"
@ -1381,4 +1377,4 @@
"message": "מעלה קובץ...",
"description": ""
}
}
}

View File

@ -309,10 +309,6 @@
}
}
},
"editorStylesButton": {
"message": "A szerkesztő stílusainak keresése",
"description": "Find styles for the editor"
},
"enableStyleLabel": {
"message": "Engedélyezés",
"description": "Label for the button to enable a style"
@ -1604,4 +1600,4 @@
"message": "Fájl feltöltése...",
"description": ""
}
}
}

View File

@ -273,10 +273,6 @@
}
}
},
"editorStylesButton": {
"message": "Cerca stili editor",
"description": "Find styles for the editor"
},
"enableStyleLabel": {
"message": "Attiva",
"description": "Label for the button to enable a style"
@ -1058,4 +1054,4 @@
"message": "questo URL",
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
}
}
}

View File

@ -313,10 +313,6 @@
}
}
},
"editorStylesButton": {
"message": "エディタのスタイルを見つける",
"description": "Find styles for the editor"
},
"enableStyleLabel": {
"message": "有効化",
"description": "Label for the button to enable a style"
@ -1636,4 +1632,4 @@
"message": "スタイルをアップロード中...",
"description": ""
}
}
}

View File

@ -317,10 +317,6 @@
}
}
},
"editorStylesButton": {
"message": "편집기 스타일 찾기",
"description": "Find styles for the editor"
},
"enableStyleLabel": {
"message": "활성화",
"description": "Label for the button to enable a style"
@ -1636,4 +1632,4 @@
"message": "파일 업로드 중...",
"description": ""
}
}
}

View File

@ -317,10 +317,6 @@
}
}
},
"editorStylesButton": {
"message": "Editorstijlen zoeken",
"description": "Find styles for the editor"
},
"enableStyleLabel": {
"message": "Inschakelen",
"description": "Label for the button to enable a style"
@ -1620,4 +1616,4 @@
"message": "Bestand uploaden...",
"description": ""
}
}
}

View File

@ -321,10 +321,6 @@
}
}
},
"editorStylesButton": {
"message": "Znajdź style edytora",
"description": "Find styles for the editor"
},
"enableStyleLabel": {
"message": "Włącz",
"description": "Label for the button to enable a style"
@ -1644,4 +1640,4 @@
"message": "Wysyłanie stylów...",
"description": ""
}
}
}

View File

@ -301,10 +301,6 @@
}
}
},
"editorStylesButton": {
"message": "Encontrar estilos para o editor",
"description": "Find styles for the editor"
},
"enableStyleLabel": {
"message": "Ativar",
"description": "Label for the button to enable a style"
@ -1224,4 +1220,4 @@
"message": "este URL",
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
}
}
}

View File

@ -269,10 +269,6 @@
}
}
},
"editorStylesButton": {
"message": "Găsiți teme pentru editor",
"description": "Find styles for the editor"
},
"enableStyleLabel": {
"message": "Activați",
"description": "Label for the button to enable a style"
@ -1140,4 +1136,4 @@
"message": "acest URL",
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
}
}
}

View File

@ -321,10 +321,6 @@
}
}
},
"editorStylesButton": {
"message": "Сменить тему Стилус",
"description": "Find styles for the editor"
},
"enableStyleLabel": {
"message": "Включить",
"description": "Label for the button to enable a style"
@ -1644,4 +1640,4 @@
"message": "Загрузка файла...",
"description": ""
}
}
}

View File

@ -309,10 +309,6 @@
}
}
},
"editorStylesButton": {
"message": "Hitta redaktörsstilar",
"description": "Find styles for the editor"
},
"enableStyleLabel": {
"message": "Aktivera",
"description": "Label for the button to enable a style"
@ -1522,4 +1518,4 @@
"message": "Skickar filen...",
"description": ""
}
}
}

View File

@ -309,10 +309,6 @@
}
}
},
"editorStylesButton": {
"message": "Editör stili bul",
"description": "Find styles for the editor"
},
"enableStyleLabel": {
"message": "Etkinleştir",
"description": "Label for the button to enable a style"
@ -896,4 +892,4 @@
"message": "Dosya Yükleniyor...",
"description": ""
}
}
}

View File

@ -321,10 +321,6 @@
}
}
},
"editorStylesButton": {
"message": "查找编辑器样式",
"description": "Find styles for the editor"
},
"enableStyleLabel": {
"message": "启用",
"description": "Label for the button to enable a style"
@ -1644,4 +1640,4 @@
"message": "正在上传文件...",
"description": ""
}
}
}

View File

@ -321,10 +321,6 @@
}
}
},
"editorStylesButton": {
"message": "找到編輯器樣式",
"description": "Find styles for the editor"
},
"enableStyleLabel": {
"message": "啟用",
"description": "Label for the button to enable a style"
@ -1644,4 +1640,4 @@
"message": "正在上傳檔案……",
"description": ""
}
}
}

View File

@ -12,7 +12,6 @@ createAPI({
compileUsercss,
parseUsercssMeta(text, indexOffset = 0) {
loadScript(
'/js/polyfill.js',
'/vendor/usercss-meta/usercss-meta.min.js',
'/vendor-overwrites/colorpicker/colorconverter.js',
'/js/meta-parser.js'
@ -21,7 +20,6 @@ createAPI({
},
nullifyInvalidVars(vars) {
loadScript(
'/js/polyfill.js',
'/vendor/usercss-meta/usercss-meta.min.js',
'/vendor-overwrites/colorpicker/colorconverter.js',
'/js/meta-parser.js'
@ -31,11 +29,15 @@ createAPI({
});
function compileUsercss(preprocessor, code, vars) {
loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js');
loadScript(
'/vendor-overwrites/csslint/parserlib.js',
'/vendor-overwrites/colorpicker/colorconverter.js',
'/js/moz-parser.js'
);
const builder = getUsercssCompiler(preprocessor);
vars = simpleVars(vars);
return Promise.resolve(builder.preprocess ? builder.preprocess(code, vars) : code)
.then(code => parseMozFormat({code}))
.then(code => parseMozFormat({code, emptyDocument: preprocessor === 'stylus'}))
.then(({sections, errors}) => {
if (builder.postprocess) {
builder.postprocess(sections, vars);
@ -122,28 +124,39 @@ function getUsercssCompiler(preprocessor) {
const pool = new Map();
return Promise.resolve(doReplace(source));
function getValue(name, rgb) {
function getValue(name, rgbName) {
if (!vars.hasOwnProperty(name)) {
if (name.endsWith('-rgb')) {
return getValue(name.slice(0, -4), true);
return getValue(name.slice(0, -4), name);
}
return null;
}
if (rgb) {
if (vars[name].type === 'color') {
const color = colorConverter.parse(vars[name].value);
if (!color) return null;
const {r, g, b} = color;
return `${r}, ${g}, ${b}`;
const {type, value} = vars[name];
switch (type) {
case 'color': {
let color = pool.get(rgbName || name);
if (color == null) {
color = colorConverter.parse(value);
if (color) {
if (color.type === 'hsl') {
color = colorConverter.HSVtoRGB(colorConverter.HSLtoHSV(color));
}
const {r, g, b} = color;
color = rgbName
? `${r}, ${g}, ${b}`
: `#${(0x1000000 + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
}
// the pool stores `false` for bad colors to differentiate from a yet unknown color
pool.set(rgbName || name, color || false);
}
return color || null;
}
return null;
case 'dropdown':
case 'select': // prevent infinite recursion
pool.set(name, '');
return doReplace(value);
}
if (vars[name].type === 'dropdown' || vars[name].type === 'select') {
// prevent infinite recursion
pool.set(name, '');
return doReplace(vars[name].value);
}
return vars[name].value;
return value;
}
function doReplace(text) {

View File

@ -1,8 +1,7 @@
/* global download prefs openURL FIREFOX CHROME
URLS ignoreChromeError usercssHelper
URLS ignoreChromeError chromeLocal semverCompare
styleManager msg navigatorUtil workerUtil contentScripts sync
findExistingTab activateTab isTabReplaceable getActiveTab
tabManager colorScheme */
findExistingTab activateTab isTabReplaceable getActiveTab colorScheme */
'use strict';
// eslint-disable-next-line no-var
@ -65,11 +64,13 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
/* Same as openURL, the only extra prop in `opts` is `message` - it'll be sent when the tab is ready,
which is needed in the popup, otherwise another extension could force the tab to open in foreground
thus auto-closing the popup (in Chrome at least) and preventing the sendMessage code from running */
openURL(opts) {
const {message} = opts;
return openURL(opts) // will pass the resolved value untouched when `message` is absent or falsy
.then(message && (tab => tab.status === 'complete' ? tab : onTabReady(tab)))
.then(message && (tab => msg.sendTab(tab.id, opts.message)));
async openURL(opts) {
const tab = await openURL(opts);
if (opts.message) {
await onTabReady(tab);
await msg.sendTab(tab.id, opts.message);
}
return tab;
function onTabReady(tab) {
return new Promise((resolve, reject) =>
setTimeout(function ping(numTries = 10, delay = 100) {
@ -112,14 +113,6 @@ navigatorUtil.onUrlChange(({tabId, frameId}, type) => {
}
});
tabManager.onUpdate(({tabId, url, oldUrl = ''}) => {
if (usercssHelper.testUrl(url) && !oldUrl.startsWith(URLS.installUsercss)) {
usercssHelper.testContents(tabId, url).then(data => {
if (data.code) usercssHelper.openInstallerPage(tabId, url, data);
});
}
});
if (FIREFOX) {
// FF misses some about:blank iframes so we inject our content script explicitly
navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, {
@ -140,7 +133,7 @@ if (chrome.commands) {
}
// *************************************************************************
chrome.runtime.onInstalled.addListener(({reason}) => {
chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
// save install type: "admin", "development", "normal", "sideload" or "other"
// "normal" = addon installed from webstore
chrome.management.getSelf(info => {
@ -157,6 +150,14 @@ chrome.runtime.onInstalled.addListener(({reason}) => {
});
// 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) {
setTimeout(async () => {
const del = Object.keys(await chromeLocal.get())
.filter(key => key.startsWith('usoSearchCache'));
if (del.length) chromeLocal.remove(del);
}, 15e3);
}
});
// *************************************************************************
@ -298,16 +299,14 @@ function openEditor(params) {
'url-prefix'?: String
}
*/
const searchParams = new URLSearchParams();
for (const key in params) {
searchParams.set(key, params[key]);
}
const search = searchParams.toString();
const u = new URL(chrome.runtime.getURL('edit.html'));
u.search = new URLSearchParams(params);
return openURL({
url: 'edit.html' + (search && `?${search}`),
newWindow: prefs.get('openEditInWindow'),
windowPosition: prefs.get('windowPosition'),
currentWindow: null
url: `${u}`,
currentWindow: null,
newWindow: prefs.get('openEditInWindow') && Object.assign({},
prefs.get('openEditInWindow.popup') && {type: 'popup'},
prefs.get('windowPosition')),
});
}
@ -329,7 +328,7 @@ function openManage({options = false, search} = {}) {
if (tab) {
return Promise.all([
activateTab(tab),
tab.url !== url && msg.sendTab(tab.id, {method: 'pushState', url})
(tab.pendingUrl || tab.url) !== url && msg.sendTab(tab.id, {method: 'pushState', url})
.catch(console.error)
]);
}

View File

@ -17,6 +17,21 @@ const contentScripts = (() => {
}
const busyTabs = new Set();
let busyTabsTimer;
// expose version on greasyfork/sleazyfork 1) info page and 2) code page
const urlMatches = '/scripts/\\d+[^/]*(/code)?([?#].*)?$';
chrome.webNavigation.onCommitted.addListener(({tabId}) => {
chrome.tabs.executeScript(tabId, {
file: '/content/install-hook-greasyfork.js',
runAt: 'document_start',
});
}, {
url: [
{hostEquals: 'greasyfork.org', urlMatches},
{hostEquals: 'sleazyfork.org', urlMatches},
]
});
return {injectToTab, injectToAllTabs};
function injectToTab({url, tabId, frameId = null}) {
@ -58,13 +73,13 @@ const contentScripts = (() => {
return browser.tabs.query({}).then(tabs => {
for (const tab of tabs) {
// skip unloaded/discarded/chrome tabs
if (!tab.width || tab.discarded || !URLS.supported(tab.url)) continue;
if (!tab.width || tab.discarded || !URLS.supported(tab.pendingUrl || tab.url)) continue;
// our content scripts may still be pending injection at browser start so it's too early to ping them
if (tab.status === 'loading') {
trackBusyTab(tab.id, true);
} else {
injectToTab({
url: tab.url,
url: tab.pendingUrl || tab.url,
tabId: tab.id
});
}

View File

@ -1,4 +1,4 @@
/* global chromeLocal ignoreChromeError workerUtil createChromeStorageDB */
/* global chromeLocal workerUtil createChromeStorageDB */
/* exported db */
/*
Initialize a database. There are some problems using IndexedDB in Firefox:
@ -10,29 +10,18 @@ https://www.reddit.com/r/firefox/comments/7ijuaq/firefox_59_webextensions_can_us
'use strict';
const db = (() => {
let exec;
const preparing = prepare();
return {
exec: (...args) =>
preparing.then(() => exec(...args))
const DATABASE = 'stylish';
const STORE = 'styles';
const FALLBACK = 'dbInChromeStorage';
const dbApi = {
async exec(...args) {
dbApi.exec = await tryUsingIndexedDB().catch(useChromeStorage);
return dbApi.exec(...args);
},
};
return dbApi;
function prepare() {
return withPromise(shouldUseIndexedDB).then(
ok => {
if (ok) {
useIndexedDB();
} else {
useChromeStorage();
}
},
err => {
useChromeStorage(err);
}
);
}
function shouldUseIndexedDB() {
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
@ -42,115 +31,81 @@ const db = (() => {
if (typeof indexedDB === 'undefined') {
throw new Error('indexedDB is undefined');
}
// test localStorage
const fallbackSet = localStorage.dbInChromeStorage;
if (fallbackSet === 'true') {
return false;
switch (await getFallback()) {
case true: throw null;
case false: break;
default: await testDB();
}
if (fallbackSet === 'false') {
return true;
}
// test storage.local
return chromeLocal.get('dbInChromeStorage')
.then(data => {
if (data && data.dbInChromeStorage) {
return false;
}
return testDBSize()
.then(ok => ok || testDBMutation());
});
return useIndexedDB();
}
function withPromise(fn) {
try {
return Promise.resolve(fn());
} catch (err) {
return Promise.reject(err);
}
async function getFallback() {
return localStorage[FALLBACK] === 'true' ? true :
localStorage[FALLBACK] === 'false' ? false :
chromeLocal.getValue(FALLBACK);
}
function testDBSize() {
return dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1)
.then(event => (
event.target.result &&
event.target.result.length &&
event.target.result[0]
));
}
function testDBMutation() {
return dbExecIndexedDB('put', {id: -1})
.then(() => dbExecIndexedDB('get', -1))
.then(event => {
if (!event.target.result) {
throw new Error('failed to get previously put item');
}
if (event.target.result.id !== -1) {
throw new Error('item id is wrong');
}
return dbExecIndexedDB('delete', -1);
})
.then(() => true);
async function testDB() {
let e = await dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1);
// throws if result is null
e = e.target.result[0];
const id = `${performance.now()}.${Math.random()}.${Date.now()}`;
await dbExecIndexedDB('put', {id});
e = await dbExecIndexedDB('get', id);
// throws if result or id is null
await dbExecIndexedDB('delete', e.target.result.id);
}
function useChromeStorage(err) {
exec = createChromeStorageDB().exec;
chromeLocal.set({dbInChromeStorage: true}, ignoreChromeError);
chromeLocal.setValue(FALLBACK, true);
if (err) {
chromeLocal.setValue('dbInChromeStorageReason', workerUtil.cloneError(err));
chromeLocal.setValue(FALLBACK + 'Reason', workerUtil.cloneError(err));
console.warn('Failed to access indexedDB. Switched to storage API.', err);
}
localStorage.dbInChromeStorage = 'true';
localStorage[FALLBACK] = 'true';
return createChromeStorageDB().exec;
}
function useIndexedDB() {
exec = dbExecIndexedDB;
chromeLocal.set({dbInChromeStorage: false}, ignoreChromeError);
localStorage.dbInChromeStorage = 'false';
chromeLocal.setValue(FALLBACK, false);
localStorage[FALLBACK] = 'false';
return dbExecIndexedDB;
}
function dbExecIndexedDB(method, ...args) {
return open().then(database => {
if (!method) {
return database;
}
if (method === 'putMany') {
return putMany(database, ...args);
}
const mode = method.startsWith('get') ? 'readonly' : 'readwrite';
const transaction = database.transaction(['styles'], mode);
const store = transaction.objectStore('styles');
return storeRequest(store, method, ...args);
async function dbExecIndexedDB(method, ...args) {
const mode = method.startsWith('get') ? 'readonly' : 'readwrite';
const store = (await open()).transaction([STORE], mode).objectStore(STORE);
const fn = method === 'putMany' ? putMany : storeRequest;
return fn(store, method, ...args);
}
function storeRequest(store, method, ...args) {
return new Promise((resolve, reject) => {
const request = store[method](...args);
request.onsuccess = resolve;
request.onerror = reject;
});
}
function storeRequest(store, method, ...args) {
return new Promise((resolve, reject) => {
const request = store[method](...args);
request.onsuccess = resolve;
request.onerror = reject;
function putMany(store, _method, items) {
return Promise.all(items.map(item => storeRequest(store, 'put', item)));
}
function open() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DATABASE, 2);
request.onsuccess = () => resolve(request.result);
request.onerror = reject;
request.onupgradeneeded = create;
});
}
function create(event) {
if (event.oldVersion === 0) {
event.target.result.createObjectStore(STORE, {
keyPath: 'id',
autoIncrement: true,
});
}
function open() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('stylish', 2);
request.onsuccess = () => resolve(request.result);
request.onerror = reject;
request.onupgradeneeded = event => {
if (event.oldVersion === 0) {
event.target.result.createObjectStore('styles', {
keyPath: 'id',
autoIncrement: true,
});
}
};
});
}
function putMany(database, items) {
const transaction = database.transaction(['styles'], 'readwrite');
const store = transaction.objectStore('styles');
return Promise.all(items.map(item => storeRequest(store, 'put', item)));
}
}
})();

View File

@ -49,8 +49,9 @@ const navigatorUtil = (() => {
}
return browser.tabs.get(data.tabId)
.then(tab => {
if (tab.url === 'chrome://newtab/') {
data.url = tab.url;
const url = tab.pendingUrl || tab.url;
if (url === 'chrome://newtab/') {
data.url = url;
}
});
}

View File

@ -57,7 +57,7 @@
continue;
}
for (const part in PARTS) {
const text = style[part];
const text = part === 'name' ? style.customName || style.name : style[part];
if (text && PARTS[part](text, rx, words, icase)) {
results.push(id);
break;

View File

@ -1,6 +1,6 @@
/* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */
/* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty
getStyleWithNoCode msg sync uuidv4 colorScheme */
/* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty styleSectionGlobal
getStyleWithNoCode msg prefs sync uuidv4 URLS colorScheme */
/* exported styleManager */
'use strict';
@ -61,6 +61,8 @@ const styleManager = (() => {
username: ''
};
const DELETE_IF_NULL = ['id', 'customName'];
handleLivePreviewConnections();
handleColorScheme();
@ -237,6 +239,13 @@ const styleManager = (() => {
if (!reason) {
reason = style ? 'update' : 'install';
}
let url = !data.url && data.updateUrl;
if (url) {
const usoId = URLS.extractUsoArchiveId(url);
url = usoId && `${URLS.usoArchive}?style=${usoId}` ||
URLS.extractGreasyForkId(url) && url.match(/^.*?\/\d+/)[0];
if (url) data.url = data.installationUrl = url;
}
// FIXME: update updateDate? what about usercss config?
return calcStyleDigest(data)
.then(digest => {
@ -391,8 +400,10 @@ const styleManager = (() => {
if (!style.name) {
throw new Error('style name is empty');
}
if (style.id == null) {
delete style.id;
for (const key of DELETE_IF_NULL) {
if (style[key] == null) {
delete style[key];
}
}
if (!style._id) {
style._id = uuidv4();
@ -460,7 +471,7 @@ const styleManager = (() => {
excludedScheme = true;
}
for (const section of data.sections) {
if (styleCodeEmpty(section.code)) {
if (styleSectionGlobal(section) && styleCodeEmpty(section.code)) {
continue;
}
const match = urlMatchSection(query, section);
@ -484,7 +495,7 @@ const styleManager = (() => {
return result;
}
function getSectionsByUrl(url, id) {
function getSectionsByUrl(url, id, isInitialApply) {
let cache = cachedStyleForUrl.get(url);
if (!cache) {
cache = {
@ -500,13 +511,13 @@ const styleManager = (() => {
.map(i => styles.get(i))
);
}
if (id) {
if (cache.sections[id]) {
return {[id]: cache.sections[id]};
}
return {};
}
return cache.sections;
const res = id
? cache.sections[id] ? {[id]: cache.sections[id]} : {}
: cache.sections;
// Avoiding flicker of needlessly applied styles by providing both styles & pref in one API call
return isInitialApply && prefs.get('disableAll')
? Object.assign({disableAll: true}, res)
: res;
function buildCache(styleList) {
const query = createMatchQuery(url);
@ -578,6 +589,16 @@ const styleManager = (() => {
touched = true;
}
}
// upgrade the old way of customizing local names
const {originalName} = style;
if (originalName) {
touched = true;
if (originalName !== style.name) {
style.customName = style.name;
style.name = originalName;
}
delete style.originalName;
}
return touched;
}
}
@ -605,7 +626,7 @@ const styleManager = (() => {
) {
return true;
}
if (section.urlPrefixes && section.urlPrefixes.some(p => query.url.startsWith(p))) {
if (section.urlPrefixes && section.urlPrefixes.some(p => p && query.url.startsWith(p))) {
return true;
}
// as per spec the fragment portion is ignored in @-moz-document:
@ -631,15 +652,7 @@ const styleManager = (() => {
return 'sloppy';
}
// TODO: check for invalid regexps?
if (
(!section.regexps || !section.regexps.length) &&
(!section.urlPrefixes || !section.urlPrefixes.length) &&
(!section.urls || !section.urls.length) &&
(!section.domains || !section.domains.length)
) {
return true;
}
return false;
return styleSectionGlobal(section);
}
function createCompiler(compile) {

View File

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

View File

@ -1,10 +1,11 @@
/* global chromeLocal promisifyChrome FIREFOX */
/* global chromeLocal promisifyChrome webextLaunchWebAuthFlow FIREFOX */
/* exported tokenManager */
'use strict';
const tokenManager = (() => {
promisifyChrome({
identity: ['launchWebAuthFlow'],
'windows': ['create', 'update', 'remove'],
'tabs': ['create', 'update', 'remove']
});
const AUTH = {
dropbox: {
@ -36,7 +37,7 @@ const tokenManager = (() => {
scopes: ['https://www.googleapis.com/auth/drive.appdata'],
revoke: token => {
const params = {token};
return postQuery(`https://accounts.google.com/o/oauth2/revoke?${stringifyQuery(params)}`);
return postQuery(`https://accounts.google.com/o/oauth2/revoke?${new URLSearchParams(params)}`);
}
},
onedrive: {
@ -136,14 +137,6 @@ const tokenManager = (() => {
});
}
function stringifyQuery(obj) {
const search = new URLSearchParams();
for (const key of Object.keys(obj)) {
search.set(key, obj[key]);
}
return search.toString();
}
function authUser(name, k, interactive = false) {
const provider = AUTH[name];
const state = Math.random().toFixed(8).slice(2);
@ -159,10 +152,11 @@ const tokenManager = (() => {
if (provider.authQuery) {
Object.assign(query, provider.authQuery);
}
const url = `${provider.authURL}?${stringifyQuery(query)}`;
return browser.identity.launchWebAuthFlow({
const url = `${provider.authURL}?${new URLSearchParams(query)}`;
return webextLaunchWebAuthFlow({
url,
interactive
interactive,
redirect_uri: query.redirect_uri
})
.then(url => {
const params = new URLSearchParams(
@ -185,7 +179,7 @@ const tokenManager = (() => {
code,
grant_type: 'authorization_code',
client_id: provider.clientId,
redirect_uri: provider.redirect_uri || chrome.identity.getRedirectURL()
redirect_uri: query.redirect_uri
};
if (provider.clientSecret) {
body.client_secret = provider.clientSecret;
@ -209,11 +203,9 @@ const tokenManager = (() => {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
},
body: body ? new URLSearchParams(body) : null,
};
if (body) {
options.body = stringifyQuery(body);
}
return fetch(url, options)
.then(r => {
if (r.ok) {

View File

@ -116,7 +116,7 @@
}
function reportSuccess(saved) {
log(STATES.UPDATED + ` #${style.id} ${style.name}`);
log(STATES.UPDATED + ` #${style.id} ${style.customName || style.name}`);
const info = {updated: true, style: saved};
if (port) port.postMessage(info);
return info;
@ -139,7 +139,7 @@
if (typeof error === 'object' && error.message) {
error = error.message;
}
log(STATES.SKIPPED + ` (${error}) #${style.id} ${style.name}`);
log(STATES.SKIPPED + ` (${error}) #${style.id} ${style.customName || style.name}`);
const info = {error, STATES, style: getStyleWithNoCode(style)};
if (port) port.postMessage(info);
return info;
@ -207,13 +207,6 @@
// keep current state
delete json.enabled;
// keep local name customizations
if (style.originalName !== style.name && style.name !== json.name) {
delete json.name;
} else {
json.originalName = json.name;
}
const newStyle = Object.assign({}, style, json);
if (styleSectionsEqual(json, style, {checkSource: true})) {
// update digest even if save === false as there might be just a space added etc.

View File

@ -1,15 +1,8 @@
/* global API_METHODS usercss styleManager deepCopy openURL download URLS */
/* global API_METHODS usercss styleManager deepCopy */
/* exported usercssHelper */
'use strict';
const usercssHelper = (() => {
const installCodeCache = {};
const clearInstallCode = url => delete installCodeCache[url];
const isResponseText = r => /^text\/(css|plain)(;.*?)?$/i.test(r.headers.get('content-type'));
// in Firefox we have to use a content script to read file://
const fileLoader = !chrome.app && // not relying on navigator.ua which can be spoofed
(tabId => browser.tabs.executeScript(tabId, {file: '/content/install-hook-usercss.js'}).then(r => r[0]));
API_METHODS.installUsercss = installUsercss;
API_METHODS.editSaveUsercss = editSaveUsercss;
API_METHODS.configUsercssVars = configUsercssVars;
@ -17,50 +10,6 @@ const usercssHelper = (() => {
API_METHODS.buildUsercss = build;
API_METHODS.findUsercss = find;
API_METHODS.getUsercssInstallCode = url => {
// when the installer tab is reloaded after the cache is expired, this will throw intentionally
const {code, timer} = installCodeCache[url];
clearInstallCode(url);
clearTimeout(timer);
return code;
};
return {
testUrl(url) {
return url.includes('.user.') &&
/^(https?|file|ftps?):/.test(url) &&
/\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]);
},
/** @return {Promise<{ code:string, inTab:boolean } | false>} */
testContents(tabId, url) {
const isFile = url.startsWith('file:');
const inTab = isFile && Boolean(fileLoader);
return Promise.resolve(isFile || fetch(url, {method: 'HEAD'}).then(isResponseText))
.then(ok => ok && (inTab ? fileLoader(tabId) : download(url)))
.then(code => /==userstyle==/i.test(code) && {code, inTab});
},
openInstallerPage(tabId, url, {code, inTab} = {}) {
const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`;
if (inTab) {
browser.tabs.get(tabId).then(tab =>
openURL({
url: `${newUrl}&tabId=${tabId}`,
active: tab.active,
index: tab.index + 1,
openerTabId: tabId,
currentWindow: null,
}));
} else {
const timer = setTimeout(clearInstallCode, 10e3, url);
installCodeCache[url] = {code, timer};
chrome.tabs.update(tabId, {url: newUrl});
}
},
};
function buildMeta(style) {
if (style.usercssData) {
return Promise.resolve(style);

View File

@ -0,0 +1,82 @@
/* global API_METHODS openURL download URLS tabManager */
'use strict';
(() => {
const installCodeCache = {};
const clearInstallCode = url => delete installCodeCache[url];
const isContentTypeText = type => /^text\/(css|plain)(;.*?)?$/i.test(type);
// in Firefox we have to use a content script to read file://
const fileLoader = !chrome.app && (
async tabId =>
(await browser.tabs.executeScript(tabId, {file: '/content/install-hook-usercss.js'}))[0]);
const urlLoader =
async (tabId, url) => (
url.startsWith('file:') ||
tabManager.get(tabId, isContentTypeText.name) ||
isContentTypeText((await fetch(url, {method: 'HEAD'})).headers.get('content-type'))
) && download(url);
API_METHODS.getUsercssInstallCode = url => {
// when the installer tab is reloaded after the cache is expired, this will throw intentionally
const {code, timer} = installCodeCache[url];
clearInstallCode(url);
clearTimeout(timer);
return code;
};
// 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
}, {
urls: [
URLS.usoArchiveRaw + 'usercss/*.user.css',
'*://greasyfork.org/scripts/*/code/*.user.css',
'*://sleazyfork.org/scripts/*/code/*.user.css',
],
types: ['main_frame'],
}, ['blocking']);
// Remember Content-Type to avoid re-fetching of the headers in urlLoader as it can be very slow
chrome.webRequest.onHeadersReceived.addListener(({tabId, responseHeaders}) => {
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(','),
types: ['main_frame'],
}, ['responseHeaders']);
tabManager.onUpdate(async ({tabId, url, oldUrl = ''}) => {
if (url.includes('.user.') &&
/^(https?|file|ftps?):/.test(url) &&
/\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]) &&
!oldUrl.startsWith(URLS.installUsercss)) {
const inTab = url.startsWith('file:') && Boolean(fileLoader);
const code = await (inTab ? fileLoader : urlLoader)(tabId, url);
if (/==userstyle==/i.test(code)) {
openInstallerPage(tabId, url, {code, inTab});
}
}
});
function openInstallerPage(tabId, url, {code, inTab} = {}) {
const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`;
if (inTab) {
browser.tabs.get(tabId).then(tab =>
openURL({
url: `${newUrl}&tabId=${tabId}`,
active: tab.active,
index: tab.index + 1,
openerTabId: tabId,
currentWindow: null,
}));
} else {
const timer = setTimeout(clearInstallCode, 10e3, url);
installCodeCache[url] = {code, timer};
chrome.tabs.update(tabId, {url: newUrl});
}
}
})();

View File

@ -20,6 +20,7 @@ self.INJECTED !== 1 && (() => {
/** @type chrome.runtime.Port */
let port;
let lazyBadge = IS_FRAME;
let parentDomain;
// the popup needs a check as it's not a tab but can be opened in a tab manually for whatever reason
if (!IS_TAB) {
@ -42,13 +43,6 @@ self.INJECTED !== 1 && (() => {
window.addEventListener(orphanEventId, orphanCheck, true);
}
let parentDomain;
prefs.subscribe(['disableAll'], (key, value) => doDisableAll(value));
if (IS_FRAME) {
prefs.subscribe(['exposeIframes'], updateExposeIframes);
}
// detect media change in content script
// FIXME: move this to background page when following bugs are fixed:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1561546
@ -59,19 +53,52 @@ self.INJECTED !== 1 && (() => {
function onInjectorUpdate() {
if (!isOrphaned) {
updateCount();
updateExposeIframes();
const onOff = prefs[styleInjector.list.length ? 'subscribe' : 'unsubscribe'];
onOff(['disableAll'], updateDisableAll);
if (IS_FRAME) {
updateExposeIframes();
onOff(['exposeIframes'], updateExposeIframes);
}
}
}
function init() {
return STYLE_VIA_API ?
API.styleViaAPI({method: 'styleApply'}) :
API.getSectionsByUrl(getMatchUrl()).then(styleInjector.apply);
async function init() {
if (STYLE_VIA_API) {
await API.styleViaAPI({method: 'styleApply'});
} else {
const styles = chrome.app && getStylesViaXhr() ||
await API.getSectionsByUrl(getMatchUrl(), null, true);
if (styles.disableAll) {
delete styles.disableAll;
styleInjector.toggle(false);
}
await styleInjector.apply(styles);
}
}
function getStylesViaXhr() {
if (new RegExp(`(^|\\s|;)${chrome.runtime.id}=\\s*([-\\w]+)\\s*(;|$)`).test(document.cookie)) {
const data = RegExp.$2;
const disableAll = data[0] === '1';
const url = 'blob:' + chrome.runtime.getURL(data.slice(1));
document.cookie = `${chrome.runtime.id}=1; max-age=0`; // remove our cookie
let res;
try {
if (!disableAll) { // will get the styles asynchronously
const xhr = new XMLHttpRequest();
xhr.open('GET', url, false); // synchronous
xhr.send();
res = JSON.parse(xhr.response);
}
URL.revokeObjectURL(url);
} catch (e) {}
return res;
}
}
function getMatchUrl() {
let matchUrl = location.href;
if (!matchUrl.match(/^(http|file|chrome|ftp)/)) {
if (!chrome.tabs && !matchUrl.match(/^(http|file|chrome|ftp)/)) {
// dynamic about: and javascript: iframes don't have an URL yet
// so we'll try the parent frame which is guaranteed to have a real URL
try {
@ -145,7 +172,7 @@ self.INJECTED !== 1 && (() => {
}
}
function doDisableAll(disableAll) {
function updateDisableAll(key, disableAll) {
if (STYLE_VIA_API) {
API.styleViaAPI({method: 'prefChanged', prefs: {disableAll}});
} else {
@ -153,22 +180,18 @@ self.INJECTED !== 1 && (() => {
}
}
function fetchParentDomain() {
return parentDomain ?
Promise.resolve() :
API.getTabUrlPrefix()
.then(newDomain => {
parentDomain = newDomain;
});
}
function updateExposeIframes() {
if (!prefs.get('exposeIframes') || window === parent || !styleInjector.list.length) {
document.documentElement.removeAttribute('stylus-iframe');
async function updateExposeIframes(key, value = prefs.get('exposeIframes')) {
const attr = 'stylus-iframe';
const el = document.documentElement;
if (!el) return; // got no styles so styleInjector didn't wait for <html>
if (!value || !styleInjector.list.length) {
el.removeAttribute(attr);
} else {
fetchParentDomain().then(() => {
document.documentElement.setAttribute('stylus-iframe', parentDomain);
});
if (!parentDomain) parentDomain = await API.getTabUrlPrefix();
// Check first to avoid triggering DOM mutation
if (el.getAttribute(attr) !== parentDomain) {
el.setAttribute(attr, parentDomain);
}
}
}

View File

@ -0,0 +1,21 @@
/* global API */
'use strict';
// onCommitted may fire twice
// Note, we're checking against a literal `1`, not just `if (truthy)`,
// because <html id="INJECTED"> is exposed per HTML spec as a global variable and `window.INJECTED`.
if (window.INJECTED_GREASYFORK !== 1) {
window.INJECTED_GREASYFORK = 1;
addEventListener('message', async function onMessage(e) {
if (e.origin === location.origin &&
e.data &&
e.data.name &&
e.data.type === 'style-version-query') {
removeEventListener('message', onMessage);
const style = await API.findUsercss(e.data) || {};
const {version} = style.usercssData || {};
postMessage({type: 'style-version', version}, '*');
}
});
}

View File

@ -5,16 +5,18 @@ if (typeof self.oldCode !== 'string') {
self.oldCode = (document.querySelector('body > pre') || document.body).textContent;
chrome.runtime.onConnect.addListener(port => {
if (port.name !== 'downloadSelf') return;
port.onMessage.addListener(({id, timer}) => {
port.onMessage.addListener(({id, force}) => {
fetch(location.href, {mode: 'same-origin'})
.then(r => r.text())
.then(code => ({id, code: timer && code === self.oldCode ? null : code}))
.then(code => ({id, code: force || code !== self.oldCode ? code : null}))
.catch(error => ({id, error: error.message || `${error}`}))
.then(msg => {
port.postMessage(msg);
if (msg.code != null) self.oldCode = msg.code;
});
});
// FF keeps content scripts connected on navigation https://github.com/openstyles/stylus/issues/864
addEventListener('pagehide', () => port.disconnect(), {once: true});
});
}

View File

@ -1,7 +1,11 @@
/* global cloneInto msg API */
'use strict';
(() => {
// eslint-disable-next-line no-unused-expressions
/^\/styles\/(\d+)(\/([^/]*))?([?#].*)?$/.test(location.pathname) && (() => {
const styleId = RegExp.$1;
const pageEventId = `${performance.now()}${Math.random()}`;
window.dispatchEvent(new CustomEvent(chrome.runtime.id + '-install'));
window.addEventListener(chrome.runtime.id + '-install', orphanCheck, true);
@ -17,35 +21,18 @@
}, '*');
});
let gotBody = false;
let currentMd5;
new MutationObserver(observeDOM).observe(document.documentElement, {
childList: true,
subtree: true,
});
observeDOM();
const md5Url = getMeta('stylish-md5-url') || `https://update.userstyles.org/${styleId}.md5`;
Promise.all([
API.findStyle({md5Url}),
getResource(md5Url),
onDOMready(),
]).then(checkUpdatability);
function observeDOM() {
if (!gotBody) {
if (!document.body) return;
gotBody = true;
// TODO: remove the following statement when USO pagination title is fixed
document.title = document.title.replace(/^(\d+)&\w+=/, '#$1: ');
const md5Url = getMeta('stylish-md5-url') || location.href;
Promise.all([
API.findStyle({md5Url}),
getResource(md5Url)
])
.then(checkUpdatability);
}
if (document.getElementById('install_button')) {
onDOMready().then(() => {
requestAnimationFrame(() => {
sendEvent(sendEvent.lastEvent);
});
});
}
}
document.documentElement.appendChild(
Object.assign(document.createElement('script'), {
textContent: `(${inPageContext})('${pageEventId}')`,
}));
function onMessage(msg) {
switch (msg.method) {
@ -72,7 +59,7 @@
function checkUpdatability([installedStyle, md5]) {
// TODO: remove the following statement when USO is fixed
document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', {
document.dispatchEvent(new CustomEvent(pageEventId, {
detail: installedStyle && installedStyle.updateUrl,
}));
currentMd5 = md5;
@ -141,7 +128,6 @@
});
}
function onClick(event) {
if (onClick.processing || !orphanCheck()) {
return;
@ -227,13 +213,11 @@
}
}
function getMeta(name) {
const e = document.querySelector(`link[rel="${name}"]`);
return e ? e.getAttribute('href') : null;
}
function getResource(url, options) {
if (url.startsWith('#')) {
return Promise.resolve(document.getElementById(url.slice(1)).textContent);
@ -280,7 +264,6 @@
.catch(() => null);
}
function styleSectionsEqual({sections: a}, {sections: b}) {
if (!a || !b) {
return undefined;
@ -318,20 +301,12 @@
}
}
function onDOMready() {
if (document.readyState !== 'loading') {
return Promise.resolve();
}
return new Promise(resolve => {
document.addEventListener('DOMContentLoaded', function _() {
document.removeEventListener('DOMContentLoaded', _);
resolve();
});
});
return document.readyState !== 'loading'
? Promise.resolve()
: new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve, {once: true}));
}
function openSettings(countdown = 10e3) {
const button = document.querySelector('.customize_button');
if (button) {
@ -349,12 +324,12 @@
}
}
function orphanCheck() {
// TODO: switch to install-hook-usercss.js impl, and remove explicit orphanCheck() calls
if (chrome.i18n && chrome.i18n.getUILanguage()) {
return true;
}
try {
if (chrome.i18n.getUILanguage()) {
return true;
}
} catch (e) {}
// In Chrome content script is orphaned on an extension update/reload
// so we need to detach event listeners
window.removeEventListener(chrome.runtime.id + '-install', orphanCheck, true);
@ -366,132 +341,56 @@
}
})();
// run in page context
document.documentElement.appendChild(document.createElement('script')).text = '(' + (
() => {
document.currentScript.remove();
// spoof Stylish extension presence in Chrome
if (window.chrome && chrome.app) {
const realImage = window.Image;
window.Image = function Image(...args) {
return new Proxy(new realImage(...args), {
get(obj, key) {
return obj[key];
},
set(obj, key, value) {
if (key === 'src' && /^chrome-extension:/i.test(value)) {
setTimeout(() => typeof obj.onload === 'function' && obj.onload());
} else {
obj[key] = value;
}
return true;
},
});
};
}
// USO bug workaround: use the actual style settings in API response
let settings;
const originalResponseJson = Response.prototype.json;
document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) {
document.removeEventListener('stylusFixBuggyUSOsettings', _);
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425)
settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search.replace(/^\?/, ''));
if (!settings) {
Response.prototype.json = originalResponseJson;
function inPageContext(eventId) {
document.currentScript.remove();
const origMethods = {
json: Response.prototype.json,
byId: document.getElementById,
};
let vars;
// USO bug workaround: prevent errors in console after install and busy cursor
document.getElementById = id =>
origMethods.byId.call(document, id) ||
(/^(stylish-code|stylish-installed-style-installed-\w+|post-install-ad|style-install-unknown)$/.test(id)
? Object.assign(document.createElement('p'), {className: 'afterdownload-ad'})
: null);
// USO bug workaround: use the actual image data in customized settings
document.addEventListener(eventId, ({detail}) => {
vars = /\?/.test(detail) && new URL(detail).searchParams;
if (!vars) Response.prototype.json = origMethods.json;
}, {once: true});
Response.prototype.json = async function () {
const json = await origMethods.json.apply(this, arguments);
if (vars && json && Array.isArray(json.style_settings)) {
Response.prototype.json = origMethods.json;
const images = new Map();
for (const ss of json.style_settings) {
const value = vars.get('ik-' + ss.install_key);
if (value && ss.setting_type === 'image' && ss.style_setting_options) {
let isListed;
for (const opt of ss.style_setting_options) {
isListed |= opt.default = (opt.value === value);
}
images.set(ss.install_key, {url: value, isListed});
}
}
});
Response.prototype.json = function (...args) {
return originalResponseJson.call(this, ...args).then(json => {
if (!settings || typeof ((json || {}).style_settings || {}).every !== 'function') {
return json;
}
Response.prototype.json = originalResponseJson;
const images = new Map();
for (const jsonSetting of json.style_settings) {
let value = settings.get('ik-' + jsonSetting.install_key);
if (!value
|| !jsonSetting.style_setting_options
|| !jsonSetting.style_setting_options[0]) {
continue;
}
if (value.startsWith('ik-')) {
value = value.replace(/^ik-/, '');
const defaultItem = jsonSetting.style_setting_options.find(item => item.default);
if (!defaultItem || defaultItem.install_key !== value) {
if (defaultItem) {
defaultItem.default = false;
}
jsonSetting.style_setting_options.some(item => {
if (item.install_key === value) {
item.default = true;
return true;
}
});
}
} else if (jsonSetting.setting_type === 'image') {
jsonSetting.style_setting_options.some(item => {
if (item.default) {
item.default = false;
return true;
}
});
images.set(jsonSetting.install_key, value);
} else {
const item = jsonSetting.style_setting_options[0];
if (item.value !== value && item.install_key === 'placeholder') {
item.value = value;
}
}
}
if (images.size) {
new MutationObserver((_, observer) => {
if (!document.getElementById('style-settings')) {
return;
}
if (images.size) {
new MutationObserver((_, observer) => {
if (document.getElementById('style-settings')) {
observer.disconnect();
for (const [name, url] of images.entries()) {
for (const [name, {url, isListed}] of images) {
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
const elUrl = elRadio &&
document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
if (elUrl) {
elRadio.checked = !isListed;
elUrl.value = url;
}
}
}).observe(document, {childList: true, subtree: true});
}
return json;
});
};
}
) + `)('${chrome.runtime.getURL('').slice(0, -1)}')`;
// TODO: remove the following statement when USO pagination is fixed
if (location.search.includes('category=')) {
document.addEventListener('DOMContentLoaded', function _() {
document.removeEventListener('DOMContentLoaded', _);
new MutationObserver((_, observer) => {
if (!document.getElementById('pagination')) {
return;
}
}).observe(document, {childList: true, subtree: true});
}
observer.disconnect();
const category = '&' + location.search.match(/category=[^&]+/)[0];
const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])');
for (let i = 0; i < links.length; i++) {
links[i].href += category;
}
}).observe(document, {childList: true, subtree: true});
});
}
if (/^https?:\/\/userstyles\.org\/styles\/\d{3,}/.test(location.href)) {
new MutationObserver((_, observer) => {
const cssButton = document.getElementsByClassName('css_button');
if (cssButton.length) {
// Click on the "Show CSS Code" button to workaround the JS error
cssButton[0].click();
cssButton[0].click();
observer.disconnect();
}
}).observe(document, {childList: true, subtree: true});
return json;
};
}

View File

@ -8,7 +8,6 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
const PATCH_ID = 'transition-patch';
// styles are out of order if any of these elements is injected between them
const ORDERED_TAGS = new Set(['head', 'body', 'frameset', 'style', 'link']);
const IS_OWN_PAGE = Boolean(chrome.tabs);
// detect Chrome 65 via a feature it added since browser version can be spoofed
const isChromePre65 = chrome.app && typeof Worklet !== 'function';
const docRewriteObserver = RewriteObserver(_sort);
@ -159,7 +158,7 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
}
function _emitUpdate(value) {
_toggleObservers(!IS_OWN_PAGE && list.length);
_toggleObservers(list.length);
onUpdate();
return value;
}

View File

@ -18,6 +18,22 @@
}
</style>
<link id="cm-theme" rel="stylesheet">
<script src="js/polyfill.js"></script>
<script src="js/dom.js"></script>
<script src="js/messaging.js"></script>
<script src="js/msg.js"></script>
<script src="js/prefs.js"></script>
<script src="js/localization.js"></script>
<script src="js/script-loader.js"></script>
<script src="js/storage-util.js"></script>
<script src="content/style-injector.js"></script>
<script src="content/apply.js"></script>
<script src="edit/edit.js"></script> <!-- run it ASAP to send a request for the style -->
<link href="vendor/codemirror/lib/codemirror.css" rel="stylesheet">
<script src="vendor/codemirror/lib/codemirror.js"></script>
@ -63,45 +79,27 @@
<script src="vendor-overwrites/codemirror-addon/match-highlighter.js"></script>
<script src="js/polyfill.js"></script>
<script src="js/dom.js"></script>
<script src="js/messaging.js"></script>
<script src="js/prefs.js"></script>
<script src="js/localization.js"></script>
<script src="js/script-loader.js"></script>
<script src="js/storage-util.js"></script>
<script src="js/msg.js"></script>
<script src="js/worker-util.js"></script>
<script src="content/style-injector.js"></script>
<script src="content/apply.js"></script>
<link href="edit/global-search.css" rel="stylesheet">
<script src="edit/global-search.js"></script>
<script src="msgbox/msgbox.js" async></script>
<link href="edit/codemirror-default.css" rel="stylesheet">
<script src="edit/codemirror-default.js"></script>
<script src="edit/codemirror-factory.js"></script>
<script src="edit/util.js"></script>
<script src="edit/regexp-tester.js"></script>
<script src="edit/live-preview.js"></script>
<script src="edit/applies-to-line-widget.js"></script>
<script src="edit/reroute-hotkeys.js"></script>
<script src="edit/codemirror-factory.js"></script>
<link href="edit/global-search.css" rel="stylesheet">
<script src="edit/global-search.js"></script>
<script src="edit/colorpicker-helper.js"></script>
<script src="edit/beautify.js"></script>
<script src="edit/show-keymap-help.js"></script>
<script src="edit/refresh-on-view.js"></script>
<script src="edit/codemirror-themes.js"></script>
<script src="edit/source-editor.js"></script>
<script src="edit/sections-editor-section.js"></script>
<script src="edit/sections-editor.js"></script>
<script src="edit/edit.js"></script>
<script src="msgbox/msgbox.js" async></script>
<script src="js/worker-util.js"></script>
<script src="edit/linter.js"></script>
<script src="edit/linter-defaults.js"></script>
<script src="edit/linter-engines.js"></script>
@ -110,8 +108,6 @@
<script src="edit/linter-report.js"></script>
<script src="edit/linter-config-dialog.js"></script>
<link id="cm-theme" rel="stylesheet">
<template data-id="appliesTo">
<li class="applies-to-item">
<div class="select-resizer">
@ -285,7 +281,13 @@
<h1 id="heading">&nbsp;</h1> <!-- nbsp allocates the actual height which prevents page shift -->
<section id="basic-info">
<div id="basic-info-name">
<input id="name" class="style-contributor" spellcheck="false" required>
<input id="name" class="style-contributor" spellcheck="false">
<a id="reset-name" href="#" i18n-title="customNameResetHint" tabindex="0" hidden>
<svg class="svg-icon" viewBox="0 0 20 20">
<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 "/>
</svg>
</a>
<a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a>
</div>
<div id="basic-info-enabled">
@ -305,7 +307,7 @@
</section>
<section id="actions">
<div>
<button id="save-button" i18n-text="styleSaveLabel" data-hotkey-tooltip="save"></button>
<button id="save-button" i18n-text="styleSaveLabel" data-hotkey-tooltip="save" disabled></button>
<button id="beautify" i18n-text="styleBeautify"></button>
<a href="manage.html" tabindex="-1"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
</div>

View File

@ -7,16 +7,18 @@
}
.CodeMirror {
border: solid #CCC 1px;
transition: box-shadow .1s;
}
#stylus#stylus .CodeMirror {
/* Using a specificity hack to override userstyles */
/* Not using the ring-color hack as it became ugly in new Chrome */
outline: none !important;
}
.CodeMirror-lint-mark-warning {
background: none;
}
.CodeMirror-dialog {
-webkit-animation: highlight 3s cubic-bezier(.18, .02, 0, .94);
}
.CodeMirror-focused {
outline: -webkit-focus-ring-color auto 5px;
outline-offset: -2px;
animation: highlight 3s cubic-bezier(.18, .02, 0, .94);
}
.CodeMirror-bookmark {
background: linear-gradient(to right, currentColor, transparent);
@ -24,13 +26,6 @@
width: 2em;
opacity: .5;
}
@supports (-moz-appearance:none) {
/* restrict to FF */
.CodeMirror-focused {
outline: #7dadd9 auto 1px;
outline-offset: -1px;
}
}
.CodeMirror-search-field {
width: 10em;
}

View File

@ -105,14 +105,9 @@
}
Object.assign(CodeMirror.mimeModes['text/css'].propertyKeywords, {
'background-position-x': true,
'background-position-y': true,
'contain': true,
'mask-image': true,
'mix-blend-mode': true,
'content-visibility': true,
'overflow-anchor': true,
'overscroll-behavior': true,
'rotate': true,
'isolation': true,
});
Object.assign(CodeMirror.mimeModes['text/css'].colorKeywords, {
'darkgrey': true,
@ -320,7 +315,7 @@ CodeMirror.hint && (() => {
}
// USO vars in usercss mode editor
const vars = editor.getStyle().usercssData.vars;
const vars = editor.style.usercssData.vars;
const list = vars ?
Object.keys(vars).filter(name => name.startsWith(leftPart)) : [];
return {
@ -348,7 +343,7 @@ CodeMirror.hint && (() => {
string[start + 3] === '[' &&
string[pos - 3] === ']' &&
string[pos - 4] === ']') {
const vars = typeof editor !== 'undefined' && (editor.getStyle().usercssData || {}).vars;
const vars = typeof editor !== 'undefined' && (editor.style.usercssData || {}).vars;
const name = vars && string.slice(start + 4, pos - 4);
if (vars && Object.hasOwnProperty.call(vars, name.endsWith('-rgb') ? name.slice(0, -4) : name)) {
token[0] = USO_VALID_VAR;

View File

@ -20,13 +20,11 @@
defaults.extraKeys[keyName] = 'colorpicker';
}
defaults.colorpicker = {
// FIXME: who uses this?
// forceUpdate: editor.getEditors().length > 0,
tooltip: t('colorpickerTooltip'),
popup: {
tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'),
hexUppercase: prefs.get('editor.colorpicker.hexUppercase'),
hideDelay: 5000,
hideDelay: 30e3,
embedderCallback: state => {
['hexUppercase', 'color']
.filter(name => state[name] !== prefs.get('editor.colorpicker.' + name))

View File

@ -50,7 +50,6 @@ label {
top: 0;
padding: 1rem;
border-right: 1px dashed #AAA;
-webkit-box-shadow: 0 0 3rem -1.2rem black;
box-shadow: 0 0 3rem -1.2rem black;
box-sizing: border-box;
z-index: 10;
@ -63,6 +62,7 @@ label {
#sections {
padding-left: 280px;
min-height: 0;
height: 100%;
}
#sections h2 {
margin-top: 1rem;
@ -88,6 +88,9 @@ label {
display: flex;
align-items: center;
}
#reset-name {
margin: 0 .25em 0 .5em;
}
#url {
margin-left: 0.25rem;
}
@ -278,12 +281,6 @@ input:invalid {
.section-editor .section:not(:first-child) {
border-top: 2px solid hsl(0, 0%, 80%);
}
.section-editor:not(.section-editor-ready) .section {
opacity: 0 !important;
}
.section-editor:not(.section-editor-ready) .CodeMirror {
height: 0;
}
.add-section:after {
content: attr(short-text);
}
@ -616,6 +613,10 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
right: 4px;
top: .5em;
}
#help-popup input[type="search"],
#help-popup .CodeMirror {
margin: 3px;
}
.keymap-list {
font-size: 12px;
@ -793,6 +794,10 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
justify-items: normal;
}
.usercss .CodeMirror-focused {
box-shadow: none;
}
html:not(.usercss) .usercss-only,
.usercss #mozilla-format-container,
.usercss #sections > h2 {
@ -812,18 +817,8 @@ body.linter-disabled .hidden-unless-compact {
margin-top: .75rem;
}
.usercss #name {
background-color: #eee;
color: #888;
}
/* FIXME: remove the ID selector */
#sections .single-editor {
.single-editor {
height: 100%;
margin: 0;
padding: 0;
display: flex;
box-sizing: border-box;
}
.single-editor .CodeMirror {
@ -843,7 +838,6 @@ body.linter-disabled .hidden-unless-compact {
}
.usercss.firefox #sections,
.usercss.firefox .single-editor,
.usercss.firefox .CodeMirror {
height: 100%;
}
@ -996,7 +990,7 @@ body.linter-disabled .hidden-unless-compact {
flex-direction: column;
flex: 1;
}
#sections > * {
#sections > :not(.single-editor) {
margin: 0 .5rem;
padding: .5rem 0;
}

View File

@ -1,15 +1,11 @@
/* global CodeMirror onDOMready prefs setupLivePrefs $ $$ $create t tHTML
createSourceEditor sessionStorageHash getOwnTab FIREFOX API tryCatch
closeCurrentTab messageBox debounce workerUtil
initBeautifyButton ignoreChromeError
closeCurrentTab messageBox debounce
initBeautifyButton ignoreChromeError dirtyReporter linter
moveFocus msg createSectionsEditor rerouteHotkeys CODEMIRROR_THEMES */
/* exported showCodeMirrorPopup editorWorker toggleContextMenuDelete */
'use strict';
const editorWorker = workerUtil.createWorker({
url: '/edit/editor-worker.js'
});
let saveSizeOnClose;
// direct & reverse mapping of @-moz-document keywords and internal property names
@ -28,48 +24,84 @@ document.addEventListener('visibilitychange', beforeUnload);
window.addEventListener('beforeunload', beforeUnload);
msg.onExtension(onRuntimeMessage);
preinit();
lazyInit();
(() => {
onDOMready().then(() => {
prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip);
addEventListener('showHotkeyInTooltip', showHotkeyInTooltip);
showHotkeyInTooltip();
(async function init() {
const [style] = await Promise.all([
initStyleData(),
onDOMready(),
prefs.initializing.then(() => new Promise(resolve => {
const theme = prefs.get('editor.theme');
const el = $('#cm-theme');
if (theme === 'default') {
resolve();
} else {
// preload the theme so CodeMirror can use the correct metrics
el.href = `vendor/codemirror/theme/${theme}.css`;
el.addEventListener('load', resolve, {once: true});
}
})),
]);
const usercss = isUsercss(style);
const dirty = dirtyReporter();
let wasDirty = false;
let nameTarget;
buildThemeElement();
buildKeymapElement();
prefs.subscribe(['editor.linter'], updateLinter);
prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip);
addEventListener('showHotkeyInTooltip', showHotkeyInTooltip);
showHotkeyInTooltip();
buildThemeElement();
buildKeymapElement();
setupLivePrefs();
initNameArea();
initBeautifyButton($('#beautify'), () => editor.getEditors());
initResizeListener();
detectLayout();
updateTitle();
setupLivePrefs();
$('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle');
$('#preview-label').classList.toggle('hidden', !style.id);
editor = (usercss ? createSourceEditor : createSectionsEditor)({
style,
dirty,
updateName,
toggleStyle,
});
dirty.onChange(updateDirty);
await editor.ready;
initEditor();
// enabling after init to prevent flash of validation failure on an empty name
$('#name').required = !usercss;
$('#save-button').onclick = editor.save;
function getCodeMirrorThemes() {
if (!chrome.runtime.getPackageDirectoryEntry) {
const themes = [
chrome.i18n.getMessage('defaultTheme'),
...CODEMIRROR_THEMES
];
localStorage.codeMirrorThemes = themes.join(' ');
return Promise.resolve(themes);
}
return new Promise(resolve => {
chrome.runtime.getPackageDirectoryEntry(rootDir => {
rootDir.getDirectory('vendor/codemirror/theme', {create: false}, themeDir => {
themeDir.createReader().readEntries(entries => {
const themes = [
chrome.i18n.getMessage('defaultTheme')
].concat(
entries.filter(entry => entry.isFile)
.sort((a, b) => (a.name < b.name ? -1 : 1))
.map(entry => entry.name.replace(/\.css$/, ''))
);
localStorage.codeMirrorThemes = themes.join(' ');
resolve(themes);
});
});
});
function initNameArea() {
const nameEl = $('#name');
const resetEl = $('#reset-name');
const isCustomName = style.updateUrl || usercss;
nameTarget = isCustomName ? 'customName' : 'name';
nameEl.placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
nameEl.title = isCustomName ? t('customNameHint') : '';
nameEl.addEventListener('input', () => {
updateName(true);
resetEl.hidden = false;
});
resetEl.hidden = !style.customName;
resetEl.onclick = () => {
const style = editor.style;
nameEl.focus();
nameEl.select();
// trying to make it undoable via Ctrl-Z
if (!document.execCommand('insertText', false, style.name)) {
nameEl.value = style.name;
updateName(true);
}
style.customName = null; // to delete it from db
resetEl.hidden = true;
};
const enabledEl = $('#enabled');
enabledEl.onchange = () => updateEnabledness(enabledEl.checked);
}
function findKeyForCommand(command, map) {
@ -88,27 +120,10 @@ preinit();
}
function buildThemeElement() {
const themeElement = $('#editor.theme');
const themeList = localStorage.codeMirrorThemes;
const optionsFromArray = options => {
const fragment = document.createDocumentFragment();
options.forEach(opt => fragment.appendChild($create('option', opt)));
themeElement.appendChild(fragment);
};
if (themeList) {
optionsFromArray(themeList.split(/\s+/));
} else {
// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
const theme = prefs.get('editor.theme');
optionsFromArray([theme === 'default' ? t('defaultTheme') : theme]);
getCodeMirrorThemes().then(() => {
const themes = (localStorage.codeMirrorThemes || '').split(/\s+/);
optionsFromArray(themes);
themeElement.selectedIndex = Math.max(0, themes.indexOf(theme));
});
}
CODEMIRROR_THEMES.unshift(chrome.i18n.getMessage('defaultTheme'));
$('#editor.theme').append(...CODEMIRROR_THEMES.map(s => $create('option', s)));
// move the theme after built-in CSS so that its same-specificity selectors win
document.head.appendChild($('#cm-theme'));
}
function buildKeymapElement() {
@ -159,134 +174,120 @@ preinit();
}
}
function initEditor() {
return Promise.all([
initStyleData(),
onDOMready(),
prefs.initializing,
])
.then(([style]) => {
const usercss = isUsercss(style);
$('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle');
$('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
$('#name').title = usercss ? t('usercssReplaceTemplateName') : '';
$('#preview-label').classList.toggle('hidden', !style.id);
initBeautifyButton($('#beautify'), () => editor.getEditors());
const {onBoundsChanged} = chrome.windows || {};
if (onBoundsChanged) {
// * movement is reported even if the window wasn't resized
// * fired just once when done so debounce is not needed
onBoundsChanged.addListener(wnd => {
// getting the current window id as it may change if the user attached/detached the tab
chrome.windows.getCurrent(ownWnd => {
if (wnd.id === ownWnd.id) rememberWindowSize();
});
});
}
window.addEventListener('resize', () => {
if (!onBoundsChanged) debounce(rememberWindowSize, 100);
detectLayout();
function initResizeListener() {
const {onBoundsChanged} = chrome.windows || {};
if (onBoundsChanged) {
// * movement is reported even if the window wasn't resized
// * fired just once when done so debounce is not needed
onBoundsChanged.addListener(wnd => {
// getting the current window id as it may change if the user attached/detached the tab
chrome.windows.getCurrent(ownWnd => {
if (wnd.id === ownWnd.id) rememberWindowSize();
});
detectLayout();
editor = (usercss ? createSourceEditor : createSectionsEditor)({
style,
onTitleChanged: updateTitle
});
editor.dirty.onChange(updateDirty);
return Promise.resolve(editor.ready && editor.ready())
.then(updateDirty);
});
}
function updateTitle() {
if (editor) {
const styleName = editor.getStyle().name;
const isDirty = editor.dirty.isDirty();
document.title = (isDirty ? '* ' : '') + styleName;
}
}
function updateDirty() {
const isDirty = editor.dirty.isDirty();
document.body.classList.toggle('dirty', isDirty);
$('#save-button').disabled = !isDirty;
updateTitle();
}
})();
function preinit() {
// preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs()
new MutationObserver((mutations, observer) => {
const themeElement = $('#cm-theme');
if (themeElement) {
themeElement.href = prefs.get('editor.theme') === 'default' ? ''
: 'vendor/codemirror/theme/' + prefs.get('editor.theme') + '.css';
observer.disconnect();
}
}).observe(document, {subtree: true, childList: true});
if (chrome.windows) {
browser.tabs.query({currentWindow: true}).then(tabs => {
const windowId = tabs[0].windowId;
if (prefs.get('openEditInWindow')) {
if (
/true/.test(sessionStorage.saveSizeOnClose) &&
'left' in prefs.get('windowPosition', {}) &&
!isWindowMaximized()
) {
// window was reopened via Ctrl-Shift-T etc.
chrome.windows.update(windowId, prefs.get('windowPosition'));
}
if (tabs.length === 1 && window.history.length === 1) {
chrome.windows.getAll(windows => {
if (windows.length > 1) {
sessionStorageHash('saveSizeOnClose').set(windowId, true);
saveSizeOnClose = true;
}
});
} else {
saveSizeOnClose = sessionStorageHash('saveSizeOnClose').value[windowId];
}
}
window.addEventListener('resize', () => {
if (!onBoundsChanged) debounce(rememberWindowSize, 100);
detectLayout();
});
}
getOwnTab().then(tab => {
const ownTabId = tab.id;
function toggleStyle() {
$('#enabled').checked = !style.enabled;
updateEnabledness(!style.enabled);
}
// use browser history back when 'back to manage' is clicked
if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) {
onDOMready().then(() => {
$('#cancel-button').onclick = event => {
event.stopPropagation();
event.preventDefault();
history.back();
};
});
function updateDirty() {
const isDirty = dirty.isDirty();
if (wasDirty !== isDirty) {
wasDirty = isDirty;
document.body.classList.toggle('dirty', isDirty);
$('#save-button').disabled = !isDirty;
}
// no windows on android
if (!chrome.windows) {
updateTitle();
}
function updateEnabledness(enabled) {
dirty.modify('enabled', style.enabled, enabled);
style.enabled = enabled;
editor.updateLivePreview();
}
function updateName(isUserInput) {
if (!editor) return;
if (isUserInput) {
const {value} = $('#name');
dirty.modify('name', style[nameTarget] || style.name, value);
style[nameTarget] = value;
}
updateTitle({});
}
function updateTitle() {
document.title = `${dirty.isDirty() ? '* ' : ''}${style.customName || style.name}`;
}
function updateLinter(key, value) {
$('body').classList.toggle('linter-disabled', value === '');
linter.run();
}
})();
/* Stuff not needed for the main init so we can let it run at its own tempo */
async function lazyInit() {
const ownTabId = (await getOwnTab()).id;
// use browser history back when 'back to manage' is clicked
if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) {
onDOMready().then(() => {
$('#cancel-button').onclick = event => {
event.stopPropagation();
event.preventDefault();
history.back();
};
});
}
// no windows on android
if (!chrome.windows) {
return;
}
const tabs = await browser.tabs.query({currentWindow: true});
const windowId = tabs[0].windowId;
if (prefs.get('openEditInWindow')) {
if (
/true/.test(sessionStorage.saveSizeOnClose) &&
'left' in prefs.get('windowPosition', {}) &&
!isWindowMaximized()
) {
// window was reopened via Ctrl-Shift-T etc.
chrome.windows.update(windowId, prefs.get('windowPosition'));
}
if (tabs.length === 1 && window.history.length === 1) {
chrome.windows.getAll(windows => {
if (windows.length > 1) {
sessionStorageHash('saveSizeOnClose').set(windowId, true);
saveSizeOnClose = true;
}
});
} else {
saveSizeOnClose = sessionStorageHash('saveSizeOnClose').value[windowId];
}
}
chrome.tabs.onAttached.addListener((tabId, info) => {
if (tabId !== ownTabId) {
return;
}
// When an edit page gets attached or detached, remember its state
// so we can do the same to the next one to open.
chrome.tabs.onAttached.addListener((tabId, info) => {
if (tabId !== ownTabId) {
return;
if (info.newPosition !== 0) {
prefs.set('openEditInWindow', false);
return;
}
chrome.windows.get(info.newWindowId, {populate: true}, win => {
// If there's only one tab in this window, it's been dragged to new window
const openEditInWindow = win.tabs.length === 1;
if (openEditInWindow && FIREFOX) {
// FF-only because Chrome retardedly resets the size during dragging
chrome.windows.update(info.newWindowId, prefs.get('windowPosition'));
}
if (info.newPosition !== 0) {
prefs.set('openEditInWindow', false);
return;
}
chrome.windows.get(info.newWindowId, {populate: true}, win => {
// If there's only one tab in this window, it's been dragged to new window
const openEditInWindow = win.tabs.length === 1;
if (openEditInWindow && FIREFOX) {
// FF-only because Chrome retardedly resets the size during dragging
chrome.windows.update(info.newWindowId, prefs.get('windowPosition'));
}
prefs.set('openEditInWindow', openEditInWindow);
});
prefs.set('openEditInWindow', openEditInWindow);
});
});
}
@ -295,7 +296,7 @@ function onRuntimeMessage(request) {
switch (request.method) {
case 'styleUpdated':
if (
editor.getStyleId() === request.style.id &&
editor.style.id === request.style.id &&
!['editPreview', 'editPreviewEnd', 'editSave', 'config']
.includes(request.reason)
) {
@ -309,7 +310,7 @@ function onRuntimeMessage(request) {
}
break;
case 'styleDeleted':
if (editor.getStyleId() === request.style.id) {
if (editor.style.id === request.style.id) {
document.removeEventListener('visibilitychange', beforeUnload);
document.removeEventListener('beforeunload', beforeUnload);
closeCurrentTab();
@ -352,8 +353,7 @@ function isUsercss(style) {
}
function initStyleData() {
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425)
const params = new URLSearchParams(location.search.replace(/^\?/, ''));
const params = new URLSearchParams(location.search);
const id = Number(params.get('id'));
const createEmptyStyle = () => ({
name: params.get('domain') ||
@ -409,7 +409,7 @@ function showHelp(title = '', body) {
!event ||
event.type === 'click' ||
(
event.which === 27 &&
event.key === 'Escape' &&
!event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey &&
!$('.CodeMirror-hints, #message-box') &&
(
@ -470,7 +470,7 @@ function showCodeMirrorPopup(title, html, options) {
popup.style.pointerEvents = 'auto';
const onKeyDown = event => {
if (event.which === 9 && !event.ctrlKey && !event.altKey && !event.metaKey) {
if (event.key === 'Tab' && !event.ctrlKey && !event.altKey && !event.metaKey) {
const search = $('#search-replace-dialog');
const area = search && search.contains(document.activeElement) ? search : popup;
moveFocus(area, event.shiftKey ? -1 : 1);
@ -479,13 +479,12 @@ function showCodeMirrorPopup(title, html, options) {
};
window.addEventListener('keydown', onKeyDown, true);
window.addEventListener('closeHelp', function _() {
window.removeEventListener('closeHelp', _);
window.addEventListener('closeHelp', () => {
window.removeEventListener('keydown', onKeyDown, true);
document.documentElement.style.removeProperty('pointer-events');
rerouteHotkeys(true);
cm = popup.codebox = null;
});
}, {once: true});
return popup;
}
@ -505,10 +504,6 @@ function rememberWindowSize() {
}
}
prefs.subscribe(['editor.linter'], (key, value) => {
$('body').classList.toggle('linter-disabled', value === '');
});
function fixedHeader() {
const scrollPoint = $('#header').clientHeight - 40;
const linterEnabled = prefs.get('editor.linter') !== '';
@ -541,7 +536,7 @@ function detectLayout() {
body.classList.add('fixed-header');
}
}, 250);
window.addEventListener('scroll', fixedHeader);
window.addEventListener('scroll', fixedHeader, {passive: true});
}
} else {
body.classList.remove('compact-layout');

View File

@ -16,7 +16,6 @@ createAPI({
},
metalint: code => {
loadScript(
'/js/polyfill.js',
'/vendor/usercss-meta/usercss-meta.min.js',
'/vendor-overwrites/colorpicker/colorconverter.js',
'/js/meta-parser.js'

View File

@ -53,11 +53,10 @@
cm.on('changes', updateButtonState);
rerouteHotkeys(false);
window.addEventListener('closeHelp', function _() {
window.removeEventListener('closeHelp', _);
window.addEventListener('closeHelp', () => {
rerouteHotkeys(true);
cm = null;
});
}, {once: true});
loadScript([
'/vendor/codemirror/mode/javascript/javascript.js',

View File

@ -20,29 +20,19 @@
}
});
function stylelint(text, config, mode) {
function stylelint(text, config) {
return editorWorker.stylelint(text, config)
.then(({results}) => {
if (!results[0]) {
return [];
}
const output = results[0].warnings.map(({line, column: ch, text, severity}) =>
({
from: {line: line - 1, ch: ch - 1},
to: {line: line - 1, ch},
message: text
.replace('Unexpected ', '')
.replace(/^./, firstLetter => firstLetter.toUpperCase())
.replace(/\s*\([^(]+\)$/, ''), // strip the rule,
rule: text.replace(/^.*?\s*\(([^(]+)\)$/, '$1'),
severity,
})
);
return mode !== 'stylus' ?
output :
output.filter(({message}) =>
!message.includes('"@css"') || !message.includes('(at-rule-no-unknown)'));
});
.then(({results}) => !results[0] ? [] :
results[0].warnings.map(({line, column: ch, text, severity}) => ({
from: {line: line - 1, ch: ch - 1},
to: {line: line - 1, ch},
message: text
.replace('Unexpected ', '')
.replace(/^./, firstLetter => firstLetter.toUpperCase())
.replace(/\s*\([^(]+\)$/, ''), // strip the rule,
rule: text.replace(/^.*?\s*\(([^(]+)\)$/, '$1'),
severity,
})));
}
function csslint(text, config) {

View File

@ -1,6 +1,11 @@
/* global prefs */
/* global workerUtil */
'use strict';
/* exported editorWorker */
const editorWorker = workerUtil.createWorker({
url: '/edit/editor-worker.js'
});
/* exported linter */
const linter = (() => {
const lintingUpdatedListeners = [];
@ -59,8 +64,3 @@ const linter = (() => {
.then(results => [].concat(...results.filter(Boolean)));
}
})();
// FIXME: this should be put inside edit.js
prefs.subscribe(['editor.linter'], () => {
linter.run();
});

View File

@ -10,7 +10,7 @@ function createLivePreview(preprocess) {
const errorContainer = $('#preview-errors');
prefs.subscribe(['editor.livePreview'], (key, value) => {
if (value && data && data.id && data.enabled) {
if (value && data && data.id && (data.enabled || editor.dirty.has('enabled'))) {
previewer = createPreviewer();
previewer.update(data);
}

View File

@ -1,29 +0,0 @@
/* global CodeMirror */
/*
Initialization of the multi-sections editor is slow if there are many editors
e.g. https://github.com/openstyles/stylus/issues/178. So we only refresh the
editor when they were scroll into view.
*/
'use strict';
CodeMirror.defineExtension('refreshOnView', function () {
const cm = this;
if (typeof IntersectionObserver === 'undefined') {
// uh
cm.isRefreshed = true;
cm.refresh();
return;
}
const wrapper = cm.display.wrapper;
const observer = new IntersectionObserver(entries => {
for (const entry of entries) {
if (entry.isIntersecting) {
// wrapper.style.visibility = 'visible';
cm.isRefreshed = true;
cm.refresh();
observer.disconnect();
}
}
});
observer.observe(wrapper);
});

View File

@ -67,8 +67,7 @@ const regExpTester = (() => {
});
const getMatchInfo = m => m && {text: m[0], pos: m.index};
browser.tabs.query({}).then(tabs => {
const supported = tabs.map(tab => tab.url)
.filter(url => URLS.supported(url));
const supported = tabs.map(tab => tab.pendingUrl || tab.url).filter(URLS.supported);
const unique = [...new Set(supported).values()];
for (const rxData of regexps) {
const {rx, urls} = rxData;

View File

@ -206,36 +206,32 @@ function createSection({
}
function handleKeydown(cm, event) {
const key = event.which;
if (key < 37 || key > 40 || event.shiftKey || event.altKey || event.metaKey) {
if (event.shiftKey || event.altKey || event.metaKey) {
return;
}
const {key} = event;
const {line, ch} = cm.getCursor();
switch (key) {
case 37:
// arrow Left
case 'ArrowLeft':
if (line || ch) {
return;
}
// fallthrough to arrow Up
case 38:
// arrow Up
// fallthrough
case 'ArrowUp':
cm = line === 0 && prevEditor(cm, false);
if (!cm) {
return;
}
event.preventDefault();
event.stopPropagation();
cm.setCursor(cm.doc.size - 1, key === 37 ? 1e20 : ch);
cm.setCursor(cm.doc.size - 1, key === 'ArrowLeft' ? 1e20 : ch);
break;
case 39:
// arrow Right
case 'ArrowRight':
if (line < cm.doc.size - 1 || ch < cm.getLine(line).length - 1) {
return;
}
// fallthrough to arrow Down
case 40:
// arrow Down
// fallthrough
case 'ArrowDown':
cm = line === cm.doc.size - 1 && nextEditor(cm, false);
if (!cm) {
return;
@ -245,13 +241,6 @@ function createSection({
cm.setCursor(0, 0);
break;
}
// FIXME: what is this?
// const animation = (cm.getSection().firstElementChild.getAnimations() || [])[0];
// if (animation) {
// animation.playbackRate = -1;
// animation.currentTime = 2000;
// animation.play();
// }
}
function showAppliesToHelp(event) {
@ -296,19 +285,14 @@ function createSection({
function insertApplyAfter(init, base) {
const apply = createApply(init);
if (base) {
const index = appliesTo.indexOf(base);
appliesTo.splice(index + 1, 0, apply);
appliesToContainer.insertBefore(apply.el, base.el.nextSibling);
} else {
appliesTo.push(apply);
appliesToContainer.appendChild(apply.el);
}
appliesTo.splice(base ? appliesTo.indexOf(base) + 1 : appliesTo.length, 0, apply);
appliesToContainer.insertBefore(apply.el, base ? base.el.nextSibling : null);
dirty.add(apply, apply);
if (appliesTo.length > 1 && appliesTo[0].all) {
removeApply(appliesTo[0]);
}
emitSectionChange();
return apply;
}
function removeApply(apply) {
@ -380,7 +364,8 @@ function createSection({
}
$('.add-applies-to', el).addEventListener('click', e => {
e.preventDefault();
insertApplyAfter({type, value: ''}, apply);
const newApply = insertApplyAfter({type, value: ''}, apply);
$('input', newApply.el).focus();
});
return apply;

View File

@ -1,38 +1,24 @@
/* global dirtyReporter showHelp toggleContextMenuDelete createSection
/* global showHelp toggleContextMenuDelete createSection
CodeMirror linter createLivePreview showCodeMirrorPopup
sectionsToMozFormat messageBox clipString
rerouteHotkeys $ $$ $create t FIREFOX API
$ $$ $create t FIREFOX API
debounce */
/* exported createSectionsEditor */
'use strict';
function createSectionsEditor({style, onTitleChanged}) {
function createSectionsEditor(editorBase) {
const {style, dirty} = editorBase;
let INC_ID = 0; // an increment id that is used by various object to track the order
const dirty = dirtyReporter();
const container = $('#sections');
const sections = [];
container.classList.add('section-editor');
const nameEl = $('#name');
nameEl.addEventListener('input', () => {
dirty.modify('name', style.name, nameEl.value);
style.name = nameEl.value;
onTitleChanged();
});
const enabledEl = $('#enabled');
enabledEl.addEventListener('change', () => {
dirty.modify('enabled', style.enabled, enabledEl.checked);
style.enabled = enabledEl.checked;
updateLivePreview();
});
updateHeader();
$('#to-mozilla').addEventListener('click', showMozillaFormat);
$('#to-mozilla-help').addEventListener('click', showToMozillaHelp);
$('#from-mozilla').addEventListener('click', () => showMozillaFormatImport());
$('#save-button').addEventListener('click', saveStyle);
document.addEventListener('wheel', scrollEntirePageOnCtrlShift, {passive: false});
CodeMirror.defaults.extraKeys['Shift-Ctrl-Wheel'] = 'scrollWindow';
@ -47,78 +33,77 @@ function createSectionsEditor({style, onTitleChanged}) {
.forEach(e => e.addEventListener('mousedown', toggleContextMenuDelete));
}
let sectionOrder = '';
const initializing = new Promise(resolve => initSection({
sections: style.sections.slice(),
done:() => {
dirty.clear();
rerouteHotkeys(true);
resolve();
updateHeader();
sections.forEach(fitToContent);
const xo = window.IntersectionObserver && new IntersectionObserver(entries => {
for (const {isIntersecting, target} of entries) {
if (isIntersecting) {
target.CodeMirror.refresh();
xo.unobserve(target);
}
}
}));
}, {rootMargin: '100%'});
const refreshOnView = (cm, force) =>
force || !xo ?
cm.refresh() :
xo.observe(cm.display.wrapper);
let sectionOrder = '';
let headerOffset; // in compact mode the header is at the top so it reduces the available height
const ready = initSections(style.sections, {isFirstInit: true});
const livePreview = createLivePreview();
livePreview.show(Boolean(style.id));
return {
ready: () => initializing,
return Object.assign({}, editorBase, {
ready,
replaceStyle,
dirty,
getStyle: () => style,
getEditors,
scrollToEditor,
getStyleId: () => style.id,
getEditorTitle: cm => {
const index = sections.filter(s => !s.isRemoved()).findIndex(s => s.cm === cm);
return `${t('sectionCode')} ${index + 1}`;
},
save: saveStyle,
toggleStyle,
save,
nextEditor,
prevEditor,
closestVisible,
getSearchableInputs,
};
updateLivePreview,
});
function fitToContent(section) {
if (section.cm.isRefreshed) {
const {el, cm, cm: {display: {wrapper, sizer}}} = section;
if (cm.display.renderedView) {
resize();
} else {
section.cm.on('update', resize);
cm.on('update', resize);
}
function resize() {
let contentHeight = section.el.querySelector('.CodeMirror-sizer').offsetHeight;
if (contentHeight < section.cm.defaultTextHeight()) {
let contentHeight = sizer.offsetHeight;
if (contentHeight < cm.defaultTextHeight()) {
return;
}
contentHeight += 9; // border & resize grip
section.cm.off('update', resize);
const cmHeight = section.cm.getWrapperElement().offsetHeight;
const maxHeight = cmHeight + window.innerHeight - section.el.offsetHeight;
section.cm.setSize(null, Math.min(contentHeight, maxHeight));
if (sections.every(s => s.cm.isRefreshed)) {
fitToAvailableSpace();
if (headerOffset == null) {
headerOffset = el.getBoundingClientRect().top;
}
setTimeout(() => {
container.classList.add('section-editor-ready');
}, 50);
contentHeight += 9; // border & resize grip
cm.off('update', resize);
const cmHeight = wrapper.offsetHeight;
const maxHeight = (window.innerHeight - headerOffset) - (section.el.offsetHeight - cmHeight);
cm.setSize(null, Math.min(contentHeight, maxHeight));
}
}
function fitToAvailableSpace() {
const available =
Math.floor(container.offsetHeight - sections.reduce((h, s) => h + s.el.offsetHeight, 0)) ||
window.innerHeight - container.offsetHeight;
if (available <= 0) {
return;
const ch = container.offsetHeight;
let available = ch - sections[sections.length - 1].el.getBoundingClientRect().bottom + headerOffset;
if (available <= 1) available = window.innerHeight - ch - headerOffset;
const delta = Math.floor(available / sections.length);
if (delta > 1) {
sections.forEach(({cm}) => {
cm.setSize(null, cm.display.wrapper.offsetHeight + delta);
});
}
const cmHeights = sections.map(s => s.cm.getWrapperElement().offsetHeight);
sections.forEach((section, i) => {
section.cm.setSize(null, cmHeights[i] + Math.floor(available / sections.length));
});
}
function genId() {
@ -240,14 +225,6 @@ function createSectionsEditor({style, onTitleChanged}) {
return sections.filter(s => !s.isRemoved()).map(s => s.cm);
}
function toggleStyle() {
const newValue = !style.enabled;
dirty.modify('enabled', style.enabled, newValue);
style.enabled = newValue;
enabledEl.checked = newValue;
updateLivePreview();
}
function nextEditor(cm, cycle = true) {
if (!cycle && findLast(sections, s => !s.isRemoved()).cm === cm) {
return;
@ -367,7 +344,7 @@ function createSectionsEditor({style, onTitleChanged}) {
if (replaceOldStyle) {
return replaceSections(sections);
}
return new Promise(resolve => initSection({sections, done: resolve, focusOn: false}));
return initSections(sections, {focusOn: false});
})
.then(() => {
$('.dismiss').dispatchEvent(new Event('click'));
@ -411,7 +388,7 @@ function createSectionsEditor({style, onTitleChanged}) {
}
function validate() {
if (!nameEl.reportValidity()) {
if (!$('#name').reportValidity()) {
messageBox.alert(t('styleMissingName'));
return false;
}
@ -429,7 +406,7 @@ function createSectionsEditor({style, onTitleChanged}) {
return true;
}
function saveStyle() {
function save() {
if (!dirty.isDirty()) {
return;
}
@ -458,10 +435,10 @@ function createSectionsEditor({style, onTitleChanged}) {
}
function updateHeader() {
nameEl.value = style.name || '';
enabledEl.checked = style.enabled !== false;
$('#name').value = style.customName || style.name || '';
$('#enabled').checked = style.enabled !== false;
$('#url').href = style.url || '';
onTitleChanged();
editorBase.updateName();
}
function updateLivePreview() {
@ -472,36 +449,35 @@ function createSectionsEditor({style, onTitleChanged}) {
livePreview.update(getModel());
}
function initSection({
sections: originalSections,
total = originalSections.length,
function initSections(originalSections, {
focusOn = 0,
done
}) {
container.classList.add('hidden');
chunk();
function chunk() {
if (!originalSections.length) {
setGlobalProgress();
if (focusOn !== false) {
setTimeout(() => sections[focusOn].cm.focus());
}
container.classList.remove('hidden');
for (const section of sections) {
section.cm.refreshOnView();
}
if (done) {
done();
}
return;
}
isFirstInit,
} = {}) {
let done;
const total = originalSections.length;
originalSections = originalSections.slice();
return new Promise(resolve => {
done = resolve;
chunk(true);
});
function chunk(forceRefresh) {
const t0 = performance.now();
while (originalSections.length && performance.now() - t0 < 100) {
insertSectionAfter(originalSections.shift());
insertSectionAfter(originalSections.shift(), undefined, forceRefresh);
if (isFirstInit) dirty.clear();
if (focusOn !== false && sections[focusOn]) {
sections[focusOn].cm.focus();
focusOn = false;
}
}
setGlobalProgress(total - originalSections.length, total);
setTimeout(chunk);
if (!originalSections.length) {
setGlobalProgress();
fitToAvailableSpace();
done();
} else {
setTimeout(chunk);
}
}
}
@ -540,7 +516,7 @@ function createSectionsEditor({style, onTitleChanged}) {
updateLivePreview();
}
function insertSectionAfter(init, base) {
function insertSectionAfter(init, base, forceRefresh) {
if (!init) {
init = {code: '', urlPrefixes: ['http://example.com']};
}
@ -557,15 +533,18 @@ function createSectionsEditor({style, onTitleChanged}) {
prevEditor,
nextEditor
});
if (base) {
const index = sections.indexOf(base);
sections.splice(index + 1, 0, section);
container.insertBefore(section.el, base.el.nextSibling);
} else {
sections.push(section);
container.appendChild(section.el);
const {cm} = section;
sections.splice(base ? sections.indexOf(base) + 1 : sections.length, 0, section);
container.insertBefore(section.el, base ? base.el.nextSibling : null);
refreshOnView(cm, forceRefresh);
if (!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(scrollToEditor, 0, cm);
}
section.render();
updateSectionOrder();
section.onChange(updateLivePreview);
updateLivePreview();
@ -593,16 +572,17 @@ function createSectionsEditor({style, onTitleChanged}) {
updateSectionOrder();
}
function replaceSections(originalSections) {
function replaceSections(...args) {
for (const section of sections) {
section.remove(true);
}
sections.length = 0;
container.textContent = '';
return new Promise(resolve => initSection({sections: originalSections, done: resolve}));
return initSections(...args);
}
function replaceStyle(newStyle, codeIsUpdated) {
dirty.clear('name');
// FIXME: avoid recreating all editors?
reinit().then(() => {
Object.assign(style, newStyle);
@ -619,7 +599,7 @@ function createSectionsEditor({style, onTitleChanged}) {
function reinit() {
if (codeIsUpdated !== false) {
return replaceSections(newStyle.sections.slice());
return replaceSections(newStyle.sections, {isFirstInit: true});
}
return Promise.resolve();
}

View File

@ -1,4 +1,4 @@
/* global dirtyReporter
/* global
createAppliesToLineWidget messageBox
sectionsToMozFormat
createMetaCompiler linter createLivePreview cmFactory $ $create API prefs t
@ -6,17 +6,16 @@
/* exported createSourceEditor */
'use strict';
function createSourceEditor({style, onTitleChanged}) {
$('#name').disabled = true;
$('#save-button').disabled = true;
function createSourceEditor(editorBase) {
const {style, dirty} = editorBase;
let placeholderName = '';
$('#mozilla-format-container').remove();
$('#save-button').onclick = save;
$('#header').addEventListener('wheel', headerOnScroll);
$('#sections').textContent = '';
$('#sections').appendChild($create('.single-editor'));
const dirty = dirtyReporter();
// normalize style
if (!style.id) setupNewStyle(style);
@ -28,13 +27,6 @@ function createSourceEditor({style, onTitleChanged}) {
const livePreview = createLivePreview(preprocess);
livePreview.show(Boolean(style.id));
$('#enabled').onchange = function () {
const value = this.checked;
dirty.modify('enabled', style.enabled, value);
style.enabled = value;
updateLivePreview();
};
cm.on('changes', () => {
dirty.modify('sourceGeneration', savedGeneration, cm.changeGeneration());
updateLivePreview();
@ -46,14 +38,14 @@ function createSourceEditor({style, onTitleChanged}) {
metaCompiler.onUpdated(meta => {
style.usercssData = meta;
style.name = meta.name;
style.url = meta.homepageURL;
style.url = meta.homepageURL || style.installationUrl;
updateMeta();
});
linter.enableForEditor(cm);
updateMeta().then(() => {
linter.enableForEditor(cm);
let prevMode = NaN;
cm.on('optionChange', (cm, option) => {
if (option !== 'mode') return;
@ -122,7 +114,7 @@ function createSourceEditor({style, onTitleChanged}) {
return name;
}
function setupNewStyle(style) {
async function setupNewStyle(style) {
style.sections[0].code = ' '.repeat(prefs.get('editor.tabSize')) +
`/* ${t('usercssReplaceTemplateSectionBody')} */`;
let section = sectionsToMozFormat(style);
@ -143,33 +135,35 @@ function createSourceEditor({style, onTitleChanged}) {
dirty.clear('sourceGeneration');
style.sourceCode = '';
chromeSync.getLZValue('usercssTemplate').then(code => {
const name = style.name || t('usercssReplaceTemplateName');
const date = new Date().toLocaleString();
code = code || DEFAULT_CODE;
code = code.replace(/@name(\s*)(?=[\r\n])/, (str, space) =>
`${str}${space ? '' : ' '}${name} - ${date}`);
// strip the last dummy section if any, add an empty line followed by the section
style.sourceCode = code.replace(/\s*@-moz-document[^{]*\{[^}]*\}\s*$|\s+$/g, '') + '\n\n' + section;
cm.startOperation();
cm.setValue(style.sourceCode);
cm.clearHistory();
cm.markClean();
cm.endOperation();
dirty.clear('sourceGeneration');
savedGeneration = cm.changeGeneration();
});
placeholderName = `${style.name || t('usercssReplaceTemplateName')} - ${new Date().toLocaleString()}`;
let code = await chromeSync.getLZValue('usercssTemplate');
code = code || DEFAULT_CODE;
code = code.replace(/@name(\s*)(?=[\r\n])/, (str, space) =>
`${str}${space ? '' : ' '}${placeholderName}`);
// strip the last dummy section if any, add an empty line followed by the section
style.sourceCode = code.replace(/\s*@-moz-document[^{]*{[^}]*}\s*$|\s+$/g, '') + '\n\n' + section;
cm.startOperation();
cm.setValue(style.sourceCode);
cm.clearHistory();
cm.markClean();
cm.endOperation();
dirty.clear('sourceGeneration');
savedGeneration = cm.changeGeneration();
}
function updateMeta() {
$('#name').value = style.name;
const name = style.customName || style.name;
if (name !== placeholderName) {
$('#name').value = name;
}
$('#enabled').checked = style.enabled;
$('#url').href = style.url;
onTitleChanged();
editorBase.updateName();
return cm.setPreprocessor((style.usercssData || {}).preprocessor);
}
function replaceStyle(newStyle, codeIsUpdated) {
dirty.clear('name');
const sameCode = newStyle.sourceCode === cm.getValue();
if (sameCode) {
savedGeneration = cm.changeGeneration();
@ -210,14 +204,6 @@ function createSourceEditor({style, onTitleChanged}) {
}
}
function toggleStyle() {
const value = !style.enabled;
dirty.modify('enabled', style.enabled, value);
style.enabled = value;
updateMeta();
$('#enabled').dispatchEvent(new Event('change', {bubbles: true}));
}
function save() {
if (!dirty.isDirty()) return;
const code = cm.getValue();
@ -226,6 +212,7 @@ function createSourceEditor({style, onTitleChanged}) {
id: style.id,
enabled: style.enabled,
sourceCode: code,
customName: style.customName,
}))
.then(replaceStyle)
.catch(err => {
@ -372,19 +359,17 @@ function createSourceEditor({style, onTitleChanged}) {
(mode.helperType || '');
}
return {
return Object.assign({}, editorBase, {
ready: Promise.resolve(),
replaceStyle,
dirty,
getStyle: () => style,
getEditors: () => [cm],
scrollToEditor: () => {},
getStyleId: () => style.id,
getEditorTitle: () => '',
save,
toggleStyle,
prevEditor: cm => nextPrevMozDocument(cm, -1),
nextEditor: cm => nextPrevMozDocument(cm, 1),
closestVisible: () => cm,
getSearchableInputs: () => []
};
getSearchableInputs: () => [],
updateLivePreview,
});
}

View File

@ -54,18 +54,20 @@ button:active {
border-color: hsl(0, 0%, 50%);
}
input {
input {
font: inherit;
border: 1px solid hsl(0, 0%, 66%);
transition: border-color .1s, box-shadow .1s;
}
input:not([type]) {
input:not([type]),
input[type=search] {
background: #fff;
color: #000;
height: 22px;
min-height: 22px!important;
line-height: 22px;
padding: 0 3px;
font: inherit;
border: 1px solid hsl(0, 0%, 66%);
}
@ -208,9 +210,19 @@ select[disabled] + .select-arrow {
display: none !important;
}
:focus,
.CodeMirror-focused,
[data-focused-via-click] input[type="text"]:focus,
[data-focused-via-click] input[type="number"]:focus {
/* Using box-shadow instead of the ugly outline in new Chrome */
outline: none;
box-shadow: 0 0 0 1px hsl(180, 100%, 38%), 0 0 3px hsla(180, 100%, 60%, .5);
}
[data-focused-via-click] :focus,
[data-focused-via-click]:focus {
outline: none;
box-shadow: none;
}
@supports (-moz-appearance: none) {

View File

@ -291,9 +291,7 @@ li {
#header:not(.meta-init) > *:not(.lds-spinner),
#header.meta-init > .lds-spinner {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
opacity: 0;
@ -302,9 +300,7 @@ li {
#header.meta-init > * {
opacity: 1;
transition: opacity .5s;
-webkit-user-select: auto;
-moz-user-select: auto;
-ms-user-select: auto;
user-select: auto;
}
@ -323,14 +319,6 @@ label {
opacity: 0;
}
}
@-webkit-keyframes lds-spinner {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.lds-spinner {
position: absolute;
width: 200px;
@ -346,104 +334,74 @@ label {
left: 94px;
top: 23px;
position: absolute;
-webkit-animation: lds-spinner linear 1s infinite;
animation: lds-spinner linear 1s infinite;
background: currentColor;
width: 12px;
height: 34px;
border-radius: 20%;
-webkit-transform-origin: 6px 77px;
transform-origin: 6px 77px;
}
.lds-spinner div:nth-child(1) {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
-webkit-animation-delay: -0.916666666666667s;
animation-delay: -0.916666666666667s;
}
.lds-spinner div:nth-child(2) {
-webkit-transform: rotate(30deg);
transform: rotate(30deg);
-webkit-animation-delay: -0.833333333333333s;
animation-delay: -0.833333333333333s;
}
.lds-spinner div:nth-child(3) {
-webkit-transform: rotate(60deg);
transform: rotate(60deg);
-webkit-animation-delay: -0.75s;
animation-delay: -0.75s;
}
.lds-spinner div:nth-child(4) {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-animation-delay: -0.666666666666667s;
animation-delay: -0.666666666666667s;
}
.lds-spinner div:nth-child(5) {
-webkit-transform: rotate(120deg);
transform: rotate(120deg);
-webkit-animation-delay: -0.583333333333333s;
animation-delay: -0.583333333333333s;
}
.lds-spinner div:nth-child(6) {
-webkit-transform: rotate(150deg);
transform: rotate(150deg);
-webkit-animation-delay: -0.5s;
animation-delay: -0.5s;
}
.lds-spinner div:nth-child(7) {
-webkit-transform: rotate(180deg);
transform: rotate(180deg);
-webkit-animation-delay: -0.416666666666667s;
animation-delay: -0.416666666666667s;
}
.lds-spinner div:nth-child(8) {
-webkit-transform: rotate(210deg);
transform: rotate(210deg);
-webkit-animation-delay: -0.333333333333333s;
animation-delay: -0.333333333333333s;
}
.lds-spinner div:nth-child(9) {
-webkit-transform: rotate(240deg);
transform: rotate(240deg);
-webkit-animation-delay: -0.25s;
animation-delay: -0.25s;
}
.lds-spinner div:nth-child(10) {
-webkit-transform: rotate(270deg);
transform: rotate(270deg);
-webkit-animation-delay: -0.166666666666667s;
animation-delay: -0.166666666666667s;
}
.lds-spinner div:nth-child(11) {
-webkit-transform: rotate(300deg);
transform: rotate(300deg);
-webkit-animation-delay: -0.083333333333333s;
animation-delay: -0.083333333333333s;
}
.lds-spinner div:nth-child(12) {
-webkit-transform: rotate(330deg);
transform: rotate(330deg);
-webkit-animation-delay: 0s;
animation-delay: 0s;
}
@-webkit-keyframes load3 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes load3 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}

View File

@ -3,8 +3,7 @@
'use strict';
(() => {
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425)
const params = new URLSearchParams(location.search.replace(/^\?/, ''));
const params = new URLSearchParams(location.search);
const tabId = params.has('tabId') ? Number(params.get('tabId')) : -1;
const initialUrl = params.get('updateUrl');
@ -249,7 +248,7 @@
(!dup ?
Promise.resolve(true) :
messageBox.confirm(t('styleInstallOverwrite', [
data.name,
data.name + (dup.customName ? ` (${dup.customName})` : ''),
dupData.version,
data.version,
]))
@ -329,7 +328,9 @@
let sequence = null;
if (tabId < 0) {
getData = DirectDownloader();
sequence = API.getUsercssInstallCode(initialUrl).catch(getData);
sequence = API.getUsercssInstallCode(initialUrl)
.then(code => code || getData())
.catch(getData);
} else {
getData = PortDownloader();
sequence = getData({timer: false});
@ -342,7 +343,11 @@
onToggled(e) {
if (e) isEnabled = e.target.checked;
if (installed || installedDup) {
(isEnabled ? start : stop)();
if (isEnabled) {
check({force: true});
} else {
stop();
}
$('.install').disabled = isEnabled;
Object.assign($('#live-reload-install-hint'), {
hidden: !isEnabled,
@ -351,8 +356,8 @@
}
},
};
function check() {
getData()
function check(opts) {
getData(opts)
.then(update, logError)
.then(() => {
timer = 0;
@ -405,14 +410,16 @@
}
});
port.onDisconnect.addListener(() => {
browser.tabs.get(tabId)
.then(tab => tab.url === initialUrl && location.reload())
.catch(closeCurrentTab);
chrome.tabs.get(tabId, tab =>
!chrome.runtime.lastError && tab.url === initialUrl
? location.reload()
: closeCurrentTab());
});
return ({timer = true} = {}) => new Promise((resolve, reject) => {
return (opts = {}) => new Promise((resolve, reject) => {
const id = performance.now();
resolvers.set(id, {resolve, reject});
port.postMessage({id, timer});
opts.id = id;
port.postMessage(opts);
});
}
}

View File

@ -20,6 +20,9 @@ for (const type of [NodeList, NamedNodeMap, HTMLCollection, HTMLAllCollection])
}
}
$.isTextLikeInput = el =>
el.localName === 'input' && /^(text|search|number)$/.test(el.type);
$.remove = (selector, base = document) => {
const el = selector && typeof selector === 'string' ? $(selector, base) : selector;
if (el) {
@ -98,7 +101,11 @@ document.addEventListener('wheel', event => {
return;
}
if (el.tagName === 'SELECT') {
el.selectedIndex = Math.max(0, Math.min(el.length - 1, el.selectedIndex + Math.sign(event.deltaY)));
const old = el.selectedIndex;
el.selectedIndex = Math.max(0, Math.min(el.length - 1, old + Math.sign(event.deltaY)));
if (el.selectedIndex !== old) {
el.dispatchEvent(new Event('change', {bubbles: true}));
}
event.preventDefault();
}
event.stopImmediatePropagation();
@ -108,15 +115,9 @@ document.addEventListener('wheel', event => {
});
function onDOMready() {
if (document.readyState !== 'loading') {
return Promise.resolve();
}
return new Promise(resolve => {
document.addEventListener('DOMContentLoaded', function _() {
document.removeEventListener('DOMContentLoaded', _);
resolve();
});
});
return document.readyState !== 'loading'
? Promise.resolve()
: new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve, {once: true}));
}
@ -140,8 +141,7 @@ function animateElement(
onComplete,
} = {}) {
return element && new Promise(resolve => {
element.addEventListener('animationend', function _() {
element.removeEventListener('animationend', _);
element.addEventListener('animationend', () => {
element.classList.remove(
className,
// In Firefox, `resolve()` might be called one frame later.
@ -153,7 +153,7 @@ function animateElement(
onComplete.call(element);
}
resolve();
});
}, {once: true});
element.classList.add(className);
});
}
@ -351,20 +351,23 @@ function focusAccessibility() {
'a',
'button',
'input',
'textarea',
'label',
'select',
'summary',
];
// try to find a focusable parent for this many parentElement jumps:
const GIVE_UP_DEPTH = 4;
// allow outline on text/search inputs in addition to textareas
const isOutlineAllowed = el =>
!focusAccessibility.ELEMENTS.includes(el.localName) ||
$.isTextLikeInput(el);
addEventListener('mousedown', suppressOutlineOnClick, {passive: true});
addEventListener('keydown', keepOutlineOnTab, {passive: true});
function suppressOutlineOnClick({target}) {
for (let el = target, i = 0; el && i++ < GIVE_UP_DEPTH; el = el.parentElement) {
if (focusAccessibility.ELEMENTS.includes(el.localName)) {
if (!isOutlineAllowed(el)) {
focusAccessibility.lastFocusedViaClick = true;
if (el.dataset.focusedViaClick === undefined) {
el.dataset.focusedViaClick = '';
@ -375,7 +378,7 @@ function focusAccessibility() {
}
function keepOutlineOnTab(event) {
if (event.which === 9) {
if (event.key === 'Tab') {
focusAccessibility.lastFocusedViaClick = false;
setTimeout(keepOutlineOnTab, 0, true);
return;
@ -383,7 +386,7 @@ function focusAccessibility() {
return;
}
let el = document.activeElement;
if (!el || !focusAccessibility.ELEMENTS.includes(el.localName)) {
if (!el || isOutlineAllowed(el)) {
return;
}
if (el.dataset.focusedViaClick !== undefined) {

View File

@ -62,7 +62,19 @@ const URLS = {
// TODO: remove when "minimum_chrome_version": "61" or higher
chromeProtectsNTP: CHROME >= 61,
userstylesOrgJson: 'https://userstyles.org/styles/chrome/',
uso: 'https://userstyles.org/',
usoJson: 'https://userstyles.org/styles/chrome/',
usoArchive: 'https://33kk.github.io/uso-archive/',
usoArchiveRaw: 'https://raw.githubusercontent.com/33kk/uso-archive/flomaster/data/',
extractUsoArchiveId: url =>
url &&
url.startsWith(URLS.usoArchiveRaw) &&
parseInt(url.match(/\/(\d+)\.user\.css|$/)[1]),
extractGreasyForkId: url =>
/^https:\/\/(?:greasy|sleazy)fork\.org\/scripts\/(\d+)[^/]*\/code\/[^/]*\.user\.css$/.test(url) &&
RegExp.$1,
supported: url => (
url.startsWith('http') && (FIREFOX || !url.startsWith(URLS.browserWebStore)) ||
@ -132,7 +144,7 @@ function findExistingTab({url, currentWindow, ignoreHash = true, ignoreSearch =
.then(tabs => tabs.find(matchTab));
function matchTab(tab) {
const tabUrl = new URL(tab.url);
const tabUrl = new URL(tab.pendingUrl || tab.url);
return tabUrl.protocol === url.protocol &&
tabUrl.username === url.username &&
tabUrl.password === url.password &&
@ -153,57 +165,48 @@ function findExistingTab({url, currentWindow, ignoreHash = true, ignoreSearch =
* @param {number} [_.openerTabId] defaults to the active tab
* @param {Boolean} [_.active=true] `true` to activate the tab
* @param {Boolean|null} [_.currentWindow=true] `null` to check all windows
* @param {Boolean} [_.newWindow=false] `true` to open a new window
* @param {chrome.windows.CreateData} [_.windowPosition] options for chrome.windows.create
* @param {chrome.windows.CreateData} [_.newWindow] creates a new window with these params if specified
* @returns {Promise<chrome.tabs.Tab>} Promise -> opened/activated tab
*/
function openURL({
async function openURL({
url,
index,
openerTabId,
active = true,
currentWindow = true,
newWindow = false,
windowPosition,
newWindow,
}) {
if (!url.includes('://')) {
url = chrome.runtime.getURL(url);
}
return findExistingTab({url, currentWindow}).then(tab => {
if (tab) {
return activateTab(tab, {
index,
openerTabId,
// when hash is different we can only set `url` if it has # otherwise the tab would reload
url: url !== tab.url && url.includes('#') ? url : undefined,
});
}
if (newWindow && browser.windows) {
return browser.windows.create(Object.assign({url}, windowPosition))
.then(wnd => wnd.tabs[0]);
}
return getActiveTab().then((activeTab = {url: ''}) =>
isTabReplaceable(activeTab, url) ?
activateTab(activeTab, {url, openerTabId}) : // not moving the tab
createTabWithOpener(activeTab, {url, index, active}));
});
function createTabWithOpener(openerTab, options) {
const id = openerTabId == null ? openerTab.id : openerTabId;
if (id != null && !openerTab.incognito && openerTabIdSupported) {
options.openerTabId = id;
}
return browser.tabs.create(options);
let tab = await findExistingTab({url, currentWindow});
if (tab) {
return activateTab(tab, {
index,
openerTabId,
// when hash is different we can only set `url` if it has # otherwise the tab would reload
url: url !== (tab.pendingUrl || tab.url) && url.includes('#') ? url : undefined,
});
}
if (newWindow && browser.windows) {
return (await browser.windows.create(Object.assign({url}, newWindow)).tabs)[0];
}
tab = await getActiveTab() || {url: ''};
if (isTabReplaceable(tab, url)) {
return activateTab(tab, {url, openerTabId});
}
const id = openerTabId == null ? tab.id : openerTabId;
const opener = id != null && !tab.incognito && openerTabIdSupported && {openerTabId: id};
return browser.tabs.create(Object.assign({url, index, active}, opener));
}
// replace empty tab (NTP or about:blank)
// except when new URL is chrome:// or chrome-extension:// and the empty tab is
// in incognito
function isTabReplaceable(tab, newUrl) {
if (!tab || !URLS.emptyTab.includes(tab.url)) {
if (!tab || !URLS.emptyTab.includes(tab.pendingUrl || tab.url)) {
return false;
}
// FIXME: but why?
if (tab.incognito && newUrl.startsWith('chrome')) {
return false;
}
@ -439,7 +442,7 @@ function download(url, {
function collapseUsoVars(url) {
if (queryPos < 0 ||
url.length < 2000 ||
!url.startsWith(URLS.userstylesOrgJson) ||
!url.startsWith(URLS.usoJson) ||
!/^get$/i.test(method)) {
return url;
}

View File

@ -7,10 +7,12 @@
* Puts the global comments into the following section to minimize the amount of global sections.
* Doesn't move the comment with ==UserStyle== inside.
* @param {string} code
* @param {boolean} emptyDocument - https://github.com/stylus/stylus/issues/2415,
* TODO: update stylus-lang and remove emptyDocument everywhere
* @param {number} styleId - used to preserve parserCache on subsequent runs over the same style
* @returns {{sections: Array, errors: Array}}
*/
function parseMozFormat({code, styleId}) {
function parseMozFormat({code, emptyDocument, styleId}) {
const CssToProperty = {
'url': 'urls',
'url-prefix': 'urlPrefixes',
@ -18,7 +20,7 @@ function parseMozFormat({code, styleId}) {
'regexp': 'regexps',
};
const hasSingleEscapes = /([^\\]|^)\\([^\\]|$)/;
const parser = new parserlib.css.Parser();
const parser = new parserlib.css.Parser({starHack: true, emptyDocument});
const sectionStack = [{code: '', start: 0}];
const errors = [];
const sections = [];
@ -70,6 +72,13 @@ function parseMozFormat({code, styleId}) {
doAddSection(section);
});
parser.addListener('emptydocument', e => {
const token = parser._tokenStream._token;
const section = sectionStack[sectionStack.length - 1];
section.code += mozStyle.slice(section.start, e.offset);
section.start = token.offset + token.value.length;
});
parser.addListener('endstylesheet', () => {
// add nonclosed outer sections (either broken or the last global one)
const lastSection = sectionStack[sectionStack.length - 1];

View File

@ -33,7 +33,8 @@ self.msg = self.INJECTED === 1 ? self.msg : (() => {
onExtension,
off,
RX_NO_RECEIVER,
RX_PORT_CLOSED
RX_PORT_CLOSED,
isBg,
};
function getBg() {
@ -106,12 +107,13 @@ self.msg = self.INJECTED === 1 ? self.msg : (() => {
.then(tabs => {
const requests = [];
for (const tab of tabs) {
const isExtension = tab.url.startsWith(EXTENSION_URL);
const tabUrl = tab.pendingUrl || tab.url;
const isExtension = tabUrl.startsWith(EXTENSION_URL);
if (
tab.discarded ||
// FIXME: use `URLS.supported`?
!/^(http|ftp|file)/.test(tab.url) &&
!tab.url.startsWith('chrome://newtab/') &&
!/^(http|ftp|file)/.test(tabUrl) &&
!tabUrl.startsWith('chrome://newtab/') &&
!isExtension ||
isExtension && ignoreExtension ||
filter && !filter(tab)

View File

@ -3,27 +3,33 @@
// eslint-disable-next-line no-unused-expressions
self.INJECTED !== 1 && (() => {
// this part runs in workers, content scripts, our extension pages
//#region for content scripts and our extension pages
if (!Object.entries) {
Object.entries = obj => Object.keys(obj).map(k => [k, obj[k]]);
if (!window.browser || !browser.runtime) {
const createTrap = (base, parent) => {
const target = typeof base === 'function' ? () => {} : {};
target.isTrap = true;
return new Proxy(target, {
get: (target, prop) => {
if (target[prop]) return target[prop];
if (base[prop] && (typeof base[prop] === 'object' || typeof base[prop] === 'function')) {
target[prop] = createTrap(base[prop], base);
return target[prop];
}
return base[prop];
},
apply: (target, thisArg, args) => base.apply(parent, args)
});
};
window.browser = createTrap(chrome, null);
}
if (!Object.values) {
Object.values = obj => Object.keys(obj).map(k => obj[k]);
}
// don't use self.chrome. It is undefined in Firefox
if (typeof chrome !== 'object') return;
// the rest is for content scripts and our extension pages
self.browser = polyfillBrowser();
/* Promisifies the specified `chrome` methods into `browser`.
The definitions is an object like this: {
'storage.sync': ['get', 'set'], // if deeper than one level, combine the path via `.`
windows: ['create', 'update'], // items and sub-objects will only be created if present in `chrome`
} */
self.promisifyChrome = definitions => {
window.promisifyChrome = definitions => {
for (const [scopeName, methods] of Object.entries(definitions)) {
const path = scopeName.split('.');
const src = path.reduce((obj, p) => obj && obj[p], chrome);
@ -43,90 +49,33 @@ self.INJECTED !== 1 && (() => {
};
if (!chrome.tabs) return;
// the rest is for our extension pages
if (typeof document === 'object') {
const ELEMENT_METH = {
append: {
base: [Element, Document, DocumentFragment],
fn: (node, frag) => {
node.appendChild(frag);
}
},
prepend: {
base: [Element, Document, DocumentFragment],
fn: (node, frag) => {
node.insertBefore(frag, node.firstChild);
}
},
before: {
base: [Element, CharacterData, DocumentType],
fn: (node, frag) => {
node.parentNode.insertBefore(frag, node);
}
},
after: {
base: [Element, CharacterData, DocumentType],
fn: (node, frag) => {
node.parentNode.insertBefore(frag, node.nextSibling);
//#endregion
//#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 {
constructor(init) {
if (init && typeof init === 'object') {
super();
for (const [key, val] of Object.entries(init)) {
this.set(key, val);
}
} else {
super(...arguments);
}
}
};
for (const [key, {base, fn}] of Object.entries(ELEMENT_METH)) {
for (const cls of base) {
if (cls.prototype[key]) {
continue;
}
cls.prototype[key] = function (...nodes) {
const frag = document.createDocumentFragment();
for (const node of nodes) {
frag.appendChild(typeof node === 'string' ? document.createTextNode(node) : node);
}
fn(this, frag);
};
}
}
}
try {
if (!localStorage) {
throw new Error('localStorage is null');
}
localStorage._access_check = 1;
delete localStorage._access_check;
} catch (err) {
Object.defineProperty(self, 'localStorage', {value: {}});
}
try {
if (!sessionStorage) {
throw new Error('sessionStorage is null');
}
sessionStorage._access_check = 1;
delete sessionStorage._access_check;
} catch (err) {
Object.defineProperty(self, 'sessionStorage', {value: {}});
}
function polyfillBrowser() {
if (typeof browser === 'object' && browser.runtime) {
return browser;
}
return createTrap(chrome, null);
function createTrap(base, parent) {
const target = typeof base === 'function' ? () => {} : {};
target.isTrap = true;
return new Proxy(target, {
get: (target, prop) => {
if (target[prop]) return target[prop];
if (base[prop] && (typeof base[prop] === 'object' || typeof base[prop] === 'function')) {
target[prop] = createTrap(base[prop], base);
return target[prop];
}
return base[prop];
},
apply: (target, thisArg, args) => base.apply(parent, args)
});
}
}
//#endregion
})();

View File

@ -1,14 +1,18 @@
/* global promisifyChrome */
/* global promisifyChrome msg API */
'use strict';
// Needs msg.js loaded first
self.prefs = self.INJECTED === 1 ? self.prefs : (() => {
const defaults = {
'openEditInWindow': false, // new editor opens in a own browser window
'openEditInWindow.popup': false, // new editor opens in a simplified browser window without omnibox
'windowPosition': {}, // detached window position
'show-badge': true, // display text on popup menu icon
'disableAll': false, // boss key
'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
// checkbox in style config dialog
'config.autosave': true,
@ -32,9 +36,9 @@ self.prefs = self.INJECTED === 1 ? self.prefs : (() => {
'manage.onlyLocal.invert': false, // display only externally installed styles
'manage.onlyUsercss.invert': false, // display only non-usercss (standard) styles
// UI element state: expanded/collapsed
'manage.actions.expanded': true,
'manage.backup.expanded': true,
'manage.filters.expanded': true,
'manage.options.expanded': true,
// the new compact layout doesn't look good on Android yet
'manage.newUI': !navigator.appVersion.includes('Android'),
'manage.newUI.favicons': false, // show favicons for the sites in applies-to
@ -115,12 +119,11 @@ self.prefs = self.INJECTED === 1 ? self.prefs : (() => {
'storage.sync': ['get', 'set'],
});
const initializing = browser.storage.sync.get('settings')
.then(result => {
if (result.settings) {
setAll(result.settings, true);
}
});
const initializing = (
msg.isBg
? browser.storage.sync.get('settings').then(res => res.settings)
: API.getPrefs()
).then(res => res && setAll(res, true));
chrome.storage.onChanged.addListener((changes, area) => {
if (area !== 'sync' || !changes.settings || !changes.settings.newValue) {

View File

@ -31,18 +31,9 @@ const router = (() => {
}
function updateSearch(key, value) {
const search = new URLSearchParams(location.search.replace(/^\?/, ''));
if (!value) {
search.delete(key);
} else {
search.set(key, value);
}
const finalSearch = search.toString();
if (finalSearch) {
history.replaceState(history.state, null, `?${finalSearch}${location.hash}`);
} else {
history.replaceState(history.state, null, `${location.pathname}${location.hash}`);
}
const u = new URL(location);
u.searchParams[value ? 'set' : 'delete'](key, value);
history.replaceState(history.state, null, `${u}`);
update(true);
}
@ -66,7 +57,7 @@ const router = (() => {
}
function getSearch(key) {
return new URLSearchParams(location.search.replace(/^\?/, '')).get(key);
return new URLSearchParams(location.search).get(key);
}
function update(replace) {
@ -86,8 +77,7 @@ const router = (() => {
if (options.hash) {
state = options.hash === location.hash;
} else if (options.search) {
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425)
const search = new URLSearchParams(location.search.replace(/^\?/, ''));
const search = new URLSearchParams(location.search);
state = options.search.map(key => search.get(key));
}
if (!deepEqual(state, options.currentState)) {

View File

@ -48,74 +48,3 @@ const loadScript = (() => {
));
};
})();
(() => {
let subscribers, observer;
// natively declared <script> elements in html can't have onload= attribute
// due to the default extension CSP that forbids inline code (and we don't want to relax it),
// so we're using MutationObserver to add onload event listener to the script element to be loaded
window.onDOMscriptReady = (srcSuffix, timeout = 1000) => {
if (!subscribers) {
subscribers = new Map();
observer = new MutationObserver(observe);
observer.observe(document.head, {childList: true});
}
return new Promise((resolve, reject) => {
const listeners = subscribers.get(srcSuffix);
if (listeners) {
listeners.push(resolve);
} else {
subscribers.set(srcSuffix, [resolve]);
}
// a resolved Promise won't reject anymore
setTimeout(() => {
emptyAfterCleanup(srcSuffix);
reject(new Error('Timeout'));
}, timeout);
});
};
return;
function observe(mutations) {
for (const {addedNodes} of mutations) {
for (const n of addedNodes) {
if (n.src && getSubscribersForSrc(n.src)) {
n.addEventListener('load', notifySubscribers);
}
}
}
}
function getSubscribersForSrc(src) {
for (const [suffix, listeners] of subscribers.entries()) {
if (src.endsWith(suffix)) {
return {suffix, listeners};
}
}
}
function notifySubscribers(event) {
this.removeEventListener('load', notifySubscribers);
for (let data; (data = getSubscribersForSrc(this.src));) {
data.listeners.forEach(fn => fn(event));
if (emptyAfterCleanup(data.suffix)) {
return;
}
}
}
function emptyAfterCleanup(suffix) {
if (!subscribers) {
return true;
}
subscribers.delete(suffix);
if (!subscribers.size) {
observer.disconnect();
observer = null;
subscribers = null;
return true;
}
}
})();

View File

@ -1,4 +1,4 @@
/* exported styleSectionsEqual styleCodeEmpty calcStyleDigest styleJSONseemsValid */
/* exported styleSectionsEqual styleCodeEmpty styleSectionGlobal calcStyleDigest styleJSONseemsValid */
'use strict';
function styleCodeEmpty(code) {
@ -14,6 +14,14 @@ function styleCodeEmpty(code) {
return false;
}
/** Checks if section is global i.e. has no targets at all */
function styleSectionGlobal(section) {
return (!section.regexps || !section.regexps.length) &&
(!section.urlPrefixes || !section.urlPrefixes.length) &&
(!section.urls || !section.urls.length) &&
(!section.domains || !section.domains.length);
}
/**
* @param {Style} a - first style object
* @param {Style} b - second style object

View File

@ -71,7 +71,7 @@ const usercss = (() => {
.then(({sections, errors}) => {
if (!errors.length) errors = false;
if (!sections.length || errors && !allowErrors) {
throw errors;
throw errors || 'Style does not contain any actual CSS to apply.';
}
style.sections = sections;
return allowErrors ? {style, errors} : style;

View File

@ -149,8 +149,8 @@
<script src="js/polyfill.js"></script>
<script src="js/dom.js"></script>
<script src="js/messaging.js"></script>
<script src="js/prefs.js"></script>
<script src="js/msg.js"></script>
<script src="js/prefs.js"></script>
<script src="js/router.js"></script>
<script src="content/style-injector.js"></script>
<script src="content/apply.js"></script>
@ -278,8 +278,11 @@
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</div>
</div>
<div id="style-actions">
<div class="settings-column">
<details id="actions" data-pref="manage.actions.expanded">
<summary><h2 i18n-text="optionsActions"></h2></summary>
<div id="update-check">
<button id="check-all-updates" i18n-text="checkAllUpdates"><span id="update-progress"></span></button>
<a href="#" id="update-history" i18n-title="genericHistoryLabel" tabindex="0">
@ -313,85 +316,19 @@
</a>
</label>
</div>
</div>
<button id="manage-options-button" i18n-text="openOptions"></button>
</details>
</div>
<div class="settings-column">
<details id="options" data-pref="manage.options.expanded">
<summary><h2 id="options-heading" i18n-text="optionsHeading"></h2></summary>
<label>
<input id="manage.newUI" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
<span i18n-text="manageNewUI"></span>
</label>
<div id="newUIoptions">
<div>
<label for="manage.newUI.favicons" i18n-text="manageFavicons">
<input id="manage.newUI.favicons" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
<a href="#" data-toggle-on-click="#faviconsHelp" tabindex="0">
<svg class="svg-icon select-arrow">
<title i18n-text="optionsSubheading"></title>
<use xlink:href="#svg-icon-select-arrow"/>
</svg>
</a>
</label>
<div id="faviconsHelp" class="hidden" i18n-text="manageFaviconsHelp">
<div>
<label for="manage.newUI.faviconsGray" i18n-text="manageFaviconsGray">
<input id="manage.newUI.faviconsGray" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
</div>
</div>
<label><input id="manage.newUI.targets" type="number" min="1" max="99"><span i18n-text="manageMaxTargets"></span></label>
</div>
<div id="options-buttons">
<button id="manage-options-button" i18n-text="openOptions"></button>
<button id="manage-shortcuts-button" class="chromium-only"
i18n-text="shortcuts"
i18n-title="shortcutsNote"></button>
<a id="find-editor-styles"
href="https://userstyles.org/styles/browse/chrome-extension"
i18n-title="editorStylesButton"
target="_blank"><button i18n-text="cm_theme" tabindex="-1"></button></a>
</div>
</details>
<details id="backup" data-pref="manage.backup.expanded">
<summary><h2 id="backup-title" i18n-text="backupButtons"></h2></summary>
<span id="backup-message" i18n-text="backupMessage"></span>
<div id="backup-buttons">
<div class="dropdown">
<button class="dropbtn">
<span>Export</span>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</button>
<div class="dropdown-content">
<a href="#" id="file-all-styles" i18n-text="bckpInstStyles"></a>
<a id="sync-dropbox-export" i18n-text="syncDropboxStyles" i18n-title="syncDropboxDeprecated"></a>
</div>
</div>
<div class="dropdown">
<button class="dropbtn">
<span>Import</span>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</button>
<div class="dropdown-content">
<a href="#" id="unfile-all-styles" i18n-text="retrieveBckp"></a>
<a id="sync-dropbox-import" i18n-text="retrieveDropboxSync" i18n-title="syncDropboxDeprecated"></a>
</div>
</div>
<button id="file-all-styles" i18n-text="exportLabel"></button>
<button id="unfile-all-styles" i18n-text="importLabel"></button>
<button id="sync-styles" i18n-text="optionsCustomizeSync"></button>
</div>
</details>

View File

@ -23,7 +23,7 @@ function configDialog(style) {
vars.forEach(renderValueState);
return messageBox({
title: `${style.name} v${data.version}`,
title: `${style.customName || style.name} v${data.version}`,
className: 'config-dialog' + (isPopup ? ' stylus-popup' : ''),
contents: [
$create('.config-heading', data.supportURL &&

View File

@ -14,7 +14,7 @@ let initialized = false;
router.watch({search: ['search']}, ([search]) => {
$('#search').value = search || '';
if (!initialized) {
init();
initFilters();
initialized = true;
} else {
searchStyles();
@ -36,7 +36,7 @@ HTMLSelectElement.prototype.adjustWidth = function () {
parent.replaceChild(this, singleSelect);
};
function init() {
function initFilters() {
$('#search').oninput = e => {
router.updateSearch('search', e.target.value);
};

View File

@ -1,24 +1,14 @@
/* global messageBox styleSectionsEqual API onDOMready
tryJSONparse scrollElementIntoView $ $$ API $create t animateElement
styleJSONseemsValid */
/* exported bulkChangeQueue bulkChangeTime */
styleJSONseemsValid bulkChangeQueue */
'use strict';
const STYLISH_DUMP_FILE_EXT = '.txt';
const STYLUS_BACKUP_FILE_EXT = '.json';
let bulkChangeQueue = [];
let bulkChangeTime = 0;
onDOMready().then(() => {
$('#file-all-styles').onclick = event => {
event.preventDefault();
exportToFile();
};
$('#unfile-all-styles').onclick = event => {
event.preventDefault();
importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT});
};
$('#file-all-styles').onclick = () => exportToFile();
$('#unfile-all-styles').onclick = () => importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT});
Object.assign(document.body, {
ondragover(event) {
@ -141,7 +131,7 @@ function importFromString(jsonString) {
}
});
bulkChangeQueue.length = 0;
bulkChangeTime = performance.now();
bulkChangeQueue.time = performance.now();
return API.importManyStyles(items.map(i => i.item))
.then(styles => {
for (let i = 0; i < styles.length; i++) {

View File

@ -24,12 +24,12 @@ onDOMready().then(() => {
document.body.appendChild(input);
window.addEventListener('keydown', maybeRefocus, true);
function incrementalSearch({which}, immediately) {
function incrementalSearch({key}, immediately) {
if (!immediately) {
debounce(incrementalSearch, 100, {}, true);
return;
}
const direction = which === 38 ? -1 : which === 40 ? 1 : 0;
const direction = key === 'ArrowUp' ? -1 : key === 'ArrowDown' ? 1 : 0;
const text = input.value.toLocaleLowerCase();
if (!text.trim() || !direction && (text === prevText || focusedName.startsWith(text))) {
prevText = text;
@ -76,40 +76,32 @@ onDOMready().then(() => {
if (event.altKey || event.metaKey || $('#message-box')) {
return;
}
const inTextInput = event.target.matches('[type=text], [type=search], [type=number]');
const {which: k, key} = event;
// focus search field on "/" or Ctrl-F key
if (event.ctrlKey
? (event.code === 'KeyF' || !event.code && k === 70) && !event.shiftKey
: (key === '/' || !key && k === 191 && !event.shiftKey) && !inTextInput) {
const inTextInput = $.isTextLikeInput(event.target);
const {key, code, ctrlKey: ctrl} = event;
// `code` is independent of the current keyboard language
if ((code === 'KeyF' && ctrl && !event.shiftKey) ||
(code === 'Slash' || key === '/') && !ctrl && !inTextInput) {
// focus search field on "/" or Ctrl-F key
event.preventDefault();
$('#search').focus();
return;
}
if (event.ctrlKey || inTextInput) {
if (ctrl || inTextInput ||
key === ' ' && !input.value /* Space or Shift-Space is for page down/up */) {
return;
}
const time = performance.now();
if (
// 0-9
k >= 48 && k <= 57 ||
// a-z
k >= 65 && k <= 90 ||
// numpad keys
k >= 96 && k <= 111 ||
// marks
k >= 186
) {
if (key.length === 1) {
input.focus();
if (time - prevTime > 1000) {
input.value = '';
}
prevTime = time;
} else
if (k === 13 && focusedLink) {
if (key === 'Enter' && focusedLink) {
focusedLink.dispatchEvent(new MouseEvent('click', {bubbles: true}));
} else
if ((k === 38 || k === 40) && !event.shiftKey &&
if ((key === 'ArrowUp' || key === 'ArrowDown') && !event.shiftKey &&
time - prevTime < 5000 && incrementalSearch(event, true)) {
prevTime = time;
} else

View File

@ -54,7 +54,6 @@ a:hover {
top: 0;
padding: 1rem;
border-right: 1px dashed #AAA;
-webkit-box-shadow: 0 0 50px -18px black;
box-shadow: 0 0 50px -18px black;
overflow: auto;
box-sizing: border-box;
@ -285,13 +284,6 @@ a:hover {
margin-top: .5rem;
}
#options-buttons {
display: flex;
flex-wrap: wrap;
padding-top: .1rem;
}
#options-buttons button,
#backup-buttons button {
margin: 0 .2rem .5rem 0;
}
@ -557,8 +549,6 @@ a:hover {
.newUI .update-done .updated svg {
top: -4px;
position: relative;
/* unprefixed since Chrome 53 */
-webkit-filter: drop-shadow(0 4px 0 currentColor);
filter: drop-shadow(0 5px 0 currentColor);
}
@ -670,8 +660,6 @@ a:hover {
margin-left: -20px;
margin-right: 4px;
transition: opacity .5s, filter .5s;
/* unprefixed since Chrome 53 */
-webkit-filter: grayscale(1);
filter: grayscale(1);
/* workaround for the buggy CSS filter: images in the hidden overflow are shown on Mac */
backface-visibility: hidden;
@ -689,68 +677,7 @@ a:hover {
.newUI .entry:hover .target img {
opacity: 1;
/* unprefixed since Chrome 53 */
-webkit-filter: grayscale(0);
filter: grayscale(0);
}
#newUIoptions {
display: none;
}
.newUI #newUIoptions {
display: initial;
}
#newUIoptions > * {
display: flex;
align-items: center;
margin-bottom: auto;
flex-wrap: wrap;
position: relative;
}
#newUIoptions input[type="number"] {
width: 3em;
margin-right: .5em;
}
#newUIoptions [data-toggle-on-click="#faviconsHelp"] {
width: 14px;
height: 14px;
display: inline-block;
vertical-align: middle;
position: relative;
top: -1px;
}
#newUIoptions [data-toggle-on-click] > svg {
position: static;
}
#newUIoptions [data-toggle-on-click]:not([open]) > svg {
/* note: without ">" FF52 also transforms the nested svg inside <use> */
transform: rotate(-90deg);
}
html:not(.newUI) #newUIoptions + * {
margin-top: .5em;
}
input[id^="manage.newUI"] {
margin-left: 0;
}
#faviconsHelp {
overflow-y: auto;
font-size: 90%;
padding: 1ex 0 2ex 16px;
}
#faviconsHelp div {
display: flex;
align-items: center;
margin-top: 1ex;
filter: none;
}
/* Default, no update buttons */
@ -1046,54 +973,6 @@ input[id^="manage.newUI"] {
text-overflow: ellipsis;
}
/* export/import buttons */
#backup-buttons .dropbtn {
padding: 3px 7px;
cursor: pointer;
text-overflow: inherit;
}
#backup-buttons .dropbtn span {
display: inline-block;
margin-right: 12px;
}
#backup-buttons .dropdown {
position: relative;
display: inline-block;
}
#backup-buttons .dropdown-content {
display: none;
position: absolute;
background-color: #f9f9f9;
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
z-index: 1;
}
#backup-buttons .dropdown-content a {
color: black;
padding: 8px;
text-decoration: none;
display: block;
}
#backup-buttons .dropdown-content a:hover {
/* background-color: #f2f2f2 */
background-color: #e9e9e9
}
#backup-buttons .dropdown:hover .dropdown-content {
display: block;
}
#backup-buttons .dropdown:hover .dropbtn {
background-color: hsl(0, 0%, 95%);
border-color: hsl(0, 0%, 52%);
/* background-color: #3e8e41; */
}
/* sort font */
@font-face {
font-family: 'sorticon';
@ -1125,6 +1004,10 @@ input[id^="manage.newUI"] {
animation: fadeout .25s ease-in-out;
}
.settings-column {
margin-top: 1rem;
}
@keyframes fadein {
from {
opacity: 0;
@ -1302,14 +1185,3 @@ input[id^="manage.newUI"] {
margin-left: -2px;
}
}
/* Deprecated dropbox backup (dropbox-sync) */
#sync-dropbox-export,
#sync-dropbox-import {
opacity: 0.5;
cursor: not-allowed;
}
#backup-buttons .dropdown-content #sync-dropbox-export:hover,
#backup-buttons .dropdown-content #sync-dropbox-import:hover {
background: transparent;
}

View File

@ -4,11 +4,10 @@ global messageBox getStyleWithNoCode
checkUpdate handleUpdateInstalled
objectDiff
configDialog
sorter msg prefs API onDOMready $ $$ $create template setupLivePrefs
URLS enforceInputRange t tWordBreak formatDate
sorter msg prefs API $ $$ $create template setupLivePrefs
t tWordBreak formatDate
getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce
scrollElementIntoView CHROME VIVALDI FIREFOX router
bulkChangeTime:true bulkChangeQueue
scrollElementIntoView CHROME VIVALDI router
*/
'use strict';
@ -18,16 +17,27 @@ const ENTRY_ID_PREFIX_RAW = 'style-';
const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW;
const BULK_THROTTLE_MS = 100;
const bulkChangeQueue = [];
bulkChangeQueue.time = 0;
// define pref-mapped ids separately
const newUI = {
enabled: prefs.get('manage.newUI'),
favicons: prefs.get('manage.newUI.favicons'),
faviconsGray: prefs.get('manage.newUI.faviconsGray'),
targets: prefs.get('manage.newUI.targets'),
renderClass() {
document.documentElement.classList.toggle('newUI', newUI.enabled);
},
enabled: null, // the global option should come first
favicons: null,
faviconsGray: null,
targets: null,
};
// ...add utility functions
Object.assign(newUI, {
ids: Object.keys(newUI),
prefGroup: 'manage.newUI',
prefKeyForId: id => id === 'enabled' ? newUI.prefGroup : `${newUI.prefGroup}.${id}`,
renderClass: () => document.documentElement.classList.toggle('newUI', newUI.enabled),
});
// ...read the actual values
for (const id of newUI.ids) {
newUI[id] = prefs.get(newUI.prefKeyForId(id));
}
newUI.renderClass();
const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps'];
@ -40,23 +50,45 @@ Promise.all([
API.getAllStyles(true),
// FIXME: integrate this into filter.js
router.getSearch('search') && API.searchDB({query: router.getSearch('search')}),
Promise.all([
onDOMready(),
prefs.initializing,
])
.then(() => {
initGlobalEvents();
if (!VIVALDI) {
$$('#header select').forEach(el => el.adjustWidth());
}
if (FIREFOX && 'update' in (chrome.commands || {})) {
const btn = $('#manage-shortcuts-button');
btn.classList.remove('chromium-only');
btn.onclick = API.optionsCustomizeHotkeys;
}
}),
]).then(args => {
showStyles(...args);
waitForSelector('#installed'), // needed to avoid flicker due to an extra frame and layout shift
prefs.initializing
]).then(([styles, ids, el]) => {
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);
document.addEventListener('visibilitychange', onVisibilityChange);
// N.B. triggers existing onchange listeners
setupLivePrefs();
sorter.init();
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')}";
}
`));
if (!VIVALDI) {
$$('#header select').forEach(el => el.adjustWidth());
}
if (CHROME >= 80 && CHROME <= 88) {
// Wrong checkboxes are randomly checked after going back in history, https://crbug.com/1138598
addEventListener('pagehide', () => {
$$('input[type=checkbox]').forEach((el, i) => (el.name = `bug${i}`));
});
}
showStyles(styles, ids);
});
msg.onExtension(onRuntimeMessage);
@ -67,7 +99,7 @@ function onRuntimeMessage(msg) {
case 'styleAdded':
case 'styleDeleted':
bulkChangeQueue.push(msg);
if (performance.now() - bulkChangeTime < BULK_THROTTLE_MS) {
if (performance.now() - bulkChangeQueue.time < BULK_THROTTLE_MS) {
debounce(handleBulkChange, BULK_THROTTLE_MS);
} else {
handleBulkChange();
@ -82,74 +114,16 @@ function onRuntimeMessage(msg) {
setTimeout(sorter.updateStripes, 0, {onlyWhenColumnsChanged: true});
}
function initGlobalEvents() {
installed = $('#installed');
installed.onclick = handleEvent.entryClicked;
$('#manage-options-button').onclick = () => {
router.updateHash('#stylus-options');
};
{
const btn = $('#manage-shortcuts-button');
btn.onclick = btn.onclick || (() => openURL({url: URLS.configureCommands}));
}
$$('#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);
document.addEventListener('visibilitychange', onVisibilityChange);
$$('[data-toggle-on-click]').forEach(el => {
// dataset on SVG doesn't work in Chrome 49-??, works in 57+
const target = $(el.getAttribute('data-toggle-on-click'));
el.onclick = event => {
event.preventDefault();
target.classList.toggle('hidden');
if (target.classList.contains('hidden')) {
el.removeAttribute('open');
} else {
el.setAttribute('open', '');
}
};
});
// triggered automatically by setupLivePrefs() below
enforceInputRange($('#manage.newUI.targets'));
// N.B. triggers existing onchange listeners
setupLivePrefs();
sorter.init();
prefs.subscribe([
'manage.newUI',
'manage.newUI.favicons',
'manage.newUI.faviconsGray',
'manage.newUI.targets',
], () => 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')}";
}
`));
}
function showStyles(styles = [], matchUrlIds) {
const sorted = sorter.sort({
styles: styles.map(style => ({
style,
name: (style.name || '').toLocaleLowerCase() + '\n' + style.name,
})),
styles: styles.map(style => {
const name = style.customName || style.name || '';
return {
style,
// sort case-insensitively the whole list then sort dupes like `Foo` and `foo` case-sensitively
name: name.toLocaleLowerCase() + '\n' + name,
};
}),
});
let index = 0;
let firstRun = true;
@ -196,7 +170,7 @@ function showStyles(styles = [], matchUrlIds) {
}
function createStyleElement({style, name}) {
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' : ''}`];
@ -225,8 +199,9 @@ function createStyleElement({style, name}) {
}
const parts = createStyleElement.parts;
const configurable = style.usercssData && style.usercssData.vars && Object.keys(style.usercssData.vars).length > 0;
const name = style.customName || style.name;
parts.checker.checked = style.enabled;
parts.nameLink.textContent = tWordBreak(style.name);
parts.nameLink.textContent = tWordBreak(name);
parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id;
parts.homepage.href = parts.homepage.title = style.url || '';
if (!newUI.enabled) {
@ -243,7 +218,7 @@ function createStyleElement({style, name}) {
const entry = parts.entry.cloneNode(true);
entry.id = ENTRY_ID_PREFIX_RAW + style.id;
entry.styleId = style.id;
entry.styleNameLowerCase = name || style.name.toLocaleLowerCase();
entry.styleNameLowerCase = nameLC || name.toLocaleLowerCase() + '\n' + name;
entry.styleMeta = style;
entry.className = parts.entryClassBase + ' ' +
(style.enabled ? 'enabled' : 'disabled') +
@ -401,10 +376,11 @@ Object.assign(handleEvent, {
const openWindow = left && shift && !ctrl;
const openBackgroundTab = (middle && !shift) || (left && ctrl && !shift);
const openForegroundTab = (middle && shift) || (left && ctrl && shift);
const url = $('[href]', event.target.closest('.entry')).href;
const entry = event.target.closest('.entry');
const url = $('[href]', entry).href;
if (openWindow || openBackgroundTab || openForegroundTab) {
if (chrome.windows && openWindow) {
chrome.windows.create(Object.assign(prefs.get('windowPosition'), {url}));
API.openEditor({id: entry.styleId});
} else {
getOwnTab().then(({index}) => {
openURL({
@ -445,7 +421,7 @@ Object.assign(handleEvent, {
animateElement(entry);
messageBox({
title: t('deleteStyleConfirm'),
contents: entry.styleMeta.name,
contents: entry.styleMeta.customName || entry.styleMeta.name,
className: 'danger center',
buttons: [t('confirmDelete'), t('confirmCancel')],
})
@ -489,7 +465,7 @@ Object.assign(handleEvent, {
const y = Math.max(0, top);
const first = document.elementFromPoint(x, y);
const lastOffset = first.offsetTop + window.innerHeight;
const numTargets = prefs.get('manage.newUI.targets');
const numTargets = newUI.targets;
let entry = first && first.closest('.entry') || installed.children[0];
while (entry && entry.offsetTop <= lastOffset) {
favicons.push(...$$('img', entry).slice(0, numTargets).filter(img => img.dataset.src));
@ -541,7 +517,7 @@ function handleBulkChange() {
const {id} = msg.style;
if (msg.method === 'styleDeleted') {
handleDelete(id);
bulkChangeTime = performance.now();
bulkChangeQueue.time = performance.now();
} else {
handleUpdateForId(id, msg);
}
@ -552,7 +528,7 @@ function handleBulkChange() {
function handleUpdateForId(id, opts) {
return API.getStyle(id, true).then(style => {
handleUpdate(style, opts);
bulkChangeTime = performance.now();
bulkChangeQueue.time = performance.now();
});
}
@ -619,10 +595,8 @@ function switchUI({styleOnly} = {}) {
const current = {};
const changed = {};
let someChanged = false;
// ensure the global option is processed first
for (const el of [$('#manage.newUI'), ...$$('[id^="manage.newUI."]')]) {
const id = el.id.replace(/^manage\.newUI\.?/, '') || 'enabled';
const value = el.type === 'checkbox' ? el.checked : Number(el.value);
for (const id of newUI.ids) {
const value = prefs.get(newUI.prefKeyForId(id));
const valueChanged = value !== newUI[id] && (id === 'enabled' || current.enabled);
current[id] = value;
changed[id] = valueChanged;
@ -642,13 +616,11 @@ function switchUI({styleOnly} = {}) {
}
` + (newUI.faviconsGray ? `
.newUI .target img {
-webkit-filter: grayscale(1);
filter: grayscale(1);
opacity: .25;
}
` : `
.newUI .target img {
-webkit-filter: none;
filter: none;
opacity: 1;
}
@ -730,6 +702,20 @@ function highlightEditedStyle() {
}
}
function waitForSelector(selector) {
// TODO: if used in other places, move to dom.js
// TODO: if used concurrently, rework to use just one observer internally
return new Promise(resolve => {
const mo = new MutationObserver(() => {
const el = $(selector);
if (el) {
mo.disconnect();
resolve(el);
}
});
mo.observe(document, {childList: true, subtree: true});
});
}
function embedOptions() {
let options = $('#stylus-embedded-options');

View File

@ -130,7 +130,7 @@ const sorter = (() => {
const sorted = sort({
styles: current.map(entry => ({
entry,
name: entry.styleNameLowerCase + '\n' + entry.styleMeta.name,
name: entry.styleNameLowerCase,
style: entry.styleMeta,
}))
});

View File

@ -1,7 +1,7 @@
{
"name": "Stylus",
"version": "1.5.13",
"minimum_chrome_version": "49",
"minimum_chrome_version": "55",
"description": "__MSG_description__",
"homepage_url": "https://add0n.com/stylus.html",
"manifest_version": 2,
@ -23,6 +23,9 @@
"identity",
"<all_urls>"
],
"optional_permissions": [
"declarativeContent"
],
"background": {
"scripts": [
"js/polyfill.js",
@ -38,6 +41,7 @@
"vendor/semver-bundle/semver.js",
"vendor/db-to-cloud/db-to-cloud.min.js",
"vendor/uuid/uuid.min.js",
"vendor/webext-launch-web-auth-flow/webext-launch-web-auth-flow.min.js",
"background/token-manager.js",
"background/sync.js",
"background/content-scripts.js",
@ -51,7 +55,9 @@
"background/icon-manager.js",
"background/background.js",
"background/usercss-helper.js",
"background/usercss-install-helper.js",
"background/style-via-api.js",
"background/style-via-xhr.js",
"background/search-db.js",
"background/update.js",
"background/openusercss-api.js"

View File

@ -135,12 +135,7 @@
}
.danger #message-box-buttons > button:not([data-focused-via-click]):first-child:focus {
outline: red auto 1px;
}
/* FF ignores color with 'auto' */
.firefox .danger #message-box-buttons > button:not([data-focused-via-click]):first-child:focus {
outline: red solid 1px;
box-shadow: 0 0 0 1px red; /* Using box-shadow instead of the ugly outline in new Chrome */
}
.non-windows #message-box-buttons {

View File

@ -62,28 +62,28 @@ function messageBox({
resolveWith({button: this.buttonIndex});
},
key(event) {
const {which, shiftKey, ctrlKey, altKey, metaKey, target} = event;
if (shiftKey && which !== 9 || ctrlKey || altKey || metaKey) {
const {key, shiftKey, ctrlKey, altKey, metaKey, target} = event;
if (shiftKey && key !== 'Tab' || ctrlKey || altKey || metaKey) {
return;
}
switch (which) {
case 13:
switch (key) {
case 'Enter':
if (target.closest(focusAccessibility.ELEMENTS.join(','))) {
return;
}
break;
case 27:
case 'Escape':
event.preventDefault();
event.stopPropagation();
break;
case 9:
case 'Tab':
moveFocus(messageBox.element, shiftKey ? -1 : 1);
event.preventDefault();
return;
default:
return;
}
resolveWith(which === 13 ? {enter: true} : {esc: true});
resolveWith(key === 'Enter' ? {enter: true} : {esc: true});
},
scroll() {
scrollTo(blockScroll.x, blockScroll.y);

View File

@ -41,6 +41,16 @@
<div id="options">
<div class="block">
<h1 i18n-text="cm_theme"></h1>
<div class="items">
<label>
<a i18n-text="optionsStylusThemes" target="_blank"
href="https://33kk.github.io/uso-archive/?category=chrome-extension&search=Stylus"></a>
</label>
</div>
</div>
<div class="block">
<h1 i18n-text="optionsCustomizeIcon"></h1>
<div class="items">
@ -101,6 +111,13 @@
<span></span>
</span>
</label>
<label>
<span i18n-text="popupOpenEditInPopup"></span>
<span class="onoffswitch">
<input type="checkbox" id="openEditInWindow.popup" class="slider">
<span></span>
</span>
</label>
<label>
<span i18n-text="popupStylesFirst"></span>
<span class="onoffswitch">
@ -125,6 +142,45 @@
</div>
</div>
<div class="block">
<h1 i18n-text="openManage"></h1>
<div class="items">
<label>
<span i18n-text="manageNewUI"></span>
<span class="onoffswitch">
<input type="checkbox" id="manage.newUI" class="slider">
<span></span>
</span>
</label>
<label>
<span i18n-text="manageFavicons">
<a data-cmd="note"
i18n-title="manageFaviconsHelp"
href="#"
class="svg-inline-wrapper"
tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</span>
<span class="onoffswitch">
<input type="checkbox" id="manage.newUI.favicons" class="slider">
<span></span>
</span>
</label>
<label>
<span i18n-text="manageFaviconsGray"></span>
<span class="onoffswitch">
<input type="checkbox" id="manage.newUI.faviconsGray" class="slider">
<span></span>
</span>
</label>
<label>
<span i18n-text="manageMaxTargets"></span>
<input id="manage.newUI.targets" type="number" min="1" max="99">
</label>
</div>
</div>
<div class="block" id="updates">
<h1 i18n-text="optionsCustomizeUpdate"></h1>
<div class="items">
@ -183,9 +239,25 @@
</h1>
</div>
<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">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</span>
<span class="onoffswitch">
<input type="checkbox" id="styleViaXhr" class="slider">
<span></span>
</span>
</label>
<label class="option-item">
<span i18n-text="optionsAdvancedExposeIframes">
<a data-cmd="note"
i18n-data-title="optionsAdvancedExposeIframesNote"
i18n-title="optionsAdvancedExposeIframesNote"
href="#"
class="svg-inline-wrapper"

View File

@ -3,9 +3,8 @@
.onoffswitch {
position: relative;
margin: 1ex 0;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.onoffswitch input {
@ -17,6 +16,7 @@
bottom: -10px;
left: -10px;
width: calc(100% + 12px);
border: 0;
}
#message-box .onoffswitch input {

View File

@ -343,10 +343,11 @@ html:not(.firefox):not(.opera) #updates {
#message-box.note {
align-items: center;
justify-content: center;
white-space: pre-wrap;
}
#message-box.note > div {
max-width: calc(100% - 5rem);
max-width: 40em;
top: unset;
right: unset;
position: relative;

View File

@ -5,16 +5,9 @@
'use strict';
setupLivePrefs();
enforceInputRange($('#popupWidth'));
$$('input[min], input[max]').forEach(enforceInputRange);
setTimeout(splitLongTooltips);
// https://github.com/openstyles/stylus/issues/822
if (!FIREFOX && CHROME >= 76 && CHROME <= 81) {
const dropboxOption = $('option[value="dropbox"]');
dropboxOption.disabled = true;
dropboxOption.setAttribute('title', t('hostDisabled'));
}
if (CHROME_HAS_BORDER_BUG) {
const borderOption = $('.chrome-no-popup-border');
if (borderOption) {
@ -44,6 +37,27 @@ if (FIREFOX && 'update' in (chrome.commands || {})) {
});
}
if (CHROME) {
// Show the option as disabled until the permission is actually granted
const el = $('#styleViaXhr');
el.addEventListener('click', () => {
if (el.checked && !chrome.declarativeContent) {
chrome.permissions.request({permissions: ['declarativeContent']}, ok => {
if (chrome.runtime.lastError || !ok) {
el.checked = false;
}
});
}
});
if (!chrome.declarativeContent) {
prefs.initializing.then(() => {
if (prefs.get('styleViaXhr')) {
el.checked = false;
}
});
}
}
// actions
$('#options-close-icon').onclick = () => {
top.dispatchEvent(new CustomEvent('closeOptions'));
@ -85,7 +99,7 @@ document.onclick = e => {
e.preventDefault();
messageBox({
className: 'note',
contents: target.title,
contents: target.dataset.title,
buttons: [t('confirmClose')],
});
}
@ -216,6 +230,7 @@ function splitLongTooltips() {
.map(s => s.replace(/(.{50,80}(?=.{40,}))\s+/g, '$1\n'))
.join('\n');
if (newTitle !== el.title) {
el.dataset.title = el.title;
el.title = newTitle;
}
}
@ -281,7 +296,7 @@ function customizeHotkeys() {
}
window.onkeydown = event => {
if (event.keyCode === 27) {
if (event.key === 'Escape') {
top.dispatchEvent(new CustomEvent('closeOptions'));
}
};

31
package-lock.json generated
View File

@ -1917,9 +1917,9 @@
"dev": true
},
"codemirror": {
"version": "5.56.0",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.56.0.tgz",
"integrity": "sha512-MfKVmYgifXjQpLSgpETuih7A7WTTIsxvKfSLGseTY5+qt0E1UD1wblZGM6WLenORo8sgmf+3X+WTe2WF7mufyw=="
"version": "5.58.0",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.58.0.tgz",
"integrity": "sha512-OUK+7EgaYnLyC0F09UWjckLWvviy02IDDGTW5Zmj60a3gdGnFtUM6rVsqrfl5+YSylQVQBNfAGG4KF7tQOb4/Q=="
},
"collection-visit": {
"version": "1.0.0",
@ -5065,9 +5065,9 @@
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
"dev": true
},
"lodash.defaults": {
@ -5324,9 +5324,9 @@
}
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
},
"minipass": {
@ -5425,14 +5425,6 @@
"dev": true,
"requires": {
"minimist": "^1.2.5"
},
"dependencies": {
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
}
}
},
"moment": {
@ -7942,6 +7934,11 @@
}
}
},
"webext-launch-web-auth-flow": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/webext-launch-web-auth-flow/-/webext-launch-web-auth-flow-0.1.0.tgz",
"integrity": "sha512-3W8ANT9/6uL6NX5SiaKQee439dfiS1NT8wSc+vmjly/2MmH7FBqGFBXLfBFw296w8OOqHNPnEdNcBkDGJQkDgg=="
},
"webext-tx-fix": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/webext-tx-fix/-/webext-tx-fix-0.3.3.tgz",

View File

@ -6,7 +6,7 @@
"repository": "openstyles/stylus",
"author": "Stylus Team",
"dependencies": {
"codemirror": "^5.56.0",
"codemirror": "^5.58.0",
"db-to-cloud": "^0.4.5",
"jsonlint": "^1.6.3",
"less-bundle": "github:openstyles/less-bundle#v0.1.0",
@ -15,7 +15,8 @@
"stylelint-bundle": "^8.0.0",
"stylus-lang-bundle": "^0.54.5",
"usercss-meta": "^0.9.0",
"uuid": "^8.1.0"
"uuid": "^8.1.0",
"webext-launch-web-auth-flow": "^0.1.0"
},
"devDependencies": {
"archiver": "^4.0.1",
@ -31,7 +32,7 @@
},
"scripts": {
"lint": "eslint \"**/*.js\" --cache",
"test": "npm run lint && web-ext lint",
"test": "npm run lint",
"update-locales": "tx pull --all && webext-tx-fix",
"update-transifex": "tx push -s",
"build-vendor": "shx rm -rf vendor/* && node tools/build-vendor",

View File

@ -120,9 +120,7 @@
<div class="search-result-actions">
<button class="search-result-install hidden" i18n-text="installButton"></button>
<button class="search-result-uninstall hidden" i18n-text="deleteStyleLabel"></button>
<button class="search-result-customize hidden"
i18n-text="configureStyle"
i18n-title="configureStyleOnHomepage"></button>
<button class="search-result-customize hidden" i18n-text="configureStyle"></button>
</div>
<dl class="search-result-meta">
<div data-type="author">
@ -182,8 +180,8 @@
<script src="js/dom.js"></script>
<script src="js/messaging.js"></script>
<script src="js/localization.js"></script>
<script src="js/prefs.js"></script>
<script src="js/msg.js"></script>
<script src="js/prefs.js"></script>
<script src="content/style-injector.js"></script>
<script src="content/apply.js"></script>
@ -254,6 +252,27 @@
<div id="search-results-error" class="hidden"></div>
<div id="search-results" class="hidden">
<div class="search-results-nav" data-type="top"></div>
<div id="search-params">
<input id="search-query" type="search" i18n-placeholder="search"
i18n-title="searchStyleQueryHint">
<div class="select-resizer">
<select id="search-order" i18n-title="sortStylesHelpTitle">
<option value="n" i18n-text="genericTitle">
<option value="u" i18n-text="searchResultUpdated">
<option value="t" i18n-text="searchResultInstallCount">
<option value="w" i18n-text="searchResultWeeklyCount">
<option value="r" i18n-text="searchResultRating">
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
<label>
<span class="checkbox-container">
<input id="search-globals" type="checkbox" checked>
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</span>
<span i18n-text="searchGlobalStyles"></span>
</label>
</div>
<div id="search-results-list"></div>
<div class="search-results-nav" data-type="bottom"></div>
</div>

View File

@ -9,14 +9,13 @@ const hotkeys = (() => {
let enabled = false;
let ready = false;
window.addEventListener('showStyles:done', function _() {
window.removeEventListener('showStyles:done', _);
window.addEventListener('showStyles:done', () => {
togglablesShown = true;
togglables = getTogglables();
ready = true;
setState(true);
initHotkeyInfo();
});
}, {once: true});
window.addEventListener('resize', adjustInfoPosition);
@ -33,44 +32,32 @@ const hotkeys = (() => {
}
function onKeyDown(event) {
if (event.ctrlKey || event.altKey || event.metaKey || !enabled) {
if (event.ctrlKey || event.altKey || event.metaKey || !enabled ||
/^(text|search)$/.test((document.activeElement || {}).type)) {
return;
}
let entry;
const {which: k, key, code} = event;
let {key, code, shiftKey} = event;
if (code.startsWith('Digit') || code.startsWith('Numpad') && code.length === 7) {
if (key >= '0' && key <= '9') {
entry = entries[(Number(key) || 10) - 1];
} else if (code >= 'Digit0' && code <= 'Digit9') {
entry = entries[(Number(code.slice(-1)) || 10) - 1];
} else if (
code === 'Backquote' || code === 'NumpadMultiply' ||
key && (key === '`' || key === '*') ||
k === 192 || k === 106) {
} else if (key === '`' || key === '*' || code === 'Backquote' || code === 'NumpadMultiply') {
invertTogglables();
} else if (
code === 'NumpadSubtract' ||
key && key === '-' ||
k === 109) {
} else if (key === '-' || code === 'NumpadSubtract') {
toggleState(entries, 'enabled', false);
} else if (
code === 'NumpadAdd' ||
key && key === '+' ||
k === 107) {
} else if (key === '+' || code === 'NumpadAdd') {
toggleState(entries, 'disabled', true);
} else if (
// any single character
key && key.length === 1 ||
k >= 65 && k <= 90) {
const letter = new RegExp(key ? '^' + key : '^\\x' + k.toString(16), 'i');
entry = [...entries].find(entry => letter.test(entry.textContent));
} else if (key.length === 1) {
shiftKey = false; // typing ':' etc. needs Shift so we hide it here to avoid opening editor
key = key.toLocaleLowerCase();
entry = [...entries].find(e => e.innerText.toLocaleLowerCase().startsWith(key));
}
if (!entry) {
return;
}
const target = $(event.shiftKey ? '.style-edit-link' : '.checker', entry);
const target = $(shiftKey ? '.style-edit-link' : '.checker', entry);
target.dispatchEvent(new MouseEvent('click', {cancelable: true}));
}

View File

@ -342,11 +342,7 @@ a.configure[target="_blank"] .svg-icon.config {
text-overflow: ellipsis;
}
#confirm button[data-cmd="ok"]:not([data-focused-via-click]):focus {
outline: red auto 1px;
}
/* FF ignores color with 'auto' */
.firefox #confirm button[data-cmd="ok"]:not([data-focused-via-click]):focus {
outline: red solid 1px;
box-shadow: 0 0 0 1px red; /* Using box-shadow instead of the ugly outline in new Chrome */
}
.menu-items-wrapper {
width: 80%;

View File

@ -13,7 +13,8 @@ const handleEvent = {};
const ABOUT_BLANK = 'about:blank';
const ENTRY_ID_PREFIX_RAW = 'style-';
const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW;
$.entry = styleOrId => $(`#${ENTRY_ID_PREFIX_RAW}${styleOrId.id || styleOrId}`);
if (CHROME >= 66 && CHROME <= 69) { // Chrome 66-69 adds a gap, https://crbug.com/821143
document.head.appendChild($create('style', 'html { overflow: overlay }'));
@ -27,7 +28,7 @@ initTabUrls()
onDOMready().then(() => initPopup(frames)),
...frames
.filter(f => f.url && !f.isDupe)
.map(({url}) => API.getStylesByUrl(url).then(styles => ({styles, url}))),
.map(({url}) => getStyleDataMerged(url).then(styles => ({styles, url}))),
]))
.then(([, ...results]) => {
if (results[0]) {
@ -53,17 +54,19 @@ if (CHROME_HAS_BORDER_BUG) {
}
function onRuntimeMessage(msg) {
if (!tabURL) return;
let ready = Promise.resolve();
switch (msg.method) {
case 'styleAdded':
case 'styleUpdated':
if (msg.reason === 'editPreview' || msg.reason === 'editPreviewEnd') return;
handleUpdate(msg);
ready = handleUpdate(msg);
break;
case 'styleDeleted':
handleDelete(msg.style.id);
break;
}
dispatchEvent(new CustomEvent(msg.method, {detail: msg}));
ready.then(() => dispatchEvent(new CustomEvent(msg.method, {detail: msg})));
}
@ -141,8 +144,7 @@ function initPopup(frames) {
}
if (!tabURL) {
document.body.classList.add('blocked');
document.body.insertBefore(template.unavailableInfo, document.body.firstChild);
blockPopup();
return;
}
@ -308,31 +310,37 @@ function sortStyles(entries) {
return entries.sort(({styleMeta: a}, {styleMeta: b}) =>
Boolean(a.frameUrl) - Boolean(b.frameUrl) ||
enabledFirst && Boolean(b.enabled) - Boolean(a.enabled) ||
a.name.localeCompare(b.name));
(a.customName || a.name).localeCompare(b.customName || b.name));
}
function showStyles(frameResults) {
const entries = new Map();
frameResults.forEach(({styles = [], url}, index) => {
styles.forEach(style => {
const {id} = style.data;
const {id} = style;
if (!entries.has(id)) {
style.frameUrl = index === 0 ? '' : url;
entries.set(id, createStyleElement(Object.assign(style.data, style)));
entries.set(id, createStyleElement(style));
}
});
});
if (entries.size) {
installed.append(...sortStyles([...entries.values()]));
resortEntries([...entries.values()]);
} else {
installed.appendChild(template.noStyles.cloneNode(true));
installed.appendChild(template.noStyles);
}
window.dispatchEvent(new Event('showStyles:done'));
}
function resortEntries(entries) {
// `entries` is specified only at startup, after that we respect the prefs
if (entries || prefs.get('popup.autoResort')) {
installed.append(...sortStyles(entries || $$('.entry', installed)));
}
}
function createStyleElement(style) {
let entry = $(ENTRY_ID_PREFIX + style.id);
let entry = $.entry(style);
if (!entry) {
entry = template.style.cloneNode(true);
entry.setAttribute('style-id', style.id);
@ -400,7 +408,7 @@ function createStyleElement(style) {
$('.checker', entry).checked = style.enabled;
const styleName = $('.style-name', entry);
styleName.lastChild.textContent = style.name;
styleName.lastChild.textContent = style.customName || style.name;
setTimeout(() => {
styleName.title =
entry.styleMeta.sloppy ? t('styleNotAppliedRegexpProblemTooltip') :
@ -470,11 +478,7 @@ Object.assign(handleEvent, {
event.stopPropagation();
API
.toggleStyle(handleEvent.getClickedStyleId(event), this.checked)
.then(() => {
if (prefs.get('popup.autoResort')) {
installed.append(...sortStyles($$('.entry', installed)));
}
});
.then(() => resortEntries());
},
toggleExclude(event, type) {
@ -504,16 +508,15 @@ Object.assign(handleEvent, {
window.onkeydown = event => {
const close = $('.menu-close', entry);
const checkbox = $('.exclude-by-domain-checkbox', entry);
const keyCode = event.keyCode || event.which;
if (document.activeElement === close && (keyCode === 9) && !event.shiftKey) {
if (document.activeElement === close && (event.key === 'Tab') && !event.shiftKey) {
event.preventDefault();
checkbox.focus();
}
if (document.activeElement === checkbox && (keyCode === 9) && event.shiftKey) {
if (document.activeElement === checkbox && (event.key === 'Tab') && event.shiftKey) {
event.preventDefault();
close.focus();
}
if (keyCode === 27) {
if (event.key === 'Escape') {
event.preventDefault();
close.click();
}
@ -539,20 +542,20 @@ Object.assign(handleEvent, {
const close = $('.menu-close', entry);
const checkbox = $('.exclude-by-domain-checkbox', entry);
const confirmActive = $('#confirm[data-display="true"]');
const keyCode = event.keyCode || event.which;
if (document.activeElement === cancel && (keyCode === 9)) {
const {key} = event;
if (document.activeElement === cancel && (key === 'Tab')) {
event.preventDefault();
affirm.focus();
}
if (document.activeElement === close && (keyCode === 9) && !event.shiftKey) {
if (document.activeElement === close && (key === 'Tab') && !event.shiftKey) {
event.preventDefault();
checkbox.focus();
}
if (document.activeElement === checkbox && (keyCode === 9) && event.shiftKey) {
if (document.activeElement === checkbox && (key === 'Tab') && event.shiftKey) {
event.preventDefault();
close.focus();
}
if (keyCode === 27) {
if (key === 'Escape') {
event.preventDefault();
if (confirmActive) {
box.dataset.display = false;
@ -673,38 +676,25 @@ Object.assign(handleEvent, {
});
function handleUpdate({style, reason}) {
if (!tabURL) return;
fetchStyle()
.then(style => {
if (!style) {
return;
}
if ($(ENTRY_ID_PREFIX + style.id)) {
createStyleElement(style);
return;
}
document.body.classList.remove('blocked');
$$.remove('.blocked-info, #no-styles');
createStyleElement(style);
})
.catch(console.error);
function fetchStyle() {
if (reason === 'toggle' && $(ENTRY_ID_PREFIX + style.id)) {
return Promise.resolve(style);
}
return API.getStylesByUrl(tabURL, style.id)
.then(([result]) => result && Object.assign(result.data, result));
async function handleUpdate({style, reason}) {
if (reason !== 'toggle' || !$.entry(style)) {
style = await getStyleDataMerged(tabURL, style.id);
if (!style) return;
}
const el = createStyleElement(style);
if (!el.parentNode) {
installed.appendChild(el);
blockPopup(false);
}
resortEntries();
}
function handleDelete(id) {
$.remove(ENTRY_ID_PREFIX + id);
if (!$('.entry')) {
installed.appendChild(template.noStyles.cloneNode(true));
const el = $.entry(id);
if (el) {
el.remove();
if (!$('.entry')) installed.appendChild(template.noStyles);
}
}
@ -722,3 +712,21 @@ function waitForTabUrlFF(tab) {
]);
});
}
/* Merges the extra props from API into style data.
* When `id` is specified returns a single object otherwise an array */
async function getStyleDataMerged(url, id) {
const styles = (await API.getStylesByUrl(url, id))
.map(r => Object.assign(r.data, r));
return id ? styles[0] : styles;
}
function blockPopup(isBlocked = true) {
document.body.classList.toggle('blocked', isBlocked);
if (isBlocked) {
document.body.prepend(template.unavailableInfo);
} else {
template.unavailableInfo.remove();
template.noStyles.remove();
}
}

View File

@ -54,21 +54,15 @@ body.search-results-shown {
background-color: #fff;
}
.search-result .lds-spinner {
#search-results .lds-spinner {
transform: scale(.5);
filter: invert(1) drop-shadow(1px 1px 3px #000);
}
.search-result-empty .lds-spinner {
transform: scale(.5);
#search-results .search-result-empty .lds-spinner {
filter: opacity(.2);
}
.search-result-fadein {
animation: fadein 1s;
animation-fill-mode: both;
}
.search-result-screenshot {
height: 140px;
width: 100%;
@ -257,11 +251,27 @@ body.search-results-shown {
padding-left: 16px;
}
#search-params {
display: flex;
position: relative;
margin-top: -.5rem;
margin-bottom: 1.25rem;
flex-wrap: wrap;
}
#search-params > * {
margin-top: .5rem;
}
#search-query {
min-width: 3em;
margin-right: .5em;
flex: 1 1 0;
}
/* spinner: https://github.com/loadingio/css-spinner */
.lds-spinner {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
position: absolute;

File diff suppressed because it is too large Load Diff

View File

@ -62,6 +62,9 @@ const files = {
],
'uuid': [
'dist/umd/uuidv4.min.js → uuid.min.js'
],
'webext-launch-web-auth-flow': [
'dist/webext-launch-web-auth-flow.min.js → webext-launch-web-auth-flow.min.js'
]
};

View File

@ -3,10 +3,11 @@
const fs = require('fs');
const archiver = require('archiver');
const manifest = require('../manifest.json');
function createZip() {
const fileName = 'stylus.zip';
const exclude = [
function createZip({isFirefox} = {}) {
const fileName = `stylus${isFirefox ? '-firefox' : ''}.zip`;
const ignore = [
'.*', // dot files/folders (glob, not regexp)
'vendor/codemirror/lib/**', // get unmodified copy from node_modules
'node_modules/**',
@ -38,15 +39,30 @@ function createZip() {
});
archive.pipe(file);
archive.glob('**', {ignore: exclude});
if (isFirefox) {
const name = 'manifest.json';
const keyOpt = 'optional_permissions';
ignore.unshift(name);
manifest[keyOpt] = manifest[keyOpt].filter(p => p !== 'declarativeContent');
if (!manifest[keyOpt].length) {
delete manifest[keyOpt];
}
archive.append(JSON.stringify(manifest, null, ' '), {name, stats: fs.lstatSync(name)});
}
archive.glob('**', {ignore});
// Don't use modified codemirror.js (see "update-libraries.js")
archive.directory('node_modules/codemirror/lib', 'vendor/codemirror/lib');
archive.finalize();
});
}
createZip()
.then(() => console.log('\x1b[32m%s\x1b[0m', 'Stylus zip complete'))
.catch(err => {
throw err;
});
(async () => {
try {
await createZip();
await createZip({isFirefox: true});
console.log('\x1b[32m%s\x1b[0m', 'Stylus zip complete');
} catch (err) {
console.error(err);
process.exit(1);
}
})();

View File

@ -86,10 +86,7 @@
border: 1px solid var(--main-border-color);
background-color: var(--main-background-color);
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.12);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
@ -139,8 +136,6 @@
position: absolute;
width: 10px;
height: 10px;
-webkit-border-radius: 50%;
-moz-border-radius: 50%;
border-radius: 50%;
left: -5px;
top: -5px;
@ -168,8 +163,6 @@
-webkit-border-radius: 50%;
-moz-border-radius: 50%;
border-radius: 50%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
border: 1px solid var(--input-border-color);
}
@ -191,8 +184,6 @@
position: relative;
width: 100%;
height: 10px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
cursor: pointer;
background: linear-gradient(to right, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
@ -212,8 +203,6 @@
width: 100%;
height: 10px;
z-index: 2;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
cursor: pointer;
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAJElEQVQYV2NctWrVfwYkEBYWxojMZ6SDAmT7QGx0K1EcRBsFAADeG/3M/HteAAAAAElFTkSuQmCC");
@ -239,9 +228,6 @@
.colorpicker-input-container {
position: relative;
-webkit-box-sizing: padding-box;
-moz-box-sizing: padding-box;
box-sizing: padding-box;
}
.colorpicker-input-group {
@ -263,8 +249,6 @@
position: relative;
flex: 1;
padding: 5px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
@ -295,10 +279,7 @@
font-size: 11px;
font-weight: bold;
box-sizing: border-box;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
-o-user-select: text;
user-select: text;
border: 1px solid var(--input-border-color);
background-color: var(--input-background-color);
@ -306,7 +287,6 @@
}
.colorpicker-theme-dark .colorpicker-input::-webkit-inner-spin-button {
-webkit-filter: invert(1);
filter: invert(1);
}
@ -377,3 +357,15 @@
.colorpicker-format-change-button:hover {
color: var(--label-color-hover);
}
.colorpicker-palette:not(:empty) {
padding: 0 8px 8px;
min-height: 14px; /* same as padding-left in .colorview-swatch */
max-height: 10vw;
overflow: auto;
box-sizing: content-box;
}
.colorpicker-palette .colorview-swatch {
padding-bottom: 14px; /* same as padding-left in .colorview-swatch */
}

View File

@ -30,6 +30,7 @@
let $swatch;
let $formatChangeButton;
let $hexCode;
let $palette;
const $inputGroups = {};
const $inputs = {};
const $rgb = {};
@ -164,6 +165,7 @@
$formatChangeButton = $('format-change-button', {textContent: '↔'}),
]}),
]}),
$palette = $('palette'),
]});
$inputs.hex = [$hexCode];
@ -221,6 +223,11 @@
if (!isNaN(options.left) && !isNaN(options.top)) {
reposition();
}
if (Array.isArray(options.palette)) {
// Might need to clear a lot of elements so this is known to be faster than textContent = ''
while ($palette.firstChild) $palette.firstChild.remove();
$palette.append(...(options.palette));
}
}
function hide() {
@ -355,29 +362,29 @@
}
function setFromKeyboard(event) {
const {which, ctrlKey: ctrl, altKey: alt, shiftKey: shift, metaKey: meta} = event;
switch (which) {
case 9: // Tab
case 33: // PgUp
case 34: // PgDn
const {key, ctrlKey: ctrl, altKey: alt, shiftKey: shift, metaKey: meta} = event;
switch (key) {
case 'Tab':
case 'PageUp':
case 'PageDown':
if (!ctrl && !alt && !meta) {
const el = document.activeElement;
const inputs = $inputs[currentFormat];
const lastInput = inputs[inputs.length - 1];
if (which === 9 && shift && el === inputs[0]) {
if (key === 'Tab' && shift && el === inputs[0]) {
maybeFocus(lastInput);
} else if (which === 9 && !shift && el === lastInput) {
} else if (key === 'Tab' && !shift && el === lastInput) {
maybeFocus(inputs[0]);
} else if (which !== 9 && !shift) {
setFromFormatElement({shift: which === 33 || shift});
} else if (key !== 'Tab' && !shift) {
setFromFormatElement({shift: key === 'PageUp' || shift});
} else {
return;
}
event.preventDefault();
}
return;
case 38: // Up
case 40: // Down
case 'ArrowUp':
case 'ArrowDown':
if (!event.metaKey &&
document.activeElement.localName === 'input' &&
document.activeElement.checkValidity()) {
@ -389,8 +396,8 @@
function setFromKeyboardIncrement(event) {
const el = document.activeElement;
const {which, ctrlKey: ctrl, altKey: alt, shiftKey: shift} = event;
const dir = which === 38 ? 1 : -1;
const {key, ctrlKey: ctrl, altKey: alt, shiftKey: shift} = event;
const dir = key === 'ArrowUp' ? 1 : -1;
let value, newValue;
if (currentFormat === 'hex') {
value = el.value.trim();
@ -454,7 +461,7 @@
const newHSV = color.type === 'hsl' ?
colorConverter.HSLtoHSV(color) :
colorConverter.RGBtoHSV(color);
if (Object.keys(newHSV).every(k => Math.abs(newHSV[k] - HSV[k]) < 1e-3)) {
if (Object.entries(newHSV).every(([k, v]) => v === HSV[k] || Math.abs(v - HSV[k]) < 1e-3)) {
return;
}
HSV = newHSV;
@ -574,6 +581,19 @@
}
}
/** @param {MouseEvent} e */
function onPaletteClicked(e) {
if (e.target !== e.currentTarget) {
e.preventDefault();
if (!e.button && setColor(e.target.__color)) {
userActivity = performance.now();
colorpickerCallback();
} else if (e.button === 2 && options.paletteCallback) {
options.paletteCallback(e.target);
}
}
}
function onMouseUp(event) {
releaseMouse(event, ['saturation', 'hue', 'opacity']);
if (onMouseDown.outsideClick) {
@ -617,9 +637,9 @@
function onKeyDown(e) {
if (!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
switch (e.which) {
case 13:
case 27:
switch (e.key) {
case 'Enter':
case 'Escape':
e.preventDefault();
e.stopPropagation();
hide();
@ -710,6 +730,8 @@
$opacity.addEventListener('mousedown', onOpacityMouseDown);
$hexLettercase.true.addEventListener('click', onHexLettercaseClicked);
$hexLettercase.false.addEventListener('click', onHexLettercaseClicked);
$palette.addEventListener('click', onPaletteClicked);
$palette.addEventListener('contextmenu', onPaletteClicked);
stopSnoozing();
if (!options.isShortCut) {
@ -735,6 +757,8 @@
$opacity.removeEventListener('mousedown', onOpacityMouseDown);
$hexLettercase.true.removeEventListener('click', onHexLettercaseClicked);
$hexLettercase.false.removeEventListener('click', onHexLettercaseClicked);
$palette.removeEventListener('click', onPaletteClicked);
$palette.removeEventListener('contextmenu', onPaletteClicked);
releaseMouse();
stopSnoozing();
}

View File

@ -532,6 +532,8 @@
color: data.color,
prevColor: data.color || '',
callback: popupOnChange,
palette: makePalette(state),
paletteCallback: el => paletteCallback(state, el),
}));
}
@ -551,6 +553,49 @@
}
}
function makePalette({cm, options}) {
const palette = new Map();
let i = 0;
let nums;
cm.eachLine(({markedSpans}) => {
++i;
if (!markedSpans) return;
for (const {from, marker: m} of markedSpans) {
if (from == null || m.className !== COLORVIEW_CLASS) continue;
nums = palette.get(m.color);
if (!nums) palette.set(m.color, (nums = []));
nums.push(i);
}
});
const res = [];
if (palette.size > 1 || nums && nums.length > 1) {
const old = new Map((options.popup.palette || []).map(el => [el.__color, el]));
for (const [color, data] of palette) {
res.push(old.get(color) || makePaletteSwatch(color, data));
}
}
return res;
}
function makePaletteSwatch(color, nums) {
const s = nums.join(', ');
const el = document.createElement('div');
el.className = COLORVIEW_SWATCH_CLASS;
el.style.cssText = COLORVIEW_SWATCH_CSS + color;
el.title = color + (!s ? '' : `\nLine: ${s.length > 50 ? s.replace(/([^,]+,\s){10}/g, '$&\n') : s}`);
el.__color = color;
return el;
}
function paletteCallback({cm}, el) {
const lines = el.title.split('\n')[1].match(/\d+/g).map(Number);
const curLine = cm.getCursor().line + 1;
const i = lines.indexOf(curLine) + 1;
const pos = {line: (lines[i] || curLine) - 1, ch: 0};
cm.scrollIntoView(pos, cm.defaultTextHeight());
cm.setCursor(pos);
}
//endregion
//region Utility

View File

@ -154,11 +154,12 @@ self.parserlib = (() => {
'azimuth': '<azimuth>',
// B
'backdrop-filter': '<filter-function-list> | none',
'backface-visibility': 'visible | hidden',
'background': '[ <bg-layer> , ]* <final-bg-layer>',
'background-attachment': '<attachment>#',
'background-blend-mode': '<blend-mode>',
'background-clip': '<box>#',
'background-clip': '[ <box> | text ]#',
'background-color': '<color>',
'background-image': '<bg-image>#',
'background-origin': '<box>#',
@ -176,38 +177,14 @@ self.parserlib = (() => {
'bookmark-level': 'none | <integer>',
'bookmark-state': 'open | closed',
'bookmark-target': 'none | <uri> | attr()',
'border': '<border-shorthand>',
'border-block-color': '<color>{1,2}',
'border-block-end': '<border-shorthand>',
'border-block-end-color': '<color>',
'border-block-end-style': '<border-style>',
'border-block-end-width': '<border-width>',
'border-block-start': '<border-shorthand>',
'border-block-start-color': '<color>',
'border-block-start-style': '<border-style>',
'border-block-start-width': '<border-width>',
'border-block-style': '<border-style>{1,2}',
'border-block-width': '<border-width>{1,2}',
'border-bottom': '<border-shorthand>',
'border-bottom-color': '<color>',
'border-bottom-left-radius': '<x-one-radius>',
'border-bottom-right-radius': '<x-one-radius>',
'border-bottom-style': '<border-style>',
'border-bottom-width': '<border-width>',
'border-boundary': 'none | parent | display',
'border-inline-color': '<color>{1,2}',
'border-inline-end': '<border-shorthand>',
'border-inline-end-color': '<color>',
'border-inline-end-style': '<border-style>',
'border-inline-end-width': '<border-width>',
'border-inline-start': '<border-shorthand>',
'border-inline-start-color': '<color>',
'border-inline-start-style': '<border-style>',
'border-inline-start-width': '<border-width>',
'border-inline-style': '<border-style>{1,2}',
'border-inline-width': '<border-width>{1,2}',
'border-top-left-radius': '<x-one-radius>',
'border-top-right-radius': '<x-one-radius>',
'border-boundary': 'none | parent | display',
'border-collapse': 'collapse | separate',
'border-color': '<color>{1,4}',
'border-image': '[ none | <image> ] || <border-image-slice> ' +
'[ / <border-image-width> | / <border-image-width>? / <border-image-outset> ]? || ' +
'<border-image-repeat>',
@ -216,24 +193,8 @@ self.parserlib = (() => {
'border-image-slice': '<border-image-slice>',
'border-image-source': '<image> | none',
'border-image-width': '<border-image-width>',
'border-left': '<border-shorthand>',
'border-left-color': '<color>',
'border-left-style': '<border-style>',
'border-left-width': '<border-width>',
'border-radius': '<border-radius>',
'border-right': '<border-shorthand>',
'border-right-color': '<color>',
'border-right-style': '<border-style>',
'border-right-width': '<border-width>',
'border-spacing': '<length>{1,2}',
'border-style': '<border-style>{1,4}',
'border-top': '<border-shorthand>',
'border-top-color': '<color>',
'border-top-left-radius': '<x-one-radius>',
'border-top-right-radius': '<x-one-radius>',
'border-top-style': '<border-style>',
'border-top-width': '<border-width>',
'border-width': '<border-width>{1,4}',
'bottom': '<width>',
'box-decoration-break': 'slice | clone',
'box-shadow': '<box-shadow>',
@ -286,6 +247,7 @@ self.parserlib = (() => {
'columns': 1,
'contain': 'none | strict | content | [ size || layout || style || paint ]',
'content': 'normal | none | <content-list> [ / <string> ]?',
'content-visibility': 'visible | auto | hidden',
'counter-increment': 1,
'counter-reset': 1,
'crop': 'rect() | inset-rect() | auto',
@ -368,7 +330,7 @@ self.parserlib = (() => {
'font-variant-ligatures': '<font-variant-ligatures> | normal | none',
'font-variant-numeric': '<font-variant-numeric> | normal',
'font-variant-position': 'normal | sub | super',
'font-variation-settings': 'normal | [ <string> <number>]#',
'font-variation-settings': 'normal | [ <string> <number> ]#',
'font-weight': '<font-weight>',
'-ms-flex-align': 'start | end | center | stretch | baseline',
'-ms-flex-order': '<number>',
@ -508,6 +470,7 @@ self.parserlib = (() => {
'outline-style': '<border-style> | auto',
'outline-width': '<border-width>',
'overflow': '<overflow>{1,2}',
'overflow-anchor': 'auto | none',
'overflow-block': '<overflow>',
'overflow-inline': '<overflow>',
'overflow-style': 1,
@ -578,6 +541,34 @@ self.parserlib = (() => {
// S
'scale': 'none | <number>{1,3}',
'scroll-behavior': 'auto | smooth',
'scroll-margin': '<length>{1,4}',
'scroll-margin-bottom': '<length>',
'scroll-margin-left': '<length>',
'scroll-margin-right': '<length>',
'scroll-margin-top': '<length>',
'scroll-margin-block': '<length>{1,2}',
'scroll-margin-block-end': '<length>',
'scroll-margin-block-start': '<length>',
'scroll-margin-inline': '<length>{1,2}',
'scroll-margin-inline-end': '<length>',
'scroll-margin-inline-start': '<length>',
'scroll-padding': '<auto-length-pct>{1,4}',
'scroll-padding-left': '<auto-length-pct>',
'scroll-padding-right': '<auto-length-pct>',
'scroll-padding-top': '<auto-length-pct>',
'scroll-padding-bottom': '<auto-length-pct>',
'scroll-padding-block': '<auto-length-pct>{1,2}',
'scroll-padding-block-end': '<auto-length-pct>',
'scroll-padding-block-start': '<auto-length-pct>',
'scroll-padding-inline': '<auto-length-pct>{1,2}',
'scroll-padding-inline-end': '<auto-length-pct>',
'scroll-padding-inline-start': '<auto-length-pct>',
'scroll-snap-align': '[ none | start | end | center ]{1,2}',
'scroll-snap-stop': 'normal | always',
'scroll-snap-type': 'none | [ x | y | block | inline | both ] [ mandatory | proximity ]?',
'scrollbar-color': 'auto | dark | light | <color>{2}',
'scrollbar-width': 'auto | thin | none',
'shape-inside': 'auto | outside-shape | [ <basic-shape> || shape-box ] | <image> | display',
@ -689,6 +680,25 @@ self.parserlib = (() => {
'-webkit-text-stroke-width': '<border-width>',
};
for (const [k, reps] of Object.entries({
'border': '{1,4}',
'border-bottom': '',
'border-left': '',
'border-right': '',
'border-top': '',
'border-block': '{1,2}',
'border-block-end': '',
'border-block-start': '',
'border-inline': '{1,2}',
'border-inline-end': '',
'border-inline-start': '',
})) {
Properties[k] = '<border-shorthand>';
Properties[`${k}-color`] = '<color>' + reps;
Properties[`${k}-style`] = '<border-style>' + reps;
Properties[`${k}-width`] = '<border-width>' + reps;
}
//endregion
//region ValidationTypes - definitions
@ -710,6 +720,8 @@ self.parserlib = (() => {
'<attachment>': 'scroll | fixed | local',
'<auto-length-pct>': 'auto | <length> | <percentage>',
'<basic-shape>': 'inset() | circle() | ellipse() | polygon()',
'<bg-image>': '<image> | none',
@ -2722,6 +2734,10 @@ self.parserlib = (() => {
constructor(input) {
this._reader = new StringReader(input ? input.toString() : '');
this.resetLT();
}
resetLT() {
// Token object for the last consumed token.
this._token = null;
// Lookahead token buffer.
@ -3148,8 +3164,7 @@ self.parserlib = (() => {
*/
atRuleToken(first, pos) {
this._reader.mark();
const ident = this.readName();
let rule = first + ident;
let rule = first + this.readName();
let tt = Tokens.type(lower(rule));
// if it's not valid, use the first character only and reset the reader
if (tt === Tokens.CHAR || tt === Tokens.UNKNOWN) {
@ -3850,6 +3865,7 @@ self.parserlib = (() => {
* @param {Boolean} [options.starHack] - allows IE6 star hack
* @param {Boolean} [options.underscoreHack] - interprets leading underscores as IE6-7 for known properties
* @param {Boolean} [options.ieFilters] - accepts IE < 8 filters instead of throwing syntax errors
* @param {Boolean} [options.emptyDocument] - accepts @document without {} block produced by stylus-lang
*/
constructor(options) {
super();
@ -4005,24 +4021,19 @@ self.parserlib = (() => {
_supportsCondition() {
const stream = this._tokenStream;
if (stream.match(Tokens.IDENT)) {
const ident = lower(stream._token.value);
if (ident === 'not') {
stream.mustMatch(Tokens.S);
this._supportsConditionInParens();
} else {
stream.unget();
}
const next = stream.LT(1);
if (next.type === Tokens.IDENT && lower(next.value) === 'not') {
stream.get();
stream.mustMatch(Tokens.S);
this._supportsConditionInParens();
} else {
this._supportsConditionInParens();
this._ws();
while (stream.peek() === Tokens.IDENT) {
const ident = lower(stream.LT(1).value);
if (ident === 'and' || ident === 'or') {
stream.mustMatch(Tokens.IDENT);
stream.get();
this._ws();
this._supportsConditionInParens();
this._ws();
}
}
}
@ -4030,36 +4041,41 @@ self.parserlib = (() => {
_supportsConditionInParens() {
const stream = this._tokenStream;
if (stream.match(Tokens.LPAREN)) {
const next = stream.LT(1);
if (next.type === Tokens.LPAREN) {
stream.get();
this._ws();
if (stream.match([Tokens.IDENT, Tokens.CUSTOM_PROP])) {
// look ahead for not keyword,
// if not given, continue with declaration condition.
const ident = lower(stream._token.value);
if (ident === 'not') {
this._ws();
const {type, value} = stream.LT(1);
if (type === Tokens.IDENT || type === Tokens.CUSTOM_PROP) {
if (lower(value) === 'not') {
this._supportsCondition();
stream.mustMatch(Tokens.RPAREN);
} else {
stream.unget();
this._supportsDeclarationCondition(false);
this._supportsDecl(false);
}
} else {
this._supportsCondition();
stream.mustMatch(Tokens.RPAREN);
}
} else if (next.type === Tokens.FUNCTION && lower(next.value) === 'selector(') {
stream.get();
this._ws();
this._selector();
stream.mustMatch(Tokens.RPAREN);
} else {
this._supportsDeclarationCondition();
this._supportsDecl();
}
this._ws();
}
_supportsDeclarationCondition(requireStartParen = true) {
_supportsDecl(requireStartParen = true) {
const stream = this._tokenStream;
if (requireStartParen) {
this._tokenStream.mustMatch(Tokens.LPAREN);
stream.mustMatch(Tokens.LPAREN);
}
this._ws();
this._declaration();
this._tokenStream.mustMatch(Tokens.RPAREN);
stream.mustMatch(Tokens.RPAREN);
}
_media() {
@ -4311,18 +4327,18 @@ self.parserlib = (() => {
_document() {
const stream = this._tokenStream;
const functions = [];
let prefix = '';
const start = stream.mustMatch(Tokens.DOCUMENT_SYM);
if (/^@-([^-]+)-/.test(start.value)) {
prefix = RegExp.$1;
}
const prefix = start.value.split('-')[1] || '';
do {
this._ws();
functions.push(this._documentFunction());
} while (stream.match(Tokens.COMMA));
this._ws();
if (this.options.emptyDocument && stream.peek() !== Tokens.LBRACE) {
this.fire({type: 'emptydocument', functions, prefix}, start);
return;
}
stream.mustMatch(Tokens.LBRACE);
this.fire({
@ -4358,9 +4374,12 @@ self.parserlib = (() => {
_documentFunction() {
const stream = this._tokenStream;
return stream.match(Tokens.URI) ?
new PropertyValuePart(stream._token) :
this._function();
if (stream.match(Tokens.URI)) {
const res = new PropertyValuePart(stream._token);
this._ws();
return res;
}
return this._function();
}
_operator(inFunction) {
@ -5296,19 +5315,62 @@ self.parserlib = (() => {
throw new SyntaxError('Unknown @ rule.', lt0);
}
this.fire({
type: 'error',
error: null,
message: 'Unknown @ rule: ' + lt0.value + '.',
}, lt0);
this._ws();
const simpleValue =
stream.match([Tokens.IDENT, Tokens.CUSTOM_PROP]) && SyntaxUnit.fromToken(stream._token) ||
stream.peek() === Tokens.FUNCTION && this._function({asText: true}) ||
this._unknownBlock([Tokens.LBRACKET, Tokens.LPAREN]);
// skip {} block
let count = 0;
do {
const brace = stream.advance([Tokens.LBRACE, Tokens.RBRACE]);
count += brace === Tokens.LBRACE ? 1 : -1;
} while (count > 0 && !stream._reader.eof());
if (count < 0) stream.unget();
this._ws();
const blockValue = this._unknownBlock();
if (!blockValue) {
stream.match(Tokens.SEMICOLON);
}
this.fire({
type: 'unknown-at-rule',
name: lt0.value,
simpleValue,
blockValue,
}, lt0);
this._ws();
}
_unknownBlock(canStartWith = [Tokens.LBRACE]) {
const stream = this._tokenStream;
if (!canStartWith.includes(stream.peek())) {
return null;
}
stream.get();
const start = stream._token;
const reader = stream._reader;
reader.mark();
reader._cursor = start.offset;
reader._line = start.startLine;
reader._col = start.startCol;
const value = [];
const endings = [];
let blockEnd;
while (!reader.eof()) {
const chunk = reader.readMatch(/[^{}()[\]]*[{}()[\]]?/y);
const c = chunk.slice(-1);
value.push(chunk);
if (c === '{' || c === '(' || c === '[') {
endings.push(blockEnd);
blockEnd = c === '{' ? '}' : c === '(' ? ')' : ']';
} else if (c === '}' || c === ')' || c === ']') {
if (c !== blockEnd) {
break;
}
blockEnd = endings.pop();
if (!blockEnd) {
stream.resetLT();
return new SyntaxUnit(value.join(''), start);
}
}
}
reader.reset();
return null;
}
_unexpectedToken(token) {
@ -5406,18 +5468,28 @@ self.parserlib = (() => {
Object.assign(Parser.prototype, TYPES);
Parser.prototype._readWhitespace = Parser.prototype._ws;
const symDocument = [Tokens.DOCUMENT_SYM, Parser.prototype._document];
const symDocMisplaced = [Tokens.DOCUMENT_SYM, Parser.prototype._documentMisplaced];
const symFontFace = [Tokens.FONT_FACE_SYM, Parser.prototype._fontFace];
const symKeyframes = [Tokens.KEYFRAMES_SYM, Parser.prototype._keyframes];
const symMedia = [Tokens.MEDIA_SYM, Parser.prototype._media];
const symPage = [Tokens.PAGE_SYM, Parser.prototype._page];
const symSupports = [Tokens.SUPPORTS_SYM, Parser.prototype._supports];
const symUnknown = [Tokens.UNKNOWN_SYM, Parser.prototype._unknownSym];
const symViewport = [Tokens.VIEWPORT_SYM, Parser.prototype._viewport];
Parser.ACTIONS = {
stylesheet: new Map([
[Tokens.MEDIA_SYM, Parser.prototype._media],
[Tokens.DOCUMENT_SYM, Parser.prototype._document],
[Tokens.SUPPORTS_SYM, Parser.prototype._supports],
[Tokens.PAGE_SYM, Parser.prototype._page],
[Tokens.FONT_FACE_SYM, Parser.prototype._fontFace],
[Tokens.KEYFRAMES_SYM, Parser.prototype._keyframes],
[Tokens.VIEWPORT_SYM, Parser.prototype._viewport],
symMedia,
symDocument,
symSupports,
symPage,
symFontFace,
symKeyframes,
symViewport,
symUnknown,
[Tokens.S, Parser.prototype._ws],
[Tokens.UNKNOWN_SYM, Parser.prototype._unknownSym],
]),
stylesheetMisplaced: new Map([
@ -5427,31 +5499,34 @@ self.parserlib = (() => {
]),
document: new Map([
[Tokens.MEDIA_SYM, Parser.prototype._media],
[Tokens.DOCUMENT_SYM, Parser.prototype._documentMisplaced],
[Tokens.SUPPORTS_SYM, Parser.prototype._supports],
[Tokens.PAGE_SYM, Parser.prototype._page],
[Tokens.FONT_FACE_SYM, Parser.prototype._fontFace],
[Tokens.VIEWPORT_SYM, Parser.prototype._viewport],
[Tokens.KEYFRAMES_SYM, Parser.prototype._keyframes],
symMedia,
symDocMisplaced,
symSupports,
symPage,
symFontFace,
symViewport,
symKeyframes,
symUnknown,
]),
supports: new Map([
[Tokens.KEYFRAMES_SYM, Parser.prototype._keyframes],
[Tokens.MEDIA_SYM, Parser.prototype._media],
[Tokens.SUPPORTS_SYM, Parser.prototype._supports],
[Tokens.DOCUMENT_SYM, Parser.prototype._documentMisplaced],
[Tokens.VIEWPORT_SYM, Parser.prototype._viewport],
symKeyframes,
symMedia,
symSupports,
symDocMisplaced,
symViewport,
symUnknown,
]),
media: new Map([
[Tokens.KEYFRAMES_SYM, Parser.prototype._keyframes],
[Tokens.MEDIA_SYM, Parser.prototype._media],
[Tokens.DOCUMENT_SYM, Parser.prototype._documentMisplaced],
[Tokens.SUPPORTS_SYM, Parser.prototype._supports],
[Tokens.PAGE_SYM, Parser.prototype._page],
[Tokens.FONT_FACE_SYM, Parser.prototype._fontFace],
[Tokens.VIEWPORT_SYM, Parser.prototype._viewport],
symKeyframes,
symMedia,
symDocMisplaced,
symSupports,
symPage,
symFontFace,
symViewport,
symUnknown,
]),
simpleSelectorSequence: new Map([

View File

@ -1,4 +1,4 @@
## codemirror v5.56.0
## codemirror v5.58.0
Following files are copied from npm (node_modules):

View File

@ -13,7 +13,7 @@
var noOptions = {};
var nonWS = /[^\s\u00a0]/;
var Pos = CodeMirror.Pos;
var Pos = CodeMirror.Pos, cmp = CodeMirror.cmpPos;
function firstNonWS(str) {
var found = str.search(nonWS);
@ -126,7 +126,9 @@
if (i != end || lastLineHasText)
self.replaceRange(lead + pad, Pos(i, 0));
} else {
var atCursor = cmp(self.getCursor("to"), to) == 0, empty = !self.somethingSelected()
self.replaceRange(endString, to);
if (atCursor) self.setSelection(empty ? to : self.getCursor("from"), to)
self.replaceRange(startString, from);
}
});

View File

@ -25,22 +25,20 @@
-ms-transition: opacity .4s;
}
.CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning {
.CodeMirror-lint-mark {
background-position: left bottom;
background-repeat: repeat-x;
}
.CodeMirror-lint-mark-error {
background-image:
url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==")
;
}
.CodeMirror-lint-mark-warning {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=");
}
.CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning {
.CodeMirror-lint-mark-error {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==");
}
.CodeMirror-lint-marker {
background-position: center center;
background-repeat: no-repeat;
cursor: pointer;
@ -51,20 +49,20 @@
position: relative;
}
.CodeMirror-lint-message-error, .CodeMirror-lint-message-warning {
.CodeMirror-lint-message {
padding-left: 18px;
background-position: top left;
background-repeat: no-repeat;
}
.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=");
}
.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=");
}
.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=");
}
.CodeMirror-lint-marker-multiple {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC");
background-repeat: no-repeat;

Some files were not shown because too many files have changed in this diff Show More