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 # https://github.com/eslint/eslint/blob/master/docs/rules/README.md
parserOptions: parserOptions:
ecmaVersion: 2015 ecmaVersion: 2017
env: env:
browser: true browser: true

View File

@ -217,10 +217,6 @@
} }
} }
}, },
"editorStylesButton": {
"message": "Стилове за редактора",
"description": "Find styles for the editor"
},
"enableStyleLabel": { "enableStyleLabel": {
"message": "Включване", "message": "Включване",
"description": "Label for the button to enable a style" "description": "Label for the button to enable a style"
@ -741,4 +737,4 @@
"message": "този адрес", "message": "този адрес",
"description": "Text for link in toolbar pop-up to write a new style for the current URL" "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": { "enableStyleLabel": {
"message": "Povolit", "message": "Povolit",
"description": "Label for the button to enable a style" "description": "Label for the button to enable a style"
@ -1306,4 +1302,4 @@
"message": "Nahrávání souboru…", "message": "Nahrávání souboru…",
"description": "" "description": ""
} }
} }

View File

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

View File

@ -250,6 +250,13 @@
"message": "Copy to clipboard", "message": "Copy to clipboard",
"description": "Tooltip for elements which can be copied" "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": { "dateInstalled": {
"message": "Date installed", "message": "Date installed",
"description": "Option text for the user to sort the style by install date" "description": "Option text for the user to sort the style by install date"
@ -319,10 +326,6 @@
}, },
"description": "Title of the page for editing styles" "description": "Title of the page for editing styles"
}, },
"editorStylesButton": {
"message": "Find editor styles",
"description": "Find styles for the editor"
},
"enableStyleLabel": { "enableStyleLabel": {
"message": "Enable", "message": "Enable",
"description": "Label for the button to enable a style" "description": "Label for the button to enable a style"
@ -984,6 +987,12 @@
"optionsAdvancedAutoSwitchSchemeByTime": { "optionsAdvancedAutoSwitchSchemeByTime": {
"message": "By night time:" "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": { "optionsBadgeDisabled": {
"message": "Background color when disabled" "message": "Background color when disabled"
}, },
@ -1036,6 +1045,9 @@
"optionsResetButton": { "optionsResetButton": {
"message": "Reset options" "message": "Reset options"
}, },
"optionsStylusThemes": {
"message": "Find a Stylus UI theme"
},
"optionsSubheading": { "optionsSubheading": {
"message": "More Options", "message": "More Options",
"description": "Subheading for options section on manage page." "description": "Subheading for options section on manage page."
@ -1147,6 +1159,10 @@
"message": "Action menu", "message": "Action menu",
"description": "Tooltip for menu button in popup." "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": { "popupOpenEditInWindow": {
"message": "Open editor in a new window", "message": "Open editor in a new window",
"description": "Label for the checkbox controlling 'edit' action behavior in the popup." "description": "Label for the checkbox controlling 'edit' action behavior in the popup."
@ -1198,6 +1214,10 @@
"message": "Case-sensitive", "message": "Case-sensitive",
"description": "Tooltip for the 'Aa' icon that enables case-sensitive search in the editor shown on Ctrl-F" "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": { "searchNumberOfResults": {
"message": "Number of matches", "message": "Number of matches",
"description": "Tooltip for the number of found search results in the editor shown on Ctrl-F" "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", "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" "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": { "searchRegexp": {
"message": "Use /re/ syntax for regexp search", "message": "Use /re/ syntax for regexp search",
"description": "Label after the search input field in the editor shown on Ctrl-F" "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": { "enableStyleLabel": {
"message": "Activar", "message": "Activar",
"description": "Label for the button to enable a style" "description": "Label for the button to enable a style"
@ -1568,4 +1564,4 @@
"message": "Subiendo el archivo....", "message": "Subiendo el archivo....",
"description": "" "description": ""
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -273,10 +273,6 @@
} }
} }
}, },
"editorStylesButton": {
"message": "Cerca stili editor",
"description": "Find styles for the editor"
},
"enableStyleLabel": { "enableStyleLabel": {
"message": "Attiva", "message": "Attiva",
"description": "Label for the button to enable a style" "description": "Label for the button to enable a style"
@ -1058,4 +1054,4 @@
"message": "questo URL", "message": "questo URL",
"description": "Text for link in toolbar pop-up to write a new style for the current 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": { "enableStyleLabel": {
"message": "有効化", "message": "有効化",
"description": "Label for the button to enable a style" "description": "Label for the button to enable a style"
@ -1636,4 +1632,4 @@
"message": "スタイルをアップロード中...", "message": "スタイルをアップロード中...",
"description": "" "description": ""
} }
} }

View File

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

View File

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

View File

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

View File

@ -301,10 +301,6 @@
} }
} }
}, },
"editorStylesButton": {
"message": "Encontrar estilos para o editor",
"description": "Find styles for the editor"
},
"enableStyleLabel": { "enableStyleLabel": {
"message": "Ativar", "message": "Ativar",
"description": "Label for the button to enable a style" "description": "Label for the button to enable a style"
@ -1224,4 +1220,4 @@
"message": "este URL", "message": "este URL",
"description": "Text for link in toolbar pop-up to write a new style for the current 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": { "enableStyleLabel": {
"message": "Activați", "message": "Activați",
"description": "Label for the button to enable a style" "description": "Label for the button to enable a style"
@ -1140,4 +1136,4 @@
"message": "acest URL", "message": "acest URL",
"description": "Text for link in toolbar pop-up to write a new style for the current 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": { "enableStyleLabel": {
"message": "Включить", "message": "Включить",
"description": "Label for the button to enable a style" "description": "Label for the button to enable a style"
@ -1644,4 +1640,4 @@
"message": "Загрузка файла...", "message": "Загрузка файла...",
"description": "" "description": ""
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,6 @@ createAPI({
compileUsercss, compileUsercss,
parseUsercssMeta(text, indexOffset = 0) { parseUsercssMeta(text, indexOffset = 0) {
loadScript( loadScript(
'/js/polyfill.js',
'/vendor/usercss-meta/usercss-meta.min.js', '/vendor/usercss-meta/usercss-meta.min.js',
'/vendor-overwrites/colorpicker/colorconverter.js', '/vendor-overwrites/colorpicker/colorconverter.js',
'/js/meta-parser.js' '/js/meta-parser.js'
@ -21,7 +20,6 @@ createAPI({
}, },
nullifyInvalidVars(vars) { nullifyInvalidVars(vars) {
loadScript( loadScript(
'/js/polyfill.js',
'/vendor/usercss-meta/usercss-meta.min.js', '/vendor/usercss-meta/usercss-meta.min.js',
'/vendor-overwrites/colorpicker/colorconverter.js', '/vendor-overwrites/colorpicker/colorconverter.js',
'/js/meta-parser.js' '/js/meta-parser.js'
@ -31,11 +29,15 @@ createAPI({
}); });
function compileUsercss(preprocessor, code, vars) { 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); const builder = getUsercssCompiler(preprocessor);
vars = simpleVars(vars); vars = simpleVars(vars);
return Promise.resolve(builder.preprocess ? builder.preprocess(code, vars) : code) return Promise.resolve(builder.preprocess ? builder.preprocess(code, vars) : code)
.then(code => parseMozFormat({code})) .then(code => parseMozFormat({code, emptyDocument: preprocessor === 'stylus'}))
.then(({sections, errors}) => { .then(({sections, errors}) => {
if (builder.postprocess) { if (builder.postprocess) {
builder.postprocess(sections, vars); builder.postprocess(sections, vars);
@ -122,28 +124,39 @@ function getUsercssCompiler(preprocessor) {
const pool = new Map(); const pool = new Map();
return Promise.resolve(doReplace(source)); return Promise.resolve(doReplace(source));
function getValue(name, rgb) { function getValue(name, rgbName) {
if (!vars.hasOwnProperty(name)) { if (!vars.hasOwnProperty(name)) {
if (name.endsWith('-rgb')) { if (name.endsWith('-rgb')) {
return getValue(name.slice(0, -4), true); return getValue(name.slice(0, -4), name);
} }
return null; return null;
} }
if (rgb) { const {type, value} = vars[name];
if (vars[name].type === 'color') { switch (type) {
const color = colorConverter.parse(vars[name].value); case 'color': {
if (!color) return null; let color = pool.get(rgbName || name);
const {r, g, b} = color; if (color == null) {
return `${r}, ${g}, ${b}`; 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') { return value;
// prevent infinite recursion
pool.set(name, '');
return doReplace(vars[name].value);
}
return vars[name].value;
} }
function doReplace(text) { function doReplace(text) {

View File

@ -1,8 +1,7 @@
/* global download prefs openURL FIREFOX CHROME /* global download prefs openURL FIREFOX CHROME
URLS ignoreChromeError usercssHelper URLS ignoreChromeError chromeLocal semverCompare
styleManager msg navigatorUtil workerUtil contentScripts sync styleManager msg navigatorUtil workerUtil contentScripts sync
findExistingTab activateTab isTabReplaceable getActiveTab findExistingTab activateTab isTabReplaceable getActiveTab colorScheme */
tabManager colorScheme */
'use strict'; 'use strict';
// eslint-disable-next-line no-var // 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, /* 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 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 */ thus auto-closing the popup (in Chrome at least) and preventing the sendMessage code from running */
openURL(opts) { async openURL(opts) {
const {message} = opts; const tab = await openURL(opts);
return openURL(opts) // will pass the resolved value untouched when `message` is absent or falsy if (opts.message) {
.then(message && (tab => tab.status === 'complete' ? tab : onTabReady(tab))) await onTabReady(tab);
.then(message && (tab => msg.sendTab(tab.id, opts.message))); await msg.sendTab(tab.id, opts.message);
}
return tab;
function onTabReady(tab) { function onTabReady(tab) {
return new Promise((resolve, reject) => return new Promise((resolve, reject) =>
setTimeout(function ping(numTries = 10, delay = 100) { 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) { if (FIREFOX) {
// FF misses some about:blank iframes so we inject our content script explicitly // FF misses some about:blank iframes so we inject our content script explicitly
navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, { 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" // save install type: "admin", "development", "normal", "sideload" or "other"
// "normal" = addon installed from webstore // "normal" = addon installed from webstore
chrome.management.getSelf(info => { chrome.management.getSelf(info => {
@ -157,6 +150,14 @@ chrome.runtime.onInstalled.addListener(({reason}) => {
}); });
// themes may change // themes may change
delete localStorage.codeMirrorThemes; 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 'url-prefix'?: String
} }
*/ */
const searchParams = new URLSearchParams(); const u = new URL(chrome.runtime.getURL('edit.html'));
for (const key in params) { u.search = new URLSearchParams(params);
searchParams.set(key, params[key]);
}
const search = searchParams.toString();
return openURL({ return openURL({
url: 'edit.html' + (search && `?${search}`), url: `${u}`,
newWindow: prefs.get('openEditInWindow'), currentWindow: null,
windowPosition: prefs.get('windowPosition'), newWindow: prefs.get('openEditInWindow') && Object.assign({},
currentWindow: null prefs.get('openEditInWindow.popup') && {type: 'popup'},
prefs.get('windowPosition')),
}); });
} }
@ -329,7 +328,7 @@ function openManage({options = false, search} = {}) {
if (tab) { if (tab) {
return Promise.all([ return Promise.all([
activateTab(tab), 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) .catch(console.error)
]); ]);
} }

View File

@ -17,6 +17,21 @@ const contentScripts = (() => {
} }
const busyTabs = new Set(); const busyTabs = new Set();
let busyTabsTimer; 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}; return {injectToTab, injectToAllTabs};
function injectToTab({url, tabId, frameId = null}) { function injectToTab({url, tabId, frameId = null}) {
@ -58,13 +73,13 @@ const contentScripts = (() => {
return browser.tabs.query({}).then(tabs => { return browser.tabs.query({}).then(tabs => {
for (const tab of tabs) { for (const tab of tabs) {
// skip unloaded/discarded/chrome 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 // our content scripts may still be pending injection at browser start so it's too early to ping them
if (tab.status === 'loading') { if (tab.status === 'loading') {
trackBusyTab(tab.id, true); trackBusyTab(tab.id, true);
} else { } else {
injectToTab({ injectToTab({
url: tab.url, url: tab.pendingUrl || tab.url,
tabId: tab.id tabId: tab.id
}); });
} }

View File

@ -1,4 +1,4 @@
/* global chromeLocal ignoreChromeError workerUtil createChromeStorageDB */ /* global chromeLocal workerUtil createChromeStorageDB */
/* exported db */ /* exported db */
/* /*
Initialize a database. There are some problems using IndexedDB in Firefox: 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'; 'use strict';
const db = (() => { const db = (() => {
let exec; const DATABASE = 'stylish';
const preparing = prepare(); const STORE = 'styles';
return { const FALLBACK = 'dbInChromeStorage';
exec: (...args) => const dbApi = {
preparing.then(() => exec(...args)) async exec(...args) {
dbApi.exec = await tryUsingIndexedDB().catch(useChromeStorage);
return dbApi.exec(...args);
},
}; };
return dbApi;
function prepare() { async function tryUsingIndexedDB() {
return withPromise(shouldUseIndexedDB).then(
ok => {
if (ok) {
useIndexedDB();
} else {
useChromeStorage();
}
},
err => {
useChromeStorage(err);
}
);
}
function shouldUseIndexedDB() {
// we use chrome.storage.local fallback if IndexedDB doesn't save data, // we use chrome.storage.local fallback if IndexedDB doesn't save data,
// which, once detected on the first run, is remembered in chrome.storage.local // which, once detected on the first run, is remembered in chrome.storage.local
// for reliablility and in localStorage for fast synchronous access // for reliablility and in localStorage for fast synchronous access
@ -42,115 +31,81 @@ const db = (() => {
if (typeof indexedDB === 'undefined') { if (typeof indexedDB === 'undefined') {
throw new Error('indexedDB is undefined'); throw new Error('indexedDB is undefined');
} }
// test localStorage switch (await getFallback()) {
const fallbackSet = localStorage.dbInChromeStorage; case true: throw null;
if (fallbackSet === 'true') { case false: break;
return false; default: await testDB();
} }
if (fallbackSet === 'false') { return useIndexedDB();
return true;
}
// test storage.local
return chromeLocal.get('dbInChromeStorage')
.then(data => {
if (data && data.dbInChromeStorage) {
return false;
}
return testDBSize()
.then(ok => ok || testDBMutation());
});
} }
function withPromise(fn) { async function getFallback() {
try { return localStorage[FALLBACK] === 'true' ? true :
return Promise.resolve(fn()); localStorage[FALLBACK] === 'false' ? false :
} catch (err) { chromeLocal.getValue(FALLBACK);
return Promise.reject(err);
}
} }
function testDBSize() { async function testDB() {
return dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1) let e = await dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1);
.then(event => ( // throws if result is null
event.target.result && e = e.target.result[0];
event.target.result.length && const id = `${performance.now()}.${Math.random()}.${Date.now()}`;
event.target.result[0] await dbExecIndexedDB('put', {id});
)); e = await dbExecIndexedDB('get', id);
} // throws if result or id is null
await dbExecIndexedDB('delete', e.target.result.id);
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);
} }
function useChromeStorage(err) { function useChromeStorage(err) {
exec = createChromeStorageDB().exec; chromeLocal.setValue(FALLBACK, true);
chromeLocal.set({dbInChromeStorage: true}, ignoreChromeError);
if (err) { 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); console.warn('Failed to access indexedDB. Switched to storage API.', err);
} }
localStorage.dbInChromeStorage = 'true'; localStorage[FALLBACK] = 'true';
return createChromeStorageDB().exec;
} }
function useIndexedDB() { function useIndexedDB() {
exec = dbExecIndexedDB; chromeLocal.setValue(FALLBACK, false);
chromeLocal.set({dbInChromeStorage: false}, ignoreChromeError); localStorage[FALLBACK] = 'false';
localStorage.dbInChromeStorage = 'false'; return dbExecIndexedDB;
} }
function dbExecIndexedDB(method, ...args) { async function dbExecIndexedDB(method, ...args) {
return open().then(database => { const mode = method.startsWith('get') ? 'readonly' : 'readwrite';
if (!method) { const store = (await open()).transaction([STORE], mode).objectStore(STORE);
return database; const fn = method === 'putMany' ? putMany : storeRequest;
} return fn(store, method, ...args);
if (method === 'putMany') { }
return putMany(database, ...args);
} function storeRequest(store, method, ...args) {
const mode = method.startsWith('get') ? 'readonly' : 'readwrite'; return new Promise((resolve, reject) => {
const transaction = database.transaction(['styles'], mode); const request = store[method](...args);
const store = transaction.objectStore('styles'); request.onsuccess = resolve;
return storeRequest(store, method, ...args); request.onerror = reject;
}); });
}
function storeRequest(store, method, ...args) { function putMany(store, _method, items) {
return new Promise((resolve, reject) => { return Promise.all(items.map(item => storeRequest(store, 'put', item)));
const request = store[method](...args); }
request.onsuccess = resolve;
request.onerror = reject; 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) return browser.tabs.get(data.tabId)
.then(tab => { .then(tab => {
if (tab.url === 'chrome://newtab/') { const url = tab.pendingUrl || tab.url;
data.url = tab.url; if (url === 'chrome://newtab/') {
data.url = url;
} }
}); });
} }

View File

@ -57,7 +57,7 @@
continue; continue;
} }
for (const part in PARTS) { 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)) { if (text && PARTS[part](text, rx, words, icase)) {
results.push(id); results.push(id);
break; break;

View File

@ -1,6 +1,6 @@
/* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */ /* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */
/* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty /* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty styleSectionGlobal
getStyleWithNoCode msg sync uuidv4 colorScheme */ getStyleWithNoCode msg prefs sync uuidv4 URLS colorScheme */
/* exported styleManager */ /* exported styleManager */
'use strict'; 'use strict';
@ -61,6 +61,8 @@ const styleManager = (() => {
username: '' username: ''
}; };
const DELETE_IF_NULL = ['id', 'customName'];
handleLivePreviewConnections(); handleLivePreviewConnections();
handleColorScheme(); handleColorScheme();
@ -237,6 +239,13 @@ const styleManager = (() => {
if (!reason) { if (!reason) {
reason = style ? 'update' : 'install'; 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? // FIXME: update updateDate? what about usercss config?
return calcStyleDigest(data) return calcStyleDigest(data)
.then(digest => { .then(digest => {
@ -391,8 +400,10 @@ const styleManager = (() => {
if (!style.name) { if (!style.name) {
throw new Error('style name is empty'); throw new Error('style name is empty');
} }
if (style.id == null) { for (const key of DELETE_IF_NULL) {
delete style.id; if (style[key] == null) {
delete style[key];
}
} }
if (!style._id) { if (!style._id) {
style._id = uuidv4(); style._id = uuidv4();
@ -460,7 +471,7 @@ const styleManager = (() => {
excludedScheme = true; excludedScheme = true;
} }
for (const section of data.sections) { for (const section of data.sections) {
if (styleCodeEmpty(section.code)) { if (styleSectionGlobal(section) && styleCodeEmpty(section.code)) {
continue; continue;
} }
const match = urlMatchSection(query, section); const match = urlMatchSection(query, section);
@ -484,7 +495,7 @@ const styleManager = (() => {
return result; return result;
} }
function getSectionsByUrl(url, id) { function getSectionsByUrl(url, id, isInitialApply) {
let cache = cachedStyleForUrl.get(url); let cache = cachedStyleForUrl.get(url);
if (!cache) { if (!cache) {
cache = { cache = {
@ -500,13 +511,13 @@ const styleManager = (() => {
.map(i => styles.get(i)) .map(i => styles.get(i))
); );
} }
if (id) { const res = id
if (cache.sections[id]) { ? cache.sections[id] ? {[id]: cache.sections[id]} : {}
return {[id]: cache.sections[id]}; : cache.sections;
} // Avoiding flicker of needlessly applied styles by providing both styles & pref in one API call
return {}; return isInitialApply && prefs.get('disableAll')
} ? Object.assign({disableAll: true}, res)
return cache.sections; : res;
function buildCache(styleList) { function buildCache(styleList) {
const query = createMatchQuery(url); const query = createMatchQuery(url);
@ -578,6 +589,16 @@ const styleManager = (() => {
touched = true; 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; return touched;
} }
} }
@ -605,7 +626,7 @@ const styleManager = (() => {
) { ) {
return true; 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; return true;
} }
// as per spec the fragment portion is ignored in @-moz-document: // as per spec the fragment portion is ignored in @-moz-document:
@ -631,15 +652,7 @@ const styleManager = (() => {
return 'sloppy'; return 'sloppy';
} }
// TODO: check for invalid regexps? // TODO: check for invalid regexps?
if ( return styleSectionGlobal(section);
(!section.regexps || !section.regexps.length) &&
(!section.urlPrefixes || !section.urlPrefixes.length) &&
(!section.urls || !section.urls.length) &&
(!section.domains || !section.domains.length)
) {
return true;
}
return false;
} }
function createCompiler(compile) { 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 */ /* exported tokenManager */
'use strict'; 'use strict';
const tokenManager = (() => { const tokenManager = (() => {
promisifyChrome({ promisifyChrome({
identity: ['launchWebAuthFlow'], 'windows': ['create', 'update', 'remove'],
'tabs': ['create', 'update', 'remove']
}); });
const AUTH = { const AUTH = {
dropbox: { dropbox: {
@ -36,7 +37,7 @@ const tokenManager = (() => {
scopes: ['https://www.googleapis.com/auth/drive.appdata'], scopes: ['https://www.googleapis.com/auth/drive.appdata'],
revoke: token => { revoke: token => {
const params = {token}; const params = {token};
return postQuery(`https://accounts.google.com/o/oauth2/revoke?${stringifyQuery(params)}`); return postQuery(`https://accounts.google.com/o/oauth2/revoke?${new URLSearchParams(params)}`);
} }
}, },
onedrive: { 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) { function authUser(name, k, interactive = false) {
const provider = AUTH[name]; const provider = AUTH[name];
const state = Math.random().toFixed(8).slice(2); const state = Math.random().toFixed(8).slice(2);
@ -159,10 +152,11 @@ const tokenManager = (() => {
if (provider.authQuery) { if (provider.authQuery) {
Object.assign(query, provider.authQuery); Object.assign(query, provider.authQuery);
} }
const url = `${provider.authURL}?${stringifyQuery(query)}`; const url = `${provider.authURL}?${new URLSearchParams(query)}`;
return browser.identity.launchWebAuthFlow({ return webextLaunchWebAuthFlow({
url, url,
interactive interactive,
redirect_uri: query.redirect_uri
}) })
.then(url => { .then(url => {
const params = new URLSearchParams( const params = new URLSearchParams(
@ -185,7 +179,7 @@ const tokenManager = (() => {
code, code,
grant_type: 'authorization_code', grant_type: 'authorization_code',
client_id: provider.clientId, client_id: provider.clientId,
redirect_uri: provider.redirect_uri || chrome.identity.getRedirectURL() redirect_uri: query.redirect_uri
}; };
if (provider.clientSecret) { if (provider.clientSecret) {
body.client_secret = provider.clientSecret; body.client_secret = provider.clientSecret;
@ -209,11 +203,9 @@ const tokenManager = (() => {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded' 'Content-Type': 'application/x-www-form-urlencoded'
} },
body: body ? new URLSearchParams(body) : null,
}; };
if (body) {
options.body = stringifyQuery(body);
}
return fetch(url, options) return fetch(url, options)
.then(r => { .then(r => {
if (r.ok) { if (r.ok) {

View File

@ -116,7 +116,7 @@
} }
function reportSuccess(saved) { 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}; const info = {updated: true, style: saved};
if (port) port.postMessage(info); if (port) port.postMessage(info);
return info; return info;
@ -139,7 +139,7 @@
if (typeof error === 'object' && error.message) { if (typeof error === 'object' && error.message) {
error = 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)}; const info = {error, STATES, style: getStyleWithNoCode(style)};
if (port) port.postMessage(info); if (port) port.postMessage(info);
return info; return info;
@ -207,13 +207,6 @@
// keep current state // keep current state
delete json.enabled; 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); const newStyle = Object.assign({}, style, json);
if (styleSectionsEqual(json, style, {checkSource: true})) { if (styleSectionsEqual(json, style, {checkSource: true})) {
// update digest even if save === false as there might be just a space added etc. // 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 */ /* exported usercssHelper */
'use strict'; 'use strict';
const usercssHelper = (() => { 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.installUsercss = installUsercss;
API_METHODS.editSaveUsercss = editSaveUsercss; API_METHODS.editSaveUsercss = editSaveUsercss;
API_METHODS.configUsercssVars = configUsercssVars; API_METHODS.configUsercssVars = configUsercssVars;
@ -17,50 +10,6 @@ const usercssHelper = (() => {
API_METHODS.buildUsercss = build; API_METHODS.buildUsercss = build;
API_METHODS.findUsercss = find; 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) { function buildMeta(style) {
if (style.usercssData) { if (style.usercssData) {
return Promise.resolve(style); 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 */ /** @type chrome.runtime.Port */
let port; let port;
let lazyBadge = IS_FRAME; 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 // 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) { if (!IS_TAB) {
@ -42,13 +43,6 @@ self.INJECTED !== 1 && (() => {
window.addEventListener(orphanEventId, orphanCheck, true); 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 // detect media change in content script
// FIXME: move this to background page when following bugs are fixed: // FIXME: move this to background page when following bugs are fixed:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1561546 // https://bugzilla.mozilla.org/show_bug.cgi?id=1561546
@ -59,19 +53,52 @@ self.INJECTED !== 1 && (() => {
function onInjectorUpdate() { function onInjectorUpdate() {
if (!isOrphaned) { if (!isOrphaned) {
updateCount(); updateCount();
updateExposeIframes(); const onOff = prefs[styleInjector.list.length ? 'subscribe' : 'unsubscribe'];
onOff(['disableAll'], updateDisableAll);
if (IS_FRAME) {
updateExposeIframes();
onOff(['exposeIframes'], updateExposeIframes);
}
} }
} }
function init() { async function init() {
return STYLE_VIA_API ? if (STYLE_VIA_API) {
API.styleViaAPI({method: 'styleApply'}) : await API.styleViaAPI({method: 'styleApply'});
API.getSectionsByUrl(getMatchUrl()).then(styleInjector.apply); } 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() { function getMatchUrl() {
let matchUrl = location.href; 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 // 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 // so we'll try the parent frame which is guaranteed to have a real URL
try { try {
@ -145,7 +172,7 @@ self.INJECTED !== 1 && (() => {
} }
} }
function doDisableAll(disableAll) { function updateDisableAll(key, disableAll) {
if (STYLE_VIA_API) { if (STYLE_VIA_API) {
API.styleViaAPI({method: 'prefChanged', prefs: {disableAll}}); API.styleViaAPI({method: 'prefChanged', prefs: {disableAll}});
} else { } else {
@ -153,22 +180,18 @@ self.INJECTED !== 1 && (() => {
} }
} }
function fetchParentDomain() { async function updateExposeIframes(key, value = prefs.get('exposeIframes')) {
return parentDomain ? const attr = 'stylus-iframe';
Promise.resolve() : const el = document.documentElement;
API.getTabUrlPrefix() if (!el) return; // got no styles so styleInjector didn't wait for <html>
.then(newDomain => { if (!value || !styleInjector.list.length) {
parentDomain = newDomain; el.removeAttribute(attr);
});
}
function updateExposeIframes() {
if (!prefs.get('exposeIframes') || window === parent || !styleInjector.list.length) {
document.documentElement.removeAttribute('stylus-iframe');
} else { } else {
fetchParentDomain().then(() => { if (!parentDomain) parentDomain = await API.getTabUrlPrefix();
document.documentElement.setAttribute('stylus-iframe', parentDomain); // 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; self.oldCode = (document.querySelector('body > pre') || document.body).textContent;
chrome.runtime.onConnect.addListener(port => { chrome.runtime.onConnect.addListener(port => {
if (port.name !== 'downloadSelf') return; if (port.name !== 'downloadSelf') return;
port.onMessage.addListener(({id, timer}) => { port.onMessage.addListener(({id, force}) => {
fetch(location.href, {mode: 'same-origin'}) fetch(location.href, {mode: 'same-origin'})
.then(r => r.text()) .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}`})) .catch(error => ({id, error: error.message || `${error}`}))
.then(msg => { .then(msg => {
port.postMessage(msg); port.postMessage(msg);
if (msg.code != null) self.oldCode = msg.code; 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 */ /* global cloneInto msg API */
'use strict'; '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.dispatchEvent(new CustomEvent(chrome.runtime.id + '-install'));
window.addEventListener(chrome.runtime.id + '-install', orphanCheck, true); window.addEventListener(chrome.runtime.id + '-install', orphanCheck, true);
@ -17,35 +21,18 @@
}, '*'); }, '*');
}); });
let gotBody = false;
let currentMd5; let currentMd5;
new MutationObserver(observeDOM).observe(document.documentElement, { const md5Url = getMeta('stylish-md5-url') || `https://update.userstyles.org/${styleId}.md5`;
childList: true, Promise.all([
subtree: true, API.findStyle({md5Url}),
}); getResource(md5Url),
observeDOM(); onDOMready(),
]).then(checkUpdatability);
function observeDOM() { document.documentElement.appendChild(
if (!gotBody) { Object.assign(document.createElement('script'), {
if (!document.body) return; textContent: `(${inPageContext})('${pageEventId}')`,
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);
});
});
}
}
function onMessage(msg) { function onMessage(msg) {
switch (msg.method) { switch (msg.method) {
@ -72,7 +59,7 @@
function checkUpdatability([installedStyle, md5]) { function checkUpdatability([installedStyle, md5]) {
// TODO: remove the following statement when USO is fixed // TODO: remove the following statement when USO is fixed
document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', { document.dispatchEvent(new CustomEvent(pageEventId, {
detail: installedStyle && installedStyle.updateUrl, detail: installedStyle && installedStyle.updateUrl,
})); }));
currentMd5 = md5; currentMd5 = md5;
@ -141,7 +128,6 @@
}); });
} }
function onClick(event) { function onClick(event) {
if (onClick.processing || !orphanCheck()) { if (onClick.processing || !orphanCheck()) {
return; return;
@ -227,13 +213,11 @@
} }
} }
function getMeta(name) { function getMeta(name) {
const e = document.querySelector(`link[rel="${name}"]`); const e = document.querySelector(`link[rel="${name}"]`);
return e ? e.getAttribute('href') : null; return e ? e.getAttribute('href') : null;
} }
function getResource(url, options) { function getResource(url, options) {
if (url.startsWith('#')) { if (url.startsWith('#')) {
return Promise.resolve(document.getElementById(url.slice(1)).textContent); return Promise.resolve(document.getElementById(url.slice(1)).textContent);
@ -280,7 +264,6 @@
.catch(() => null); .catch(() => null);
} }
function styleSectionsEqual({sections: a}, {sections: b}) { function styleSectionsEqual({sections: a}, {sections: b}) {
if (!a || !b) { if (!a || !b) {
return undefined; return undefined;
@ -318,20 +301,12 @@
} }
} }
function onDOMready() { function onDOMready() {
if (document.readyState !== 'loading') { return document.readyState !== 'loading'
return Promise.resolve(); ? Promise.resolve()
} : new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve, {once: true}));
return new Promise(resolve => {
document.addEventListener('DOMContentLoaded', function _() {
document.removeEventListener('DOMContentLoaded', _);
resolve();
});
});
} }
function openSettings(countdown = 10e3) { function openSettings(countdown = 10e3) {
const button = document.querySelector('.customize_button'); const button = document.querySelector('.customize_button');
if (button) { if (button) {
@ -349,12 +324,12 @@
} }
} }
function orphanCheck() { function orphanCheck() {
// TODO: switch to install-hook-usercss.js impl, and remove explicit orphanCheck() calls try {
if (chrome.i18n && chrome.i18n.getUILanguage()) { if (chrome.i18n.getUILanguage()) {
return true; return true;
} }
} catch (e) {}
// In Chrome content script is orphaned on an extension update/reload // In Chrome content script is orphaned on an extension update/reload
// so we need to detach event listeners // so we need to detach event listeners
window.removeEventListener(chrome.runtime.id + '-install', orphanCheck, true); window.removeEventListener(chrome.runtime.id + '-install', orphanCheck, true);
@ -366,132 +341,56 @@
} }
})(); })();
// run in page context function inPageContext(eventId) {
document.documentElement.appendChild(document.createElement('script')).text = '(' + ( document.currentScript.remove();
() => { const origMethods = {
document.currentScript.remove(); json: Response.prototype.json,
byId: document.getElementById,
// spoof Stylish extension presence in Chrome };
if (window.chrome && chrome.app) { let vars;
const realImage = window.Image; // USO bug workaround: prevent errors in console after install and busy cursor
window.Image = function Image(...args) { document.getElementById = id =>
return new Proxy(new realImage(...args), { origMethods.byId.call(document, id) ||
get(obj, key) { (/^(stylish-code|stylish-installed-style-installed-\w+|post-install-ad|style-install-unknown)$/.test(id)
return obj[key]; ? Object.assign(document.createElement('p'), {className: 'afterdownload-ad'})
}, : null);
set(obj, key, value) { // USO bug workaround: use the actual image data in customized settings
if (key === 'src' && /^chrome-extension:/i.test(value)) { document.addEventListener(eventId, ({detail}) => {
setTimeout(() => typeof obj.onload === 'function' && obj.onload()); vars = /\?/.test(detail) && new URL(detail).searchParams;
} else { if (!vars) Response.prototype.json = origMethods.json;
obj[key] = value; }, {once: true});
} Response.prototype.json = async function () {
return true; 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);
// USO bug workaround: use the actual style settings in API response if (value && ss.setting_type === 'image' && ss.style_setting_options) {
let settings; let isListed;
const originalResponseJson = Response.prototype.json; for (const opt of ss.style_setting_options) {
document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) { isListed |= opt.default = (opt.value === value);
document.removeEventListener('stylusFixBuggyUSOsettings', _); }
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425) images.set(ss.install_key, {url: value, isListed});
settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search.replace(/^\?/, '')); }
if (!settings) {
Response.prototype.json = originalResponseJson;
} }
}); if (images.size) {
Response.prototype.json = function (...args) { new MutationObserver((_, observer) => {
return originalResponseJson.call(this, ...args).then(json => { if (document.getElementById('style-settings')) {
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;
}
observer.disconnect(); 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 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) { if (elUrl) {
elRadio.checked = !isListed;
elUrl.value = url; elUrl.value = url;
} }
} }
}).observe(document, {childList: true, subtree: true}); }
} }).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;
} }
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'; const PATCH_ID = 'transition-patch';
// styles are out of order if any of these elements is injected between them // 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 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 // detect Chrome 65 via a feature it added since browser version can be spoofed
const isChromePre65 = chrome.app && typeof Worklet !== 'function'; const isChromePre65 = chrome.app && typeof Worklet !== 'function';
const docRewriteObserver = RewriteObserver(_sort); const docRewriteObserver = RewriteObserver(_sort);
@ -159,7 +158,7 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
} }
function _emitUpdate(value) { function _emitUpdate(value) {
_toggleObservers(!IS_OWN_PAGE && list.length); _toggleObservers(list.length);
onUpdate(); onUpdate();
return value; return value;
} }

View File

@ -18,6 +18,22 @@
} }
</style> </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"> <link href="vendor/codemirror/lib/codemirror.css" rel="stylesheet">
<script src="vendor/codemirror/lib/codemirror.js"></script> <script src="vendor/codemirror/lib/codemirror.js"></script>
@ -63,45 +79,27 @@
<script src="vendor-overwrites/codemirror-addon/match-highlighter.js"></script> <script src="vendor-overwrites/codemirror-addon/match-highlighter.js"></script>
<script src="js/polyfill.js"></script> <script src="msgbox/msgbox.js" async></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>
<link href="edit/codemirror-default.css" rel="stylesheet"> <link href="edit/codemirror-default.css" rel="stylesheet">
<script src="edit/codemirror-default.js"></script> <script src="edit/codemirror-default.js"></script>
<script src="edit/codemirror-factory.js"></script>
<script src="edit/util.js"></script> <script src="edit/util.js"></script>
<script src="edit/regexp-tester.js"></script> <script src="edit/regexp-tester.js"></script>
<script src="edit/live-preview.js"></script> <script src="edit/live-preview.js"></script>
<script src="edit/applies-to-line-widget.js"></script> <script src="edit/applies-to-line-widget.js"></script>
<script src="edit/reroute-hotkeys.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/colorpicker-helper.js"></script>
<script src="edit/beautify.js"></script> <script src="edit/beautify.js"></script>
<script src="edit/show-keymap-help.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/codemirror-themes.js"></script>
<script src="edit/source-editor.js"></script> <script src="edit/source-editor.js"></script>
<script src="edit/sections-editor-section.js"></script> <script src="edit/sections-editor-section.js"></script>
<script src="edit/sections-editor.js"></script> <script src="edit/sections-editor.js"></script>
<script src="edit/edit.js"></script> <script src="js/worker-util.js"></script>
<script src="msgbox/msgbox.js" async></script>
<script src="edit/linter.js"></script> <script src="edit/linter.js"></script>
<script src="edit/linter-defaults.js"></script> <script src="edit/linter-defaults.js"></script>
<script src="edit/linter-engines.js"></script> <script src="edit/linter-engines.js"></script>
@ -110,8 +108,6 @@
<script src="edit/linter-report.js"></script> <script src="edit/linter-report.js"></script>
<script src="edit/linter-config-dialog.js"></script> <script src="edit/linter-config-dialog.js"></script>
<link id="cm-theme" rel="stylesheet">
<template data-id="appliesTo"> <template data-id="appliesTo">
<li class="applies-to-item"> <li class="applies-to-item">
<div class="select-resizer"> <div class="select-resizer">
@ -285,7 +281,13 @@
<h1 id="heading">&nbsp;</h1> <!-- nbsp allocates the actual height which prevents page shift --> <h1 id="heading">&nbsp;</h1> <!-- nbsp allocates the actual height which prevents page shift -->
<section id="basic-info"> <section id="basic-info">
<div id="basic-info-name"> <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> <a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a>
</div> </div>
<div id="basic-info-enabled"> <div id="basic-info-enabled">
@ -305,7 +307,7 @@
</section> </section>
<section id="actions"> <section id="actions">
<div> <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> <button id="beautify" i18n-text="styleBeautify"></button>
<a href="manage.html" tabindex="-1"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a> <a href="manage.html" tabindex="-1"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
</div> </div>

View File

@ -7,16 +7,18 @@
} }
.CodeMirror { .CodeMirror {
border: solid #CCC 1px; 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 { .CodeMirror-lint-mark-warning {
background: none; background: none;
} }
.CodeMirror-dialog { .CodeMirror-dialog {
-webkit-animation: highlight 3s cubic-bezier(.18, .02, 0, .94); animation: highlight 3s cubic-bezier(.18, .02, 0, .94);
}
.CodeMirror-focused {
outline: -webkit-focus-ring-color auto 5px;
outline-offset: -2px;
} }
.CodeMirror-bookmark { .CodeMirror-bookmark {
background: linear-gradient(to right, currentColor, transparent); background: linear-gradient(to right, currentColor, transparent);
@ -24,13 +26,6 @@
width: 2em; width: 2em;
opacity: .5; opacity: .5;
} }
@supports (-moz-appearance:none) {
/* restrict to FF */
.CodeMirror-focused {
outline: #7dadd9 auto 1px;
outline-offset: -1px;
}
}
.CodeMirror-search-field { .CodeMirror-search-field {
width: 10em; width: 10em;
} }

View File

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

View File

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

View File

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

View File

@ -1,15 +1,11 @@
/* global CodeMirror onDOMready prefs setupLivePrefs $ $$ $create t tHTML /* global CodeMirror onDOMready prefs setupLivePrefs $ $$ $create t tHTML
createSourceEditor sessionStorageHash getOwnTab FIREFOX API tryCatch createSourceEditor sessionStorageHash getOwnTab FIREFOX API tryCatch
closeCurrentTab messageBox debounce workerUtil closeCurrentTab messageBox debounce
initBeautifyButton ignoreChromeError initBeautifyButton ignoreChromeError dirtyReporter linter
moveFocus msg createSectionsEditor rerouteHotkeys CODEMIRROR_THEMES */ moveFocus msg createSectionsEditor rerouteHotkeys CODEMIRROR_THEMES */
/* exported showCodeMirrorPopup editorWorker toggleContextMenuDelete */ /* exported showCodeMirrorPopup editorWorker toggleContextMenuDelete */
'use strict'; 'use strict';
const editorWorker = workerUtil.createWorker({
url: '/edit/editor-worker.js'
});
let saveSizeOnClose; let saveSizeOnClose;
// direct & reverse mapping of @-moz-document keywords and internal property names // direct & reverse mapping of @-moz-document keywords and internal property names
@ -28,48 +24,84 @@ document.addEventListener('visibilitychange', beforeUnload);
window.addEventListener('beforeunload', beforeUnload); window.addEventListener('beforeunload', beforeUnload);
msg.onExtension(onRuntimeMessage); msg.onExtension(onRuntimeMessage);
preinit(); lazyInit();
(() => { (async function init() {
onDOMready().then(() => { const [style] = await Promise.all([
prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip); initStyleData(),
addEventListener('showHotkeyInTooltip', showHotkeyInTooltip); onDOMready(),
showHotkeyInTooltip(); 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(); prefs.subscribe(['editor.linter'], updateLinter);
buildKeymapElement(); 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() { function initNameArea() {
if (!chrome.runtime.getPackageDirectoryEntry) { const nameEl = $('#name');
const themes = [ const resetEl = $('#reset-name');
chrome.i18n.getMessage('defaultTheme'), const isCustomName = style.updateUrl || usercss;
...CODEMIRROR_THEMES nameTarget = isCustomName ? 'customName' : 'name';
]; nameEl.placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
localStorage.codeMirrorThemes = themes.join(' '); nameEl.title = isCustomName ? t('customNameHint') : '';
return Promise.resolve(themes); nameEl.addEventListener('input', () => {
} updateName(true);
return new Promise(resolve => { resetEl.hidden = false;
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);
});
});
});
}); });
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) { function findKeyForCommand(command, map) {
@ -88,27 +120,10 @@ preinit();
} }
function buildThemeElement() { function buildThemeElement() {
const themeElement = $('#editor.theme'); CODEMIRROR_THEMES.unshift(chrome.i18n.getMessage('defaultTheme'));
const themeList = localStorage.codeMirrorThemes; $('#editor.theme').append(...CODEMIRROR_THEMES.map(s => $create('option', s)));
// move the theme after built-in CSS so that its same-specificity selectors win
const optionsFromArray = options => { document.head.appendChild($('#cm-theme'));
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));
});
}
} }
function buildKeymapElement() { function buildKeymapElement() {
@ -159,134 +174,120 @@ preinit();
} }
} }
function initEditor() { function initResizeListener() {
return Promise.all([ const {onBoundsChanged} = chrome.windows || {};
initStyleData(), if (onBoundsChanged) {
onDOMready(), // * movement is reported even if the window wasn't resized
prefs.initializing, // * fired just once when done so debounce is not needed
]) onBoundsChanged.addListener(wnd => {
.then(([style]) => { // getting the current window id as it may change if the user attached/detached the tab
const usercss = isUsercss(style); chrome.windows.getCurrent(ownWnd => {
$('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle'); if (wnd.id === ownWnd.id) rememberWindowSize();
$('#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();
}); });
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;
} }
} window.addEventListener('resize', () => {
if (!onBoundsChanged) debounce(rememberWindowSize, 100);
function updateDirty() { detectLayout();
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];
}
}
}); });
} }
getOwnTab().then(tab => { function toggleStyle() {
const ownTabId = tab.id; $('#enabled').checked = !style.enabled;
updateEnabledness(!style.enabled);
}
// use browser history back when 'back to manage' is clicked function updateDirty() {
if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) { const isDirty = dirty.isDirty();
onDOMready().then(() => { if (wasDirty !== isDirty) {
$('#cancel-button').onclick = event => { wasDirty = isDirty;
event.stopPropagation(); document.body.classList.toggle('dirty', isDirty);
event.preventDefault(); $('#save-button').disabled = !isDirty;
history.back();
};
});
} }
// no windows on android updateTitle();
if (!chrome.windows) { }
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; return;
} }
// When an edit page gets attached or detached, remember its state if (info.newPosition !== 0) {
// so we can do the same to the next one to open. prefs.set('openEditInWindow', false);
chrome.tabs.onAttached.addListener((tabId, info) => { return;
if (tabId !== ownTabId) { }
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', openEditInWindow);
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);
});
}); });
}); });
} }
@ -295,7 +296,7 @@ function onRuntimeMessage(request) {
switch (request.method) { switch (request.method) {
case 'styleUpdated': case 'styleUpdated':
if ( if (
editor.getStyleId() === request.style.id && editor.style.id === request.style.id &&
!['editPreview', 'editPreviewEnd', 'editSave', 'config'] !['editPreview', 'editPreviewEnd', 'editSave', 'config']
.includes(request.reason) .includes(request.reason)
) { ) {
@ -309,7 +310,7 @@ function onRuntimeMessage(request) {
} }
break; break;
case 'styleDeleted': case 'styleDeleted':
if (editor.getStyleId() === request.style.id) { if (editor.style.id === request.style.id) {
document.removeEventListener('visibilitychange', beforeUnload); document.removeEventListener('visibilitychange', beforeUnload);
document.removeEventListener('beforeunload', beforeUnload); document.removeEventListener('beforeunload', beforeUnload);
closeCurrentTab(); closeCurrentTab();
@ -352,8 +353,7 @@ function isUsercss(style) {
} }
function initStyleData() { function initStyleData() {
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425) const params = new URLSearchParams(location.search);
const params = new URLSearchParams(location.search.replace(/^\?/, ''));
const id = Number(params.get('id')); const id = Number(params.get('id'));
const createEmptyStyle = () => ({ const createEmptyStyle = () => ({
name: params.get('domain') || name: params.get('domain') ||
@ -409,7 +409,7 @@ function showHelp(title = '', body) {
!event || !event ||
event.type === 'click' || event.type === 'click' ||
( (
event.which === 27 && event.key === 'Escape' &&
!event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey && !event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey &&
!$('.CodeMirror-hints, #message-box') && !$('.CodeMirror-hints, #message-box') &&
( (
@ -470,7 +470,7 @@ function showCodeMirrorPopup(title, html, options) {
popup.style.pointerEvents = 'auto'; popup.style.pointerEvents = 'auto';
const onKeyDown = event => { 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 search = $('#search-replace-dialog');
const area = search && search.contains(document.activeElement) ? search : popup; const area = search && search.contains(document.activeElement) ? search : popup;
moveFocus(area, event.shiftKey ? -1 : 1); moveFocus(area, event.shiftKey ? -1 : 1);
@ -479,13 +479,12 @@ function showCodeMirrorPopup(title, html, options) {
}; };
window.addEventListener('keydown', onKeyDown, true); window.addEventListener('keydown', onKeyDown, true);
window.addEventListener('closeHelp', function _() { window.addEventListener('closeHelp', () => {
window.removeEventListener('closeHelp', _);
window.removeEventListener('keydown', onKeyDown, true); window.removeEventListener('keydown', onKeyDown, true);
document.documentElement.style.removeProperty('pointer-events'); document.documentElement.style.removeProperty('pointer-events');
rerouteHotkeys(true); rerouteHotkeys(true);
cm = popup.codebox = null; cm = popup.codebox = null;
}); }, {once: true});
return popup; return popup;
} }
@ -505,10 +504,6 @@ function rememberWindowSize() {
} }
} }
prefs.subscribe(['editor.linter'], (key, value) => {
$('body').classList.toggle('linter-disabled', value === '');
});
function fixedHeader() { function fixedHeader() {
const scrollPoint = $('#header').clientHeight - 40; const scrollPoint = $('#header').clientHeight - 40;
const linterEnabled = prefs.get('editor.linter') !== ''; const linterEnabled = prefs.get('editor.linter') !== '';
@ -541,7 +536,7 @@ function detectLayout() {
body.classList.add('fixed-header'); body.classList.add('fixed-header');
} }
}, 250); }, 250);
window.addEventListener('scroll', fixedHeader); window.addEventListener('scroll', fixedHeader, {passive: true});
} }
} else { } else {
body.classList.remove('compact-layout'); body.classList.remove('compact-layout');

View File

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

View File

@ -53,11 +53,10 @@
cm.on('changes', updateButtonState); cm.on('changes', updateButtonState);
rerouteHotkeys(false); rerouteHotkeys(false);
window.addEventListener('closeHelp', function _() { window.addEventListener('closeHelp', () => {
window.removeEventListener('closeHelp', _);
rerouteHotkeys(true); rerouteHotkeys(true);
cm = null; cm = null;
}); }, {once: true});
loadScript([ loadScript([
'/vendor/codemirror/mode/javascript/javascript.js', '/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) return editorWorker.stylelint(text, config)
.then(({results}) => { .then(({results}) => !results[0] ? [] :
if (!results[0]) { results[0].warnings.map(({line, column: ch, text, severity}) => ({
return []; from: {line: line - 1, ch: ch - 1},
} to: {line: line - 1, ch},
const output = results[0].warnings.map(({line, column: ch, text, severity}) => message: text
({ .replace('Unexpected ', '')
from: {line: line - 1, ch: ch - 1}, .replace(/^./, firstLetter => firstLetter.toUpperCase())
to: {line: line - 1, ch}, .replace(/\s*\([^(]+\)$/, ''), // strip the rule,
message: text rule: text.replace(/^.*?\s*\(([^(]+)\)$/, '$1'),
.replace('Unexpected ', '') severity,
.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)'));
});
} }
function csslint(text, config) { function csslint(text, config) {

View File

@ -1,6 +1,11 @@
/* global prefs */ /* global workerUtil */
'use strict'; 'use strict';
/* exported editorWorker */
const editorWorker = workerUtil.createWorker({
url: '/edit/editor-worker.js'
});
/* exported linter */ /* exported linter */
const linter = (() => { const linter = (() => {
const lintingUpdatedListeners = []; const lintingUpdatedListeners = [];
@ -59,8 +64,3 @@ const linter = (() => {
.then(results => [].concat(...results.filter(Boolean))); .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'); const errorContainer = $('#preview-errors');
prefs.subscribe(['editor.livePreview'], (key, value) => { 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 = createPreviewer();
previewer.update(data); 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}; const getMatchInfo = m => m && {text: m[0], pos: m.index};
browser.tabs.query({}).then(tabs => { browser.tabs.query({}).then(tabs => {
const supported = tabs.map(tab => tab.url) const supported = tabs.map(tab => tab.pendingUrl || tab.url).filter(URLS.supported);
.filter(url => URLS.supported(url));
const unique = [...new Set(supported).values()]; const unique = [...new Set(supported).values()];
for (const rxData of regexps) { for (const rxData of regexps) {
const {rx, urls} = rxData; const {rx, urls} = rxData;

View File

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

View File

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

View File

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

View File

@ -54,18 +54,20 @@ button:active {
border-color: hsl(0, 0%, 50%); border-color: hsl(0, 0%, 50%);
} }
input { input {
font: inherit; 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; background: #fff;
color: #000; color: #000;
height: 22px; height: 22px;
min-height: 22px!important; min-height: 22px!important;
line-height: 22px; line-height: 22px;
padding: 0 3px; padding: 0 3px;
font: inherit;
border: 1px solid hsl(0, 0%, 66%); border: 1px solid hsl(0, 0%, 66%);
} }
@ -208,9 +210,19 @@ select[disabled] + .select-arrow {
display: none !important; 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,
[data-focused-via-click]:focus { [data-focused-via-click]:focus {
outline: none; outline: none;
box-shadow: none;
} }
@supports (-moz-appearance: none) { @supports (-moz-appearance: none) {

View File

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

View File

@ -3,8 +3,7 @@
'use strict'; 'use strict';
(() => { (() => {
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425) const params = new URLSearchParams(location.search);
const params = new URLSearchParams(location.search.replace(/^\?/, ''));
const tabId = params.has('tabId') ? Number(params.get('tabId')) : -1; const tabId = params.has('tabId') ? Number(params.get('tabId')) : -1;
const initialUrl = params.get('updateUrl'); const initialUrl = params.get('updateUrl');
@ -249,7 +248,7 @@
(!dup ? (!dup ?
Promise.resolve(true) : Promise.resolve(true) :
messageBox.confirm(t('styleInstallOverwrite', [ messageBox.confirm(t('styleInstallOverwrite', [
data.name, data.name + (dup.customName ? ` (${dup.customName})` : ''),
dupData.version, dupData.version,
data.version, data.version,
])) ]))
@ -329,7 +328,9 @@
let sequence = null; let sequence = null;
if (tabId < 0) { if (tabId < 0) {
getData = DirectDownloader(); getData = DirectDownloader();
sequence = API.getUsercssInstallCode(initialUrl).catch(getData); sequence = API.getUsercssInstallCode(initialUrl)
.then(code => code || getData())
.catch(getData);
} else { } else {
getData = PortDownloader(); getData = PortDownloader();
sequence = getData({timer: false}); sequence = getData({timer: false});
@ -342,7 +343,11 @@
onToggled(e) { onToggled(e) {
if (e) isEnabled = e.target.checked; if (e) isEnabled = e.target.checked;
if (installed || installedDup) { if (installed || installedDup) {
(isEnabled ? start : stop)(); if (isEnabled) {
check({force: true});
} else {
stop();
}
$('.install').disabled = isEnabled; $('.install').disabled = isEnabled;
Object.assign($('#live-reload-install-hint'), { Object.assign($('#live-reload-install-hint'), {
hidden: !isEnabled, hidden: !isEnabled,
@ -351,8 +356,8 @@
} }
}, },
}; };
function check() { function check(opts) {
getData() getData(opts)
.then(update, logError) .then(update, logError)
.then(() => { .then(() => {
timer = 0; timer = 0;
@ -405,14 +410,16 @@
} }
}); });
port.onDisconnect.addListener(() => { port.onDisconnect.addListener(() => {
browser.tabs.get(tabId) chrome.tabs.get(tabId, tab =>
.then(tab => tab.url === initialUrl && location.reload()) !chrome.runtime.lastError && tab.url === initialUrl
.catch(closeCurrentTab); ? location.reload()
: closeCurrentTab());
}); });
return ({timer = true} = {}) => new Promise((resolve, reject) => { return (opts = {}) => new Promise((resolve, reject) => {
const id = performance.now(); const id = performance.now();
resolvers.set(id, {resolve, reject}); 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) => { $.remove = (selector, base = document) => {
const el = selector && typeof selector === 'string' ? $(selector, base) : selector; const el = selector && typeof selector === 'string' ? $(selector, base) : selector;
if (el) { if (el) {
@ -98,7 +101,11 @@ document.addEventListener('wheel', event => {
return; return;
} }
if (el.tagName === 'SELECT') { 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.preventDefault();
} }
event.stopImmediatePropagation(); event.stopImmediatePropagation();
@ -108,15 +115,9 @@ document.addEventListener('wheel', event => {
}); });
function onDOMready() { function onDOMready() {
if (document.readyState !== 'loading') { return document.readyState !== 'loading'
return Promise.resolve(); ? Promise.resolve()
} : new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve, {once: true}));
return new Promise(resolve => {
document.addEventListener('DOMContentLoaded', function _() {
document.removeEventListener('DOMContentLoaded', _);
resolve();
});
});
} }
@ -140,8 +141,7 @@ function animateElement(
onComplete, onComplete,
} = {}) { } = {}) {
return element && new Promise(resolve => { return element && new Promise(resolve => {
element.addEventListener('animationend', function _() { element.addEventListener('animationend', () => {
element.removeEventListener('animationend', _);
element.classList.remove( element.classList.remove(
className, className,
// In Firefox, `resolve()` might be called one frame later. // In Firefox, `resolve()` might be called one frame later.
@ -153,7 +153,7 @@ function animateElement(
onComplete.call(element); onComplete.call(element);
} }
resolve(); resolve();
}); }, {once: true});
element.classList.add(className); element.classList.add(className);
}); });
} }
@ -351,20 +351,23 @@ function focusAccessibility() {
'a', 'a',
'button', 'button',
'input', 'input',
'textarea',
'label', 'label',
'select', 'select',
'summary', 'summary',
]; ];
// try to find a focusable parent for this many parentElement jumps: // try to find a focusable parent for this many parentElement jumps:
const GIVE_UP_DEPTH = 4; 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('mousedown', suppressOutlineOnClick, {passive: true});
addEventListener('keydown', keepOutlineOnTab, {passive: true}); addEventListener('keydown', keepOutlineOnTab, {passive: true});
function suppressOutlineOnClick({target}) { function suppressOutlineOnClick({target}) {
for (let el = target, i = 0; el && i++ < GIVE_UP_DEPTH; el = el.parentElement) { 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; focusAccessibility.lastFocusedViaClick = true;
if (el.dataset.focusedViaClick === undefined) { if (el.dataset.focusedViaClick === undefined) {
el.dataset.focusedViaClick = ''; el.dataset.focusedViaClick = '';
@ -375,7 +378,7 @@ function focusAccessibility() {
} }
function keepOutlineOnTab(event) { function keepOutlineOnTab(event) {
if (event.which === 9) { if (event.key === 'Tab') {
focusAccessibility.lastFocusedViaClick = false; focusAccessibility.lastFocusedViaClick = false;
setTimeout(keepOutlineOnTab, 0, true); setTimeout(keepOutlineOnTab, 0, true);
return; return;
@ -383,7 +386,7 @@ function focusAccessibility() {
return; return;
} }
let el = document.activeElement; let el = document.activeElement;
if (!el || !focusAccessibility.ELEMENTS.includes(el.localName)) { if (!el || isOutlineAllowed(el)) {
return; return;
} }
if (el.dataset.focusedViaClick !== undefined) { if (el.dataset.focusedViaClick !== undefined) {

View File

@ -62,7 +62,19 @@ const URLS = {
// TODO: remove when "minimum_chrome_version": "61" or higher // TODO: remove when "minimum_chrome_version": "61" or higher
chromeProtectsNTP: CHROME >= 61, 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 => ( supported: url => (
url.startsWith('http') && (FIREFOX || !url.startsWith(URLS.browserWebStore)) || url.startsWith('http') && (FIREFOX || !url.startsWith(URLS.browserWebStore)) ||
@ -132,7 +144,7 @@ function findExistingTab({url, currentWindow, ignoreHash = true, ignoreSearch =
.then(tabs => tabs.find(matchTab)); .then(tabs => tabs.find(matchTab));
function matchTab(tab) { function matchTab(tab) {
const tabUrl = new URL(tab.url); const tabUrl = new URL(tab.pendingUrl || tab.url);
return tabUrl.protocol === url.protocol && return tabUrl.protocol === url.protocol &&
tabUrl.username === url.username && tabUrl.username === url.username &&
tabUrl.password === url.password && tabUrl.password === url.password &&
@ -153,57 +165,48 @@ function findExistingTab({url, currentWindow, ignoreHash = true, ignoreSearch =
* @param {number} [_.openerTabId] defaults to the active tab * @param {number} [_.openerTabId] defaults to the active tab
* @param {Boolean} [_.active=true] `true` to activate the tab * @param {Boolean} [_.active=true] `true` to activate the tab
* @param {Boolean|null} [_.currentWindow=true] `null` to check all windows * @param {Boolean|null} [_.currentWindow=true] `null` to check all windows
* @param {Boolean} [_.newWindow=false] `true` to open a new window * @param {chrome.windows.CreateData} [_.newWindow] creates a new window with these params if specified
* @param {chrome.windows.CreateData} [_.windowPosition] options for chrome.windows.create
* @returns {Promise<chrome.tabs.Tab>} Promise -> opened/activated tab * @returns {Promise<chrome.tabs.Tab>} Promise -> opened/activated tab
*/ */
function openURL({ async function openURL({
url, url,
index, index,
openerTabId, openerTabId,
active = true, active = true,
currentWindow = true, currentWindow = true,
newWindow = false, newWindow,
windowPosition,
}) { }) {
if (!url.includes('://')) { if (!url.includes('://')) {
url = chrome.runtime.getURL(url); url = chrome.runtime.getURL(url);
} }
return findExistingTab({url, currentWindow}).then(tab => { let tab = await findExistingTab({url, currentWindow});
if (tab) { if (tab) {
return activateTab(tab, { return activateTab(tab, {
index, index,
openerTabId, openerTabId,
// when hash is different we can only set `url` if it has # otherwise the tab would reload // 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, url: url !== (tab.pendingUrl || 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);
} }
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) // replace empty tab (NTP or about:blank)
// except when new URL is chrome:// or chrome-extension:// and the empty tab is // except when new URL is chrome:// or chrome-extension:// and the empty tab is
// in incognito // in incognito
function isTabReplaceable(tab, newUrl) { function isTabReplaceable(tab, newUrl) {
if (!tab || !URLS.emptyTab.includes(tab.url)) { if (!tab || !URLS.emptyTab.includes(tab.pendingUrl || tab.url)) {
return false; return false;
} }
// FIXME: but why?
if (tab.incognito && newUrl.startsWith('chrome')) { if (tab.incognito && newUrl.startsWith('chrome')) {
return false; return false;
} }
@ -439,7 +442,7 @@ function download(url, {
function collapseUsoVars(url) { function collapseUsoVars(url) {
if (queryPos < 0 || if (queryPos < 0 ||
url.length < 2000 || url.length < 2000 ||
!url.startsWith(URLS.userstylesOrgJson) || !url.startsWith(URLS.usoJson) ||
!/^get$/i.test(method)) { !/^get$/i.test(method)) {
return url; return url;
} }

View File

@ -7,10 +7,12 @@
* Puts the global comments into the following section to minimize the amount of global sections. * Puts the global comments into the following section to minimize the amount of global sections.
* Doesn't move the comment with ==UserStyle== inside. * Doesn't move the comment with ==UserStyle== inside.
* @param {string} code * @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 * @param {number} styleId - used to preserve parserCache on subsequent runs over the same style
* @returns {{sections: Array, errors: Array}} * @returns {{sections: Array, errors: Array}}
*/ */
function parseMozFormat({code, styleId}) { function parseMozFormat({code, emptyDocument, styleId}) {
const CssToProperty = { const CssToProperty = {
'url': 'urls', 'url': 'urls',
'url-prefix': 'urlPrefixes', 'url-prefix': 'urlPrefixes',
@ -18,7 +20,7 @@ function parseMozFormat({code, styleId}) {
'regexp': 'regexps', 'regexp': 'regexps',
}; };
const hasSingleEscapes = /([^\\]|^)\\([^\\]|$)/; const hasSingleEscapes = /([^\\]|^)\\([^\\]|$)/;
const parser = new parserlib.css.Parser(); const parser = new parserlib.css.Parser({starHack: true, emptyDocument});
const sectionStack = [{code: '', start: 0}]; const sectionStack = [{code: '', start: 0}];
const errors = []; const errors = [];
const sections = []; const sections = [];
@ -70,6 +72,13 @@ function parseMozFormat({code, styleId}) {
doAddSection(section); 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', () => { parser.addListener('endstylesheet', () => {
// add nonclosed outer sections (either broken or the last global one) // add nonclosed outer sections (either broken or the last global one)
const lastSection = sectionStack[sectionStack.length - 1]; const lastSection = sectionStack[sectionStack.length - 1];

View File

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

View File

@ -3,27 +3,33 @@
// eslint-disable-next-line no-unused-expressions // eslint-disable-next-line no-unused-expressions
self.INJECTED !== 1 && (() => { self.INJECTED !== 1 && (() => {
// this part runs in workers, content scripts, our extension pages //#region for content scripts and our extension pages
if (!Object.entries) { if (!window.browser || !browser.runtime) {
Object.entries = obj => Object.keys(obj).map(k => [k, obj[k]]); 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`. /* Promisifies the specified `chrome` methods into `browser`.
The definitions is an object like this: { The definitions is an object like this: {
'storage.sync': ['get', 'set'], // if deeper than one level, combine the path via `.` '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` 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)) { for (const [scopeName, methods] of Object.entries(definitions)) {
const path = scopeName.split('.'); const path = scopeName.split('.');
const src = path.reduce((obj, p) => obj && obj[p], chrome); const src = path.reduce((obj, p) => obj && obj[p], chrome);
@ -43,90 +49,33 @@ self.INJECTED !== 1 && (() => {
}; };
if (!chrome.tabs) return; if (!chrome.tabs) return;
// the rest is for our extension pages
if (typeof document === 'object') { //#endregion
const ELEMENT_METH = { //#region for our extension pages
append: {
base: [Element, Document, DocumentFragment], for (const storage of ['localStorage', 'sessionStorage']) {
fn: (node, frag) => { try {
node.appendChild(frag); window[storage]._access_check = 1;
} delete window[storage]._access_check;
}, } catch (err) {
prepend: { Object.defineProperty(window, storage, {value: {}});
base: [Element, Document, DocumentFragment], }
fn: (node, frag) => { }
node.insertBefore(frag, node.firstChild);
} if (!(new URLSearchParams({foo: 1})).get('foo')) {
}, // TODO: remove when minimum_chrome_version >= 61
before: { window.URLSearchParams = class extends URLSearchParams {
base: [Element, CharacterData, DocumentType], constructor(init) {
fn: (node, frag) => { if (init && typeof init === 'object') {
node.parentNode.insertBefore(frag, node); super();
} for (const [key, val] of Object.entries(init)) {
}, this.set(key, val);
after: { }
base: [Element, CharacterData, DocumentType], } else {
fn: (node, frag) => { super(...arguments);
node.parentNode.insertBefore(frag, node.nextSibling);
} }
} }
}; };
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'; 'use strict';
// Needs msg.js loaded first
self.prefs = self.INJECTED === 1 ? self.prefs : (() => { self.prefs = self.INJECTED === 1 ? self.prefs : (() => {
const defaults = { const defaults = {
'openEditInWindow': false, // new editor opens in a own browser window '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 'windowPosition': {}, // detached window position
'show-badge': true, // display text on popup menu icon 'show-badge': true, // display text on popup menu icon
'disableAll': false, // boss key 'disableAll': false, // boss key
'exposeIframes': false, // Add 'stylus-iframe' attribute to HTML element in all iframes 'exposeIframes': false, // Add 'stylus-iframe' attribute to HTML element in all iframes
'newStyleAsUsercss': false, // create new style in usercss format 'newStyleAsUsercss': false, // create new style in usercss format
'styleViaXhr': false, // early style injection to avoid FOUC
// checkbox in style config dialog // checkbox in style config dialog
'config.autosave': true, 'config.autosave': true,
@ -32,9 +36,9 @@ self.prefs = self.INJECTED === 1 ? self.prefs : (() => {
'manage.onlyLocal.invert': false, // display only externally installed styles 'manage.onlyLocal.invert': false, // display only externally installed styles
'manage.onlyUsercss.invert': false, // display only non-usercss (standard) styles 'manage.onlyUsercss.invert': false, // display only non-usercss (standard) styles
// UI element state: expanded/collapsed // UI element state: expanded/collapsed
'manage.actions.expanded': true,
'manage.backup.expanded': true, 'manage.backup.expanded': true,
'manage.filters.expanded': true, 'manage.filters.expanded': true,
'manage.options.expanded': true,
// the new compact layout doesn't look good on Android yet // the new compact layout doesn't look good on Android yet
'manage.newUI': !navigator.appVersion.includes('Android'), 'manage.newUI': !navigator.appVersion.includes('Android'),
'manage.newUI.favicons': false, // show favicons for the sites in applies-to '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'], 'storage.sync': ['get', 'set'],
}); });
const initializing = browser.storage.sync.get('settings') const initializing = (
.then(result => { msg.isBg
if (result.settings) { ? browser.storage.sync.get('settings').then(res => res.settings)
setAll(result.settings, true); : API.getPrefs()
} ).then(res => res && setAll(res, true));
});
chrome.storage.onChanged.addListener((changes, area) => { chrome.storage.onChanged.addListener((changes, area) => {
if (area !== 'sync' || !changes.settings || !changes.settings.newValue) { if (area !== 'sync' || !changes.settings || !changes.settings.newValue) {

View File

@ -31,18 +31,9 @@ const router = (() => {
} }
function updateSearch(key, value) { function updateSearch(key, value) {
const search = new URLSearchParams(location.search.replace(/^\?/, '')); const u = new URL(location);
if (!value) { u.searchParams[value ? 'set' : 'delete'](key, value);
search.delete(key); history.replaceState(history.state, null, `${u}`);
} 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}`);
}
update(true); update(true);
} }
@ -66,7 +57,7 @@ const router = (() => {
} }
function getSearch(key) { function getSearch(key) {
return new URLSearchParams(location.search.replace(/^\?/, '')).get(key); return new URLSearchParams(location.search).get(key);
} }
function update(replace) { function update(replace) {
@ -86,8 +77,7 @@ const router = (() => {
if (options.hash) { if (options.hash) {
state = options.hash === location.hash; state = options.hash === location.hash;
} else if (options.search) { } else if (options.search) {
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425) const search = new URLSearchParams(location.search);
const search = new URLSearchParams(location.search.replace(/^\?/, ''));
state = options.search.map(key => search.get(key)); state = options.search.map(key => search.get(key));
} }
if (!deepEqual(state, options.currentState)) { 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'; 'use strict';
function styleCodeEmpty(code) { function styleCodeEmpty(code) {
@ -14,6 +14,14 @@ function styleCodeEmpty(code) {
return false; 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} a - first style object
* @param {Style} b - second style object * @param {Style} b - second style object

View File

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

View File

@ -149,8 +149,8 @@
<script src="js/polyfill.js"></script> <script src="js/polyfill.js"></script>
<script src="js/dom.js"></script> <script src="js/dom.js"></script>
<script src="js/messaging.js"></script> <script src="js/messaging.js"></script>
<script src="js/prefs.js"></script>
<script src="js/msg.js"></script> <script src="js/msg.js"></script>
<script src="js/prefs.js"></script>
<script src="js/router.js"></script> <script src="js/router.js"></script>
<script src="content/style-injector.js"></script> <script src="content/style-injector.js"></script>
<script src="content/apply.js"></script> <script src="content/apply.js"></script>
@ -278,8 +278,11 @@
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg> <svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a> </a>
</div> </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"> <div id="update-check">
<button id="check-all-updates" i18n-text="checkAllUpdates"><span id="update-progress"></span></button> <button id="check-all-updates" i18n-text="checkAllUpdates"><span id="update-progress"></span></button>
<a href="#" id="update-history" i18n-title="genericHistoryLabel" tabindex="0"> <a href="#" id="update-history" i18n-title="genericHistoryLabel" tabindex="0">
@ -313,85 +316,19 @@
</a> </a>
</label> </label>
</div> </div>
</div>
<button id="manage-options-button" i18n-text="openOptions"></button>
</details>
</div> </div>
<div class="settings-column"> <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"> <details id="backup" data-pref="manage.backup.expanded">
<summary><h2 id="backup-title" i18n-text="backupButtons"></h2></summary> <summary><h2 id="backup-title" i18n-text="backupButtons"></h2></summary>
<span id="backup-message" i18n-text="backupMessage"></span> <span id="backup-message" i18n-text="backupMessage"></span>
<div id="backup-buttons"> <div id="backup-buttons">
<div class="dropdown"> <button id="file-all-styles" i18n-text="exportLabel"></button>
<button class="dropbtn"> <button id="unfile-all-styles" i18n-text="importLabel"></button>
<span>Export</span> <button id="sync-styles" i18n-text="optionsCustomizeSync"></button>
<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>
</div> </div>
</details> </details>

View File

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

View File

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

View File

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

View File

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

View File

@ -54,7 +54,6 @@ a:hover {
top: 0; top: 0;
padding: 1rem; padding: 1rem;
border-right: 1px dashed #AAA; border-right: 1px dashed #AAA;
-webkit-box-shadow: 0 0 50px -18px black;
box-shadow: 0 0 50px -18px black; box-shadow: 0 0 50px -18px black;
overflow: auto; overflow: auto;
box-sizing: border-box; box-sizing: border-box;
@ -285,13 +284,6 @@ a:hover {
margin-top: .5rem; margin-top: .5rem;
} }
#options-buttons {
display: flex;
flex-wrap: wrap;
padding-top: .1rem;
}
#options-buttons button,
#backup-buttons button { #backup-buttons button {
margin: 0 .2rem .5rem 0; margin: 0 .2rem .5rem 0;
} }
@ -557,8 +549,6 @@ a:hover {
.newUI .update-done .updated svg { .newUI .update-done .updated svg {
top: -4px; top: -4px;
position: relative; position: relative;
/* unprefixed since Chrome 53 */
-webkit-filter: drop-shadow(0 4px 0 currentColor);
filter: drop-shadow(0 5px 0 currentColor); filter: drop-shadow(0 5px 0 currentColor);
} }
@ -670,8 +660,6 @@ a:hover {
margin-left: -20px; margin-left: -20px;
margin-right: 4px; margin-right: 4px;
transition: opacity .5s, filter .5s; transition: opacity .5s, filter .5s;
/* unprefixed since Chrome 53 */
-webkit-filter: grayscale(1);
filter: grayscale(1); filter: grayscale(1);
/* workaround for the buggy CSS filter: images in the hidden overflow are shown on Mac */ /* workaround for the buggy CSS filter: images in the hidden overflow are shown on Mac */
backface-visibility: hidden; backface-visibility: hidden;
@ -689,68 +677,7 @@ a:hover {
.newUI .entry:hover .target img { .newUI .entry:hover .target img {
opacity: 1; opacity: 1;
/* unprefixed since Chrome 53 */ filter: none;
-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;
} }
/* Default, no update buttons */ /* Default, no update buttons */
@ -1046,54 +973,6 @@ input[id^="manage.newUI"] {
text-overflow: ellipsis; 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 */ /* sort font */
@font-face { @font-face {
font-family: 'sorticon'; font-family: 'sorticon';
@ -1125,6 +1004,10 @@ input[id^="manage.newUI"] {
animation: fadeout .25s ease-in-out; animation: fadeout .25s ease-in-out;
} }
.settings-column {
margin-top: 1rem;
}
@keyframes fadein { @keyframes fadein {
from { from {
opacity: 0; opacity: 0;
@ -1302,14 +1185,3 @@ input[id^="manage.newUI"] {
margin-left: -2px; 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 checkUpdate handleUpdateInstalled
objectDiff objectDiff
configDialog configDialog
sorter msg prefs API onDOMready $ $$ $create template setupLivePrefs sorter msg prefs API $ $$ $create template setupLivePrefs
URLS enforceInputRange t tWordBreak formatDate t tWordBreak formatDate
getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce
scrollElementIntoView CHROME VIVALDI FIREFOX router scrollElementIntoView CHROME VIVALDI router
bulkChangeTime:true bulkChangeQueue
*/ */
'use strict'; 'use strict';
@ -18,16 +17,27 @@ const ENTRY_ID_PREFIX_RAW = 'style-';
const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW; const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW;
const BULK_THROTTLE_MS = 100; const BULK_THROTTLE_MS = 100;
const bulkChangeQueue = [];
bulkChangeQueue.time = 0;
// define pref-mapped ids separately
const newUI = { const newUI = {
enabled: prefs.get('manage.newUI'), enabled: null, // the global option should come first
favicons: prefs.get('manage.newUI.favicons'), favicons: null,
faviconsGray: prefs.get('manage.newUI.faviconsGray'), faviconsGray: null,
targets: prefs.get('manage.newUI.targets'), targets: null,
renderClass() {
document.documentElement.classList.toggle('newUI', newUI.enabled);
},
}; };
// ...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(); newUI.renderClass();
const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps']; const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps'];
@ -40,23 +50,45 @@ Promise.all([
API.getAllStyles(true), API.getAllStyles(true),
// FIXME: integrate this into filter.js // FIXME: integrate this into filter.js
router.getSearch('search') && API.searchDB({query: router.getSearch('search')}), router.getSearch('search') && API.searchDB({query: router.getSearch('search')}),
Promise.all([ waitForSelector('#installed'), // needed to avoid flicker due to an extra frame and layout shift
onDOMready(), prefs.initializing
prefs.initializing, ]).then(([styles, ids, el]) => {
]) installed = el;
.then(() => { installed.onclick = handleEvent.entryClicked;
initGlobalEvents(); $('#manage-options-button').onclick = () => router.updateHash('#stylus-options');
if (!VIVALDI) { $('#sync-styles').onclick = () => router.updateHash('#stylus-options');
$$('#header select').forEach(el => el.adjustWidth()); $$('#header a[href^="http"]').forEach(a => (a.onclick = handleEvent.external));
} // show date installed & last update on hover
if (FIREFOX && 'update' in (chrome.commands || {})) { installed.addEventListener('mouseover', handleEvent.lazyAddEntryTitle);
const btn = $('#manage-shortcuts-button'); installed.addEventListener('mouseout', handleEvent.lazyAddEntryTitle);
btn.classList.remove('chromium-only'); document.addEventListener('visibilitychange', onVisibilityChange);
btn.onclick = API.optionsCustomizeHotkeys; // N.B. triggers existing onchange listeners
} setupLivePrefs();
}), sorter.init();
]).then(args => { prefs.subscribe(newUI.ids.map(newUI.prefKeyForId), () => switchUI());
showStyles(...args); 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); msg.onExtension(onRuntimeMessage);
@ -67,7 +99,7 @@ function onRuntimeMessage(msg) {
case 'styleAdded': case 'styleAdded':
case 'styleDeleted': case 'styleDeleted':
bulkChangeQueue.push(msg); bulkChangeQueue.push(msg);
if (performance.now() - bulkChangeTime < BULK_THROTTLE_MS) { if (performance.now() - bulkChangeQueue.time < BULK_THROTTLE_MS) {
debounce(handleBulkChange, BULK_THROTTLE_MS); debounce(handleBulkChange, BULK_THROTTLE_MS);
} else { } else {
handleBulkChange(); handleBulkChange();
@ -82,74 +114,16 @@ function onRuntimeMessage(msg) {
setTimeout(sorter.updateStripes, 0, {onlyWhenColumnsChanged: true}); 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) { function showStyles(styles = [], matchUrlIds) {
const sorted = sorter.sort({ const sorted = sorter.sort({
styles: styles.map(style => ({ styles: styles.map(style => {
style, const name = style.customName || style.name || '';
name: (style.name || '').toLocaleLowerCase() + '\n' + 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 index = 0;
let firstRun = true; 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 // query the sub-elements just once, then reuse the references
if ((createStyleElement.parts || {}).newUI !== newUI.enabled) { if ((createStyleElement.parts || {}).newUI !== newUI.enabled) {
const entry = template[`style${newUI.enabled ? 'Compact' : ''}`]; const entry = template[`style${newUI.enabled ? 'Compact' : ''}`];
@ -225,8 +199,9 @@ function createStyleElement({style, name}) {
} }
const parts = createStyleElement.parts; const parts = createStyleElement.parts;
const configurable = style.usercssData && style.usercssData.vars && Object.keys(style.usercssData.vars).length > 0; 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.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.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id;
parts.homepage.href = parts.homepage.title = style.url || ''; parts.homepage.href = parts.homepage.title = style.url || '';
if (!newUI.enabled) { if (!newUI.enabled) {
@ -243,7 +218,7 @@ function createStyleElement({style, name}) {
const entry = parts.entry.cloneNode(true); const entry = parts.entry.cloneNode(true);
entry.id = ENTRY_ID_PREFIX_RAW + style.id; entry.id = ENTRY_ID_PREFIX_RAW + style.id;
entry.styleId = style.id; entry.styleId = style.id;
entry.styleNameLowerCase = name || style.name.toLocaleLowerCase(); entry.styleNameLowerCase = nameLC || name.toLocaleLowerCase() + '\n' + name;
entry.styleMeta = style; entry.styleMeta = style;
entry.className = parts.entryClassBase + ' ' + entry.className = parts.entryClassBase + ' ' +
(style.enabled ? 'enabled' : 'disabled') + (style.enabled ? 'enabled' : 'disabled') +
@ -401,10 +376,11 @@ Object.assign(handleEvent, {
const openWindow = left && shift && !ctrl; const openWindow = left && shift && !ctrl;
const openBackgroundTab = (middle && !shift) || (left && ctrl && !shift); const openBackgroundTab = (middle && !shift) || (left && ctrl && !shift);
const openForegroundTab = (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 (openWindow || openBackgroundTab || openForegroundTab) {
if (chrome.windows && openWindow) { if (chrome.windows && openWindow) {
chrome.windows.create(Object.assign(prefs.get('windowPosition'), {url})); API.openEditor({id: entry.styleId});
} else { } else {
getOwnTab().then(({index}) => { getOwnTab().then(({index}) => {
openURL({ openURL({
@ -445,7 +421,7 @@ Object.assign(handleEvent, {
animateElement(entry); animateElement(entry);
messageBox({ messageBox({
title: t('deleteStyleConfirm'), title: t('deleteStyleConfirm'),
contents: entry.styleMeta.name, contents: entry.styleMeta.customName || entry.styleMeta.name,
className: 'danger center', className: 'danger center',
buttons: [t('confirmDelete'), t('confirmCancel')], buttons: [t('confirmDelete'), t('confirmCancel')],
}) })
@ -489,7 +465,7 @@ Object.assign(handleEvent, {
const y = Math.max(0, top); const y = Math.max(0, top);
const first = document.elementFromPoint(x, y); const first = document.elementFromPoint(x, y);
const lastOffset = first.offsetTop + window.innerHeight; 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]; let entry = first && first.closest('.entry') || installed.children[0];
while (entry && entry.offsetTop <= lastOffset) { while (entry && entry.offsetTop <= lastOffset) {
favicons.push(...$$('img', entry).slice(0, numTargets).filter(img => img.dataset.src)); favicons.push(...$$('img', entry).slice(0, numTargets).filter(img => img.dataset.src));
@ -541,7 +517,7 @@ function handleBulkChange() {
const {id} = msg.style; const {id} = msg.style;
if (msg.method === 'styleDeleted') { if (msg.method === 'styleDeleted') {
handleDelete(id); handleDelete(id);
bulkChangeTime = performance.now(); bulkChangeQueue.time = performance.now();
} else { } else {
handleUpdateForId(id, msg); handleUpdateForId(id, msg);
} }
@ -552,7 +528,7 @@ function handleBulkChange() {
function handleUpdateForId(id, opts) { function handleUpdateForId(id, opts) {
return API.getStyle(id, true).then(style => { return API.getStyle(id, true).then(style => {
handleUpdate(style, opts); handleUpdate(style, opts);
bulkChangeTime = performance.now(); bulkChangeQueue.time = performance.now();
}); });
} }
@ -619,10 +595,8 @@ function switchUI({styleOnly} = {}) {
const current = {}; const current = {};
const changed = {}; const changed = {};
let someChanged = false; let someChanged = false;
// ensure the global option is processed first for (const id of newUI.ids) {
for (const el of [$('#manage.newUI'), ...$$('[id^="manage.newUI."]')]) { const value = prefs.get(newUI.prefKeyForId(id));
const id = el.id.replace(/^manage\.newUI\.?/, '') || 'enabled';
const value = el.type === 'checkbox' ? el.checked : Number(el.value);
const valueChanged = value !== newUI[id] && (id === 'enabled' || current.enabled); const valueChanged = value !== newUI[id] && (id === 'enabled' || current.enabled);
current[id] = value; current[id] = value;
changed[id] = valueChanged; changed[id] = valueChanged;
@ -642,13 +616,11 @@ function switchUI({styleOnly} = {}) {
} }
` + (newUI.faviconsGray ? ` ` + (newUI.faviconsGray ? `
.newUI .target img { .newUI .target img {
-webkit-filter: grayscale(1);
filter: grayscale(1); filter: grayscale(1);
opacity: .25; opacity: .25;
} }
` : ` ` : `
.newUI .target img { .newUI .target img {
-webkit-filter: none;
filter: none; filter: none;
opacity: 1; 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() { function embedOptions() {
let options = $('#stylus-embedded-options'); let options = $('#stylus-embedded-options');

View File

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

View File

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

View File

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

View File

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

View File

@ -41,6 +41,16 @@
<div id="options"> <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"> <div class="block">
<h1 i18n-text="optionsCustomizeIcon"></h1> <h1 i18n-text="optionsCustomizeIcon"></h1>
<div class="items"> <div class="items">
@ -101,6 +111,13 @@
<span></span> <span></span>
</span> </span>
</label> </label>
<label>
<span i18n-text="popupOpenEditInPopup"></span>
<span class="onoffswitch">
<input type="checkbox" id="openEditInWindow.popup" class="slider">
<span></span>
</span>
</label>
<label> <label>
<span i18n-text="popupStylesFirst"></span> <span i18n-text="popupStylesFirst"></span>
<span class="onoffswitch"> <span class="onoffswitch">
@ -125,6 +142,45 @@
</div> </div>
</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"> <div class="block" id="updates">
<h1 i18n-text="optionsCustomizeUpdate"></h1> <h1 i18n-text="optionsCustomizeUpdate"></h1>
<div class="items"> <div class="items">
@ -183,9 +239,25 @@
</h1> </h1>
</div> </div>
<div class="items"> <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"> <label class="option-item">
<span i18n-text="optionsAdvancedExposeIframes"> <span i18n-text="optionsAdvancedExposeIframes">
<a data-cmd="note" <a data-cmd="note"
i18n-data-title="optionsAdvancedExposeIframesNote"
i18n-title="optionsAdvancedExposeIframesNote" i18n-title="optionsAdvancedExposeIframesNote"
href="#" href="#"
class="svg-inline-wrapper" class="svg-inline-wrapper"

View File

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

View File

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

View File

@ -5,16 +5,9 @@
'use strict'; 'use strict';
setupLivePrefs(); setupLivePrefs();
enforceInputRange($('#popupWidth')); $$('input[min], input[max]').forEach(enforceInputRange);
setTimeout(splitLongTooltips); 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) { if (CHROME_HAS_BORDER_BUG) {
const borderOption = $('.chrome-no-popup-border'); const borderOption = $('.chrome-no-popup-border');
if (borderOption) { 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 // actions
$('#options-close-icon').onclick = () => { $('#options-close-icon').onclick = () => {
top.dispatchEvent(new CustomEvent('closeOptions')); top.dispatchEvent(new CustomEvent('closeOptions'));
@ -85,7 +99,7 @@ document.onclick = e => {
e.preventDefault(); e.preventDefault();
messageBox({ messageBox({
className: 'note', className: 'note',
contents: target.title, contents: target.dataset.title,
buttons: [t('confirmClose')], buttons: [t('confirmClose')],
}); });
} }
@ -216,6 +230,7 @@ function splitLongTooltips() {
.map(s => s.replace(/(.{50,80}(?=.{40,}))\s+/g, '$1\n')) .map(s => s.replace(/(.{50,80}(?=.{40,}))\s+/g, '$1\n'))
.join('\n'); .join('\n');
if (newTitle !== el.title) { if (newTitle !== el.title) {
el.dataset.title = el.title;
el.title = newTitle; el.title = newTitle;
} }
} }
@ -281,7 +296,7 @@ function customizeHotkeys() {
} }
window.onkeydown = event => { window.onkeydown = event => {
if (event.keyCode === 27) { if (event.key === 'Escape') {
top.dispatchEvent(new CustomEvent('closeOptions')); top.dispatchEvent(new CustomEvent('closeOptions'));
} }
}; };

31
package-lock.json generated
View File

@ -1917,9 +1917,9 @@
"dev": true "dev": true
}, },
"codemirror": { "codemirror": {
"version": "5.56.0", "version": "5.58.0",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.56.0.tgz", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.58.0.tgz",
"integrity": "sha512-MfKVmYgifXjQpLSgpETuih7A7WTTIsxvKfSLGseTY5+qt0E1UD1wblZGM6WLenORo8sgmf+3X+WTe2WF7mufyw==" "integrity": "sha512-OUK+7EgaYnLyC0F09UWjckLWvviy02IDDGTW5Zmj60a3gdGnFtUM6rVsqrfl5+YSylQVQBNfAGG4KF7tQOb4/Q=="
}, },
"collection-visit": { "collection-visit": {
"version": "1.0.0", "version": "1.0.0",
@ -5065,9 +5065,9 @@
} }
}, },
"lodash": { "lodash": {
"version": "4.17.15", "version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
"dev": true "dev": true
}, },
"lodash.defaults": { "lodash.defaults": {
@ -5324,9 +5324,9 @@
} }
}, },
"minimist": { "minimist": {
"version": "1.2.0", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true "dev": true
}, },
"minipass": { "minipass": {
@ -5425,14 +5425,6 @@
"dev": true, "dev": true,
"requires": { "requires": {
"minimist": "^1.2.5" "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": { "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": { "webext-tx-fix": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/webext-tx-fix/-/webext-tx-fix-0.3.3.tgz", "resolved": "https://registry.npmjs.org/webext-tx-fix/-/webext-tx-fix-0.3.3.tgz",

View File

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

View File

@ -120,9 +120,7 @@
<div class="search-result-actions"> <div class="search-result-actions">
<button class="search-result-install hidden" i18n-text="installButton"></button> <button class="search-result-install hidden" i18n-text="installButton"></button>
<button class="search-result-uninstall hidden" i18n-text="deleteStyleLabel"></button> <button class="search-result-uninstall hidden" i18n-text="deleteStyleLabel"></button>
<button class="search-result-customize hidden" <button class="search-result-customize hidden" i18n-text="configureStyle"></button>
i18n-text="configureStyle"
i18n-title="configureStyleOnHomepage"></button>
</div> </div>
<dl class="search-result-meta"> <dl class="search-result-meta">
<div data-type="author"> <div data-type="author">
@ -182,8 +180,8 @@
<script src="js/dom.js"></script> <script src="js/dom.js"></script>
<script src="js/messaging.js"></script> <script src="js/messaging.js"></script>
<script src="js/localization.js"></script> <script src="js/localization.js"></script>
<script src="js/prefs.js"></script>
<script src="js/msg.js"></script> <script src="js/msg.js"></script>
<script src="js/prefs.js"></script>
<script src="content/style-injector.js"></script> <script src="content/style-injector.js"></script>
<script src="content/apply.js"></script> <script src="content/apply.js"></script>
@ -254,6 +252,27 @@
<div id="search-results-error" class="hidden"></div> <div id="search-results-error" class="hidden"></div>
<div id="search-results" class="hidden"> <div id="search-results" class="hidden">
<div class="search-results-nav" data-type="top"></div> <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 id="search-results-list"></div>
<div class="search-results-nav" data-type="bottom"></div> <div class="search-results-nav" data-type="bottom"></div>
</div> </div>

View File

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

View File

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

View File

@ -13,7 +13,8 @@ const handleEvent = {};
const ABOUT_BLANK = 'about:blank'; const ABOUT_BLANK = 'about:blank';
const ENTRY_ID_PREFIX_RAW = 'style-'; 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 if (CHROME >= 66 && CHROME <= 69) { // Chrome 66-69 adds a gap, https://crbug.com/821143
document.head.appendChild($create('style', 'html { overflow: overlay }')); document.head.appendChild($create('style', 'html { overflow: overlay }'));
@ -27,7 +28,7 @@ initTabUrls()
onDOMready().then(() => initPopup(frames)), onDOMready().then(() => initPopup(frames)),
...frames ...frames
.filter(f => f.url && !f.isDupe) .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]) => { .then(([, ...results]) => {
if (results[0]) { if (results[0]) {
@ -53,17 +54,19 @@ if (CHROME_HAS_BORDER_BUG) {
} }
function onRuntimeMessage(msg) { function onRuntimeMessage(msg) {
if (!tabURL) return;
let ready = Promise.resolve();
switch (msg.method) { switch (msg.method) {
case 'styleAdded': case 'styleAdded':
case 'styleUpdated': case 'styleUpdated':
if (msg.reason === 'editPreview' || msg.reason === 'editPreviewEnd') return; if (msg.reason === 'editPreview' || msg.reason === 'editPreviewEnd') return;
handleUpdate(msg); ready = handleUpdate(msg);
break; break;
case 'styleDeleted': case 'styleDeleted':
handleDelete(msg.style.id); handleDelete(msg.style.id);
break; 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) { if (!tabURL) {
document.body.classList.add('blocked'); blockPopup();
document.body.insertBefore(template.unavailableInfo, document.body.firstChild);
return; return;
} }
@ -308,31 +310,37 @@ function sortStyles(entries) {
return entries.sort(({styleMeta: a}, {styleMeta: b}) => return entries.sort(({styleMeta: a}, {styleMeta: b}) =>
Boolean(a.frameUrl) - Boolean(b.frameUrl) || Boolean(a.frameUrl) - Boolean(b.frameUrl) ||
enabledFirst && Boolean(b.enabled) - Boolean(a.enabled) || enabledFirst && Boolean(b.enabled) - Boolean(a.enabled) ||
a.name.localeCompare(b.name)); (a.customName || a.name).localeCompare(b.customName || b.name));
} }
function showStyles(frameResults) { function showStyles(frameResults) {
const entries = new Map(); const entries = new Map();
frameResults.forEach(({styles = [], url}, index) => { frameResults.forEach(({styles = [], url}, index) => {
styles.forEach(style => { styles.forEach(style => {
const {id} = style.data; const {id} = style;
if (!entries.has(id)) { if (!entries.has(id)) {
style.frameUrl = index === 0 ? '' : url; style.frameUrl = index === 0 ? '' : url;
entries.set(id, createStyleElement(Object.assign(style.data, style))); entries.set(id, createStyleElement(style));
} }
}); });
}); });
if (entries.size) { if (entries.size) {
installed.append(...sortStyles([...entries.values()])); resortEntries([...entries.values()]);
} else { } else {
installed.appendChild(template.noStyles.cloneNode(true)); installed.appendChild(template.noStyles);
} }
window.dispatchEvent(new Event('showStyles:done')); 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) { function createStyleElement(style) {
let entry = $(ENTRY_ID_PREFIX + style.id); let entry = $.entry(style);
if (!entry) { if (!entry) {
entry = template.style.cloneNode(true); entry = template.style.cloneNode(true);
entry.setAttribute('style-id', style.id); entry.setAttribute('style-id', style.id);
@ -400,7 +408,7 @@ function createStyleElement(style) {
$('.checker', entry).checked = style.enabled; $('.checker', entry).checked = style.enabled;
const styleName = $('.style-name', entry); const styleName = $('.style-name', entry);
styleName.lastChild.textContent = style.name; styleName.lastChild.textContent = style.customName || style.name;
setTimeout(() => { setTimeout(() => {
styleName.title = styleName.title =
entry.styleMeta.sloppy ? t('styleNotAppliedRegexpProblemTooltip') : entry.styleMeta.sloppy ? t('styleNotAppliedRegexpProblemTooltip') :
@ -470,11 +478,7 @@ Object.assign(handleEvent, {
event.stopPropagation(); event.stopPropagation();
API API
.toggleStyle(handleEvent.getClickedStyleId(event), this.checked) .toggleStyle(handleEvent.getClickedStyleId(event), this.checked)
.then(() => { .then(() => resortEntries());
if (prefs.get('popup.autoResort')) {
installed.append(...sortStyles($$('.entry', installed)));
}
});
}, },
toggleExclude(event, type) { toggleExclude(event, type) {
@ -504,16 +508,15 @@ Object.assign(handleEvent, {
window.onkeydown = event => { window.onkeydown = event => {
const close = $('.menu-close', entry); const close = $('.menu-close', entry);
const checkbox = $('.exclude-by-domain-checkbox', entry); const checkbox = $('.exclude-by-domain-checkbox', entry);
const keyCode = event.keyCode || event.which; if (document.activeElement === close && (event.key === 'Tab') && !event.shiftKey) {
if (document.activeElement === close && (keyCode === 9) && !event.shiftKey) {
event.preventDefault(); event.preventDefault();
checkbox.focus(); checkbox.focus();
} }
if (document.activeElement === checkbox && (keyCode === 9) && event.shiftKey) { if (document.activeElement === checkbox && (event.key === 'Tab') && event.shiftKey) {
event.preventDefault(); event.preventDefault();
close.focus(); close.focus();
} }
if (keyCode === 27) { if (event.key === 'Escape') {
event.preventDefault(); event.preventDefault();
close.click(); close.click();
} }
@ -539,20 +542,20 @@ Object.assign(handleEvent, {
const close = $('.menu-close', entry); const close = $('.menu-close', entry);
const checkbox = $('.exclude-by-domain-checkbox', entry); const checkbox = $('.exclude-by-domain-checkbox', entry);
const confirmActive = $('#confirm[data-display="true"]'); const confirmActive = $('#confirm[data-display="true"]');
const keyCode = event.keyCode || event.which; const {key} = event;
if (document.activeElement === cancel && (keyCode === 9)) { if (document.activeElement === cancel && (key === 'Tab')) {
event.preventDefault(); event.preventDefault();
affirm.focus(); affirm.focus();
} }
if (document.activeElement === close && (keyCode === 9) && !event.shiftKey) { if (document.activeElement === close && (key === 'Tab') && !event.shiftKey) {
event.preventDefault(); event.preventDefault();
checkbox.focus(); checkbox.focus();
} }
if (document.activeElement === checkbox && (keyCode === 9) && event.shiftKey) { if (document.activeElement === checkbox && (key === 'Tab') && event.shiftKey) {
event.preventDefault(); event.preventDefault();
close.focus(); close.focus();
} }
if (keyCode === 27) { if (key === 'Escape') {
event.preventDefault(); event.preventDefault();
if (confirmActive) { if (confirmActive) {
box.dataset.display = false; box.dataset.display = false;
@ -673,38 +676,25 @@ Object.assign(handleEvent, {
}); });
function handleUpdate({style, reason}) { async function handleUpdate({style, reason}) {
if (!tabURL) return; if (reason !== 'toggle' || !$.entry(style)) {
style = await getStyleDataMerged(tabURL, style.id);
fetchStyle() if (!style) return;
.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));
} }
const el = createStyleElement(style);
if (!el.parentNode) {
installed.appendChild(el);
blockPopup(false);
}
resortEntries();
} }
function handleDelete(id) { function handleDelete(id) {
$.remove(ENTRY_ID_PREFIX + id); const el = $.entry(id);
if (!$('.entry')) { if (el) {
installed.appendChild(template.noStyles.cloneNode(true)); 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; background-color: #fff;
} }
.search-result .lds-spinner { #search-results .lds-spinner {
transform: scale(.5); transform: scale(.5);
filter: invert(1) drop-shadow(1px 1px 3px #000); filter: invert(1) drop-shadow(1px 1px 3px #000);
} }
.search-result-empty .lds-spinner { #search-results .search-result-empty .lds-spinner {
transform: scale(.5);
filter: opacity(.2); filter: opacity(.2);
} }
.search-result-fadein {
animation: fadein 1s;
animation-fill-mode: both;
}
.search-result-screenshot { .search-result-screenshot {
height: 140px; height: 140px;
width: 100%; width: 100%;
@ -257,11 +251,27 @@ body.search-results-shown {
padding-left: 16px; 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 */ /* spinner: https://github.com/loadingio/css-spinner */
.lds-spinner { .lds-spinner {
-webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none;
user-select: none; user-select: none;
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;

File diff suppressed because it is too large Load Diff

View File

@ -62,6 +62,9 @@ const files = {
], ],
'uuid': [ 'uuid': [
'dist/umd/uuidv4.min.js → uuid.min.js' '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 fs = require('fs');
const archiver = require('archiver'); const archiver = require('archiver');
const manifest = require('../manifest.json');
function createZip() { function createZip({isFirefox} = {}) {
const fileName = 'stylus.zip'; const fileName = `stylus${isFirefox ? '-firefox' : ''}.zip`;
const exclude = [ const ignore = [
'.*', // dot files/folders (glob, not regexp) '.*', // dot files/folders (glob, not regexp)
'vendor/codemirror/lib/**', // get unmodified copy from node_modules 'vendor/codemirror/lib/**', // get unmodified copy from node_modules
'node_modules/**', 'node_modules/**',
@ -38,15 +39,30 @@ function createZip() {
}); });
archive.pipe(file); 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") // Don't use modified codemirror.js (see "update-libraries.js")
archive.directory('node_modules/codemirror/lib', 'vendor/codemirror/lib'); archive.directory('node_modules/codemirror/lib', 'vendor/codemirror/lib');
archive.finalize(); archive.finalize();
}); });
} }
createZip() (async () => {
.then(() => console.log('\x1b[32m%s\x1b[0m', 'Stylus zip complete')) try {
.catch(err => { await createZip();
throw err; 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); border: 1px solid var(--main-border-color);
background-color: var(--main-background-color); background-color: var(--main-background-color);
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.12); box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.12);
-webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none; user-select: none;
} }
@ -139,8 +136,6 @@
position: absolute; position: absolute;
width: 10px; width: 10px;
height: 10px; height: 10px;
-webkit-border-radius: 50%;
-moz-border-radius: 50%;
border-radius: 50%; border-radius: 50%;
left: -5px; left: -5px;
top: -5px; top: -5px;
@ -168,8 +163,6 @@
-webkit-border-radius: 50%; -webkit-border-radius: 50%;
-moz-border-radius: 50%; -moz-border-radius: 50%;
border-radius: 50%; border-radius: 50%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid var(--input-border-color); border: 1px solid var(--input-border-color);
} }
@ -191,8 +184,6 @@
position: relative; position: relative;
width: 100%; width: 100%;
height: 10px; height: 10px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px; border-radius: 3px;
cursor: pointer; cursor: pointer;
background: linear-gradient(to right, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); 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%; width: 100%;
height: 10px; height: 10px;
z-index: 2; z-index: 2;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px; border-radius: 3px;
cursor: pointer; cursor: pointer;
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAJElEQVQYV2NctWrVfwYkEBYWxojMZ6SDAmT7QGx0K1EcRBsFAADeG/3M/HteAAAAAElFTkSuQmCC"); background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAJElEQVQYV2NctWrVfwYkEBYWxojMZ6SDAmT7QGx0K1EcRBsFAADeG/3M/HteAAAAAElFTkSuQmCC");
@ -239,9 +228,6 @@
.colorpicker-input-container { .colorpicker-input-container {
position: relative; position: relative;
-webkit-box-sizing: padding-box;
-moz-box-sizing: padding-box;
box-sizing: padding-box;
} }
.colorpicker-input-group { .colorpicker-input-group {
@ -263,8 +249,6 @@
position: relative; position: relative;
flex: 1; flex: 1;
padding: 5px; padding: 5px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
} }
@ -295,10 +279,7 @@
font-size: 11px; font-size: 11px;
font-weight: bold; font-weight: bold;
box-sizing: border-box; box-sizing: border-box;
-webkit-user-select: text;
-moz-user-select: text; -moz-user-select: text;
-ms-user-select: text;
-o-user-select: text;
user-select: text; user-select: text;
border: 1px solid var(--input-border-color); border: 1px solid var(--input-border-color);
background-color: var(--input-background-color); background-color: var(--input-background-color);
@ -306,7 +287,6 @@
} }
.colorpicker-theme-dark .colorpicker-input::-webkit-inner-spin-button { .colorpicker-theme-dark .colorpicker-input::-webkit-inner-spin-button {
-webkit-filter: invert(1);
filter: invert(1); filter: invert(1);
} }
@ -377,3 +357,15 @@
.colorpicker-format-change-button:hover { .colorpicker-format-change-button:hover {
color: var(--label-color-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 $swatch;
let $formatChangeButton; let $formatChangeButton;
let $hexCode; let $hexCode;
let $palette;
const $inputGroups = {}; const $inputGroups = {};
const $inputs = {}; const $inputs = {};
const $rgb = {}; const $rgb = {};
@ -164,6 +165,7 @@
$formatChangeButton = $('format-change-button', {textContent: '↔'}), $formatChangeButton = $('format-change-button', {textContent: '↔'}),
]}), ]}),
]}), ]}),
$palette = $('palette'),
]}); ]});
$inputs.hex = [$hexCode]; $inputs.hex = [$hexCode];
@ -221,6 +223,11 @@
if (!isNaN(options.left) && !isNaN(options.top)) { if (!isNaN(options.left) && !isNaN(options.top)) {
reposition(); 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() { function hide() {
@ -355,29 +362,29 @@
} }
function setFromKeyboard(event) { function setFromKeyboard(event) {
const {which, ctrlKey: ctrl, altKey: alt, shiftKey: shift, metaKey: meta} = event; const {key, ctrlKey: ctrl, altKey: alt, shiftKey: shift, metaKey: meta} = event;
switch (which) { switch (key) {
case 9: // Tab case 'Tab':
case 33: // PgUp case 'PageUp':
case 34: // PgDn case 'PageDown':
if (!ctrl && !alt && !meta) { if (!ctrl && !alt && !meta) {
const el = document.activeElement; const el = document.activeElement;
const inputs = $inputs[currentFormat]; const inputs = $inputs[currentFormat];
const lastInput = inputs[inputs.length - 1]; const lastInput = inputs[inputs.length - 1];
if (which === 9 && shift && el === inputs[0]) { if (key === 'Tab' && shift && el === inputs[0]) {
maybeFocus(lastInput); maybeFocus(lastInput);
} else if (which === 9 && !shift && el === lastInput) { } else if (key === 'Tab' && !shift && el === lastInput) {
maybeFocus(inputs[0]); maybeFocus(inputs[0]);
} else if (which !== 9 && !shift) { } else if (key !== 'Tab' && !shift) {
setFromFormatElement({shift: which === 33 || shift}); setFromFormatElement({shift: key === 'PageUp' || shift});
} else { } else {
return; return;
} }
event.preventDefault(); event.preventDefault();
} }
return; return;
case 38: // Up case 'ArrowUp':
case 40: // Down case 'ArrowDown':
if (!event.metaKey && if (!event.metaKey &&
document.activeElement.localName === 'input' && document.activeElement.localName === 'input' &&
document.activeElement.checkValidity()) { document.activeElement.checkValidity()) {
@ -389,8 +396,8 @@
function setFromKeyboardIncrement(event) { function setFromKeyboardIncrement(event) {
const el = document.activeElement; const el = document.activeElement;
const {which, ctrlKey: ctrl, altKey: alt, shiftKey: shift} = event; const {key, ctrlKey: ctrl, altKey: alt, shiftKey: shift} = event;
const dir = which === 38 ? 1 : -1; const dir = key === 'ArrowUp' ? 1 : -1;
let value, newValue; let value, newValue;
if (currentFormat === 'hex') { if (currentFormat === 'hex') {
value = el.value.trim(); value = el.value.trim();
@ -454,7 +461,7 @@
const newHSV = color.type === 'hsl' ? const newHSV = color.type === 'hsl' ?
colorConverter.HSLtoHSV(color) : colorConverter.HSLtoHSV(color) :
colorConverter.RGBtoHSV(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; return;
} }
HSV = newHSV; 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) { function onMouseUp(event) {
releaseMouse(event, ['saturation', 'hue', 'opacity']); releaseMouse(event, ['saturation', 'hue', 'opacity']);
if (onMouseDown.outsideClick) { if (onMouseDown.outsideClick) {
@ -617,9 +637,9 @@
function onKeyDown(e) { function onKeyDown(e) {
if (!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { if (!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
switch (e.which) { switch (e.key) {
case 13: case 'Enter':
case 27: case 'Escape':
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
hide(); hide();
@ -710,6 +730,8 @@
$opacity.addEventListener('mousedown', onOpacityMouseDown); $opacity.addEventListener('mousedown', onOpacityMouseDown);
$hexLettercase.true.addEventListener('click', onHexLettercaseClicked); $hexLettercase.true.addEventListener('click', onHexLettercaseClicked);
$hexLettercase.false.addEventListener('click', onHexLettercaseClicked); $hexLettercase.false.addEventListener('click', onHexLettercaseClicked);
$palette.addEventListener('click', onPaletteClicked);
$palette.addEventListener('contextmenu', onPaletteClicked);
stopSnoozing(); stopSnoozing();
if (!options.isShortCut) { if (!options.isShortCut) {
@ -735,6 +757,8 @@
$opacity.removeEventListener('mousedown', onOpacityMouseDown); $opacity.removeEventListener('mousedown', onOpacityMouseDown);
$hexLettercase.true.removeEventListener('click', onHexLettercaseClicked); $hexLettercase.true.removeEventListener('click', onHexLettercaseClicked);
$hexLettercase.false.removeEventListener('click', onHexLettercaseClicked); $hexLettercase.false.removeEventListener('click', onHexLettercaseClicked);
$palette.removeEventListener('click', onPaletteClicked);
$palette.removeEventListener('contextmenu', onPaletteClicked);
releaseMouse(); releaseMouse();
stopSnoozing(); stopSnoozing();
} }

View File

@ -532,6 +532,8 @@
color: data.color, color: data.color,
prevColor: data.color || '', prevColor: data.color || '',
callback: popupOnChange, 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 //endregion
//region Utility //region Utility

View File

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

View File

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

View File

@ -25,22 +25,20 @@
-ms-transition: opacity .4s; -ms-transition: opacity .4s;
} }
.CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning { .CodeMirror-lint-mark {
background-position: left bottom; background-position: left bottom;
background-repeat: repeat-x; 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 { .CodeMirror-lint-mark-warning {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII="); 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-position: center center;
background-repeat: no-repeat; background-repeat: no-repeat;
cursor: pointer; cursor: pointer;
@ -51,20 +49,20 @@
position: relative; position: relative;
} }
.CodeMirror-lint-message-error, .CodeMirror-lint-message-warning { .CodeMirror-lint-message {
padding-left: 18px; padding-left: 18px;
background-position: top left; background-position: top left;
background-repeat: no-repeat; 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 { .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="); 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 { .CodeMirror-lint-marker-multiple {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC"); background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC");
background-repeat: no-repeat; background-repeat: no-repeat;

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