Merge branch 'master' into dev-color-scheme
This commit is contained in:
commit
5f46a008c5
|
@ -1,7 +1,7 @@
|
|||
# https://github.com/eslint/eslint/blob/master/docs/rules/README.md
|
||||
|
||||
parserOptions:
|
||||
ecmaVersion: 2015
|
||||
ecmaVersion: 2017
|
||||
|
||||
env:
|
||||
browser: true
|
||||
|
|
|
@ -217,10 +217,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "Стилове за редактора",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Включване",
|
||||
"description": "Label for the button to enable a style"
|
||||
|
@ -741,4 +737,4 @@
|
|||
"message": "този адрес",
|
||||
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -305,10 +305,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "Najít styly pro editor",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Povolit",
|
||||
"description": "Label for the button to enable a style"
|
||||
|
@ -1306,4 +1302,4 @@
|
|||
"message": "Nahrávání souboru…",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -309,10 +309,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "Editor Styles finden",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Aktivieren",
|
||||
"description": "Label for the button to enable a style"
|
||||
|
@ -1600,4 +1596,4 @@
|
|||
"message": "Lade Styles hoch...",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -250,6 +250,13 @@
|
|||
"message": "Copy to clipboard",
|
||||
"description": "Tooltip for elements which can be copied"
|
||||
},
|
||||
"customNameHint": {
|
||||
"message": "Enter a custom name here to rename the style in UI without breaking its updates"
|
||||
},
|
||||
"customNameResetHint": {
|
||||
"message": "Stop using customized name, switch to the style's own name",
|
||||
"description": "Tooltip of 'x' button shown in editor when changing the name input of a) styles updated from a URL i.e. not locally created, b) UserCSS styles"
|
||||
},
|
||||
"dateInstalled": {
|
||||
"message": "Date installed",
|
||||
"description": "Option text for the user to sort the style by install date"
|
||||
|
@ -319,10 +326,6 @@
|
|||
},
|
||||
"description": "Title of the page for editing styles"
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "Find editor styles",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Enable",
|
||||
"description": "Label for the button to enable a style"
|
||||
|
@ -984,6 +987,12 @@
|
|||
"optionsAdvancedAutoSwitchSchemeByTime": {
|
||||
"message": "By night time:"
|
||||
},
|
||||
"optionsAdvancedStyleViaXhr": {
|
||||
"message": "Instant inject mode"
|
||||
},
|
||||
"optionsAdvancedStyleViaXhrNote": {
|
||||
"message": "Enable this if you encounter flashing of unstyled content (FOUC) when browsing, which is especially noticeable with dark themes.\n\nThe technical reason is that Chrome/Chromium postpones asynchronous communication of extensions, in a usually meaningless attempt to improve page load speed, potentially causing styles to be late to apply. To circumvent this, since web extensions are not provided a synchronous API, Stylus provides this option to utilize the \"deprecated\" synchronous XMLHttpRequest web API to fetch applicable styles. There shouldn't be any detrimental effects, since the request is fulfilled within a few milliseconds while the page is still being downloaded from the server.\n\nNevertheless, Chromium will print a warning in devtools' console. Right-clicking a warning, and hiding them, will prevent future warnings from being shown."
|
||||
},
|
||||
"optionsBadgeDisabled": {
|
||||
"message": "Background color when disabled"
|
||||
},
|
||||
|
@ -1036,6 +1045,9 @@
|
|||
"optionsResetButton": {
|
||||
"message": "Reset options"
|
||||
},
|
||||
"optionsStylusThemes": {
|
||||
"message": "Find a Stylus UI theme"
|
||||
},
|
||||
"optionsSubheading": {
|
||||
"message": "More Options",
|
||||
"description": "Subheading for options section on manage page."
|
||||
|
@ -1147,6 +1159,10 @@
|
|||
"message": "Action menu",
|
||||
"description": "Tooltip for menu button in popup."
|
||||
},
|
||||
"popupOpenEditInPopup": {
|
||||
"message": "Use a simple window (no omnibox)",
|
||||
"description": "Label for the checkbox controlling 'edit' action behavior in the popup."
|
||||
},
|
||||
"popupOpenEditInWindow": {
|
||||
"message": "Open editor in a new window",
|
||||
"description": "Label for the checkbox controlling 'edit' action behavior in the popup."
|
||||
|
@ -1198,6 +1214,10 @@
|
|||
"message": "Case-sensitive",
|
||||
"description": "Tooltip for the 'Aa' icon that enables case-sensitive search in the editor shown on Ctrl-F"
|
||||
},
|
||||
"searchGlobalStyles": {
|
||||
"message": "Also search global styles",
|
||||
"description": "Checkbox label in the popup's inline style search, shown when the text to search is entered"
|
||||
},
|
||||
"searchNumberOfResults": {
|
||||
"message": "Number of matches",
|
||||
"description": "Tooltip for the number of found search results in the editor shown on Ctrl-F"
|
||||
|
@ -1206,6 +1226,10 @@
|
|||
"message": "Number of matches in code and applies-to values",
|
||||
"description": "Tooltip for the number of found search results in the editor shown on Ctrl-F"
|
||||
},
|
||||
"searchStyleQueryHint": {
|
||||
"message": "Search style names case-insensitively:\nsome words - all words in any order\n\"some phrase\" - this exact phrase without quotes\n2020 - a year like this also shows styles updated in 2020",
|
||||
"description": "Tooltip shown for the text input in the popup's inline style finder"
|
||||
},
|
||||
"searchRegexp": {
|
||||
"message": "Use /re/ syntax for regexp search",
|
||||
"description": "Label after the search input field in the editor shown on Ctrl-F"
|
||||
|
|
|
@ -313,10 +313,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "Buscar estilos del editor",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Activar",
|
||||
"description": "Label for the button to enable a style"
|
||||
|
@ -1568,4 +1564,4 @@
|
|||
"message": "Subiendo el archivo....",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -313,10 +313,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "Leia redaktori stiile",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Luba",
|
||||
"description": "Label for the button to enable a style"
|
||||
|
@ -1486,4 +1482,4 @@
|
|||
"message": "Faili üleslaadimine...",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -321,10 +321,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "Trouver des styles pour l’éditeur",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Activer",
|
||||
"description": "Label for the button to enable a style"
|
||||
|
@ -1616,4 +1612,4 @@
|
|||
"message": "Envoi du fichier…",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -321,10 +321,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "מצא עיצובים לעורך",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "אפשר",
|
||||
"description": "Label for the button to enable a style"
|
||||
|
@ -1381,4 +1377,4 @@
|
|||
"message": "מעלה קובץ...",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -309,10 +309,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "A szerkesztő stílusainak keresése",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Engedélyezés",
|
||||
"description": "Label for the button to enable a style"
|
||||
|
@ -1604,4 +1600,4 @@
|
|||
"message": "Fájl feltöltése...",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -273,10 +273,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "Cerca stili editor",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Attiva",
|
||||
"description": "Label for the button to enable a style"
|
||||
|
@ -1058,4 +1054,4 @@
|
|||
"message": "questo URL",
|
||||
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -313,10 +313,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "エディタのスタイルを見つける",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "有効化",
|
||||
"description": "Label for the button to enable a style"
|
||||
|
@ -1636,4 +1632,4 @@
|
|||
"message": "スタイルをアップロード中...",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -317,10 +317,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "편집기 스타일 찾기",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "활성화",
|
||||
"description": "Label for the button to enable a style"
|
||||
|
@ -1636,4 +1632,4 @@
|
|||
"message": "파일 업로드 중...",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -317,10 +317,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "Editorstijlen zoeken",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Inschakelen",
|
||||
"description": "Label for the button to enable a style"
|
||||
|
@ -1620,4 +1616,4 @@
|
|||
"message": "Bestand uploaden...",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -321,10 +321,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "Znajdź style edytora",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Włącz",
|
||||
"description": "Label for the button to enable a style"
|
||||
|
@ -1644,4 +1640,4 @@
|
|||
"message": "Wysyłanie stylów...",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -301,10 +301,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "Encontrar estilos para o editor",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Ativar",
|
||||
"description": "Label for the button to enable a style"
|
||||
|
@ -1224,4 +1220,4 @@
|
|||
"message": "este URL",
|
||||
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -269,10 +269,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "Găsiți teme pentru editor",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Activați",
|
||||
"description": "Label for the button to enable a style"
|
||||
|
@ -1140,4 +1136,4 @@
|
|||
"message": "acest URL",
|
||||
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -321,10 +321,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "Сменить тему Стилус",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Включить",
|
||||
"description": "Label for the button to enable a style"
|
||||
|
@ -1644,4 +1640,4 @@
|
|||
"message": "Загрузка файла...",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -309,10 +309,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "Hitta redaktörsstilar",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Aktivera",
|
||||
"description": "Label for the button to enable a style"
|
||||
|
@ -1522,4 +1518,4 @@
|
|||
"message": "Skickar filen...",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -309,10 +309,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "Editör stili bul",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "Etkinleştir",
|
||||
"description": "Label for the button to enable a style"
|
||||
|
@ -896,4 +892,4 @@
|
|||
"message": "Dosya Yükleniyor...",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -321,10 +321,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "查找编辑器样式",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "启用",
|
||||
"description": "Label for the button to enable a style"
|
||||
|
@ -1644,4 +1640,4 @@
|
|||
"message": "正在上传文件...",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -321,10 +321,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"editorStylesButton": {
|
||||
"message": "找到編輯器樣式",
|
||||
"description": "Find styles for the editor"
|
||||
},
|
||||
"enableStyleLabel": {
|
||||
"message": "啟用",
|
||||
"description": "Label for the button to enable a style"
|
||||
|
@ -1644,4 +1640,4 @@
|
|||
"message": "正在上傳檔案……",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ createAPI({
|
|||
compileUsercss,
|
||||
parseUsercssMeta(text, indexOffset = 0) {
|
||||
loadScript(
|
||||
'/js/polyfill.js',
|
||||
'/vendor/usercss-meta/usercss-meta.min.js',
|
||||
'/vendor-overwrites/colorpicker/colorconverter.js',
|
||||
'/js/meta-parser.js'
|
||||
|
@ -21,7 +20,6 @@ createAPI({
|
|||
},
|
||||
nullifyInvalidVars(vars) {
|
||||
loadScript(
|
||||
'/js/polyfill.js',
|
||||
'/vendor/usercss-meta/usercss-meta.min.js',
|
||||
'/vendor-overwrites/colorpicker/colorconverter.js',
|
||||
'/js/meta-parser.js'
|
||||
|
@ -31,11 +29,15 @@ createAPI({
|
|||
});
|
||||
|
||||
function compileUsercss(preprocessor, code, vars) {
|
||||
loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js');
|
||||
loadScript(
|
||||
'/vendor-overwrites/csslint/parserlib.js',
|
||||
'/vendor-overwrites/colorpicker/colorconverter.js',
|
||||
'/js/moz-parser.js'
|
||||
);
|
||||
const builder = getUsercssCompiler(preprocessor);
|
||||
vars = simpleVars(vars);
|
||||
return Promise.resolve(builder.preprocess ? builder.preprocess(code, vars) : code)
|
||||
.then(code => parseMozFormat({code}))
|
||||
.then(code => parseMozFormat({code, emptyDocument: preprocessor === 'stylus'}))
|
||||
.then(({sections, errors}) => {
|
||||
if (builder.postprocess) {
|
||||
builder.postprocess(sections, vars);
|
||||
|
@ -122,28 +124,39 @@ function getUsercssCompiler(preprocessor) {
|
|||
const pool = new Map();
|
||||
return Promise.resolve(doReplace(source));
|
||||
|
||||
function getValue(name, rgb) {
|
||||
function getValue(name, rgbName) {
|
||||
if (!vars.hasOwnProperty(name)) {
|
||||
if (name.endsWith('-rgb')) {
|
||||
return getValue(name.slice(0, -4), true);
|
||||
return getValue(name.slice(0, -4), name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (rgb) {
|
||||
if (vars[name].type === 'color') {
|
||||
const color = colorConverter.parse(vars[name].value);
|
||||
if (!color) return null;
|
||||
const {r, g, b} = color;
|
||||
return `${r}, ${g}, ${b}`;
|
||||
const {type, value} = vars[name];
|
||||
switch (type) {
|
||||
case 'color': {
|
||||
let color = pool.get(rgbName || name);
|
||||
if (color == null) {
|
||||
color = colorConverter.parse(value);
|
||||
if (color) {
|
||||
if (color.type === 'hsl') {
|
||||
color = colorConverter.HSVtoRGB(colorConverter.HSLtoHSV(color));
|
||||
}
|
||||
const {r, g, b} = color;
|
||||
color = rgbName
|
||||
? `${r}, ${g}, ${b}`
|
||||
: `#${(0x1000000 + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||
}
|
||||
// the pool stores `false` for bad colors to differentiate from a yet unknown color
|
||||
pool.set(rgbName || name, color || false);
|
||||
}
|
||||
return color || null;
|
||||
}
|
||||
return null;
|
||||
case 'dropdown':
|
||||
case 'select': // prevent infinite recursion
|
||||
pool.set(name, '');
|
||||
return doReplace(value);
|
||||
}
|
||||
if (vars[name].type === 'dropdown' || vars[name].type === 'select') {
|
||||
// prevent infinite recursion
|
||||
pool.set(name, '');
|
||||
return doReplace(vars[name].value);
|
||||
}
|
||||
return vars[name].value;
|
||||
return value;
|
||||
}
|
||||
|
||||
function doReplace(text) {
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
/* global download prefs openURL FIREFOX CHROME
|
||||
URLS ignoreChromeError usercssHelper
|
||||
URLS ignoreChromeError chromeLocal semverCompare
|
||||
styleManager msg navigatorUtil workerUtil contentScripts sync
|
||||
findExistingTab activateTab isTabReplaceable getActiveTab
|
||||
tabManager colorScheme */
|
||||
findExistingTab activateTab isTabReplaceable getActiveTab colorScheme */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
|
@ -65,11 +64,13 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
|
|||
/* Same as openURL, the only extra prop in `opts` is `message` - it'll be sent when the tab is ready,
|
||||
which is needed in the popup, otherwise another extension could force the tab to open in foreground
|
||||
thus auto-closing the popup (in Chrome at least) and preventing the sendMessage code from running */
|
||||
openURL(opts) {
|
||||
const {message} = opts;
|
||||
return openURL(opts) // will pass the resolved value untouched when `message` is absent or falsy
|
||||
.then(message && (tab => tab.status === 'complete' ? tab : onTabReady(tab)))
|
||||
.then(message && (tab => msg.sendTab(tab.id, opts.message)));
|
||||
async openURL(opts) {
|
||||
const tab = await openURL(opts);
|
||||
if (opts.message) {
|
||||
await onTabReady(tab);
|
||||
await msg.sendTab(tab.id, opts.message);
|
||||
}
|
||||
return tab;
|
||||
function onTabReady(tab) {
|
||||
return new Promise((resolve, reject) =>
|
||||
setTimeout(function ping(numTries = 10, delay = 100) {
|
||||
|
@ -112,14 +113,6 @@ navigatorUtil.onUrlChange(({tabId, frameId}, type) => {
|
|||
}
|
||||
});
|
||||
|
||||
tabManager.onUpdate(({tabId, url, oldUrl = ''}) => {
|
||||
if (usercssHelper.testUrl(url) && !oldUrl.startsWith(URLS.installUsercss)) {
|
||||
usercssHelper.testContents(tabId, url).then(data => {
|
||||
if (data.code) usercssHelper.openInstallerPage(tabId, url, data);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (FIREFOX) {
|
||||
// FF misses some about:blank iframes so we inject our content script explicitly
|
||||
navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, {
|
||||
|
@ -140,7 +133,7 @@ if (chrome.commands) {
|
|||
}
|
||||
|
||||
// *************************************************************************
|
||||
chrome.runtime.onInstalled.addListener(({reason}) => {
|
||||
chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
|
||||
// save install type: "admin", "development", "normal", "sideload" or "other"
|
||||
// "normal" = addon installed from webstore
|
||||
chrome.management.getSelf(info => {
|
||||
|
@ -157,6 +150,14 @@ chrome.runtime.onInstalled.addListener(({reason}) => {
|
|||
});
|
||||
// themes may change
|
||||
delete localStorage.codeMirrorThemes;
|
||||
// inline search cache for USO is not needed anymore, TODO: remove this by the middle of 2021
|
||||
if (semverCompare(previousVersion, '1.5.13') <= 0) {
|
||||
setTimeout(async () => {
|
||||
const del = Object.keys(await chromeLocal.get())
|
||||
.filter(key => key.startsWith('usoSearchCache'));
|
||||
if (del.length) chromeLocal.remove(del);
|
||||
}, 15e3);
|
||||
}
|
||||
});
|
||||
|
||||
// *************************************************************************
|
||||
|
@ -298,16 +299,14 @@ function openEditor(params) {
|
|||
'url-prefix'?: String
|
||||
}
|
||||
*/
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const key in params) {
|
||||
searchParams.set(key, params[key]);
|
||||
}
|
||||
const search = searchParams.toString();
|
||||
const u = new URL(chrome.runtime.getURL('edit.html'));
|
||||
u.search = new URLSearchParams(params);
|
||||
return openURL({
|
||||
url: 'edit.html' + (search && `?${search}`),
|
||||
newWindow: prefs.get('openEditInWindow'),
|
||||
windowPosition: prefs.get('windowPosition'),
|
||||
currentWindow: null
|
||||
url: `${u}`,
|
||||
currentWindow: null,
|
||||
newWindow: prefs.get('openEditInWindow') && Object.assign({},
|
||||
prefs.get('openEditInWindow.popup') && {type: 'popup'},
|
||||
prefs.get('windowPosition')),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -329,7 +328,7 @@ function openManage({options = false, search} = {}) {
|
|||
if (tab) {
|
||||
return Promise.all([
|
||||
activateTab(tab),
|
||||
tab.url !== url && msg.sendTab(tab.id, {method: 'pushState', url})
|
||||
(tab.pendingUrl || tab.url) !== url && msg.sendTab(tab.id, {method: 'pushState', url})
|
||||
.catch(console.error)
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,21 @@ const contentScripts = (() => {
|
|||
}
|
||||
const busyTabs = new Set();
|
||||
let busyTabsTimer;
|
||||
|
||||
// expose version on greasyfork/sleazyfork 1) info page and 2) code page
|
||||
const urlMatches = '/scripts/\\d+[^/]*(/code)?([?#].*)?$';
|
||||
chrome.webNavigation.onCommitted.addListener(({tabId}) => {
|
||||
chrome.tabs.executeScript(tabId, {
|
||||
file: '/content/install-hook-greasyfork.js',
|
||||
runAt: 'document_start',
|
||||
});
|
||||
}, {
|
||||
url: [
|
||||
{hostEquals: 'greasyfork.org', urlMatches},
|
||||
{hostEquals: 'sleazyfork.org', urlMatches},
|
||||
]
|
||||
});
|
||||
|
||||
return {injectToTab, injectToAllTabs};
|
||||
|
||||
function injectToTab({url, tabId, frameId = null}) {
|
||||
|
@ -58,13 +73,13 @@ const contentScripts = (() => {
|
|||
return browser.tabs.query({}).then(tabs => {
|
||||
for (const tab of tabs) {
|
||||
// skip unloaded/discarded/chrome tabs
|
||||
if (!tab.width || tab.discarded || !URLS.supported(tab.url)) continue;
|
||||
if (!tab.width || tab.discarded || !URLS.supported(tab.pendingUrl || tab.url)) continue;
|
||||
// our content scripts may still be pending injection at browser start so it's too early to ping them
|
||||
if (tab.status === 'loading') {
|
||||
trackBusyTab(tab.id, true);
|
||||
} else {
|
||||
injectToTab({
|
||||
url: tab.url,
|
||||
url: tab.pendingUrl || tab.url,
|
||||
tabId: tab.id
|
||||
});
|
||||
}
|
||||
|
|
179
background/db.js
179
background/db.js
|
@ -1,4 +1,4 @@
|
|||
/* global chromeLocal ignoreChromeError workerUtil createChromeStorageDB */
|
||||
/* global chromeLocal workerUtil createChromeStorageDB */
|
||||
/* exported db */
|
||||
/*
|
||||
Initialize a database. There are some problems using IndexedDB in Firefox:
|
||||
|
@ -10,29 +10,18 @@ https://www.reddit.com/r/firefox/comments/7ijuaq/firefox_59_webextensions_can_us
|
|||
'use strict';
|
||||
|
||||
const db = (() => {
|
||||
let exec;
|
||||
const preparing = prepare();
|
||||
return {
|
||||
exec: (...args) =>
|
||||
preparing.then(() => exec(...args))
|
||||
const DATABASE = 'stylish';
|
||||
const STORE = 'styles';
|
||||
const FALLBACK = 'dbInChromeStorage';
|
||||
const dbApi = {
|
||||
async exec(...args) {
|
||||
dbApi.exec = await tryUsingIndexedDB().catch(useChromeStorage);
|
||||
return dbApi.exec(...args);
|
||||
},
|
||||
};
|
||||
return dbApi;
|
||||
|
||||
function prepare() {
|
||||
return withPromise(shouldUseIndexedDB).then(
|
||||
ok => {
|
||||
if (ok) {
|
||||
useIndexedDB();
|
||||
} else {
|
||||
useChromeStorage();
|
||||
}
|
||||
},
|
||||
err => {
|
||||
useChromeStorage(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function shouldUseIndexedDB() {
|
||||
async function tryUsingIndexedDB() {
|
||||
// we use chrome.storage.local fallback if IndexedDB doesn't save data,
|
||||
// which, once detected on the first run, is remembered in chrome.storage.local
|
||||
// for reliablility and in localStorage for fast synchronous access
|
||||
|
@ -42,115 +31,81 @@ const db = (() => {
|
|||
if (typeof indexedDB === 'undefined') {
|
||||
throw new Error('indexedDB is undefined');
|
||||
}
|
||||
// test localStorage
|
||||
const fallbackSet = localStorage.dbInChromeStorage;
|
||||
if (fallbackSet === 'true') {
|
||||
return false;
|
||||
switch (await getFallback()) {
|
||||
case true: throw null;
|
||||
case false: break;
|
||||
default: await testDB();
|
||||
}
|
||||
if (fallbackSet === 'false') {
|
||||
return true;
|
||||
}
|
||||
// test storage.local
|
||||
return chromeLocal.get('dbInChromeStorage')
|
||||
.then(data => {
|
||||
if (data && data.dbInChromeStorage) {
|
||||
return false;
|
||||
}
|
||||
return testDBSize()
|
||||
.then(ok => ok || testDBMutation());
|
||||
});
|
||||
return useIndexedDB();
|
||||
}
|
||||
|
||||
function withPromise(fn) {
|
||||
try {
|
||||
return Promise.resolve(fn());
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
async function getFallback() {
|
||||
return localStorage[FALLBACK] === 'true' ? true :
|
||||
localStorage[FALLBACK] === 'false' ? false :
|
||||
chromeLocal.getValue(FALLBACK);
|
||||
}
|
||||
|
||||
function testDBSize() {
|
||||
return dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1)
|
||||
.then(event => (
|
||||
event.target.result &&
|
||||
event.target.result.length &&
|
||||
event.target.result[0]
|
||||
));
|
||||
}
|
||||
|
||||
function testDBMutation() {
|
||||
return dbExecIndexedDB('put', {id: -1})
|
||||
.then(() => dbExecIndexedDB('get', -1))
|
||||
.then(event => {
|
||||
if (!event.target.result) {
|
||||
throw new Error('failed to get previously put item');
|
||||
}
|
||||
if (event.target.result.id !== -1) {
|
||||
throw new Error('item id is wrong');
|
||||
}
|
||||
return dbExecIndexedDB('delete', -1);
|
||||
})
|
||||
.then(() => true);
|
||||
async function testDB() {
|
||||
let e = await dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1);
|
||||
// throws if result is null
|
||||
e = e.target.result[0];
|
||||
const id = `${performance.now()}.${Math.random()}.${Date.now()}`;
|
||||
await dbExecIndexedDB('put', {id});
|
||||
e = await dbExecIndexedDB('get', id);
|
||||
// throws if result or id is null
|
||||
await dbExecIndexedDB('delete', e.target.result.id);
|
||||
}
|
||||
|
||||
function useChromeStorage(err) {
|
||||
exec = createChromeStorageDB().exec;
|
||||
chromeLocal.set({dbInChromeStorage: true}, ignoreChromeError);
|
||||
chromeLocal.setValue(FALLBACK, true);
|
||||
if (err) {
|
||||
chromeLocal.setValue('dbInChromeStorageReason', workerUtil.cloneError(err));
|
||||
chromeLocal.setValue(FALLBACK + 'Reason', workerUtil.cloneError(err));
|
||||
console.warn('Failed to access indexedDB. Switched to storage API.', err);
|
||||
}
|
||||
localStorage.dbInChromeStorage = 'true';
|
||||
localStorage[FALLBACK] = 'true';
|
||||
return createChromeStorageDB().exec;
|
||||
}
|
||||
|
||||
function useIndexedDB() {
|
||||
exec = dbExecIndexedDB;
|
||||
chromeLocal.set({dbInChromeStorage: false}, ignoreChromeError);
|
||||
localStorage.dbInChromeStorage = 'false';
|
||||
chromeLocal.setValue(FALLBACK, false);
|
||||
localStorage[FALLBACK] = 'false';
|
||||
return dbExecIndexedDB;
|
||||
}
|
||||
|
||||
function dbExecIndexedDB(method, ...args) {
|
||||
return open().then(database => {
|
||||
if (!method) {
|
||||
return database;
|
||||
}
|
||||
if (method === 'putMany') {
|
||||
return putMany(database, ...args);
|
||||
}
|
||||
const mode = method.startsWith('get') ? 'readonly' : 'readwrite';
|
||||
const transaction = database.transaction(['styles'], mode);
|
||||
const store = transaction.objectStore('styles');
|
||||
return storeRequest(store, method, ...args);
|
||||
async function dbExecIndexedDB(method, ...args) {
|
||||
const mode = method.startsWith('get') ? 'readonly' : 'readwrite';
|
||||
const store = (await open()).transaction([STORE], mode).objectStore(STORE);
|
||||
const fn = method === 'putMany' ? putMany : storeRequest;
|
||||
return fn(store, method, ...args);
|
||||
}
|
||||
|
||||
function storeRequest(store, method, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store[method](...args);
|
||||
request.onsuccess = resolve;
|
||||
request.onerror = reject;
|
||||
});
|
||||
}
|
||||
|
||||
function storeRequest(store, method, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store[method](...args);
|
||||
request.onsuccess = resolve;
|
||||
request.onerror = reject;
|
||||
function putMany(store, _method, items) {
|
||||
return Promise.all(items.map(item => storeRequest(store, 'put', item)));
|
||||
}
|
||||
|
||||
function open() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DATABASE, 2);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = reject;
|
||||
request.onupgradeneeded = create;
|
||||
});
|
||||
}
|
||||
|
||||
function create(event) {
|
||||
if (event.oldVersion === 0) {
|
||||
event.target.result.createObjectStore(STORE, {
|
||||
keyPath: 'id',
|
||||
autoIncrement: true,
|
||||
});
|
||||
}
|
||||
|
||||
function open() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open('stylish', 2);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = reject;
|
||||
request.onupgradeneeded = event => {
|
||||
if (event.oldVersion === 0) {
|
||||
event.target.result.createObjectStore('styles', {
|
||||
keyPath: 'id',
|
||||
autoIncrement: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function putMany(database, items) {
|
||||
const transaction = database.transaction(['styles'], 'readwrite');
|
||||
const store = transaction.objectStore('styles');
|
||||
return Promise.all(items.map(item => storeRequest(store, 'put', item)));
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -49,8 +49,9 @@ const navigatorUtil = (() => {
|
|||
}
|
||||
return browser.tabs.get(data.tabId)
|
||||
.then(tab => {
|
||||
if (tab.url === 'chrome://newtab/') {
|
||||
data.url = tab.url;
|
||||
const url = tab.pendingUrl || tab.url;
|
||||
if (url === 'chrome://newtab/') {
|
||||
data.url = url;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
continue;
|
||||
}
|
||||
for (const part in PARTS) {
|
||||
const text = style[part];
|
||||
const text = part === 'name' ? style.customName || style.name : style[part];
|
||||
if (text && PARTS[part](text, rx, words, icase)) {
|
||||
results.push(id);
|
||||
break;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */
|
||||
/* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty
|
||||
getStyleWithNoCode msg sync uuidv4 colorScheme */
|
||||
/* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty styleSectionGlobal
|
||||
getStyleWithNoCode msg prefs sync uuidv4 URLS colorScheme */
|
||||
/* exported styleManager */
|
||||
'use strict';
|
||||
|
||||
|
@ -61,6 +61,8 @@ const styleManager = (() => {
|
|||
username: ''
|
||||
};
|
||||
|
||||
const DELETE_IF_NULL = ['id', 'customName'];
|
||||
|
||||
handleLivePreviewConnections();
|
||||
handleColorScheme();
|
||||
|
||||
|
@ -237,6 +239,13 @@ const styleManager = (() => {
|
|||
if (!reason) {
|
||||
reason = style ? 'update' : 'install';
|
||||
}
|
||||
let url = !data.url && data.updateUrl;
|
||||
if (url) {
|
||||
const usoId = URLS.extractUsoArchiveId(url);
|
||||
url = usoId && `${URLS.usoArchive}?style=${usoId}` ||
|
||||
URLS.extractGreasyForkId(url) && url.match(/^.*?\/\d+/)[0];
|
||||
if (url) data.url = data.installationUrl = url;
|
||||
}
|
||||
// FIXME: update updateDate? what about usercss config?
|
||||
return calcStyleDigest(data)
|
||||
.then(digest => {
|
||||
|
@ -391,8 +400,10 @@ const styleManager = (() => {
|
|||
if (!style.name) {
|
||||
throw new Error('style name is empty');
|
||||
}
|
||||
if (style.id == null) {
|
||||
delete style.id;
|
||||
for (const key of DELETE_IF_NULL) {
|
||||
if (style[key] == null) {
|
||||
delete style[key];
|
||||
}
|
||||
}
|
||||
if (!style._id) {
|
||||
style._id = uuidv4();
|
||||
|
@ -460,7 +471,7 @@ const styleManager = (() => {
|
|||
excludedScheme = true;
|
||||
}
|
||||
for (const section of data.sections) {
|
||||
if (styleCodeEmpty(section.code)) {
|
||||
if (styleSectionGlobal(section) && styleCodeEmpty(section.code)) {
|
||||
continue;
|
||||
}
|
||||
const match = urlMatchSection(query, section);
|
||||
|
@ -484,7 +495,7 @@ const styleManager = (() => {
|
|||
return result;
|
||||
}
|
||||
|
||||
function getSectionsByUrl(url, id) {
|
||||
function getSectionsByUrl(url, id, isInitialApply) {
|
||||
let cache = cachedStyleForUrl.get(url);
|
||||
if (!cache) {
|
||||
cache = {
|
||||
|
@ -500,13 +511,13 @@ const styleManager = (() => {
|
|||
.map(i => styles.get(i))
|
||||
);
|
||||
}
|
||||
if (id) {
|
||||
if (cache.sections[id]) {
|
||||
return {[id]: cache.sections[id]};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
return cache.sections;
|
||||
const res = id
|
||||
? cache.sections[id] ? {[id]: cache.sections[id]} : {}
|
||||
: cache.sections;
|
||||
// Avoiding flicker of needlessly applied styles by providing both styles & pref in one API call
|
||||
return isInitialApply && prefs.get('disableAll')
|
||||
? Object.assign({disableAll: true}, res)
|
||||
: res;
|
||||
|
||||
function buildCache(styleList) {
|
||||
const query = createMatchQuery(url);
|
||||
|
@ -578,6 +589,16 @@ const styleManager = (() => {
|
|||
touched = true;
|
||||
}
|
||||
}
|
||||
// upgrade the old way of customizing local names
|
||||
const {originalName} = style;
|
||||
if (originalName) {
|
||||
touched = true;
|
||||
if (originalName !== style.name) {
|
||||
style.customName = style.name;
|
||||
style.name = originalName;
|
||||
}
|
||||
delete style.originalName;
|
||||
}
|
||||
return touched;
|
||||
}
|
||||
}
|
||||
|
@ -605,7 +626,7 @@ const styleManager = (() => {
|
|||
) {
|
||||
return true;
|
||||
}
|
||||
if (section.urlPrefixes && section.urlPrefixes.some(p => query.url.startsWith(p))) {
|
||||
if (section.urlPrefixes && section.urlPrefixes.some(p => p && query.url.startsWith(p))) {
|
||||
return true;
|
||||
}
|
||||
// as per spec the fragment portion is ignored in @-moz-document:
|
||||
|
@ -631,15 +652,7 @@ const styleManager = (() => {
|
|||
return 'sloppy';
|
||||
}
|
||||
// TODO: check for invalid regexps?
|
||||
if (
|
||||
(!section.regexps || !section.regexps.length) &&
|
||||
(!section.urlPrefixes || !section.urlPrefixes.length) &&
|
||||
(!section.urls || !section.urls.length) &&
|
||||
(!section.domains || !section.domains.length)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return styleSectionGlobal(section);
|
||||
}
|
||||
|
||||
function createCompiler(compile) {
|
||||
|
|
85
background/style-via-xhr.js
Normal file
85
background/style-via-xhr.js
Normal 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);
|
||||
}
|
||||
})();
|
|
@ -1,10 +1,11 @@
|
|||
/* global chromeLocal promisifyChrome FIREFOX */
|
||||
/* global chromeLocal promisifyChrome webextLaunchWebAuthFlow FIREFOX */
|
||||
/* exported tokenManager */
|
||||
'use strict';
|
||||
|
||||
const tokenManager = (() => {
|
||||
promisifyChrome({
|
||||
identity: ['launchWebAuthFlow'],
|
||||
'windows': ['create', 'update', 'remove'],
|
||||
'tabs': ['create', 'update', 'remove']
|
||||
});
|
||||
const AUTH = {
|
||||
dropbox: {
|
||||
|
@ -36,7 +37,7 @@ const tokenManager = (() => {
|
|||
scopes: ['https://www.googleapis.com/auth/drive.appdata'],
|
||||
revoke: token => {
|
||||
const params = {token};
|
||||
return postQuery(`https://accounts.google.com/o/oauth2/revoke?${stringifyQuery(params)}`);
|
||||
return postQuery(`https://accounts.google.com/o/oauth2/revoke?${new URLSearchParams(params)}`);
|
||||
}
|
||||
},
|
||||
onedrive: {
|
||||
|
@ -136,14 +137,6 @@ const tokenManager = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
function stringifyQuery(obj) {
|
||||
const search = new URLSearchParams();
|
||||
for (const key of Object.keys(obj)) {
|
||||
search.set(key, obj[key]);
|
||||
}
|
||||
return search.toString();
|
||||
}
|
||||
|
||||
function authUser(name, k, interactive = false) {
|
||||
const provider = AUTH[name];
|
||||
const state = Math.random().toFixed(8).slice(2);
|
||||
|
@ -159,10 +152,11 @@ const tokenManager = (() => {
|
|||
if (provider.authQuery) {
|
||||
Object.assign(query, provider.authQuery);
|
||||
}
|
||||
const url = `${provider.authURL}?${stringifyQuery(query)}`;
|
||||
return browser.identity.launchWebAuthFlow({
|
||||
const url = `${provider.authURL}?${new URLSearchParams(query)}`;
|
||||
return webextLaunchWebAuthFlow({
|
||||
url,
|
||||
interactive
|
||||
interactive,
|
||||
redirect_uri: query.redirect_uri
|
||||
})
|
||||
.then(url => {
|
||||
const params = new URLSearchParams(
|
||||
|
@ -185,7 +179,7 @@ const tokenManager = (() => {
|
|||
code,
|
||||
grant_type: 'authorization_code',
|
||||
client_id: provider.clientId,
|
||||
redirect_uri: provider.redirect_uri || chrome.identity.getRedirectURL()
|
||||
redirect_uri: query.redirect_uri
|
||||
};
|
||||
if (provider.clientSecret) {
|
||||
body.client_secret = provider.clientSecret;
|
||||
|
@ -209,11 +203,9 @@ const tokenManager = (() => {
|
|||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
},
|
||||
body: body ? new URLSearchParams(body) : null,
|
||||
};
|
||||
if (body) {
|
||||
options.body = stringifyQuery(body);
|
||||
}
|
||||
return fetch(url, options)
|
||||
.then(r => {
|
||||
if (r.ok) {
|
||||
|
|
|
@ -116,7 +116,7 @@
|
|||
}
|
||||
|
||||
function reportSuccess(saved) {
|
||||
log(STATES.UPDATED + ` #${style.id} ${style.name}`);
|
||||
log(STATES.UPDATED + ` #${style.id} ${style.customName || style.name}`);
|
||||
const info = {updated: true, style: saved};
|
||||
if (port) port.postMessage(info);
|
||||
return info;
|
||||
|
@ -139,7 +139,7 @@
|
|||
if (typeof error === 'object' && error.message) {
|
||||
error = error.message;
|
||||
}
|
||||
log(STATES.SKIPPED + ` (${error}) #${style.id} ${style.name}`);
|
||||
log(STATES.SKIPPED + ` (${error}) #${style.id} ${style.customName || style.name}`);
|
||||
const info = {error, STATES, style: getStyleWithNoCode(style)};
|
||||
if (port) port.postMessage(info);
|
||||
return info;
|
||||
|
@ -207,13 +207,6 @@
|
|||
// keep current state
|
||||
delete json.enabled;
|
||||
|
||||
// keep local name customizations
|
||||
if (style.originalName !== style.name && style.name !== json.name) {
|
||||
delete json.name;
|
||||
} else {
|
||||
json.originalName = json.name;
|
||||
}
|
||||
|
||||
const newStyle = Object.assign({}, style, json);
|
||||
if (styleSectionsEqual(json, style, {checkSource: true})) {
|
||||
// update digest even if save === false as there might be just a space added etc.
|
||||
|
|
|
@ -1,15 +1,8 @@
|
|||
/* global API_METHODS usercss styleManager deepCopy openURL download URLS */
|
||||
/* global API_METHODS usercss styleManager deepCopy */
|
||||
/* exported usercssHelper */
|
||||
'use strict';
|
||||
|
||||
const usercssHelper = (() => {
|
||||
const installCodeCache = {};
|
||||
const clearInstallCode = url => delete installCodeCache[url];
|
||||
const isResponseText = r => /^text\/(css|plain)(;.*?)?$/i.test(r.headers.get('content-type'));
|
||||
// in Firefox we have to use a content script to read file://
|
||||
const fileLoader = !chrome.app && // not relying on navigator.ua which can be spoofed
|
||||
(tabId => browser.tabs.executeScript(tabId, {file: '/content/install-hook-usercss.js'}).then(r => r[0]));
|
||||
|
||||
API_METHODS.installUsercss = installUsercss;
|
||||
API_METHODS.editSaveUsercss = editSaveUsercss;
|
||||
API_METHODS.configUsercssVars = configUsercssVars;
|
||||
|
@ -17,50 +10,6 @@ const usercssHelper = (() => {
|
|||
API_METHODS.buildUsercss = build;
|
||||
API_METHODS.findUsercss = find;
|
||||
|
||||
API_METHODS.getUsercssInstallCode = url => {
|
||||
// when the installer tab is reloaded after the cache is expired, this will throw intentionally
|
||||
const {code, timer} = installCodeCache[url];
|
||||
clearInstallCode(url);
|
||||
clearTimeout(timer);
|
||||
return code;
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
testUrl(url) {
|
||||
return url.includes('.user.') &&
|
||||
/^(https?|file|ftps?):/.test(url) &&
|
||||
/\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]);
|
||||
},
|
||||
|
||||
/** @return {Promise<{ code:string, inTab:boolean } | false>} */
|
||||
testContents(tabId, url) {
|
||||
const isFile = url.startsWith('file:');
|
||||
const inTab = isFile && Boolean(fileLoader);
|
||||
return Promise.resolve(isFile || fetch(url, {method: 'HEAD'}).then(isResponseText))
|
||||
.then(ok => ok && (inTab ? fileLoader(tabId) : download(url)))
|
||||
.then(code => /==userstyle==/i.test(code) && {code, inTab});
|
||||
},
|
||||
|
||||
openInstallerPage(tabId, url, {code, inTab} = {}) {
|
||||
const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`;
|
||||
if (inTab) {
|
||||
browser.tabs.get(tabId).then(tab =>
|
||||
openURL({
|
||||
url: `${newUrl}&tabId=${tabId}`,
|
||||
active: tab.active,
|
||||
index: tab.index + 1,
|
||||
openerTabId: tabId,
|
||||
currentWindow: null,
|
||||
}));
|
||||
} else {
|
||||
const timer = setTimeout(clearInstallCode, 10e3, url);
|
||||
installCodeCache[url] = {code, timer};
|
||||
chrome.tabs.update(tabId, {url: newUrl});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function buildMeta(style) {
|
||||
if (style.usercssData) {
|
||||
return Promise.resolve(style);
|
||||
|
|
82
background/usercss-install-helper.js
Normal file
82
background/usercss-install-helper.js
Normal 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});
|
||||
}
|
||||
}
|
||||
})();
|
|
@ -20,6 +20,7 @@ self.INJECTED !== 1 && (() => {
|
|||
/** @type chrome.runtime.Port */
|
||||
let port;
|
||||
let lazyBadge = IS_FRAME;
|
||||
let parentDomain;
|
||||
|
||||
// the popup needs a check as it's not a tab but can be opened in a tab manually for whatever reason
|
||||
if (!IS_TAB) {
|
||||
|
@ -42,13 +43,6 @@ self.INJECTED !== 1 && (() => {
|
|||
window.addEventListener(orphanEventId, orphanCheck, true);
|
||||
}
|
||||
|
||||
let parentDomain;
|
||||
|
||||
prefs.subscribe(['disableAll'], (key, value) => doDisableAll(value));
|
||||
if (IS_FRAME) {
|
||||
prefs.subscribe(['exposeIframes'], updateExposeIframes);
|
||||
}
|
||||
|
||||
// detect media change in content script
|
||||
// FIXME: move this to background page when following bugs are fixed:
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1561546
|
||||
|
@ -59,19 +53,52 @@ self.INJECTED !== 1 && (() => {
|
|||
function onInjectorUpdate() {
|
||||
if (!isOrphaned) {
|
||||
updateCount();
|
||||
updateExposeIframes();
|
||||
const onOff = prefs[styleInjector.list.length ? 'subscribe' : 'unsubscribe'];
|
||||
onOff(['disableAll'], updateDisableAll);
|
||||
if (IS_FRAME) {
|
||||
updateExposeIframes();
|
||||
onOff(['exposeIframes'], updateExposeIframes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
return STYLE_VIA_API ?
|
||||
API.styleViaAPI({method: 'styleApply'}) :
|
||||
API.getSectionsByUrl(getMatchUrl()).then(styleInjector.apply);
|
||||
async function init() {
|
||||
if (STYLE_VIA_API) {
|
||||
await API.styleViaAPI({method: 'styleApply'});
|
||||
} else {
|
||||
const styles = chrome.app && getStylesViaXhr() ||
|
||||
await API.getSectionsByUrl(getMatchUrl(), null, true);
|
||||
if (styles.disableAll) {
|
||||
delete styles.disableAll;
|
||||
styleInjector.toggle(false);
|
||||
}
|
||||
await styleInjector.apply(styles);
|
||||
}
|
||||
}
|
||||
|
||||
function getStylesViaXhr() {
|
||||
if (new RegExp(`(^|\\s|;)${chrome.runtime.id}=\\s*([-\\w]+)\\s*(;|$)`).test(document.cookie)) {
|
||||
const data = RegExp.$2;
|
||||
const disableAll = data[0] === '1';
|
||||
const url = 'blob:' + chrome.runtime.getURL(data.slice(1));
|
||||
document.cookie = `${chrome.runtime.id}=1; max-age=0`; // remove our cookie
|
||||
let res;
|
||||
try {
|
||||
if (!disableAll) { // will get the styles asynchronously
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', url, false); // synchronous
|
||||
xhr.send();
|
||||
res = JSON.parse(xhr.response);
|
||||
}
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (e) {}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
function getMatchUrl() {
|
||||
let matchUrl = location.href;
|
||||
if (!matchUrl.match(/^(http|file|chrome|ftp)/)) {
|
||||
if (!chrome.tabs && !matchUrl.match(/^(http|file|chrome|ftp)/)) {
|
||||
// dynamic about: and javascript: iframes don't have an URL yet
|
||||
// so we'll try the parent frame which is guaranteed to have a real URL
|
||||
try {
|
||||
|
@ -145,7 +172,7 @@ self.INJECTED !== 1 && (() => {
|
|||
}
|
||||
}
|
||||
|
||||
function doDisableAll(disableAll) {
|
||||
function updateDisableAll(key, disableAll) {
|
||||
if (STYLE_VIA_API) {
|
||||
API.styleViaAPI({method: 'prefChanged', prefs: {disableAll}});
|
||||
} else {
|
||||
|
@ -153,22 +180,18 @@ self.INJECTED !== 1 && (() => {
|
|||
}
|
||||
}
|
||||
|
||||
function fetchParentDomain() {
|
||||
return parentDomain ?
|
||||
Promise.resolve() :
|
||||
API.getTabUrlPrefix()
|
||||
.then(newDomain => {
|
||||
parentDomain = newDomain;
|
||||
});
|
||||
}
|
||||
|
||||
function updateExposeIframes() {
|
||||
if (!prefs.get('exposeIframes') || window === parent || !styleInjector.list.length) {
|
||||
document.documentElement.removeAttribute('stylus-iframe');
|
||||
async function updateExposeIframes(key, value = prefs.get('exposeIframes')) {
|
||||
const attr = 'stylus-iframe';
|
||||
const el = document.documentElement;
|
||||
if (!el) return; // got no styles so styleInjector didn't wait for <html>
|
||||
if (!value || !styleInjector.list.length) {
|
||||
el.removeAttribute(attr);
|
||||
} else {
|
||||
fetchParentDomain().then(() => {
|
||||
document.documentElement.setAttribute('stylus-iframe', parentDomain);
|
||||
});
|
||||
if (!parentDomain) parentDomain = await API.getTabUrlPrefix();
|
||||
// Check first to avoid triggering DOM mutation
|
||||
if (el.getAttribute(attr) !== parentDomain) {
|
||||
el.setAttribute(attr, parentDomain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
21
content/install-hook-greasyfork.js
Normal file
21
content/install-hook-greasyfork.js
Normal 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}, '*');
|
||||
}
|
||||
});
|
||||
}
|
|
@ -5,16 +5,18 @@ if (typeof self.oldCode !== 'string') {
|
|||
self.oldCode = (document.querySelector('body > pre') || document.body).textContent;
|
||||
chrome.runtime.onConnect.addListener(port => {
|
||||
if (port.name !== 'downloadSelf') return;
|
||||
port.onMessage.addListener(({id, timer}) => {
|
||||
port.onMessage.addListener(({id, force}) => {
|
||||
fetch(location.href, {mode: 'same-origin'})
|
||||
.then(r => r.text())
|
||||
.then(code => ({id, code: timer && code === self.oldCode ? null : code}))
|
||||
.then(code => ({id, code: force || code !== self.oldCode ? code : null}))
|
||||
.catch(error => ({id, error: error.message || `${error}`}))
|
||||
.then(msg => {
|
||||
port.postMessage(msg);
|
||||
if (msg.code != null) self.oldCode = msg.code;
|
||||
});
|
||||
});
|
||||
// FF keeps content scripts connected on navigation https://github.com/openstyles/stylus/issues/864
|
||||
addEventListener('pagehide', () => port.disconnect(), {once: true});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
/* global cloneInto msg API */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
/^\/styles\/(\d+)(\/([^/]*))?([?#].*)?$/.test(location.pathname) && (() => {
|
||||
const styleId = RegExp.$1;
|
||||
const pageEventId = `${performance.now()}${Math.random()}`;
|
||||
|
||||
window.dispatchEvent(new CustomEvent(chrome.runtime.id + '-install'));
|
||||
window.addEventListener(chrome.runtime.id + '-install', orphanCheck, true);
|
||||
|
||||
|
@ -17,35 +21,18 @@
|
|||
}, '*');
|
||||
});
|
||||
|
||||
let gotBody = false;
|
||||
let currentMd5;
|
||||
new MutationObserver(observeDOM).observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
observeDOM();
|
||||
const md5Url = getMeta('stylish-md5-url') || `https://update.userstyles.org/${styleId}.md5`;
|
||||
Promise.all([
|
||||
API.findStyle({md5Url}),
|
||||
getResource(md5Url),
|
||||
onDOMready(),
|
||||
]).then(checkUpdatability);
|
||||
|
||||
function observeDOM() {
|
||||
if (!gotBody) {
|
||||
if (!document.body) return;
|
||||
gotBody = true;
|
||||
// TODO: remove the following statement when USO pagination title is fixed
|
||||
document.title = document.title.replace(/^(\d+)&\w+=/, '#$1: ');
|
||||
const md5Url = getMeta('stylish-md5-url') || location.href;
|
||||
Promise.all([
|
||||
API.findStyle({md5Url}),
|
||||
getResource(md5Url)
|
||||
])
|
||||
.then(checkUpdatability);
|
||||
}
|
||||
if (document.getElementById('install_button')) {
|
||||
onDOMready().then(() => {
|
||||
requestAnimationFrame(() => {
|
||||
sendEvent(sendEvent.lastEvent);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
document.documentElement.appendChild(
|
||||
Object.assign(document.createElement('script'), {
|
||||
textContent: `(${inPageContext})('${pageEventId}')`,
|
||||
}));
|
||||
|
||||
function onMessage(msg) {
|
||||
switch (msg.method) {
|
||||
|
@ -72,7 +59,7 @@
|
|||
|
||||
function checkUpdatability([installedStyle, md5]) {
|
||||
// TODO: remove the following statement when USO is fixed
|
||||
document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', {
|
||||
document.dispatchEvent(new CustomEvent(pageEventId, {
|
||||
detail: installedStyle && installedStyle.updateUrl,
|
||||
}));
|
||||
currentMd5 = md5;
|
||||
|
@ -141,7 +128,6 @@
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
function onClick(event) {
|
||||
if (onClick.processing || !orphanCheck()) {
|
||||
return;
|
||||
|
@ -227,13 +213,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function getMeta(name) {
|
||||
const e = document.querySelector(`link[rel="${name}"]`);
|
||||
return e ? e.getAttribute('href') : null;
|
||||
}
|
||||
|
||||
|
||||
function getResource(url, options) {
|
||||
if (url.startsWith('#')) {
|
||||
return Promise.resolve(document.getElementById(url.slice(1)).textContent);
|
||||
|
@ -280,7 +264,6 @@
|
|||
.catch(() => null);
|
||||
}
|
||||
|
||||
|
||||
function styleSectionsEqual({sections: a}, {sections: b}) {
|
||||
if (!a || !b) {
|
||||
return undefined;
|
||||
|
@ -318,20 +301,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function onDOMready() {
|
||||
if (document.readyState !== 'loading') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
document.addEventListener('DOMContentLoaded', function _() {
|
||||
document.removeEventListener('DOMContentLoaded', _);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return document.readyState !== 'loading'
|
||||
? Promise.resolve()
|
||||
: new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve, {once: true}));
|
||||
}
|
||||
|
||||
|
||||
function openSettings(countdown = 10e3) {
|
||||
const button = document.querySelector('.customize_button');
|
||||
if (button) {
|
||||
|
@ -349,12 +324,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function orphanCheck() {
|
||||
// TODO: switch to install-hook-usercss.js impl, and remove explicit orphanCheck() calls
|
||||
if (chrome.i18n && chrome.i18n.getUILanguage()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
if (chrome.i18n.getUILanguage()) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {}
|
||||
// In Chrome content script is orphaned on an extension update/reload
|
||||
// so we need to detach event listeners
|
||||
window.removeEventListener(chrome.runtime.id + '-install', orphanCheck, true);
|
||||
|
@ -366,132 +341,56 @@
|
|||
}
|
||||
})();
|
||||
|
||||
// run in page context
|
||||
document.documentElement.appendChild(document.createElement('script')).text = '(' + (
|
||||
() => {
|
||||
document.currentScript.remove();
|
||||
|
||||
// spoof Stylish extension presence in Chrome
|
||||
if (window.chrome && chrome.app) {
|
||||
const realImage = window.Image;
|
||||
window.Image = function Image(...args) {
|
||||
return new Proxy(new realImage(...args), {
|
||||
get(obj, key) {
|
||||
return obj[key];
|
||||
},
|
||||
set(obj, key, value) {
|
||||
if (key === 'src' && /^chrome-extension:/i.test(value)) {
|
||||
setTimeout(() => typeof obj.onload === 'function' && obj.onload());
|
||||
} else {
|
||||
obj[key] = value;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// USO bug workaround: use the actual style settings in API response
|
||||
let settings;
|
||||
const originalResponseJson = Response.prototype.json;
|
||||
document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) {
|
||||
document.removeEventListener('stylusFixBuggyUSOsettings', _);
|
||||
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425)
|
||||
settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search.replace(/^\?/, ''));
|
||||
if (!settings) {
|
||||
Response.prototype.json = originalResponseJson;
|
||||
function inPageContext(eventId) {
|
||||
document.currentScript.remove();
|
||||
const origMethods = {
|
||||
json: Response.prototype.json,
|
||||
byId: document.getElementById,
|
||||
};
|
||||
let vars;
|
||||
// USO bug workaround: prevent errors in console after install and busy cursor
|
||||
document.getElementById = id =>
|
||||
origMethods.byId.call(document, id) ||
|
||||
(/^(stylish-code|stylish-installed-style-installed-\w+|post-install-ad|style-install-unknown)$/.test(id)
|
||||
? Object.assign(document.createElement('p'), {className: 'afterdownload-ad'})
|
||||
: null);
|
||||
// USO bug workaround: use the actual image data in customized settings
|
||||
document.addEventListener(eventId, ({detail}) => {
|
||||
vars = /\?/.test(detail) && new URL(detail).searchParams;
|
||||
if (!vars) Response.prototype.json = origMethods.json;
|
||||
}, {once: true});
|
||||
Response.prototype.json = async function () {
|
||||
const json = await origMethods.json.apply(this, arguments);
|
||||
if (vars && json && Array.isArray(json.style_settings)) {
|
||||
Response.prototype.json = origMethods.json;
|
||||
const images = new Map();
|
||||
for (const ss of json.style_settings) {
|
||||
const value = vars.get('ik-' + ss.install_key);
|
||||
if (value && ss.setting_type === 'image' && ss.style_setting_options) {
|
||||
let isListed;
|
||||
for (const opt of ss.style_setting_options) {
|
||||
isListed |= opt.default = (opt.value === value);
|
||||
}
|
||||
images.set(ss.install_key, {url: value, isListed});
|
||||
}
|
||||
}
|
||||
});
|
||||
Response.prototype.json = function (...args) {
|
||||
return originalResponseJson.call(this, ...args).then(json => {
|
||||
if (!settings || typeof ((json || {}).style_settings || {}).every !== 'function') {
|
||||
return json;
|
||||
}
|
||||
Response.prototype.json = originalResponseJson;
|
||||
const images = new Map();
|
||||
for (const jsonSetting of json.style_settings) {
|
||||
let value = settings.get('ik-' + jsonSetting.install_key);
|
||||
if (!value
|
||||
|| !jsonSetting.style_setting_options
|
||||
|| !jsonSetting.style_setting_options[0]) {
|
||||
continue;
|
||||
}
|
||||
if (value.startsWith('ik-')) {
|
||||
value = value.replace(/^ik-/, '');
|
||||
const defaultItem = jsonSetting.style_setting_options.find(item => item.default);
|
||||
if (!defaultItem || defaultItem.install_key !== value) {
|
||||
if (defaultItem) {
|
||||
defaultItem.default = false;
|
||||
}
|
||||
jsonSetting.style_setting_options.some(item => {
|
||||
if (item.install_key === value) {
|
||||
item.default = true;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (jsonSetting.setting_type === 'image') {
|
||||
jsonSetting.style_setting_options.some(item => {
|
||||
if (item.default) {
|
||||
item.default = false;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
images.set(jsonSetting.install_key, value);
|
||||
} else {
|
||||
const item = jsonSetting.style_setting_options[0];
|
||||
if (item.value !== value && item.install_key === 'placeholder') {
|
||||
item.value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (images.size) {
|
||||
new MutationObserver((_, observer) => {
|
||||
if (!document.getElementById('style-settings')) {
|
||||
return;
|
||||
}
|
||||
if (images.size) {
|
||||
new MutationObserver((_, observer) => {
|
||||
if (document.getElementById('style-settings')) {
|
||||
observer.disconnect();
|
||||
for (const [name, url] of images.entries()) {
|
||||
for (const [name, {url, isListed}] of images) {
|
||||
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
|
||||
const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
|
||||
const elUrl = elRadio &&
|
||||
document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
|
||||
if (elUrl) {
|
||||
elRadio.checked = !isListed;
|
||||
elUrl.value = url;
|
||||
}
|
||||
}
|
||||
}).observe(document, {childList: true, subtree: true});
|
||||
}
|
||||
return json;
|
||||
});
|
||||
};
|
||||
}
|
||||
) + `)('${chrome.runtime.getURL('').slice(0, -1)}')`;
|
||||
|
||||
// TODO: remove the following statement when USO pagination is fixed
|
||||
if (location.search.includes('category=')) {
|
||||
document.addEventListener('DOMContentLoaded', function _() {
|
||||
document.removeEventListener('DOMContentLoaded', _);
|
||||
new MutationObserver((_, observer) => {
|
||||
if (!document.getElementById('pagination')) {
|
||||
return;
|
||||
}
|
||||
}).observe(document, {childList: true, subtree: true});
|
||||
}
|
||||
observer.disconnect();
|
||||
const category = '&' + location.search.match(/category=[^&]+/)[0];
|
||||
const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])');
|
||||
for (let i = 0; i < links.length; i++) {
|
||||
links[i].href += category;
|
||||
}
|
||||
}).observe(document, {childList: true, subtree: true});
|
||||
});
|
||||
}
|
||||
|
||||
if (/^https?:\/\/userstyles\.org\/styles\/\d{3,}/.test(location.href)) {
|
||||
new MutationObserver((_, observer) => {
|
||||
const cssButton = document.getElementsByClassName('css_button');
|
||||
if (cssButton.length) {
|
||||
// Click on the "Show CSS Code" button to workaround the JS error
|
||||
cssButton[0].click();
|
||||
cssButton[0].click();
|
||||
observer.disconnect();
|
||||
}
|
||||
}).observe(document, {childList: true, subtree: true});
|
||||
return json;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
|
|||
const PATCH_ID = 'transition-patch';
|
||||
// styles are out of order if any of these elements is injected between them
|
||||
const ORDERED_TAGS = new Set(['head', 'body', 'frameset', 'style', 'link']);
|
||||
const IS_OWN_PAGE = Boolean(chrome.tabs);
|
||||
// detect Chrome 65 via a feature it added since browser version can be spoofed
|
||||
const isChromePre65 = chrome.app && typeof Worklet !== 'function';
|
||||
const docRewriteObserver = RewriteObserver(_sort);
|
||||
|
@ -159,7 +158,7 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
|
|||
}
|
||||
|
||||
function _emitUpdate(value) {
|
||||
_toggleObservers(!IS_OWN_PAGE && list.length);
|
||||
_toggleObservers(list.length);
|
||||
onUpdate();
|
||||
return value;
|
||||
}
|
||||
|
|
56
edit.html
56
edit.html
|
@ -18,6 +18,22 @@
|
|||
}
|
||||
</style>
|
||||
|
||||
<link id="cm-theme" rel="stylesheet">
|
||||
|
||||
<script src="js/polyfill.js"></script>
|
||||
<script src="js/dom.js"></script>
|
||||
<script src="js/messaging.js"></script>
|
||||
<script src="js/msg.js"></script>
|
||||
<script src="js/prefs.js"></script>
|
||||
<script src="js/localization.js"></script>
|
||||
<script src="js/script-loader.js"></script>
|
||||
<script src="js/storage-util.js"></script>
|
||||
|
||||
<script src="content/style-injector.js"></script>
|
||||
<script src="content/apply.js"></script>
|
||||
|
||||
<script src="edit/edit.js"></script> <!-- run it ASAP to send a request for the style -->
|
||||
|
||||
<link href="vendor/codemirror/lib/codemirror.css" rel="stylesheet">
|
||||
<script src="vendor/codemirror/lib/codemirror.js"></script>
|
||||
|
||||
|
@ -63,45 +79,27 @@
|
|||
|
||||
<script src="vendor-overwrites/codemirror-addon/match-highlighter.js"></script>
|
||||
|
||||
<script src="js/polyfill.js"></script>
|
||||
<script src="js/dom.js"></script>
|
||||
<script src="js/messaging.js"></script>
|
||||
<script src="js/prefs.js"></script>
|
||||
<script src="js/localization.js"></script>
|
||||
<script src="js/script-loader.js"></script>
|
||||
<script src="js/storage-util.js"></script>
|
||||
<script src="js/msg.js"></script>
|
||||
<script src="js/worker-util.js"></script>
|
||||
|
||||
<script src="content/style-injector.js"></script>
|
||||
<script src="content/apply.js"></script>
|
||||
|
||||
<link href="edit/global-search.css" rel="stylesheet">
|
||||
<script src="edit/global-search.js"></script>
|
||||
<script src="msgbox/msgbox.js" async></script>
|
||||
|
||||
<link href="edit/codemirror-default.css" rel="stylesheet">
|
||||
<script src="edit/codemirror-default.js"></script>
|
||||
|
||||
<script src="edit/codemirror-factory.js"></script>
|
||||
<script src="edit/util.js"></script>
|
||||
<script src="edit/regexp-tester.js"></script>
|
||||
<script src="edit/live-preview.js"></script>
|
||||
<script src="edit/applies-to-line-widget.js"></script>
|
||||
<script src="edit/reroute-hotkeys.js"></script>
|
||||
<script src="edit/codemirror-factory.js"></script>
|
||||
<link href="edit/global-search.css" rel="stylesheet">
|
||||
<script src="edit/global-search.js"></script>
|
||||
<script src="edit/colorpicker-helper.js"></script>
|
||||
<script src="edit/beautify.js"></script>
|
||||
<script src="edit/show-keymap-help.js"></script>
|
||||
<script src="edit/refresh-on-view.js"></script>
|
||||
<script src="edit/codemirror-themes.js"></script>
|
||||
|
||||
<script src="edit/source-editor.js"></script>
|
||||
<script src="edit/sections-editor-section.js"></script>
|
||||
<script src="edit/sections-editor.js"></script>
|
||||
|
||||
<script src="edit/edit.js"></script>
|
||||
|
||||
<script src="msgbox/msgbox.js" async></script>
|
||||
|
||||
<script src="js/worker-util.js"></script>
|
||||
<script src="edit/linter.js"></script>
|
||||
<script src="edit/linter-defaults.js"></script>
|
||||
<script src="edit/linter-engines.js"></script>
|
||||
|
@ -110,8 +108,6 @@
|
|||
<script src="edit/linter-report.js"></script>
|
||||
<script src="edit/linter-config-dialog.js"></script>
|
||||
|
||||
<link id="cm-theme" rel="stylesheet">
|
||||
|
||||
<template data-id="appliesTo">
|
||||
<li class="applies-to-item">
|
||||
<div class="select-resizer">
|
||||
|
@ -285,7 +281,13 @@
|
|||
<h1 id="heading"> </h1> <!-- nbsp allocates the actual height which prevents page shift -->
|
||||
<section id="basic-info">
|
||||
<div id="basic-info-name">
|
||||
<input id="name" class="style-contributor" spellcheck="false" required>
|
||||
<input id="name" class="style-contributor" spellcheck="false">
|
||||
<a id="reset-name" href="#" i18n-title="customNameResetHint" tabindex="0" hidden>
|
||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||
<polygon points="16.2,5.5 14.5,3.8 10,8.3 5.5,3.8 3.8,5.5 8.3,10 3.8,14.5
|
||||
5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10 "/>
|
||||
</svg>
|
||||
</a>
|
||||
<a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a>
|
||||
</div>
|
||||
<div id="basic-info-enabled">
|
||||
|
@ -305,7 +307,7 @@
|
|||
</section>
|
||||
<section id="actions">
|
||||
<div>
|
||||
<button id="save-button" i18n-text="styleSaveLabel" data-hotkey-tooltip="save"></button>
|
||||
<button id="save-button" i18n-text="styleSaveLabel" data-hotkey-tooltip="save" disabled></button>
|
||||
<button id="beautify" i18n-text="styleBeautify"></button>
|
||||
<a href="manage.html" tabindex="-1"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
|
||||
</div>
|
||||
|
|
|
@ -7,16 +7,18 @@
|
|||
}
|
||||
.CodeMirror {
|
||||
border: solid #CCC 1px;
|
||||
transition: box-shadow .1s;
|
||||
}
|
||||
#stylus#stylus .CodeMirror {
|
||||
/* Using a specificity hack to override userstyles */
|
||||
/* Not using the ring-color hack as it became ugly in new Chrome */
|
||||
outline: none !important;
|
||||
}
|
||||
.CodeMirror-lint-mark-warning {
|
||||
background: none;
|
||||
}
|
||||
.CodeMirror-dialog {
|
||||
-webkit-animation: highlight 3s cubic-bezier(.18, .02, 0, .94);
|
||||
}
|
||||
.CodeMirror-focused {
|
||||
outline: -webkit-focus-ring-color auto 5px;
|
||||
outline-offset: -2px;
|
||||
animation: highlight 3s cubic-bezier(.18, .02, 0, .94);
|
||||
}
|
||||
.CodeMirror-bookmark {
|
||||
background: linear-gradient(to right, currentColor, transparent);
|
||||
|
@ -24,13 +26,6 @@
|
|||
width: 2em;
|
||||
opacity: .5;
|
||||
}
|
||||
@supports (-moz-appearance:none) {
|
||||
/* restrict to FF */
|
||||
.CodeMirror-focused {
|
||||
outline: #7dadd9 auto 1px;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
}
|
||||
.CodeMirror-search-field {
|
||||
width: 10em;
|
||||
}
|
||||
|
|
|
@ -105,14 +105,9 @@
|
|||
}
|
||||
|
||||
Object.assign(CodeMirror.mimeModes['text/css'].propertyKeywords, {
|
||||
'background-position-x': true,
|
||||
'background-position-y': true,
|
||||
'contain': true,
|
||||
'mask-image': true,
|
||||
'mix-blend-mode': true,
|
||||
'content-visibility': true,
|
||||
'overflow-anchor': true,
|
||||
'overscroll-behavior': true,
|
||||
'rotate': true,
|
||||
'isolation': true,
|
||||
});
|
||||
Object.assign(CodeMirror.mimeModes['text/css'].colorKeywords, {
|
||||
'darkgrey': true,
|
||||
|
@ -320,7 +315,7 @@ CodeMirror.hint && (() => {
|
|||
}
|
||||
|
||||
// USO vars in usercss mode editor
|
||||
const vars = editor.getStyle().usercssData.vars;
|
||||
const vars = editor.style.usercssData.vars;
|
||||
const list = vars ?
|
||||
Object.keys(vars).filter(name => name.startsWith(leftPart)) : [];
|
||||
return {
|
||||
|
@ -348,7 +343,7 @@ CodeMirror.hint && (() => {
|
|||
string[start + 3] === '[' &&
|
||||
string[pos - 3] === ']' &&
|
||||
string[pos - 4] === ']') {
|
||||
const vars = typeof editor !== 'undefined' && (editor.getStyle().usercssData || {}).vars;
|
||||
const vars = typeof editor !== 'undefined' && (editor.style.usercssData || {}).vars;
|
||||
const name = vars && string.slice(start + 4, pos - 4);
|
||||
if (vars && Object.hasOwnProperty.call(vars, name.endsWith('-rgb') ? name.slice(0, -4) : name)) {
|
||||
token[0] = USO_VALID_VAR;
|
||||
|
|
|
@ -20,13 +20,11 @@
|
|||
defaults.extraKeys[keyName] = 'colorpicker';
|
||||
}
|
||||
defaults.colorpicker = {
|
||||
// FIXME: who uses this?
|
||||
// forceUpdate: editor.getEditors().length > 0,
|
||||
tooltip: t('colorpickerTooltip'),
|
||||
popup: {
|
||||
tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'),
|
||||
hexUppercase: prefs.get('editor.colorpicker.hexUppercase'),
|
||||
hideDelay: 5000,
|
||||
hideDelay: 30e3,
|
||||
embedderCallback: state => {
|
||||
['hexUppercase', 'color']
|
||||
.filter(name => state[name] !== prefs.get('editor.colorpicker.' + name))
|
||||
|
|
|
@ -50,7 +50,6 @@ label {
|
|||
top: 0;
|
||||
padding: 1rem;
|
||||
border-right: 1px dashed #AAA;
|
||||
-webkit-box-shadow: 0 0 3rem -1.2rem black;
|
||||
box-shadow: 0 0 3rem -1.2rem black;
|
||||
box-sizing: border-box;
|
||||
z-index: 10;
|
||||
|
@ -63,6 +62,7 @@ label {
|
|||
#sections {
|
||||
padding-left: 280px;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
#sections h2 {
|
||||
margin-top: 1rem;
|
||||
|
@ -88,6 +88,9 @@ label {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
#reset-name {
|
||||
margin: 0 .25em 0 .5em;
|
||||
}
|
||||
#url {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
@ -278,12 +281,6 @@ input:invalid {
|
|||
.section-editor .section:not(:first-child) {
|
||||
border-top: 2px solid hsl(0, 0%, 80%);
|
||||
}
|
||||
.section-editor:not(.section-editor-ready) .section {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
.section-editor:not(.section-editor-ready) .CodeMirror {
|
||||
height: 0;
|
||||
}
|
||||
.add-section:after {
|
||||
content: attr(short-text);
|
||||
}
|
||||
|
@ -616,6 +613,10 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
|
|||
right: 4px;
|
||||
top: .5em;
|
||||
}
|
||||
#help-popup input[type="search"],
|
||||
#help-popup .CodeMirror {
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.keymap-list {
|
||||
font-size: 12px;
|
||||
|
@ -793,6 +794,10 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
|
|||
justify-items: normal;
|
||||
}
|
||||
|
||||
.usercss .CodeMirror-focused {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
html:not(.usercss) .usercss-only,
|
||||
.usercss #mozilla-format-container,
|
||||
.usercss #sections > h2 {
|
||||
|
@ -812,18 +817,8 @@ body.linter-disabled .hidden-unless-compact {
|
|||
margin-top: .75rem;
|
||||
}
|
||||
|
||||
.usercss #name {
|
||||
background-color: #eee;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* FIXME: remove the ID selector */
|
||||
#sections .single-editor {
|
||||
.single-editor {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.single-editor .CodeMirror {
|
||||
|
@ -843,7 +838,6 @@ body.linter-disabled .hidden-unless-compact {
|
|||
}
|
||||
|
||||
.usercss.firefox #sections,
|
||||
.usercss.firefox .single-editor,
|
||||
.usercss.firefox .CodeMirror {
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -996,7 +990,7 @@ body.linter-disabled .hidden-unless-compact {
|
|||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
#sections > * {
|
||||
#sections > :not(.single-editor) {
|
||||
margin: 0 .5rem;
|
||||
padding: .5rem 0;
|
||||
}
|
||||
|
|
381
edit/edit.js
381
edit/edit.js
|
@ -1,15 +1,11 @@
|
|||
/* global CodeMirror onDOMready prefs setupLivePrefs $ $$ $create t tHTML
|
||||
createSourceEditor sessionStorageHash getOwnTab FIREFOX API tryCatch
|
||||
closeCurrentTab messageBox debounce workerUtil
|
||||
initBeautifyButton ignoreChromeError
|
||||
closeCurrentTab messageBox debounce
|
||||
initBeautifyButton ignoreChromeError dirtyReporter linter
|
||||
moveFocus msg createSectionsEditor rerouteHotkeys CODEMIRROR_THEMES */
|
||||
/* exported showCodeMirrorPopup editorWorker toggleContextMenuDelete */
|
||||
'use strict';
|
||||
|
||||
const editorWorker = workerUtil.createWorker({
|
||||
url: '/edit/editor-worker.js'
|
||||
});
|
||||
|
||||
let saveSizeOnClose;
|
||||
|
||||
// direct & reverse mapping of @-moz-document keywords and internal property names
|
||||
|
@ -28,48 +24,84 @@ document.addEventListener('visibilitychange', beforeUnload);
|
|||
window.addEventListener('beforeunload', beforeUnload);
|
||||
msg.onExtension(onRuntimeMessage);
|
||||
|
||||
preinit();
|
||||
lazyInit();
|
||||
|
||||
(() => {
|
||||
onDOMready().then(() => {
|
||||
prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip);
|
||||
addEventListener('showHotkeyInTooltip', showHotkeyInTooltip);
|
||||
showHotkeyInTooltip();
|
||||
(async function init() {
|
||||
const [style] = await Promise.all([
|
||||
initStyleData(),
|
||||
onDOMready(),
|
||||
prefs.initializing.then(() => new Promise(resolve => {
|
||||
const theme = prefs.get('editor.theme');
|
||||
const el = $('#cm-theme');
|
||||
if (theme === 'default') {
|
||||
resolve();
|
||||
} else {
|
||||
// preload the theme so CodeMirror can use the correct metrics
|
||||
el.href = `vendor/codemirror/theme/${theme}.css`;
|
||||
el.addEventListener('load', resolve, {once: true});
|
||||
}
|
||||
})),
|
||||
]);
|
||||
const usercss = isUsercss(style);
|
||||
const dirty = dirtyReporter();
|
||||
let wasDirty = false;
|
||||
let nameTarget;
|
||||
|
||||
buildThemeElement();
|
||||
buildKeymapElement();
|
||||
prefs.subscribe(['editor.linter'], updateLinter);
|
||||
prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip);
|
||||
addEventListener('showHotkeyInTooltip', showHotkeyInTooltip);
|
||||
showHotkeyInTooltip();
|
||||
buildThemeElement();
|
||||
buildKeymapElement();
|
||||
setupLivePrefs();
|
||||
initNameArea();
|
||||
initBeautifyButton($('#beautify'), () => editor.getEditors());
|
||||
initResizeListener();
|
||||
detectLayout();
|
||||
updateTitle();
|
||||
|
||||
setupLivePrefs();
|
||||
$('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle');
|
||||
$('#preview-label').classList.toggle('hidden', !style.id);
|
||||
|
||||
editor = (usercss ? createSourceEditor : createSectionsEditor)({
|
||||
style,
|
||||
dirty,
|
||||
updateName,
|
||||
toggleStyle,
|
||||
});
|
||||
dirty.onChange(updateDirty);
|
||||
await editor.ready;
|
||||
|
||||
initEditor();
|
||||
// enabling after init to prevent flash of validation failure on an empty name
|
||||
$('#name').required = !usercss;
|
||||
$('#save-button').onclick = editor.save;
|
||||
|
||||
function getCodeMirrorThemes() {
|
||||
if (!chrome.runtime.getPackageDirectoryEntry) {
|
||||
const themes = [
|
||||
chrome.i18n.getMessage('defaultTheme'),
|
||||
...CODEMIRROR_THEMES
|
||||
];
|
||||
localStorage.codeMirrorThemes = themes.join(' ');
|
||||
return Promise.resolve(themes);
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
chrome.runtime.getPackageDirectoryEntry(rootDir => {
|
||||
rootDir.getDirectory('vendor/codemirror/theme', {create: false}, themeDir => {
|
||||
themeDir.createReader().readEntries(entries => {
|
||||
const themes = [
|
||||
chrome.i18n.getMessage('defaultTheme')
|
||||
].concat(
|
||||
entries.filter(entry => entry.isFile)
|
||||
.sort((a, b) => (a.name < b.name ? -1 : 1))
|
||||
.map(entry => entry.name.replace(/\.css$/, ''))
|
||||
);
|
||||
localStorage.codeMirrorThemes = themes.join(' ');
|
||||
resolve(themes);
|
||||
});
|
||||
});
|
||||
});
|
||||
function initNameArea() {
|
||||
const nameEl = $('#name');
|
||||
const resetEl = $('#reset-name');
|
||||
const isCustomName = style.updateUrl || usercss;
|
||||
nameTarget = isCustomName ? 'customName' : 'name';
|
||||
nameEl.placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
|
||||
nameEl.title = isCustomName ? t('customNameHint') : '';
|
||||
nameEl.addEventListener('input', () => {
|
||||
updateName(true);
|
||||
resetEl.hidden = false;
|
||||
});
|
||||
resetEl.hidden = !style.customName;
|
||||
resetEl.onclick = () => {
|
||||
const style = editor.style;
|
||||
nameEl.focus();
|
||||
nameEl.select();
|
||||
// trying to make it undoable via Ctrl-Z
|
||||
if (!document.execCommand('insertText', false, style.name)) {
|
||||
nameEl.value = style.name;
|
||||
updateName(true);
|
||||
}
|
||||
style.customName = null; // to delete it from db
|
||||
resetEl.hidden = true;
|
||||
};
|
||||
const enabledEl = $('#enabled');
|
||||
enabledEl.onchange = () => updateEnabledness(enabledEl.checked);
|
||||
}
|
||||
|
||||
function findKeyForCommand(command, map) {
|
||||
|
@ -88,27 +120,10 @@ preinit();
|
|||
}
|
||||
|
||||
function buildThemeElement() {
|
||||
const themeElement = $('#editor.theme');
|
||||
const themeList = localStorage.codeMirrorThemes;
|
||||
|
||||
const optionsFromArray = options => {
|
||||
const fragment = document.createDocumentFragment();
|
||||
options.forEach(opt => fragment.appendChild($create('option', opt)));
|
||||
themeElement.appendChild(fragment);
|
||||
};
|
||||
|
||||
if (themeList) {
|
||||
optionsFromArray(themeList.split(/\s+/));
|
||||
} else {
|
||||
// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
|
||||
const theme = prefs.get('editor.theme');
|
||||
optionsFromArray([theme === 'default' ? t('defaultTheme') : theme]);
|
||||
getCodeMirrorThemes().then(() => {
|
||||
const themes = (localStorage.codeMirrorThemes || '').split(/\s+/);
|
||||
optionsFromArray(themes);
|
||||
themeElement.selectedIndex = Math.max(0, themes.indexOf(theme));
|
||||
});
|
||||
}
|
||||
CODEMIRROR_THEMES.unshift(chrome.i18n.getMessage('defaultTheme'));
|
||||
$('#editor.theme').append(...CODEMIRROR_THEMES.map(s => $create('option', s)));
|
||||
// move the theme after built-in CSS so that its same-specificity selectors win
|
||||
document.head.appendChild($('#cm-theme'));
|
||||
}
|
||||
|
||||
function buildKeymapElement() {
|
||||
|
@ -159,134 +174,120 @@ preinit();
|
|||
}
|
||||
}
|
||||
|
||||
function initEditor() {
|
||||
return Promise.all([
|
||||
initStyleData(),
|
||||
onDOMready(),
|
||||
prefs.initializing,
|
||||
])
|
||||
.then(([style]) => {
|
||||
const usercss = isUsercss(style);
|
||||
$('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle');
|
||||
$('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
|
||||
$('#name').title = usercss ? t('usercssReplaceTemplateName') : '';
|
||||
$('#preview-label').classList.toggle('hidden', !style.id);
|
||||
initBeautifyButton($('#beautify'), () => editor.getEditors());
|
||||
const {onBoundsChanged} = chrome.windows || {};
|
||||
if (onBoundsChanged) {
|
||||
// * movement is reported even if the window wasn't resized
|
||||
// * fired just once when done so debounce is not needed
|
||||
onBoundsChanged.addListener(wnd => {
|
||||
// getting the current window id as it may change if the user attached/detached the tab
|
||||
chrome.windows.getCurrent(ownWnd => {
|
||||
if (wnd.id === ownWnd.id) rememberWindowSize();
|
||||
});
|
||||
});
|
||||
}
|
||||
window.addEventListener('resize', () => {
|
||||
if (!onBoundsChanged) debounce(rememberWindowSize, 100);
|
||||
detectLayout();
|
||||
function initResizeListener() {
|
||||
const {onBoundsChanged} = chrome.windows || {};
|
||||
if (onBoundsChanged) {
|
||||
// * movement is reported even if the window wasn't resized
|
||||
// * fired just once when done so debounce is not needed
|
||||
onBoundsChanged.addListener(wnd => {
|
||||
// getting the current window id as it may change if the user attached/detached the tab
|
||||
chrome.windows.getCurrent(ownWnd => {
|
||||
if (wnd.id === ownWnd.id) rememberWindowSize();
|
||||
});
|
||||
detectLayout();
|
||||
editor = (usercss ? createSourceEditor : createSectionsEditor)({
|
||||
style,
|
||||
onTitleChanged: updateTitle
|
||||
});
|
||||
editor.dirty.onChange(updateDirty);
|
||||
return Promise.resolve(editor.ready && editor.ready())
|
||||
.then(updateDirty);
|
||||
});
|
||||
}
|
||||
|
||||
function updateTitle() {
|
||||
if (editor) {
|
||||
const styleName = editor.getStyle().name;
|
||||
const isDirty = editor.dirty.isDirty();
|
||||
document.title = (isDirty ? '* ' : '') + styleName;
|
||||
}
|
||||
}
|
||||
|
||||
function updateDirty() {
|
||||
const isDirty = editor.dirty.isDirty();
|
||||
document.body.classList.toggle('dirty', isDirty);
|
||||
$('#save-button').disabled = !isDirty;
|
||||
updateTitle();
|
||||
}
|
||||
})();
|
||||
|
||||
function preinit() {
|
||||
// preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs()
|
||||
new MutationObserver((mutations, observer) => {
|
||||
const themeElement = $('#cm-theme');
|
||||
if (themeElement) {
|
||||
themeElement.href = prefs.get('editor.theme') === 'default' ? ''
|
||||
: 'vendor/codemirror/theme/' + prefs.get('editor.theme') + '.css';
|
||||
observer.disconnect();
|
||||
}
|
||||
}).observe(document, {subtree: true, childList: true});
|
||||
|
||||
if (chrome.windows) {
|
||||
browser.tabs.query({currentWindow: true}).then(tabs => {
|
||||
const windowId = tabs[0].windowId;
|
||||
if (prefs.get('openEditInWindow')) {
|
||||
if (
|
||||
/true/.test(sessionStorage.saveSizeOnClose) &&
|
||||
'left' in prefs.get('windowPosition', {}) &&
|
||||
!isWindowMaximized()
|
||||
) {
|
||||
// window was reopened via Ctrl-Shift-T etc.
|
||||
chrome.windows.update(windowId, prefs.get('windowPosition'));
|
||||
}
|
||||
if (tabs.length === 1 && window.history.length === 1) {
|
||||
chrome.windows.getAll(windows => {
|
||||
if (windows.length > 1) {
|
||||
sessionStorageHash('saveSizeOnClose').set(windowId, true);
|
||||
saveSizeOnClose = true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
saveSizeOnClose = sessionStorageHash('saveSizeOnClose').value[windowId];
|
||||
}
|
||||
}
|
||||
window.addEventListener('resize', () => {
|
||||
if (!onBoundsChanged) debounce(rememberWindowSize, 100);
|
||||
detectLayout();
|
||||
});
|
||||
}
|
||||
|
||||
getOwnTab().then(tab => {
|
||||
const ownTabId = tab.id;
|
||||
function toggleStyle() {
|
||||
$('#enabled').checked = !style.enabled;
|
||||
updateEnabledness(!style.enabled);
|
||||
}
|
||||
|
||||
// use browser history back when 'back to manage' is clicked
|
||||
if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) {
|
||||
onDOMready().then(() => {
|
||||
$('#cancel-button').onclick = event => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
history.back();
|
||||
};
|
||||
});
|
||||
function updateDirty() {
|
||||
const isDirty = dirty.isDirty();
|
||||
if (wasDirty !== isDirty) {
|
||||
wasDirty = isDirty;
|
||||
document.body.classList.toggle('dirty', isDirty);
|
||||
$('#save-button').disabled = !isDirty;
|
||||
}
|
||||
// no windows on android
|
||||
if (!chrome.windows) {
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
function updateEnabledness(enabled) {
|
||||
dirty.modify('enabled', style.enabled, enabled);
|
||||
style.enabled = enabled;
|
||||
editor.updateLivePreview();
|
||||
}
|
||||
|
||||
function updateName(isUserInput) {
|
||||
if (!editor) return;
|
||||
if (isUserInput) {
|
||||
const {value} = $('#name');
|
||||
dirty.modify('name', style[nameTarget] || style.name, value);
|
||||
style[nameTarget] = value;
|
||||
}
|
||||
updateTitle({});
|
||||
}
|
||||
|
||||
function updateTitle() {
|
||||
document.title = `${dirty.isDirty() ? '* ' : ''}${style.customName || style.name}`;
|
||||
}
|
||||
|
||||
function updateLinter(key, value) {
|
||||
$('body').classList.toggle('linter-disabled', value === '');
|
||||
linter.run();
|
||||
}
|
||||
})();
|
||||
|
||||
/* Stuff not needed for the main init so we can let it run at its own tempo */
|
||||
async function lazyInit() {
|
||||
const ownTabId = (await getOwnTab()).id;
|
||||
// use browser history back when 'back to manage' is clicked
|
||||
if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) {
|
||||
onDOMready().then(() => {
|
||||
$('#cancel-button').onclick = event => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
history.back();
|
||||
};
|
||||
});
|
||||
}
|
||||
// no windows on android
|
||||
if (!chrome.windows) {
|
||||
return;
|
||||
}
|
||||
const tabs = await browser.tabs.query({currentWindow: true});
|
||||
const windowId = tabs[0].windowId;
|
||||
if (prefs.get('openEditInWindow')) {
|
||||
if (
|
||||
/true/.test(sessionStorage.saveSizeOnClose) &&
|
||||
'left' in prefs.get('windowPosition', {}) &&
|
||||
!isWindowMaximized()
|
||||
) {
|
||||
// window was reopened via Ctrl-Shift-T etc.
|
||||
chrome.windows.update(windowId, prefs.get('windowPosition'));
|
||||
}
|
||||
if (tabs.length === 1 && window.history.length === 1) {
|
||||
chrome.windows.getAll(windows => {
|
||||
if (windows.length > 1) {
|
||||
sessionStorageHash('saveSizeOnClose').set(windowId, true);
|
||||
saveSizeOnClose = true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
saveSizeOnClose = sessionStorageHash('saveSizeOnClose').value[windowId];
|
||||
}
|
||||
}
|
||||
chrome.tabs.onAttached.addListener((tabId, info) => {
|
||||
if (tabId !== ownTabId) {
|
||||
return;
|
||||
}
|
||||
// When an edit page gets attached or detached, remember its state
|
||||
// so we can do the same to the next one to open.
|
||||
chrome.tabs.onAttached.addListener((tabId, info) => {
|
||||
if (tabId !== ownTabId) {
|
||||
return;
|
||||
if (info.newPosition !== 0) {
|
||||
prefs.set('openEditInWindow', false);
|
||||
return;
|
||||
}
|
||||
chrome.windows.get(info.newWindowId, {populate: true}, win => {
|
||||
// If there's only one tab in this window, it's been dragged to new window
|
||||
const openEditInWindow = win.tabs.length === 1;
|
||||
if (openEditInWindow && FIREFOX) {
|
||||
// FF-only because Chrome retardedly resets the size during dragging
|
||||
chrome.windows.update(info.newWindowId, prefs.get('windowPosition'));
|
||||
}
|
||||
if (info.newPosition !== 0) {
|
||||
prefs.set('openEditInWindow', false);
|
||||
return;
|
||||
}
|
||||
chrome.windows.get(info.newWindowId, {populate: true}, win => {
|
||||
// If there's only one tab in this window, it's been dragged to new window
|
||||
const openEditInWindow = win.tabs.length === 1;
|
||||
if (openEditInWindow && FIREFOX) {
|
||||
// FF-only because Chrome retardedly resets the size during dragging
|
||||
chrome.windows.update(info.newWindowId, prefs.get('windowPosition'));
|
||||
}
|
||||
prefs.set('openEditInWindow', openEditInWindow);
|
||||
});
|
||||
prefs.set('openEditInWindow', openEditInWindow);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -295,7 +296,7 @@ function onRuntimeMessage(request) {
|
|||
switch (request.method) {
|
||||
case 'styleUpdated':
|
||||
if (
|
||||
editor.getStyleId() === request.style.id &&
|
||||
editor.style.id === request.style.id &&
|
||||
!['editPreview', 'editPreviewEnd', 'editSave', 'config']
|
||||
.includes(request.reason)
|
||||
) {
|
||||
|
@ -309,7 +310,7 @@ function onRuntimeMessage(request) {
|
|||
}
|
||||
break;
|
||||
case 'styleDeleted':
|
||||
if (editor.getStyleId() === request.style.id) {
|
||||
if (editor.style.id === request.style.id) {
|
||||
document.removeEventListener('visibilitychange', beforeUnload);
|
||||
document.removeEventListener('beforeunload', beforeUnload);
|
||||
closeCurrentTab();
|
||||
|
@ -352,8 +353,7 @@ function isUsercss(style) {
|
|||
}
|
||||
|
||||
function initStyleData() {
|
||||
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425)
|
||||
const params = new URLSearchParams(location.search.replace(/^\?/, ''));
|
||||
const params = new URLSearchParams(location.search);
|
||||
const id = Number(params.get('id'));
|
||||
const createEmptyStyle = () => ({
|
||||
name: params.get('domain') ||
|
||||
|
@ -409,7 +409,7 @@ function showHelp(title = '', body) {
|
|||
!event ||
|
||||
event.type === 'click' ||
|
||||
(
|
||||
event.which === 27 &&
|
||||
event.key === 'Escape' &&
|
||||
!event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey &&
|
||||
!$('.CodeMirror-hints, #message-box') &&
|
||||
(
|
||||
|
@ -470,7 +470,7 @@ function showCodeMirrorPopup(title, html, options) {
|
|||
popup.style.pointerEvents = 'auto';
|
||||
|
||||
const onKeyDown = event => {
|
||||
if (event.which === 9 && !event.ctrlKey && !event.altKey && !event.metaKey) {
|
||||
if (event.key === 'Tab' && !event.ctrlKey && !event.altKey && !event.metaKey) {
|
||||
const search = $('#search-replace-dialog');
|
||||
const area = search && search.contains(document.activeElement) ? search : popup;
|
||||
moveFocus(area, event.shiftKey ? -1 : 1);
|
||||
|
@ -479,13 +479,12 @@ function showCodeMirrorPopup(title, html, options) {
|
|||
};
|
||||
window.addEventListener('keydown', onKeyDown, true);
|
||||
|
||||
window.addEventListener('closeHelp', function _() {
|
||||
window.removeEventListener('closeHelp', _);
|
||||
window.addEventListener('closeHelp', () => {
|
||||
window.removeEventListener('keydown', onKeyDown, true);
|
||||
document.documentElement.style.removeProperty('pointer-events');
|
||||
rerouteHotkeys(true);
|
||||
cm = popup.codebox = null;
|
||||
});
|
||||
}, {once: true});
|
||||
|
||||
return popup;
|
||||
}
|
||||
|
@ -505,10 +504,6 @@ function rememberWindowSize() {
|
|||
}
|
||||
}
|
||||
|
||||
prefs.subscribe(['editor.linter'], (key, value) => {
|
||||
$('body').classList.toggle('linter-disabled', value === '');
|
||||
});
|
||||
|
||||
function fixedHeader() {
|
||||
const scrollPoint = $('#header').clientHeight - 40;
|
||||
const linterEnabled = prefs.get('editor.linter') !== '';
|
||||
|
@ -541,7 +536,7 @@ function detectLayout() {
|
|||
body.classList.add('fixed-header');
|
||||
}
|
||||
}, 250);
|
||||
window.addEventListener('scroll', fixedHeader);
|
||||
window.addEventListener('scroll', fixedHeader, {passive: true});
|
||||
}
|
||||
} else {
|
||||
body.classList.remove('compact-layout');
|
||||
|
|
|
@ -16,7 +16,6 @@ createAPI({
|
|||
},
|
||||
metalint: code => {
|
||||
loadScript(
|
||||
'/js/polyfill.js',
|
||||
'/vendor/usercss-meta/usercss-meta.min.js',
|
||||
'/vendor-overwrites/colorpicker/colorconverter.js',
|
||||
'/js/meta-parser.js'
|
||||
|
|
|
@ -53,11 +53,10 @@
|
|||
cm.on('changes', updateButtonState);
|
||||
|
||||
rerouteHotkeys(false);
|
||||
window.addEventListener('closeHelp', function _() {
|
||||
window.removeEventListener('closeHelp', _);
|
||||
window.addEventListener('closeHelp', () => {
|
||||
rerouteHotkeys(true);
|
||||
cm = null;
|
||||
});
|
||||
}, {once: true});
|
||||
|
||||
loadScript([
|
||||
'/vendor/codemirror/mode/javascript/javascript.js',
|
||||
|
|
|
@ -20,29 +20,19 @@
|
|||
}
|
||||
});
|
||||
|
||||
function stylelint(text, config, mode) {
|
||||
function stylelint(text, config) {
|
||||
return editorWorker.stylelint(text, config)
|
||||
.then(({results}) => {
|
||||
if (!results[0]) {
|
||||
return [];
|
||||
}
|
||||
const output = results[0].warnings.map(({line, column: ch, text, severity}) =>
|
||||
({
|
||||
from: {line: line - 1, ch: ch - 1},
|
||||
to: {line: line - 1, ch},
|
||||
message: text
|
||||
.replace('Unexpected ', '')
|
||||
.replace(/^./, firstLetter => firstLetter.toUpperCase())
|
||||
.replace(/\s*\([^(]+\)$/, ''), // strip the rule,
|
||||
rule: text.replace(/^.*?\s*\(([^(]+)\)$/, '$1'),
|
||||
severity,
|
||||
})
|
||||
);
|
||||
return mode !== 'stylus' ?
|
||||
output :
|
||||
output.filter(({message}) =>
|
||||
!message.includes('"@css"') || !message.includes('(at-rule-no-unknown)'));
|
||||
});
|
||||
.then(({results}) => !results[0] ? [] :
|
||||
results[0].warnings.map(({line, column: ch, text, severity}) => ({
|
||||
from: {line: line - 1, ch: ch - 1},
|
||||
to: {line: line - 1, ch},
|
||||
message: text
|
||||
.replace('Unexpected ', '')
|
||||
.replace(/^./, firstLetter => firstLetter.toUpperCase())
|
||||
.replace(/\s*\([^(]+\)$/, ''), // strip the rule,
|
||||
rule: text.replace(/^.*?\s*\(([^(]+)\)$/, '$1'),
|
||||
severity,
|
||||
})));
|
||||
}
|
||||
|
||||
function csslint(text, config) {
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
/* global prefs */
|
||||
/* global workerUtil */
|
||||
'use strict';
|
||||
|
||||
/* exported editorWorker */
|
||||
const editorWorker = workerUtil.createWorker({
|
||||
url: '/edit/editor-worker.js'
|
||||
});
|
||||
|
||||
/* exported linter */
|
||||
const linter = (() => {
|
||||
const lintingUpdatedListeners = [];
|
||||
|
@ -59,8 +64,3 @@ const linter = (() => {
|
|||
.then(results => [].concat(...results.filter(Boolean)));
|
||||
}
|
||||
})();
|
||||
|
||||
// FIXME: this should be put inside edit.js
|
||||
prefs.subscribe(['editor.linter'], () => {
|
||||
linter.run();
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@ function createLivePreview(preprocess) {
|
|||
const errorContainer = $('#preview-errors');
|
||||
|
||||
prefs.subscribe(['editor.livePreview'], (key, value) => {
|
||||
if (value && data && data.id && data.enabled) {
|
||||
if (value && data && data.id && (data.enabled || editor.dirty.has('enabled'))) {
|
||||
previewer = createPreviewer();
|
||||
previewer.update(data);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
|
@ -67,8 +67,7 @@ const regExpTester = (() => {
|
|||
});
|
||||
const getMatchInfo = m => m && {text: m[0], pos: m.index};
|
||||
browser.tabs.query({}).then(tabs => {
|
||||
const supported = tabs.map(tab => tab.url)
|
||||
.filter(url => URLS.supported(url));
|
||||
const supported = tabs.map(tab => tab.pendingUrl || tab.url).filter(URLS.supported);
|
||||
const unique = [...new Set(supported).values()];
|
||||
for (const rxData of regexps) {
|
||||
const {rx, urls} = rxData;
|
||||
|
|
|
@ -206,36 +206,32 @@ function createSection({
|
|||
}
|
||||
|
||||
function handleKeydown(cm, event) {
|
||||
const key = event.which;
|
||||
if (key < 37 || key > 40 || event.shiftKey || event.altKey || event.metaKey) {
|
||||
if (event.shiftKey || event.altKey || event.metaKey) {
|
||||
return;
|
||||
}
|
||||
const {key} = event;
|
||||
const {line, ch} = cm.getCursor();
|
||||
switch (key) {
|
||||
case 37:
|
||||
// arrow Left
|
||||
case 'ArrowLeft':
|
||||
if (line || ch) {
|
||||
return;
|
||||
}
|
||||
// fallthrough to arrow Up
|
||||
case 38:
|
||||
// arrow Up
|
||||
// fallthrough
|
||||
case 'ArrowUp':
|
||||
cm = line === 0 && prevEditor(cm, false);
|
||||
if (!cm) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
cm.setCursor(cm.doc.size - 1, key === 37 ? 1e20 : ch);
|
||||
cm.setCursor(cm.doc.size - 1, key === 'ArrowLeft' ? 1e20 : ch);
|
||||
break;
|
||||
case 39:
|
||||
// arrow Right
|
||||
case 'ArrowRight':
|
||||
if (line < cm.doc.size - 1 || ch < cm.getLine(line).length - 1) {
|
||||
return;
|
||||
}
|
||||
// fallthrough to arrow Down
|
||||
case 40:
|
||||
// arrow Down
|
||||
// fallthrough
|
||||
case 'ArrowDown':
|
||||
cm = line === cm.doc.size - 1 && nextEditor(cm, false);
|
||||
if (!cm) {
|
||||
return;
|
||||
|
@ -245,13 +241,6 @@ function createSection({
|
|||
cm.setCursor(0, 0);
|
||||
break;
|
||||
}
|
||||
// FIXME: what is this?
|
||||
// const animation = (cm.getSection().firstElementChild.getAnimations() || [])[0];
|
||||
// if (animation) {
|
||||
// animation.playbackRate = -1;
|
||||
// animation.currentTime = 2000;
|
||||
// animation.play();
|
||||
// }
|
||||
}
|
||||
|
||||
function showAppliesToHelp(event) {
|
||||
|
@ -296,19 +285,14 @@ function createSection({
|
|||
|
||||
function insertApplyAfter(init, base) {
|
||||
const apply = createApply(init);
|
||||
if (base) {
|
||||
const index = appliesTo.indexOf(base);
|
||||
appliesTo.splice(index + 1, 0, apply);
|
||||
appliesToContainer.insertBefore(apply.el, base.el.nextSibling);
|
||||
} else {
|
||||
appliesTo.push(apply);
|
||||
appliesToContainer.appendChild(apply.el);
|
||||
}
|
||||
appliesTo.splice(base ? appliesTo.indexOf(base) + 1 : appliesTo.length, 0, apply);
|
||||
appliesToContainer.insertBefore(apply.el, base ? base.el.nextSibling : null);
|
||||
dirty.add(apply, apply);
|
||||
if (appliesTo.length > 1 && appliesTo[0].all) {
|
||||
removeApply(appliesTo[0]);
|
||||
}
|
||||
emitSectionChange();
|
||||
return apply;
|
||||
}
|
||||
|
||||
function removeApply(apply) {
|
||||
|
@ -380,7 +364,8 @@ function createSection({
|
|||
}
|
||||
$('.add-applies-to', el).addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
insertApplyAfter({type, value: ''}, apply);
|
||||
const newApply = insertApplyAfter({type, value: ''}, apply);
|
||||
$('input', newApply.el).focus();
|
||||
});
|
||||
|
||||
return apply;
|
||||
|
|
|
@ -1,38 +1,24 @@
|
|||
/* global dirtyReporter showHelp toggleContextMenuDelete createSection
|
||||
/* global showHelp toggleContextMenuDelete createSection
|
||||
CodeMirror linter createLivePreview showCodeMirrorPopup
|
||||
sectionsToMozFormat messageBox clipString
|
||||
rerouteHotkeys $ $$ $create t FIREFOX API
|
||||
$ $$ $create t FIREFOX API
|
||||
debounce */
|
||||
/* exported createSectionsEditor */
|
||||
'use strict';
|
||||
|
||||
function createSectionsEditor({style, onTitleChanged}) {
|
||||
function createSectionsEditor(editorBase) {
|
||||
const {style, dirty} = editorBase;
|
||||
|
||||
let INC_ID = 0; // an increment id that is used by various object to track the order
|
||||
const dirty = dirtyReporter();
|
||||
|
||||
const container = $('#sections');
|
||||
const sections = [];
|
||||
|
||||
container.classList.add('section-editor');
|
||||
|
||||
const nameEl = $('#name');
|
||||
nameEl.addEventListener('input', () => {
|
||||
dirty.modify('name', style.name, nameEl.value);
|
||||
style.name = nameEl.value;
|
||||
onTitleChanged();
|
||||
});
|
||||
|
||||
const enabledEl = $('#enabled');
|
||||
enabledEl.addEventListener('change', () => {
|
||||
dirty.modify('enabled', style.enabled, enabledEl.checked);
|
||||
style.enabled = enabledEl.checked;
|
||||
updateLivePreview();
|
||||
});
|
||||
|
||||
updateHeader();
|
||||
$('#to-mozilla').addEventListener('click', showMozillaFormat);
|
||||
$('#to-mozilla-help').addEventListener('click', showToMozillaHelp);
|
||||
$('#from-mozilla').addEventListener('click', () => showMozillaFormatImport());
|
||||
$('#save-button').addEventListener('click', saveStyle);
|
||||
|
||||
document.addEventListener('wheel', scrollEntirePageOnCtrlShift, {passive: false});
|
||||
CodeMirror.defaults.extraKeys['Shift-Ctrl-Wheel'] = 'scrollWindow';
|
||||
|
@ -47,78 +33,77 @@ function createSectionsEditor({style, onTitleChanged}) {
|
|||
.forEach(e => e.addEventListener('mousedown', toggleContextMenuDelete));
|
||||
}
|
||||
|
||||
let sectionOrder = '';
|
||||
const initializing = new Promise(resolve => initSection({
|
||||
sections: style.sections.slice(),
|
||||
done:() => {
|
||||
dirty.clear();
|
||||
rerouteHotkeys(true);
|
||||
resolve();
|
||||
updateHeader();
|
||||
sections.forEach(fitToContent);
|
||||
const xo = window.IntersectionObserver && new IntersectionObserver(entries => {
|
||||
for (const {isIntersecting, target} of entries) {
|
||||
if (isIntersecting) {
|
||||
target.CodeMirror.refresh();
|
||||
xo.unobserve(target);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}, {rootMargin: '100%'});
|
||||
const refreshOnView = (cm, force) =>
|
||||
force || !xo ?
|
||||
cm.refresh() :
|
||||
xo.observe(cm.display.wrapper);
|
||||
|
||||
let sectionOrder = '';
|
||||
let headerOffset; // in compact mode the header is at the top so it reduces the available height
|
||||
const ready = initSections(style.sections, {isFirstInit: true});
|
||||
|
||||
const livePreview = createLivePreview();
|
||||
livePreview.show(Boolean(style.id));
|
||||
|
||||
return {
|
||||
ready: () => initializing,
|
||||
return Object.assign({}, editorBase, {
|
||||
ready,
|
||||
replaceStyle,
|
||||
dirty,
|
||||
getStyle: () => style,
|
||||
getEditors,
|
||||
scrollToEditor,
|
||||
getStyleId: () => style.id,
|
||||
getEditorTitle: cm => {
|
||||
const index = sections.filter(s => !s.isRemoved()).findIndex(s => s.cm === cm);
|
||||
return `${t('sectionCode')} ${index + 1}`;
|
||||
},
|
||||
save: saveStyle,
|
||||
toggleStyle,
|
||||
save,
|
||||
nextEditor,
|
||||
prevEditor,
|
||||
closestVisible,
|
||||
getSearchableInputs,
|
||||
};
|
||||
updateLivePreview,
|
||||
});
|
||||
|
||||
function fitToContent(section) {
|
||||
if (section.cm.isRefreshed) {
|
||||
const {el, cm, cm: {display: {wrapper, sizer}}} = section;
|
||||
if (cm.display.renderedView) {
|
||||
resize();
|
||||
} else {
|
||||
section.cm.on('update', resize);
|
||||
cm.on('update', resize);
|
||||
}
|
||||
|
||||
function resize() {
|
||||
let contentHeight = section.el.querySelector('.CodeMirror-sizer').offsetHeight;
|
||||
if (contentHeight < section.cm.defaultTextHeight()) {
|
||||
let contentHeight = sizer.offsetHeight;
|
||||
if (contentHeight < cm.defaultTextHeight()) {
|
||||
return;
|
||||
}
|
||||
contentHeight += 9; // border & resize grip
|
||||
section.cm.off('update', resize);
|
||||
const cmHeight = section.cm.getWrapperElement().offsetHeight;
|
||||
const maxHeight = cmHeight + window.innerHeight - section.el.offsetHeight;
|
||||
section.cm.setSize(null, Math.min(contentHeight, maxHeight));
|
||||
if (sections.every(s => s.cm.isRefreshed)) {
|
||||
fitToAvailableSpace();
|
||||
if (headerOffset == null) {
|
||||
headerOffset = el.getBoundingClientRect().top;
|
||||
}
|
||||
setTimeout(() => {
|
||||
container.classList.add('section-editor-ready');
|
||||
}, 50);
|
||||
contentHeight += 9; // border & resize grip
|
||||
cm.off('update', resize);
|
||||
const cmHeight = wrapper.offsetHeight;
|
||||
const maxHeight = (window.innerHeight - headerOffset) - (section.el.offsetHeight - cmHeight);
|
||||
cm.setSize(null, Math.min(contentHeight, maxHeight));
|
||||
}
|
||||
}
|
||||
|
||||
function fitToAvailableSpace() {
|
||||
const available =
|
||||
Math.floor(container.offsetHeight - sections.reduce((h, s) => h + s.el.offsetHeight, 0)) ||
|
||||
window.innerHeight - container.offsetHeight;
|
||||
if (available <= 0) {
|
||||
return;
|
||||
const ch = container.offsetHeight;
|
||||
let available = ch - sections[sections.length - 1].el.getBoundingClientRect().bottom + headerOffset;
|
||||
if (available <= 1) available = window.innerHeight - ch - headerOffset;
|
||||
const delta = Math.floor(available / sections.length);
|
||||
if (delta > 1) {
|
||||
sections.forEach(({cm}) => {
|
||||
cm.setSize(null, cm.display.wrapper.offsetHeight + delta);
|
||||
});
|
||||
}
|
||||
const cmHeights = sections.map(s => s.cm.getWrapperElement().offsetHeight);
|
||||
sections.forEach((section, i) => {
|
||||
section.cm.setSize(null, cmHeights[i] + Math.floor(available / sections.length));
|
||||
});
|
||||
}
|
||||
|
||||
function genId() {
|
||||
|
@ -240,14 +225,6 @@ function createSectionsEditor({style, onTitleChanged}) {
|
|||
return sections.filter(s => !s.isRemoved()).map(s => s.cm);
|
||||
}
|
||||
|
||||
function toggleStyle() {
|
||||
const newValue = !style.enabled;
|
||||
dirty.modify('enabled', style.enabled, newValue);
|
||||
style.enabled = newValue;
|
||||
enabledEl.checked = newValue;
|
||||
updateLivePreview();
|
||||
}
|
||||
|
||||
function nextEditor(cm, cycle = true) {
|
||||
if (!cycle && findLast(sections, s => !s.isRemoved()).cm === cm) {
|
||||
return;
|
||||
|
@ -367,7 +344,7 @@ function createSectionsEditor({style, onTitleChanged}) {
|
|||
if (replaceOldStyle) {
|
||||
return replaceSections(sections);
|
||||
}
|
||||
return new Promise(resolve => initSection({sections, done: resolve, focusOn: false}));
|
||||
return initSections(sections, {focusOn: false});
|
||||
})
|
||||
.then(() => {
|
||||
$('.dismiss').dispatchEvent(new Event('click'));
|
||||
|
@ -411,7 +388,7 @@ function createSectionsEditor({style, onTitleChanged}) {
|
|||
}
|
||||
|
||||
function validate() {
|
||||
if (!nameEl.reportValidity()) {
|
||||
if (!$('#name').reportValidity()) {
|
||||
messageBox.alert(t('styleMissingName'));
|
||||
return false;
|
||||
}
|
||||
|
@ -429,7 +406,7 @@ function createSectionsEditor({style, onTitleChanged}) {
|
|||
return true;
|
||||
}
|
||||
|
||||
function saveStyle() {
|
||||
function save() {
|
||||
if (!dirty.isDirty()) {
|
||||
return;
|
||||
}
|
||||
|
@ -458,10 +435,10 @@ function createSectionsEditor({style, onTitleChanged}) {
|
|||
}
|
||||
|
||||
function updateHeader() {
|
||||
nameEl.value = style.name || '';
|
||||
enabledEl.checked = style.enabled !== false;
|
||||
$('#name').value = style.customName || style.name || '';
|
||||
$('#enabled').checked = style.enabled !== false;
|
||||
$('#url').href = style.url || '';
|
||||
onTitleChanged();
|
||||
editorBase.updateName();
|
||||
}
|
||||
|
||||
function updateLivePreview() {
|
||||
|
@ -472,36 +449,35 @@ function createSectionsEditor({style, onTitleChanged}) {
|
|||
livePreview.update(getModel());
|
||||
}
|
||||
|
||||
function initSection({
|
||||
sections: originalSections,
|
||||
total = originalSections.length,
|
||||
function initSections(originalSections, {
|
||||
focusOn = 0,
|
||||
done
|
||||
}) {
|
||||
container.classList.add('hidden');
|
||||
chunk();
|
||||
|
||||
function chunk() {
|
||||
if (!originalSections.length) {
|
||||
setGlobalProgress();
|
||||
if (focusOn !== false) {
|
||||
setTimeout(() => sections[focusOn].cm.focus());
|
||||
}
|
||||
container.classList.remove('hidden');
|
||||
for (const section of sections) {
|
||||
section.cm.refreshOnView();
|
||||
}
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
return;
|
||||
}
|
||||
isFirstInit,
|
||||
} = {}) {
|
||||
let done;
|
||||
const total = originalSections.length;
|
||||
originalSections = originalSections.slice();
|
||||
return new Promise(resolve => {
|
||||
done = resolve;
|
||||
chunk(true);
|
||||
});
|
||||
function chunk(forceRefresh) {
|
||||
const t0 = performance.now();
|
||||
while (originalSections.length && performance.now() - t0 < 100) {
|
||||
insertSectionAfter(originalSections.shift());
|
||||
insertSectionAfter(originalSections.shift(), undefined, forceRefresh);
|
||||
if (isFirstInit) dirty.clear();
|
||||
if (focusOn !== false && sections[focusOn]) {
|
||||
sections[focusOn].cm.focus();
|
||||
focusOn = false;
|
||||
}
|
||||
}
|
||||
setGlobalProgress(total - originalSections.length, total);
|
||||
setTimeout(chunk);
|
||||
if (!originalSections.length) {
|
||||
setGlobalProgress();
|
||||
fitToAvailableSpace();
|
||||
done();
|
||||
} else {
|
||||
setTimeout(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -540,7 +516,7 @@ function createSectionsEditor({style, onTitleChanged}) {
|
|||
updateLivePreview();
|
||||
}
|
||||
|
||||
function insertSectionAfter(init, base) {
|
||||
function insertSectionAfter(init, base, forceRefresh) {
|
||||
if (!init) {
|
||||
init = {code: '', urlPrefixes: ['http://example.com']};
|
||||
}
|
||||
|
@ -557,15 +533,18 @@ function createSectionsEditor({style, onTitleChanged}) {
|
|||
prevEditor,
|
||||
nextEditor
|
||||
});
|
||||
if (base) {
|
||||
const index = sections.indexOf(base);
|
||||
sections.splice(index + 1, 0, section);
|
||||
container.insertBefore(section.el, base.el.nextSibling);
|
||||
} else {
|
||||
sections.push(section);
|
||||
container.appendChild(section.el);
|
||||
const {cm} = section;
|
||||
sections.splice(base ? sections.indexOf(base) + 1 : sections.length, 0, section);
|
||||
container.insertBefore(section.el, base ? base.el.nextSibling : null);
|
||||
refreshOnView(cm, forceRefresh);
|
||||
if (!base || init.code) {
|
||||
// Fit a) during startup or b) when the clone button is clicked on a section with some code
|
||||
fitToContent(section);
|
||||
}
|
||||
if (base) {
|
||||
cm.focus();
|
||||
setTimeout(scrollToEditor, 0, cm);
|
||||
}
|
||||
section.render();
|
||||
updateSectionOrder();
|
||||
section.onChange(updateLivePreview);
|
||||
updateLivePreview();
|
||||
|
@ -593,16 +572,17 @@ function createSectionsEditor({style, onTitleChanged}) {
|
|||
updateSectionOrder();
|
||||
}
|
||||
|
||||
function replaceSections(originalSections) {
|
||||
function replaceSections(...args) {
|
||||
for (const section of sections) {
|
||||
section.remove(true);
|
||||
}
|
||||
sections.length = 0;
|
||||
container.textContent = '';
|
||||
return new Promise(resolve => initSection({sections: originalSections, done: resolve}));
|
||||
return initSections(...args);
|
||||
}
|
||||
|
||||
function replaceStyle(newStyle, codeIsUpdated) {
|
||||
dirty.clear('name');
|
||||
// FIXME: avoid recreating all editors?
|
||||
reinit().then(() => {
|
||||
Object.assign(style, newStyle);
|
||||
|
@ -619,7 +599,7 @@ function createSectionsEditor({style, onTitleChanged}) {
|
|||
|
||||
function reinit() {
|
||||
if (codeIsUpdated !== false) {
|
||||
return replaceSections(newStyle.sections.slice());
|
||||
return replaceSections(newStyle.sections, {isFirstInit: true});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global dirtyReporter
|
||||
/* global
|
||||
createAppliesToLineWidget messageBox
|
||||
sectionsToMozFormat
|
||||
createMetaCompiler linter createLivePreview cmFactory $ $create API prefs t
|
||||
|
@ -6,17 +6,16 @@
|
|||
/* exported createSourceEditor */
|
||||
'use strict';
|
||||
|
||||
function createSourceEditor({style, onTitleChanged}) {
|
||||
$('#name').disabled = true;
|
||||
$('#save-button').disabled = true;
|
||||
function createSourceEditor(editorBase) {
|
||||
const {style, dirty} = editorBase;
|
||||
|
||||
let placeholderName = '';
|
||||
|
||||
$('#mozilla-format-container').remove();
|
||||
$('#save-button').onclick = save;
|
||||
$('#header').addEventListener('wheel', headerOnScroll);
|
||||
$('#sections').textContent = '';
|
||||
$('#sections').appendChild($create('.single-editor'));
|
||||
|
||||
const dirty = dirtyReporter();
|
||||
|
||||
// normalize style
|
||||
if (!style.id) setupNewStyle(style);
|
||||
|
||||
|
@ -28,13 +27,6 @@ function createSourceEditor({style, onTitleChanged}) {
|
|||
const livePreview = createLivePreview(preprocess);
|
||||
livePreview.show(Boolean(style.id));
|
||||
|
||||
$('#enabled').onchange = function () {
|
||||
const value = this.checked;
|
||||
dirty.modify('enabled', style.enabled, value);
|
||||
style.enabled = value;
|
||||
updateLivePreview();
|
||||
};
|
||||
|
||||
cm.on('changes', () => {
|
||||
dirty.modify('sourceGeneration', savedGeneration, cm.changeGeneration());
|
||||
updateLivePreview();
|
||||
|
@ -46,14 +38,14 @@ function createSourceEditor({style, onTitleChanged}) {
|
|||
metaCompiler.onUpdated(meta => {
|
||||
style.usercssData = meta;
|
||||
style.name = meta.name;
|
||||
style.url = meta.homepageURL;
|
||||
style.url = meta.homepageURL || style.installationUrl;
|
||||
updateMeta();
|
||||
});
|
||||
|
||||
linter.enableForEditor(cm);
|
||||
|
||||
updateMeta().then(() => {
|
||||
|
||||
linter.enableForEditor(cm);
|
||||
|
||||
let prevMode = NaN;
|
||||
cm.on('optionChange', (cm, option) => {
|
||||
if (option !== 'mode') return;
|
||||
|
@ -122,7 +114,7 @@ function createSourceEditor({style, onTitleChanged}) {
|
|||
return name;
|
||||
}
|
||||
|
||||
function setupNewStyle(style) {
|
||||
async function setupNewStyle(style) {
|
||||
style.sections[0].code = ' '.repeat(prefs.get('editor.tabSize')) +
|
||||
`/* ${t('usercssReplaceTemplateSectionBody')} */`;
|
||||
let section = sectionsToMozFormat(style);
|
||||
|
@ -143,33 +135,35 @@ function createSourceEditor({style, onTitleChanged}) {
|
|||
dirty.clear('sourceGeneration');
|
||||
style.sourceCode = '';
|
||||
|
||||
chromeSync.getLZValue('usercssTemplate').then(code => {
|
||||
const name = style.name || t('usercssReplaceTemplateName');
|
||||
const date = new Date().toLocaleString();
|
||||
code = code || DEFAULT_CODE;
|
||||
code = code.replace(/@name(\s*)(?=[\r\n])/, (str, space) =>
|
||||
`${str}${space ? '' : ' '}${name} - ${date}`);
|
||||
// strip the last dummy section if any, add an empty line followed by the section
|
||||
style.sourceCode = code.replace(/\s*@-moz-document[^{]*\{[^}]*\}\s*$|\s+$/g, '') + '\n\n' + section;
|
||||
cm.startOperation();
|
||||
cm.setValue(style.sourceCode);
|
||||
cm.clearHistory();
|
||||
cm.markClean();
|
||||
cm.endOperation();
|
||||
dirty.clear('sourceGeneration');
|
||||
savedGeneration = cm.changeGeneration();
|
||||
});
|
||||
placeholderName = `${style.name || t('usercssReplaceTemplateName')} - ${new Date().toLocaleString()}`;
|
||||
let code = await chromeSync.getLZValue('usercssTemplate');
|
||||
code = code || DEFAULT_CODE;
|
||||
code = code.replace(/@name(\s*)(?=[\r\n])/, (str, space) =>
|
||||
`${str}${space ? '' : ' '}${placeholderName}`);
|
||||
// strip the last dummy section if any, add an empty line followed by the section
|
||||
style.sourceCode = code.replace(/\s*@-moz-document[^{]*{[^}]*}\s*$|\s+$/g, '') + '\n\n' + section;
|
||||
cm.startOperation();
|
||||
cm.setValue(style.sourceCode);
|
||||
cm.clearHistory();
|
||||
cm.markClean();
|
||||
cm.endOperation();
|
||||
dirty.clear('sourceGeneration');
|
||||
savedGeneration = cm.changeGeneration();
|
||||
}
|
||||
|
||||
function updateMeta() {
|
||||
$('#name').value = style.name;
|
||||
const name = style.customName || style.name;
|
||||
if (name !== placeholderName) {
|
||||
$('#name').value = name;
|
||||
}
|
||||
$('#enabled').checked = style.enabled;
|
||||
$('#url').href = style.url;
|
||||
onTitleChanged();
|
||||
editorBase.updateName();
|
||||
return cm.setPreprocessor((style.usercssData || {}).preprocessor);
|
||||
}
|
||||
|
||||
function replaceStyle(newStyle, codeIsUpdated) {
|
||||
dirty.clear('name');
|
||||
const sameCode = newStyle.sourceCode === cm.getValue();
|
||||
if (sameCode) {
|
||||
savedGeneration = cm.changeGeneration();
|
||||
|
@ -210,14 +204,6 @@ function createSourceEditor({style, onTitleChanged}) {
|
|||
}
|
||||
}
|
||||
|
||||
function toggleStyle() {
|
||||
const value = !style.enabled;
|
||||
dirty.modify('enabled', style.enabled, value);
|
||||
style.enabled = value;
|
||||
updateMeta();
|
||||
$('#enabled').dispatchEvent(new Event('change', {bubbles: true}));
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (!dirty.isDirty()) return;
|
||||
const code = cm.getValue();
|
||||
|
@ -226,6 +212,7 @@ function createSourceEditor({style, onTitleChanged}) {
|
|||
id: style.id,
|
||||
enabled: style.enabled,
|
||||
sourceCode: code,
|
||||
customName: style.customName,
|
||||
}))
|
||||
.then(replaceStyle)
|
||||
.catch(err => {
|
||||
|
@ -372,19 +359,17 @@ function createSourceEditor({style, onTitleChanged}) {
|
|||
(mode.helperType || '');
|
||||
}
|
||||
|
||||
return {
|
||||
return Object.assign({}, editorBase, {
|
||||
ready: Promise.resolve(),
|
||||
replaceStyle,
|
||||
dirty,
|
||||
getStyle: () => style,
|
||||
getEditors: () => [cm],
|
||||
scrollToEditor: () => {},
|
||||
getStyleId: () => style.id,
|
||||
getEditorTitle: () => '',
|
||||
save,
|
||||
toggleStyle,
|
||||
prevEditor: cm => nextPrevMozDocument(cm, -1),
|
||||
nextEditor: cm => nextPrevMozDocument(cm, 1),
|
||||
closestVisible: () => cm,
|
||||
getSearchableInputs: () => []
|
||||
};
|
||||
getSearchableInputs: () => [],
|
||||
updateLivePreview,
|
||||
});
|
||||
}
|
||||
|
|
18
global.css
18
global.css
|
@ -54,18 +54,20 @@ button:active {
|
|||
border-color: hsl(0, 0%, 50%);
|
||||
}
|
||||
|
||||
input {
|
||||
input {
|
||||
font: inherit;
|
||||
border: 1px solid hsl(0, 0%, 66%);
|
||||
transition: border-color .1s, box-shadow .1s;
|
||||
}
|
||||
|
||||
input:not([type]) {
|
||||
input:not([type]),
|
||||
input[type=search] {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
height: 22px;
|
||||
min-height: 22px!important;
|
||||
line-height: 22px;
|
||||
padding: 0 3px;
|
||||
font: inherit;
|
||||
border: 1px solid hsl(0, 0%, 66%);
|
||||
}
|
||||
|
||||
|
@ -208,9 +210,19 @@ select[disabled] + .select-arrow {
|
|||
display: none !important;
|
||||
}
|
||||
|
||||
:focus,
|
||||
.CodeMirror-focused,
|
||||
[data-focused-via-click] input[type="text"]:focus,
|
||||
[data-focused-via-click] input[type="number"]:focus {
|
||||
/* Using box-shadow instead of the ugly outline in new Chrome */
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 1px hsl(180, 100%, 38%), 0 0 3px hsla(180, 100%, 60%, .5);
|
||||
}
|
||||
|
||||
[data-focused-via-click] :focus,
|
||||
[data-focused-via-click]:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@supports (-moz-appearance: none) {
|
||||
|
|
|
@ -291,9 +291,7 @@ li {
|
|||
|
||||
#header:not(.meta-init) > *:not(.lds-spinner),
|
||||
#header.meta-init > .lds-spinner {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
|
@ -302,9 +300,7 @@ li {
|
|||
#header.meta-init > * {
|
||||
opacity: 1;
|
||||
transition: opacity .5s;
|
||||
-webkit-user-select: auto;
|
||||
-moz-user-select: auto;
|
||||
-ms-user-select: auto;
|
||||
user-select: auto;
|
||||
}
|
||||
|
||||
|
@ -323,14 +319,6 @@ label {
|
|||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes lds-spinner {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.lds-spinner {
|
||||
position: absolute;
|
||||
width: 200px;
|
||||
|
@ -346,104 +334,74 @@ label {
|
|||
left: 94px;
|
||||
top: 23px;
|
||||
position: absolute;
|
||||
-webkit-animation: lds-spinner linear 1s infinite;
|
||||
animation: lds-spinner linear 1s infinite;
|
||||
background: currentColor;
|
||||
width: 12px;
|
||||
height: 34px;
|
||||
border-radius: 20%;
|
||||
-webkit-transform-origin: 6px 77px;
|
||||
transform-origin: 6px 77px;
|
||||
}
|
||||
.lds-spinner div:nth-child(1) {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
-webkit-animation-delay: -0.916666666666667s;
|
||||
animation-delay: -0.916666666666667s;
|
||||
}
|
||||
.lds-spinner div:nth-child(2) {
|
||||
-webkit-transform: rotate(30deg);
|
||||
transform: rotate(30deg);
|
||||
-webkit-animation-delay: -0.833333333333333s;
|
||||
animation-delay: -0.833333333333333s;
|
||||
}
|
||||
.lds-spinner div:nth-child(3) {
|
||||
-webkit-transform: rotate(60deg);
|
||||
transform: rotate(60deg);
|
||||
-webkit-animation-delay: -0.75s;
|
||||
animation-delay: -0.75s;
|
||||
}
|
||||
.lds-spinner div:nth-child(4) {
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
-webkit-animation-delay: -0.666666666666667s;
|
||||
animation-delay: -0.666666666666667s;
|
||||
}
|
||||
.lds-spinner div:nth-child(5) {
|
||||
-webkit-transform: rotate(120deg);
|
||||
transform: rotate(120deg);
|
||||
-webkit-animation-delay: -0.583333333333333s;
|
||||
animation-delay: -0.583333333333333s;
|
||||
}
|
||||
.lds-spinner div:nth-child(6) {
|
||||
-webkit-transform: rotate(150deg);
|
||||
transform: rotate(150deg);
|
||||
-webkit-animation-delay: -0.5s;
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
.lds-spinner div:nth-child(7) {
|
||||
-webkit-transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
-webkit-animation-delay: -0.416666666666667s;
|
||||
animation-delay: -0.416666666666667s;
|
||||
}
|
||||
.lds-spinner div:nth-child(8) {
|
||||
-webkit-transform: rotate(210deg);
|
||||
transform: rotate(210deg);
|
||||
-webkit-animation-delay: -0.333333333333333s;
|
||||
animation-delay: -0.333333333333333s;
|
||||
}
|
||||
.lds-spinner div:nth-child(9) {
|
||||
-webkit-transform: rotate(240deg);
|
||||
transform: rotate(240deg);
|
||||
-webkit-animation-delay: -0.25s;
|
||||
animation-delay: -0.25s;
|
||||
}
|
||||
.lds-spinner div:nth-child(10) {
|
||||
-webkit-transform: rotate(270deg);
|
||||
transform: rotate(270deg);
|
||||
-webkit-animation-delay: -0.166666666666667s;
|
||||
animation-delay: -0.166666666666667s;
|
||||
}
|
||||
.lds-spinner div:nth-child(11) {
|
||||
-webkit-transform: rotate(300deg);
|
||||
transform: rotate(300deg);
|
||||
-webkit-animation-delay: -0.083333333333333s;
|
||||
animation-delay: -0.083333333333333s;
|
||||
}
|
||||
.lds-spinner div:nth-child(12) {
|
||||
-webkit-transform: rotate(330deg);
|
||||
transform: rotate(330deg);
|
||||
-webkit-animation-delay: 0s;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
@-webkit-keyframes load3 {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes load3 {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
'use strict';
|
||||
|
||||
(() => {
|
||||
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425)
|
||||
const params = new URLSearchParams(location.search.replace(/^\?/, ''));
|
||||
const params = new URLSearchParams(location.search);
|
||||
const tabId = params.has('tabId') ? Number(params.get('tabId')) : -1;
|
||||
const initialUrl = params.get('updateUrl');
|
||||
|
||||
|
@ -249,7 +248,7 @@
|
|||
(!dup ?
|
||||
Promise.resolve(true) :
|
||||
messageBox.confirm(t('styleInstallOverwrite', [
|
||||
data.name,
|
||||
data.name + (dup.customName ? ` (${dup.customName})` : ''),
|
||||
dupData.version,
|
||||
data.version,
|
||||
]))
|
||||
|
@ -329,7 +328,9 @@
|
|||
let sequence = null;
|
||||
if (tabId < 0) {
|
||||
getData = DirectDownloader();
|
||||
sequence = API.getUsercssInstallCode(initialUrl).catch(getData);
|
||||
sequence = API.getUsercssInstallCode(initialUrl)
|
||||
.then(code => code || getData())
|
||||
.catch(getData);
|
||||
} else {
|
||||
getData = PortDownloader();
|
||||
sequence = getData({timer: false});
|
||||
|
@ -342,7 +343,11 @@
|
|||
onToggled(e) {
|
||||
if (e) isEnabled = e.target.checked;
|
||||
if (installed || installedDup) {
|
||||
(isEnabled ? start : stop)();
|
||||
if (isEnabled) {
|
||||
check({force: true});
|
||||
} else {
|
||||
stop();
|
||||
}
|
||||
$('.install').disabled = isEnabled;
|
||||
Object.assign($('#live-reload-install-hint'), {
|
||||
hidden: !isEnabled,
|
||||
|
@ -351,8 +356,8 @@
|
|||
}
|
||||
},
|
||||
};
|
||||
function check() {
|
||||
getData()
|
||||
function check(opts) {
|
||||
getData(opts)
|
||||
.then(update, logError)
|
||||
.then(() => {
|
||||
timer = 0;
|
||||
|
@ -405,14 +410,16 @@
|
|||
}
|
||||
});
|
||||
port.onDisconnect.addListener(() => {
|
||||
browser.tabs.get(tabId)
|
||||
.then(tab => tab.url === initialUrl && location.reload())
|
||||
.catch(closeCurrentTab);
|
||||
chrome.tabs.get(tabId, tab =>
|
||||
!chrome.runtime.lastError && tab.url === initialUrl
|
||||
? location.reload()
|
||||
: closeCurrentTab());
|
||||
});
|
||||
return ({timer = true} = {}) => new Promise((resolve, reject) => {
|
||||
return (opts = {}) => new Promise((resolve, reject) => {
|
||||
const id = performance.now();
|
||||
resolvers.set(id, {resolve, reject});
|
||||
port.postMessage({id, timer});
|
||||
opts.id = id;
|
||||
port.postMessage(opts);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
37
js/dom.js
37
js/dom.js
|
@ -20,6 +20,9 @@ for (const type of [NodeList, NamedNodeMap, HTMLCollection, HTMLAllCollection])
|
|||
}
|
||||
}
|
||||
|
||||
$.isTextLikeInput = el =>
|
||||
el.localName === 'input' && /^(text|search|number)$/.test(el.type);
|
||||
|
||||
$.remove = (selector, base = document) => {
|
||||
const el = selector && typeof selector === 'string' ? $(selector, base) : selector;
|
||||
if (el) {
|
||||
|
@ -98,7 +101,11 @@ document.addEventListener('wheel', event => {
|
|||
return;
|
||||
}
|
||||
if (el.tagName === 'SELECT') {
|
||||
el.selectedIndex = Math.max(0, Math.min(el.length - 1, el.selectedIndex + Math.sign(event.deltaY)));
|
||||
const old = el.selectedIndex;
|
||||
el.selectedIndex = Math.max(0, Math.min(el.length - 1, old + Math.sign(event.deltaY)));
|
||||
if (el.selectedIndex !== old) {
|
||||
el.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
event.stopImmediatePropagation();
|
||||
|
@ -108,15 +115,9 @@ document.addEventListener('wheel', event => {
|
|||
});
|
||||
|
||||
function onDOMready() {
|
||||
if (document.readyState !== 'loading') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
document.addEventListener('DOMContentLoaded', function _() {
|
||||
document.removeEventListener('DOMContentLoaded', _);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return document.readyState !== 'loading'
|
||||
? Promise.resolve()
|
||||
: new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve, {once: true}));
|
||||
}
|
||||
|
||||
|
||||
|
@ -140,8 +141,7 @@ function animateElement(
|
|||
onComplete,
|
||||
} = {}) {
|
||||
return element && new Promise(resolve => {
|
||||
element.addEventListener('animationend', function _() {
|
||||
element.removeEventListener('animationend', _);
|
||||
element.addEventListener('animationend', () => {
|
||||
element.classList.remove(
|
||||
className,
|
||||
// In Firefox, `resolve()` might be called one frame later.
|
||||
|
@ -153,7 +153,7 @@ function animateElement(
|
|||
onComplete.call(element);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
}, {once: true});
|
||||
element.classList.add(className);
|
||||
});
|
||||
}
|
||||
|
@ -351,20 +351,23 @@ function focusAccessibility() {
|
|||
'a',
|
||||
'button',
|
||||
'input',
|
||||
'textarea',
|
||||
'label',
|
||||
'select',
|
||||
'summary',
|
||||
];
|
||||
// try to find a focusable parent for this many parentElement jumps:
|
||||
const GIVE_UP_DEPTH = 4;
|
||||
// allow outline on text/search inputs in addition to textareas
|
||||
const isOutlineAllowed = el =>
|
||||
!focusAccessibility.ELEMENTS.includes(el.localName) ||
|
||||
$.isTextLikeInput(el);
|
||||
|
||||
addEventListener('mousedown', suppressOutlineOnClick, {passive: true});
|
||||
addEventListener('keydown', keepOutlineOnTab, {passive: true});
|
||||
|
||||
function suppressOutlineOnClick({target}) {
|
||||
for (let el = target, i = 0; el && i++ < GIVE_UP_DEPTH; el = el.parentElement) {
|
||||
if (focusAccessibility.ELEMENTS.includes(el.localName)) {
|
||||
if (!isOutlineAllowed(el)) {
|
||||
focusAccessibility.lastFocusedViaClick = true;
|
||||
if (el.dataset.focusedViaClick === undefined) {
|
||||
el.dataset.focusedViaClick = '';
|
||||
|
@ -375,7 +378,7 @@ function focusAccessibility() {
|
|||
}
|
||||
|
||||
function keepOutlineOnTab(event) {
|
||||
if (event.which === 9) {
|
||||
if (event.key === 'Tab') {
|
||||
focusAccessibility.lastFocusedViaClick = false;
|
||||
setTimeout(keepOutlineOnTab, 0, true);
|
||||
return;
|
||||
|
@ -383,7 +386,7 @@ function focusAccessibility() {
|
|||
return;
|
||||
}
|
||||
let el = document.activeElement;
|
||||
if (!el || !focusAccessibility.ELEMENTS.includes(el.localName)) {
|
||||
if (!el || isOutlineAllowed(el)) {
|
||||
return;
|
||||
}
|
||||
if (el.dataset.focusedViaClick !== undefined) {
|
||||
|
|
|
@ -62,7 +62,19 @@ const URLS = {
|
|||
// TODO: remove when "minimum_chrome_version": "61" or higher
|
||||
chromeProtectsNTP: CHROME >= 61,
|
||||
|
||||
userstylesOrgJson: 'https://userstyles.org/styles/chrome/',
|
||||
uso: 'https://userstyles.org/',
|
||||
usoJson: 'https://userstyles.org/styles/chrome/',
|
||||
|
||||
usoArchive: 'https://33kk.github.io/uso-archive/',
|
||||
usoArchiveRaw: 'https://raw.githubusercontent.com/33kk/uso-archive/flomaster/data/',
|
||||
extractUsoArchiveId: url =>
|
||||
url &&
|
||||
url.startsWith(URLS.usoArchiveRaw) &&
|
||||
parseInt(url.match(/\/(\d+)\.user\.css|$/)[1]),
|
||||
|
||||
extractGreasyForkId: url =>
|
||||
/^https:\/\/(?:greasy|sleazy)fork\.org\/scripts\/(\d+)[^/]*\/code\/[^/]*\.user\.css$/.test(url) &&
|
||||
RegExp.$1,
|
||||
|
||||
supported: url => (
|
||||
url.startsWith('http') && (FIREFOX || !url.startsWith(URLS.browserWebStore)) ||
|
||||
|
@ -132,7 +144,7 @@ function findExistingTab({url, currentWindow, ignoreHash = true, ignoreSearch =
|
|||
.then(tabs => tabs.find(matchTab));
|
||||
|
||||
function matchTab(tab) {
|
||||
const tabUrl = new URL(tab.url);
|
||||
const tabUrl = new URL(tab.pendingUrl || tab.url);
|
||||
return tabUrl.protocol === url.protocol &&
|
||||
tabUrl.username === url.username &&
|
||||
tabUrl.password === url.password &&
|
||||
|
@ -153,57 +165,48 @@ function findExistingTab({url, currentWindow, ignoreHash = true, ignoreSearch =
|
|||
* @param {number} [_.openerTabId] defaults to the active tab
|
||||
* @param {Boolean} [_.active=true] `true` to activate the tab
|
||||
* @param {Boolean|null} [_.currentWindow=true] `null` to check all windows
|
||||
* @param {Boolean} [_.newWindow=false] `true` to open a new window
|
||||
* @param {chrome.windows.CreateData} [_.windowPosition] options for chrome.windows.create
|
||||
* @param {chrome.windows.CreateData} [_.newWindow] creates a new window with these params if specified
|
||||
* @returns {Promise<chrome.tabs.Tab>} Promise -> opened/activated tab
|
||||
*/
|
||||
function openURL({
|
||||
async function openURL({
|
||||
url,
|
||||
index,
|
||||
openerTabId,
|
||||
active = true,
|
||||
currentWindow = true,
|
||||
newWindow = false,
|
||||
windowPosition,
|
||||
newWindow,
|
||||
}) {
|
||||
if (!url.includes('://')) {
|
||||
url = chrome.runtime.getURL(url);
|
||||
}
|
||||
return findExistingTab({url, currentWindow}).then(tab => {
|
||||
if (tab) {
|
||||
return activateTab(tab, {
|
||||
index,
|
||||
openerTabId,
|
||||
// when hash is different we can only set `url` if it has # otherwise the tab would reload
|
||||
url: url !== tab.url && url.includes('#') ? url : undefined,
|
||||
});
|
||||
}
|
||||
if (newWindow && browser.windows) {
|
||||
return browser.windows.create(Object.assign({url}, windowPosition))
|
||||
.then(wnd => wnd.tabs[0]);
|
||||
}
|
||||
return getActiveTab().then((activeTab = {url: ''}) =>
|
||||
isTabReplaceable(activeTab, url) ?
|
||||
activateTab(activeTab, {url, openerTabId}) : // not moving the tab
|
||||
createTabWithOpener(activeTab, {url, index, active}));
|
||||
});
|
||||
function createTabWithOpener(openerTab, options) {
|
||||
const id = openerTabId == null ? openerTab.id : openerTabId;
|
||||
if (id != null && !openerTab.incognito && openerTabIdSupported) {
|
||||
options.openerTabId = id;
|
||||
}
|
||||
return browser.tabs.create(options);
|
||||
let tab = await findExistingTab({url, currentWindow});
|
||||
if (tab) {
|
||||
return activateTab(tab, {
|
||||
index,
|
||||
openerTabId,
|
||||
// when hash is different we can only set `url` if it has # otherwise the tab would reload
|
||||
url: url !== (tab.pendingUrl || tab.url) && url.includes('#') ? url : undefined,
|
||||
});
|
||||
}
|
||||
if (newWindow && browser.windows) {
|
||||
return (await browser.windows.create(Object.assign({url}, newWindow)).tabs)[0];
|
||||
}
|
||||
tab = await getActiveTab() || {url: ''};
|
||||
if (isTabReplaceable(tab, url)) {
|
||||
return activateTab(tab, {url, openerTabId});
|
||||
}
|
||||
const id = openerTabId == null ? tab.id : openerTabId;
|
||||
const opener = id != null && !tab.incognito && openerTabIdSupported && {openerTabId: id};
|
||||
return browser.tabs.create(Object.assign({url, index, active}, opener));
|
||||
}
|
||||
|
||||
// replace empty tab (NTP or about:blank)
|
||||
// except when new URL is chrome:// or chrome-extension:// and the empty tab is
|
||||
// in incognito
|
||||
function isTabReplaceable(tab, newUrl) {
|
||||
if (!tab || !URLS.emptyTab.includes(tab.url)) {
|
||||
if (!tab || !URLS.emptyTab.includes(tab.pendingUrl || tab.url)) {
|
||||
return false;
|
||||
}
|
||||
// FIXME: but why?
|
||||
if (tab.incognito && newUrl.startsWith('chrome')) {
|
||||
return false;
|
||||
}
|
||||
|
@ -439,7 +442,7 @@ function download(url, {
|
|||
function collapseUsoVars(url) {
|
||||
if (queryPos < 0 ||
|
||||
url.length < 2000 ||
|
||||
!url.startsWith(URLS.userstylesOrgJson) ||
|
||||
!url.startsWith(URLS.usoJson) ||
|
||||
!/^get$/i.test(method)) {
|
||||
return url;
|
||||
}
|
||||
|
|
|
@ -7,10 +7,12 @@
|
|||
* Puts the global comments into the following section to minimize the amount of global sections.
|
||||
* Doesn't move the comment with ==UserStyle== inside.
|
||||
* @param {string} code
|
||||
* @param {boolean} emptyDocument - https://github.com/stylus/stylus/issues/2415,
|
||||
* TODO: update stylus-lang and remove emptyDocument everywhere
|
||||
* @param {number} styleId - used to preserve parserCache on subsequent runs over the same style
|
||||
* @returns {{sections: Array, errors: Array}}
|
||||
*/
|
||||
function parseMozFormat({code, styleId}) {
|
||||
function parseMozFormat({code, emptyDocument, styleId}) {
|
||||
const CssToProperty = {
|
||||
'url': 'urls',
|
||||
'url-prefix': 'urlPrefixes',
|
||||
|
@ -18,7 +20,7 @@ function parseMozFormat({code, styleId}) {
|
|||
'regexp': 'regexps',
|
||||
};
|
||||
const hasSingleEscapes = /([^\\]|^)\\([^\\]|$)/;
|
||||
const parser = new parserlib.css.Parser();
|
||||
const parser = new parserlib.css.Parser({starHack: true, emptyDocument});
|
||||
const sectionStack = [{code: '', start: 0}];
|
||||
const errors = [];
|
||||
const sections = [];
|
||||
|
@ -70,6 +72,13 @@ function parseMozFormat({code, styleId}) {
|
|||
doAddSection(section);
|
||||
});
|
||||
|
||||
parser.addListener('emptydocument', e => {
|
||||
const token = parser._tokenStream._token;
|
||||
const section = sectionStack[sectionStack.length - 1];
|
||||
section.code += mozStyle.slice(section.start, e.offset);
|
||||
section.start = token.offset + token.value.length;
|
||||
});
|
||||
|
||||
parser.addListener('endstylesheet', () => {
|
||||
// add nonclosed outer sections (either broken or the last global one)
|
||||
const lastSection = sectionStack[sectionStack.length - 1];
|
||||
|
|
10
js/msg.js
10
js/msg.js
|
@ -33,7 +33,8 @@ self.msg = self.INJECTED === 1 ? self.msg : (() => {
|
|||
onExtension,
|
||||
off,
|
||||
RX_NO_RECEIVER,
|
||||
RX_PORT_CLOSED
|
||||
RX_PORT_CLOSED,
|
||||
isBg,
|
||||
};
|
||||
|
||||
function getBg() {
|
||||
|
@ -106,12 +107,13 @@ self.msg = self.INJECTED === 1 ? self.msg : (() => {
|
|||
.then(tabs => {
|
||||
const requests = [];
|
||||
for (const tab of tabs) {
|
||||
const isExtension = tab.url.startsWith(EXTENSION_URL);
|
||||
const tabUrl = tab.pendingUrl || tab.url;
|
||||
const isExtension = tabUrl.startsWith(EXTENSION_URL);
|
||||
if (
|
||||
tab.discarded ||
|
||||
// FIXME: use `URLS.supported`?
|
||||
!/^(http|ftp|file)/.test(tab.url) &&
|
||||
!tab.url.startsWith('chrome://newtab/') &&
|
||||
!/^(http|ftp|file)/.test(tabUrl) &&
|
||||
!tabUrl.startsWith('chrome://newtab/') &&
|
||||
!isExtension ||
|
||||
isExtension && ignoreExtension ||
|
||||
filter && !filter(tab)
|
||||
|
|
137
js/polyfill.js
137
js/polyfill.js
|
@ -3,27 +3,33 @@
|
|||
// eslint-disable-next-line no-unused-expressions
|
||||
self.INJECTED !== 1 && (() => {
|
||||
|
||||
// this part runs in workers, content scripts, our extension pages
|
||||
//#region for content scripts and our extension pages
|
||||
|
||||
if (!Object.entries) {
|
||||
Object.entries = obj => Object.keys(obj).map(k => [k, obj[k]]);
|
||||
if (!window.browser || !browser.runtime) {
|
||||
const createTrap = (base, parent) => {
|
||||
const target = typeof base === 'function' ? () => {} : {};
|
||||
target.isTrap = true;
|
||||
return new Proxy(target, {
|
||||
get: (target, prop) => {
|
||||
if (target[prop]) return target[prop];
|
||||
if (base[prop] && (typeof base[prop] === 'object' || typeof base[prop] === 'function')) {
|
||||
target[prop] = createTrap(base[prop], base);
|
||||
return target[prop];
|
||||
}
|
||||
return base[prop];
|
||||
},
|
||||
apply: (target, thisArg, args) => base.apply(parent, args)
|
||||
});
|
||||
};
|
||||
window.browser = createTrap(chrome, null);
|
||||
}
|
||||
if (!Object.values) {
|
||||
Object.values = obj => Object.keys(obj).map(k => obj[k]);
|
||||
}
|
||||
|
||||
// don't use self.chrome. It is undefined in Firefox
|
||||
if (typeof chrome !== 'object') return;
|
||||
// the rest is for content scripts and our extension pages
|
||||
|
||||
self.browser = polyfillBrowser();
|
||||
|
||||
/* Promisifies the specified `chrome` methods into `browser`.
|
||||
The definitions is an object like this: {
|
||||
'storage.sync': ['get', 'set'], // if deeper than one level, combine the path via `.`
|
||||
windows: ['create', 'update'], // items and sub-objects will only be created if present in `chrome`
|
||||
} */
|
||||
self.promisifyChrome = definitions => {
|
||||
window.promisifyChrome = definitions => {
|
||||
for (const [scopeName, methods] of Object.entries(definitions)) {
|
||||
const path = scopeName.split('.');
|
||||
const src = path.reduce((obj, p) => obj && obj[p], chrome);
|
||||
|
@ -43,90 +49,33 @@ self.INJECTED !== 1 && (() => {
|
|||
};
|
||||
|
||||
if (!chrome.tabs) return;
|
||||
// the rest is for our extension pages
|
||||
|
||||
if (typeof document === 'object') {
|
||||
const ELEMENT_METH = {
|
||||
append: {
|
||||
base: [Element, Document, DocumentFragment],
|
||||
fn: (node, frag) => {
|
||||
node.appendChild(frag);
|
||||
}
|
||||
},
|
||||
prepend: {
|
||||
base: [Element, Document, DocumentFragment],
|
||||
fn: (node, frag) => {
|
||||
node.insertBefore(frag, node.firstChild);
|
||||
}
|
||||
},
|
||||
before: {
|
||||
base: [Element, CharacterData, DocumentType],
|
||||
fn: (node, frag) => {
|
||||
node.parentNode.insertBefore(frag, node);
|
||||
}
|
||||
},
|
||||
after: {
|
||||
base: [Element, CharacterData, DocumentType],
|
||||
fn: (node, frag) => {
|
||||
node.parentNode.insertBefore(frag, node.nextSibling);
|
||||
//#endregion
|
||||
//#region for our extension pages
|
||||
|
||||
for (const storage of ['localStorage', 'sessionStorage']) {
|
||||
try {
|
||||
window[storage]._access_check = 1;
|
||||
delete window[storage]._access_check;
|
||||
} catch (err) {
|
||||
Object.defineProperty(window, storage, {value: {}});
|
||||
}
|
||||
}
|
||||
|
||||
if (!(new URLSearchParams({foo: 1})).get('foo')) {
|
||||
// TODO: remove when minimum_chrome_version >= 61
|
||||
window.URLSearchParams = class extends URLSearchParams {
|
||||
constructor(init) {
|
||||
if (init && typeof init === 'object') {
|
||||
super();
|
||||
for (const [key, val] of Object.entries(init)) {
|
||||
this.set(key, val);
|
||||
}
|
||||
} else {
|
||||
super(...arguments);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const [key, {base, fn}] of Object.entries(ELEMENT_METH)) {
|
||||
for (const cls of base) {
|
||||
if (cls.prototype[key]) {
|
||||
continue;
|
||||
}
|
||||
cls.prototype[key] = function (...nodes) {
|
||||
const frag = document.createDocumentFragment();
|
||||
for (const node of nodes) {
|
||||
frag.appendChild(typeof node === 'string' ? document.createTextNode(node) : node);
|
||||
}
|
||||
fn(this, frag);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (!localStorage) {
|
||||
throw new Error('localStorage is null');
|
||||
}
|
||||
localStorage._access_check = 1;
|
||||
delete localStorage._access_check;
|
||||
} catch (err) {
|
||||
Object.defineProperty(self, 'localStorage', {value: {}});
|
||||
}
|
||||
try {
|
||||
if (!sessionStorage) {
|
||||
throw new Error('sessionStorage is null');
|
||||
}
|
||||
sessionStorage._access_check = 1;
|
||||
delete sessionStorage._access_check;
|
||||
} catch (err) {
|
||||
Object.defineProperty(self, 'sessionStorage', {value: {}});
|
||||
}
|
||||
|
||||
function polyfillBrowser() {
|
||||
if (typeof browser === 'object' && browser.runtime) {
|
||||
return browser;
|
||||
}
|
||||
return createTrap(chrome, null);
|
||||
|
||||
function createTrap(base, parent) {
|
||||
const target = typeof base === 'function' ? () => {} : {};
|
||||
target.isTrap = true;
|
||||
return new Proxy(target, {
|
||||
get: (target, prop) => {
|
||||
if (target[prop]) return target[prop];
|
||||
if (base[prop] && (typeof base[prop] === 'object' || typeof base[prop] === 'function')) {
|
||||
target[prop] = createTrap(base[prop], base);
|
||||
return target[prop];
|
||||
}
|
||||
return base[prop];
|
||||
},
|
||||
apply: (target, thisArg, args) => base.apply(parent, args)
|
||||
});
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
})();
|
||||
|
|
19
js/prefs.js
19
js/prefs.js
|
@ -1,14 +1,18 @@
|
|||
/* global promisifyChrome */
|
||||
/* global promisifyChrome msg API */
|
||||
'use strict';
|
||||
|
||||
// Needs msg.js loaded first
|
||||
|
||||
self.prefs = self.INJECTED === 1 ? self.prefs : (() => {
|
||||
const defaults = {
|
||||
'openEditInWindow': false, // new editor opens in a own browser window
|
||||
'openEditInWindow.popup': false, // new editor opens in a simplified browser window without omnibox
|
||||
'windowPosition': {}, // detached window position
|
||||
'show-badge': true, // display text on popup menu icon
|
||||
'disableAll': false, // boss key
|
||||
'exposeIframes': false, // Add 'stylus-iframe' attribute to HTML element in all iframes
|
||||
'newStyleAsUsercss': false, // create new style in usercss format
|
||||
'styleViaXhr': false, // early style injection to avoid FOUC
|
||||
|
||||
// checkbox in style config dialog
|
||||
'config.autosave': true,
|
||||
|
@ -32,9 +36,9 @@ self.prefs = self.INJECTED === 1 ? self.prefs : (() => {
|
|||
'manage.onlyLocal.invert': false, // display only externally installed styles
|
||||
'manage.onlyUsercss.invert': false, // display only non-usercss (standard) styles
|
||||
// UI element state: expanded/collapsed
|
||||
'manage.actions.expanded': true,
|
||||
'manage.backup.expanded': true,
|
||||
'manage.filters.expanded': true,
|
||||
'manage.options.expanded': true,
|
||||
// the new compact layout doesn't look good on Android yet
|
||||
'manage.newUI': !navigator.appVersion.includes('Android'),
|
||||
'manage.newUI.favicons': false, // show favicons for the sites in applies-to
|
||||
|
@ -115,12 +119,11 @@ self.prefs = self.INJECTED === 1 ? self.prefs : (() => {
|
|||
'storage.sync': ['get', 'set'],
|
||||
});
|
||||
|
||||
const initializing = browser.storage.sync.get('settings')
|
||||
.then(result => {
|
||||
if (result.settings) {
|
||||
setAll(result.settings, true);
|
||||
}
|
||||
});
|
||||
const initializing = (
|
||||
msg.isBg
|
||||
? browser.storage.sync.get('settings').then(res => res.settings)
|
||||
: API.getPrefs()
|
||||
).then(res => res && setAll(res, true));
|
||||
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (area !== 'sync' || !changes.settings || !changes.settings.newValue) {
|
||||
|
|
20
js/router.js
20
js/router.js
|
@ -31,18 +31,9 @@ const router = (() => {
|
|||
}
|
||||
|
||||
function updateSearch(key, value) {
|
||||
const search = new URLSearchParams(location.search.replace(/^\?/, ''));
|
||||
if (!value) {
|
||||
search.delete(key);
|
||||
} else {
|
||||
search.set(key, value);
|
||||
}
|
||||
const finalSearch = search.toString();
|
||||
if (finalSearch) {
|
||||
history.replaceState(history.state, null, `?${finalSearch}${location.hash}`);
|
||||
} else {
|
||||
history.replaceState(history.state, null, `${location.pathname}${location.hash}`);
|
||||
}
|
||||
const u = new URL(location);
|
||||
u.searchParams[value ? 'set' : 'delete'](key, value);
|
||||
history.replaceState(history.state, null, `${u}`);
|
||||
update(true);
|
||||
}
|
||||
|
||||
|
@ -66,7 +57,7 @@ const router = (() => {
|
|||
}
|
||||
|
||||
function getSearch(key) {
|
||||
return new URLSearchParams(location.search.replace(/^\?/, '')).get(key);
|
||||
return new URLSearchParams(location.search).get(key);
|
||||
}
|
||||
|
||||
function update(replace) {
|
||||
|
@ -86,8 +77,7 @@ const router = (() => {
|
|||
if (options.hash) {
|
||||
state = options.hash === location.hash;
|
||||
} else if (options.search) {
|
||||
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425)
|
||||
const search = new URLSearchParams(location.search.replace(/^\?/, ''));
|
||||
const search = new URLSearchParams(location.search);
|
||||
state = options.search.map(key => search.get(key));
|
||||
}
|
||||
if (!deepEqual(state, options.currentState)) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* exported styleSectionsEqual styleCodeEmpty calcStyleDigest styleJSONseemsValid */
|
||||
/* exported styleSectionsEqual styleCodeEmpty styleSectionGlobal calcStyleDigest styleJSONseemsValid */
|
||||
'use strict';
|
||||
|
||||
function styleCodeEmpty(code) {
|
||||
|
@ -14,6 +14,14 @@ function styleCodeEmpty(code) {
|
|||
return false;
|
||||
}
|
||||
|
||||
/** Checks if section is global i.e. has no targets at all */
|
||||
function styleSectionGlobal(section) {
|
||||
return (!section.regexps || !section.regexps.length) &&
|
||||
(!section.urlPrefixes || !section.urlPrefixes.length) &&
|
||||
(!section.urls || !section.urls.length) &&
|
||||
(!section.domains || !section.domains.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Style} a - first style object
|
||||
* @param {Style} b - second style object
|
||||
|
|
|
@ -71,7 +71,7 @@ const usercss = (() => {
|
|||
.then(({sections, errors}) => {
|
||||
if (!errors.length) errors = false;
|
||||
if (!sections.length || errors && !allowErrors) {
|
||||
throw errors;
|
||||
throw errors || 'Style does not contain any actual CSS to apply.';
|
||||
}
|
||||
style.sections = sections;
|
||||
return allowErrors ? {style, errors} : style;
|
||||
|
|
85
manage.html
85
manage.html
|
@ -149,8 +149,8 @@
|
|||
<script src="js/polyfill.js"></script>
|
||||
<script src="js/dom.js"></script>
|
||||
<script src="js/messaging.js"></script>
|
||||
<script src="js/prefs.js"></script>
|
||||
<script src="js/msg.js"></script>
|
||||
<script src="js/prefs.js"></script>
|
||||
<script src="js/router.js"></script>
|
||||
<script src="content/style-injector.js"></script>
|
||||
<script src="content/apply.js"></script>
|
||||
|
@ -278,8 +278,11 @@
|
|||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="style-actions">
|
||||
<div class="settings-column">
|
||||
<details id="actions" data-pref="manage.actions.expanded">
|
||||
<summary><h2 i18n-text="optionsActions"></h2></summary>
|
||||
<div id="update-check">
|
||||
<button id="check-all-updates" i18n-text="checkAllUpdates"><span id="update-progress"></span></button>
|
||||
<a href="#" id="update-history" i18n-title="genericHistoryLabel" tabindex="0">
|
||||
|
@ -313,85 +316,19 @@
|
|||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="manage-options-button" i18n-text="openOptions"></button>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="settings-column">
|
||||
<details id="options" data-pref="manage.options.expanded">
|
||||
|
||||
<summary><h2 id="options-heading" i18n-text="optionsHeading"></h2></summary>
|
||||
|
||||
<label>
|
||||
<input id="manage.newUI" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
<span i18n-text="manageNewUI"></span>
|
||||
</label>
|
||||
|
||||
<div id="newUIoptions">
|
||||
<div>
|
||||
<label for="manage.newUI.favicons" i18n-text="manageFavicons">
|
||||
<input id="manage.newUI.favicons" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
<a href="#" data-toggle-on-click="#faviconsHelp" tabindex="0">
|
||||
<svg class="svg-icon select-arrow">
|
||||
<title i18n-text="optionsSubheading"></title>
|
||||
<use xlink:href="#svg-icon-select-arrow"/>
|
||||
</svg>
|
||||
</a>
|
||||
</label>
|
||||
<div id="faviconsHelp" class="hidden" i18n-text="manageFaviconsHelp">
|
||||
<div>
|
||||
<label for="manage.newUI.faviconsGray" i18n-text="manageFaviconsGray">
|
||||
<input id="manage.newUI.faviconsGray" type="checkbox">
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label><input id="manage.newUI.targets" type="number" min="1" max="99"><span i18n-text="manageMaxTargets"></span></label>
|
||||
</div>
|
||||
|
||||
<div id="options-buttons">
|
||||
<button id="manage-options-button" i18n-text="openOptions"></button>
|
||||
<button id="manage-shortcuts-button" class="chromium-only"
|
||||
i18n-text="shortcuts"
|
||||
i18n-title="shortcutsNote"></button>
|
||||
<a id="find-editor-styles"
|
||||
href="https://userstyles.org/styles/browse/chrome-extension"
|
||||
i18n-title="editorStylesButton"
|
||||
target="_blank"><button i18n-text="cm_theme" tabindex="-1"></button></a>
|
||||
</div>
|
||||
|
||||
</details>
|
||||
|
||||
<details id="backup" data-pref="manage.backup.expanded">
|
||||
<summary><h2 id="backup-title" i18n-text="backupButtons"></h2></summary>
|
||||
<span id="backup-message" i18n-text="backupMessage"></span>
|
||||
<div id="backup-buttons">
|
||||
<div class="dropdown">
|
||||
<button class="dropbtn">
|
||||
<span>Export</span>
|
||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||
</button>
|
||||
|
||||
<div class="dropdown-content">
|
||||
<a href="#" id="file-all-styles" i18n-text="bckpInstStyles"></a>
|
||||
<a id="sync-dropbox-export" i18n-text="syncDropboxStyles" i18n-title="syncDropboxDeprecated"></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="dropbtn">
|
||||
<span>Import</span>
|
||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||
</button>
|
||||
|
||||
<div class="dropdown-content">
|
||||
<a href="#" id="unfile-all-styles" i18n-text="retrieveBckp"></a>
|
||||
<a id="sync-dropbox-import" i18n-text="retrieveDropboxSync" i18n-title="syncDropboxDeprecated"></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="file-all-styles" i18n-text="exportLabel"></button>
|
||||
<button id="unfile-all-styles" i18n-text="importLabel"></button>
|
||||
<button id="sync-styles" i18n-text="optionsCustomizeSync"></button>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ function configDialog(style) {
|
|||
vars.forEach(renderValueState);
|
||||
|
||||
return messageBox({
|
||||
title: `${style.name} v${data.version}`,
|
||||
title: `${style.customName || style.name} v${data.version}`,
|
||||
className: 'config-dialog' + (isPopup ? ' stylus-popup' : ''),
|
||||
contents: [
|
||||
$create('.config-heading', data.supportURL &&
|
||||
|
|
|
@ -14,7 +14,7 @@ let initialized = false;
|
|||
router.watch({search: ['search']}, ([search]) => {
|
||||
$('#search').value = search || '';
|
||||
if (!initialized) {
|
||||
init();
|
||||
initFilters();
|
||||
initialized = true;
|
||||
} else {
|
||||
searchStyles();
|
||||
|
@ -36,7 +36,7 @@ HTMLSelectElement.prototype.adjustWidth = function () {
|
|||
parent.replaceChild(this, singleSelect);
|
||||
};
|
||||
|
||||
function init() {
|
||||
function initFilters() {
|
||||
$('#search').oninput = e => {
|
||||
router.updateSearch('search', e.target.value);
|
||||
};
|
||||
|
|
|
@ -1,24 +1,14 @@
|
|||
/* global messageBox styleSectionsEqual API onDOMready
|
||||
tryJSONparse scrollElementIntoView $ $$ API $create t animateElement
|
||||
styleJSONseemsValid */
|
||||
/* exported bulkChangeQueue bulkChangeTime */
|
||||
styleJSONseemsValid bulkChangeQueue */
|
||||
'use strict';
|
||||
|
||||
const STYLISH_DUMP_FILE_EXT = '.txt';
|
||||
const STYLUS_BACKUP_FILE_EXT = '.json';
|
||||
|
||||
let bulkChangeQueue = [];
|
||||
let bulkChangeTime = 0;
|
||||
|
||||
onDOMready().then(() => {
|
||||
$('#file-all-styles').onclick = event => {
|
||||
event.preventDefault();
|
||||
exportToFile();
|
||||
};
|
||||
$('#unfile-all-styles').onclick = event => {
|
||||
event.preventDefault();
|
||||
importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT});
|
||||
};
|
||||
$('#file-all-styles').onclick = () => exportToFile();
|
||||
$('#unfile-all-styles').onclick = () => importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT});
|
||||
|
||||
Object.assign(document.body, {
|
||||
ondragover(event) {
|
||||
|
@ -141,7 +131,7 @@ function importFromString(jsonString) {
|
|||
}
|
||||
});
|
||||
bulkChangeQueue.length = 0;
|
||||
bulkChangeTime = performance.now();
|
||||
bulkChangeQueue.time = performance.now();
|
||||
return API.importManyStyles(items.map(i => i.item))
|
||||
.then(styles => {
|
||||
for (let i = 0; i < styles.length; i++) {
|
||||
|
|
|
@ -24,12 +24,12 @@ onDOMready().then(() => {
|
|||
document.body.appendChild(input);
|
||||
window.addEventListener('keydown', maybeRefocus, true);
|
||||
|
||||
function incrementalSearch({which}, immediately) {
|
||||
function incrementalSearch({key}, immediately) {
|
||||
if (!immediately) {
|
||||
debounce(incrementalSearch, 100, {}, true);
|
||||
return;
|
||||
}
|
||||
const direction = which === 38 ? -1 : which === 40 ? 1 : 0;
|
||||
const direction = key === 'ArrowUp' ? -1 : key === 'ArrowDown' ? 1 : 0;
|
||||
const text = input.value.toLocaleLowerCase();
|
||||
if (!text.trim() || !direction && (text === prevText || focusedName.startsWith(text))) {
|
||||
prevText = text;
|
||||
|
@ -76,40 +76,32 @@ onDOMready().then(() => {
|
|||
if (event.altKey || event.metaKey || $('#message-box')) {
|
||||
return;
|
||||
}
|
||||
const inTextInput = event.target.matches('[type=text], [type=search], [type=number]');
|
||||
const {which: k, key} = event;
|
||||
// focus search field on "/" or Ctrl-F key
|
||||
if (event.ctrlKey
|
||||
? (event.code === 'KeyF' || !event.code && k === 70) && !event.shiftKey
|
||||
: (key === '/' || !key && k === 191 && !event.shiftKey) && !inTextInput) {
|
||||
const inTextInput = $.isTextLikeInput(event.target);
|
||||
const {key, code, ctrlKey: ctrl} = event;
|
||||
// `code` is independent of the current keyboard language
|
||||
if ((code === 'KeyF' && ctrl && !event.shiftKey) ||
|
||||
(code === 'Slash' || key === '/') && !ctrl && !inTextInput) {
|
||||
// focus search field on "/" or Ctrl-F key
|
||||
event.preventDefault();
|
||||
$('#search').focus();
|
||||
return;
|
||||
}
|
||||
if (event.ctrlKey || inTextInput) {
|
||||
if (ctrl || inTextInput ||
|
||||
key === ' ' && !input.value /* Space or Shift-Space is for page down/up */) {
|
||||
return;
|
||||
}
|
||||
const time = performance.now();
|
||||
if (
|
||||
// 0-9
|
||||
k >= 48 && k <= 57 ||
|
||||
// a-z
|
||||
k >= 65 && k <= 90 ||
|
||||
// numpad keys
|
||||
k >= 96 && k <= 111 ||
|
||||
// marks
|
||||
k >= 186
|
||||
) {
|
||||
if (key.length === 1) {
|
||||
input.focus();
|
||||
if (time - prevTime > 1000) {
|
||||
input.value = '';
|
||||
}
|
||||
prevTime = time;
|
||||
} else
|
||||
if (k === 13 && focusedLink) {
|
||||
if (key === 'Enter' && focusedLink) {
|
||||
focusedLink.dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
||||
} else
|
||||
if ((k === 38 || k === 40) && !event.shiftKey &&
|
||||
if ((key === 'ArrowUp' || key === 'ArrowDown') && !event.shiftKey &&
|
||||
time - prevTime < 5000 && incrementalSearch(event, true)) {
|
||||
prevTime = time;
|
||||
} else
|
||||
|
|
|
@ -54,7 +54,6 @@ a:hover {
|
|||
top: 0;
|
||||
padding: 1rem;
|
||||
border-right: 1px dashed #AAA;
|
||||
-webkit-box-shadow: 0 0 50px -18px black;
|
||||
box-shadow: 0 0 50px -18px black;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
|
@ -285,13 +284,6 @@ a:hover {
|
|||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
#options-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding-top: .1rem;
|
||||
}
|
||||
|
||||
#options-buttons button,
|
||||
#backup-buttons button {
|
||||
margin: 0 .2rem .5rem 0;
|
||||
}
|
||||
|
@ -557,8 +549,6 @@ a:hover {
|
|||
.newUI .update-done .updated svg {
|
||||
top: -4px;
|
||||
position: relative;
|
||||
/* unprefixed since Chrome 53 */
|
||||
-webkit-filter: drop-shadow(0 4px 0 currentColor);
|
||||
filter: drop-shadow(0 5px 0 currentColor);
|
||||
}
|
||||
|
||||
|
@ -670,8 +660,6 @@ a:hover {
|
|||
margin-left: -20px;
|
||||
margin-right: 4px;
|
||||
transition: opacity .5s, filter .5s;
|
||||
/* unprefixed since Chrome 53 */
|
||||
-webkit-filter: grayscale(1);
|
||||
filter: grayscale(1);
|
||||
/* workaround for the buggy CSS filter: images in the hidden overflow are shown on Mac */
|
||||
backface-visibility: hidden;
|
||||
|
@ -689,68 +677,7 @@ a:hover {
|
|||
|
||||
.newUI .entry:hover .target img {
|
||||
opacity: 1;
|
||||
/* unprefixed since Chrome 53 */
|
||||
-webkit-filter: grayscale(0);
|
||||
filter: grayscale(0);
|
||||
}
|
||||
|
||||
#newUIoptions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.newUI #newUIoptions {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
#newUIoptions > * {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: auto;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#newUIoptions input[type="number"] {
|
||||
width: 3em;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
#newUIoptions [data-toggle-on-click="#faviconsHelp"] {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
#newUIoptions [data-toggle-on-click] > svg {
|
||||
position: static;
|
||||
}
|
||||
|
||||
#newUIoptions [data-toggle-on-click]:not([open]) > svg {
|
||||
/* note: without ">" FF52 also transforms the nested svg inside <use> */
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
html:not(.newUI) #newUIoptions + * {
|
||||
margin-top: .5em;
|
||||
}
|
||||
|
||||
input[id^="manage.newUI"] {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#faviconsHelp {
|
||||
overflow-y: auto;
|
||||
font-size: 90%;
|
||||
padding: 1ex 0 2ex 16px;
|
||||
}
|
||||
|
||||
#faviconsHelp div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 1ex;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
/* Default, no update buttons */
|
||||
|
@ -1046,54 +973,6 @@ input[id^="manage.newUI"] {
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* export/import buttons */
|
||||
#backup-buttons .dropbtn {
|
||||
padding: 3px 7px;
|
||||
cursor: pointer;
|
||||
text-overflow: inherit;
|
||||
}
|
||||
|
||||
#backup-buttons .dropbtn span {
|
||||
display: inline-block;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
#backup-buttons .dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#backup-buttons .dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: #f9f9f9;
|
||||
min-width: 160px;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#backup-buttons .dropdown-content a {
|
||||
color: black;
|
||||
padding: 8px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#backup-buttons .dropdown-content a:hover {
|
||||
/* background-color: #f2f2f2 */
|
||||
background-color: #e9e9e9
|
||||
}
|
||||
|
||||
#backup-buttons .dropdown:hover .dropdown-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#backup-buttons .dropdown:hover .dropbtn {
|
||||
background-color: hsl(0, 0%, 95%);
|
||||
border-color: hsl(0, 0%, 52%);
|
||||
/* background-color: #3e8e41; */
|
||||
}
|
||||
|
||||
/* sort font */
|
||||
@font-face {
|
||||
font-family: 'sorticon';
|
||||
|
@ -1125,6 +1004,10 @@ input[id^="manage.newUI"] {
|
|||
animation: fadeout .25s ease-in-out;
|
||||
}
|
||||
|
||||
.settings-column {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
@ -1302,14 +1185,3 @@ input[id^="manage.newUI"] {
|
|||
margin-left: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Deprecated dropbox backup (dropbox-sync) */
|
||||
#sync-dropbox-export,
|
||||
#sync-dropbox-import {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
#backup-buttons .dropdown-content #sync-dropbox-export:hover,
|
||||
#backup-buttons .dropdown-content #sync-dropbox-import:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
|
206
manage/manage.js
206
manage/manage.js
|
@ -4,11 +4,10 @@ global messageBox getStyleWithNoCode
|
|||
checkUpdate handleUpdateInstalled
|
||||
objectDiff
|
||||
configDialog
|
||||
sorter msg prefs API onDOMready $ $$ $create template setupLivePrefs
|
||||
URLS enforceInputRange t tWordBreak formatDate
|
||||
sorter msg prefs API $ $$ $create template setupLivePrefs
|
||||
t tWordBreak formatDate
|
||||
getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce
|
||||
scrollElementIntoView CHROME VIVALDI FIREFOX router
|
||||
bulkChangeTime:true bulkChangeQueue
|
||||
scrollElementIntoView CHROME VIVALDI router
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
@ -18,16 +17,27 @@ const ENTRY_ID_PREFIX_RAW = 'style-';
|
|||
const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW;
|
||||
|
||||
const BULK_THROTTLE_MS = 100;
|
||||
const bulkChangeQueue = [];
|
||||
bulkChangeQueue.time = 0;
|
||||
|
||||
// define pref-mapped ids separately
|
||||
const newUI = {
|
||||
enabled: prefs.get('manage.newUI'),
|
||||
favicons: prefs.get('manage.newUI.favicons'),
|
||||
faviconsGray: prefs.get('manage.newUI.faviconsGray'),
|
||||
targets: prefs.get('manage.newUI.targets'),
|
||||
renderClass() {
|
||||
document.documentElement.classList.toggle('newUI', newUI.enabled);
|
||||
},
|
||||
enabled: null, // the global option should come first
|
||||
favicons: null,
|
||||
faviconsGray: null,
|
||||
targets: null,
|
||||
};
|
||||
// ...add utility functions
|
||||
Object.assign(newUI, {
|
||||
ids: Object.keys(newUI),
|
||||
prefGroup: 'manage.newUI',
|
||||
prefKeyForId: id => id === 'enabled' ? newUI.prefGroup : `${newUI.prefGroup}.${id}`,
|
||||
renderClass: () => document.documentElement.classList.toggle('newUI', newUI.enabled),
|
||||
});
|
||||
// ...read the actual values
|
||||
for (const id of newUI.ids) {
|
||||
newUI[id] = prefs.get(newUI.prefKeyForId(id));
|
||||
}
|
||||
newUI.renderClass();
|
||||
|
||||
const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps'];
|
||||
|
@ -40,23 +50,45 @@ Promise.all([
|
|||
API.getAllStyles(true),
|
||||
// FIXME: integrate this into filter.js
|
||||
router.getSearch('search') && API.searchDB({query: router.getSearch('search')}),
|
||||
Promise.all([
|
||||
onDOMready(),
|
||||
prefs.initializing,
|
||||
])
|
||||
.then(() => {
|
||||
initGlobalEvents();
|
||||
if (!VIVALDI) {
|
||||
$$('#header select').forEach(el => el.adjustWidth());
|
||||
}
|
||||
if (FIREFOX && 'update' in (chrome.commands || {})) {
|
||||
const btn = $('#manage-shortcuts-button');
|
||||
btn.classList.remove('chromium-only');
|
||||
btn.onclick = API.optionsCustomizeHotkeys;
|
||||
}
|
||||
}),
|
||||
]).then(args => {
|
||||
showStyles(...args);
|
||||
waitForSelector('#installed'), // needed to avoid flicker due to an extra frame and layout shift
|
||||
prefs.initializing
|
||||
]).then(([styles, ids, el]) => {
|
||||
installed = el;
|
||||
installed.onclick = handleEvent.entryClicked;
|
||||
$('#manage-options-button').onclick = () => router.updateHash('#stylus-options');
|
||||
$('#sync-styles').onclick = () => router.updateHash('#stylus-options');
|
||||
$$('#header a[href^="http"]').forEach(a => (a.onclick = handleEvent.external));
|
||||
// show date installed & last update on hover
|
||||
installed.addEventListener('mouseover', handleEvent.lazyAddEntryTitle);
|
||||
installed.addEventListener('mouseout', handleEvent.lazyAddEntryTitle);
|
||||
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||
// N.B. triggers existing onchange listeners
|
||||
setupLivePrefs();
|
||||
sorter.init();
|
||||
prefs.subscribe(newUI.ids.map(newUI.prefKeyForId), () => switchUI());
|
||||
switchUI({styleOnly: true});
|
||||
// translate CSS manually
|
||||
document.head.appendChild($create('style', `
|
||||
.disabled h2::after {
|
||||
content: "${t('genericDisabledLabel')}";
|
||||
}
|
||||
#update-all-no-updates[data-skipped-edited="true"]::after {
|
||||
content: " ${t('updateAllCheckSucceededSomeEdited')}";
|
||||
}
|
||||
body.all-styles-hidden-by-filters::after {
|
||||
content: "${t('filteredStylesAllHidden')}";
|
||||
}
|
||||
`));
|
||||
if (!VIVALDI) {
|
||||
$$('#header select').forEach(el => el.adjustWidth());
|
||||
}
|
||||
if (CHROME >= 80 && CHROME <= 88) {
|
||||
// Wrong checkboxes are randomly checked after going back in history, https://crbug.com/1138598
|
||||
addEventListener('pagehide', () => {
|
||||
$$('input[type=checkbox]').forEach((el, i) => (el.name = `bug${i}`));
|
||||
});
|
||||
}
|
||||
showStyles(styles, ids);
|
||||
});
|
||||
|
||||
msg.onExtension(onRuntimeMessage);
|
||||
|
@ -67,7 +99,7 @@ function onRuntimeMessage(msg) {
|
|||
case 'styleAdded':
|
||||
case 'styleDeleted':
|
||||
bulkChangeQueue.push(msg);
|
||||
if (performance.now() - bulkChangeTime < BULK_THROTTLE_MS) {
|
||||
if (performance.now() - bulkChangeQueue.time < BULK_THROTTLE_MS) {
|
||||
debounce(handleBulkChange, BULK_THROTTLE_MS);
|
||||
} else {
|
||||
handleBulkChange();
|
||||
|
@ -82,74 +114,16 @@ function onRuntimeMessage(msg) {
|
|||
setTimeout(sorter.updateStripes, 0, {onlyWhenColumnsChanged: true});
|
||||
}
|
||||
|
||||
|
||||
function initGlobalEvents() {
|
||||
installed = $('#installed');
|
||||
installed.onclick = handleEvent.entryClicked;
|
||||
$('#manage-options-button').onclick = () => {
|
||||
router.updateHash('#stylus-options');
|
||||
};
|
||||
{
|
||||
const btn = $('#manage-shortcuts-button');
|
||||
btn.onclick = btn.onclick || (() => openURL({url: URLS.configureCommands}));
|
||||
}
|
||||
$$('#header a[href^="http"]').forEach(a => (a.onclick = handleEvent.external));
|
||||
// show date installed & last update on hover
|
||||
installed.addEventListener('mouseover', handleEvent.lazyAddEntryTitle);
|
||||
installed.addEventListener('mouseout', handleEvent.lazyAddEntryTitle);
|
||||
|
||||
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||
|
||||
$$('[data-toggle-on-click]').forEach(el => {
|
||||
// dataset on SVG doesn't work in Chrome 49-??, works in 57+
|
||||
const target = $(el.getAttribute('data-toggle-on-click'));
|
||||
el.onclick = event => {
|
||||
event.preventDefault();
|
||||
target.classList.toggle('hidden');
|
||||
if (target.classList.contains('hidden')) {
|
||||
el.removeAttribute('open');
|
||||
} else {
|
||||
el.setAttribute('open', '');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// triggered automatically by setupLivePrefs() below
|
||||
enforceInputRange($('#manage.newUI.targets'));
|
||||
|
||||
// N.B. triggers existing onchange listeners
|
||||
setupLivePrefs();
|
||||
sorter.init();
|
||||
|
||||
prefs.subscribe([
|
||||
'manage.newUI',
|
||||
'manage.newUI.favicons',
|
||||
'manage.newUI.faviconsGray',
|
||||
'manage.newUI.targets',
|
||||
], () => switchUI());
|
||||
|
||||
switchUI({styleOnly: true});
|
||||
|
||||
// translate CSS manually
|
||||
document.head.appendChild($create('style', `
|
||||
.disabled h2::after {
|
||||
content: "${t('genericDisabledLabel')}";
|
||||
}
|
||||
#update-all-no-updates[data-skipped-edited="true"]::after {
|
||||
content: " ${t('updateAllCheckSucceededSomeEdited')}";
|
||||
}
|
||||
body.all-styles-hidden-by-filters::after {
|
||||
content: "${t('filteredStylesAllHidden')}";
|
||||
}
|
||||
`));
|
||||
}
|
||||
|
||||
function showStyles(styles = [], matchUrlIds) {
|
||||
const sorted = sorter.sort({
|
||||
styles: styles.map(style => ({
|
||||
style,
|
||||
name: (style.name || '').toLocaleLowerCase() + '\n' + style.name,
|
||||
})),
|
||||
styles: styles.map(style => {
|
||||
const name = style.customName || style.name || '';
|
||||
return {
|
||||
style,
|
||||
// sort case-insensitively the whole list then sort dupes like `Foo` and `foo` case-sensitively
|
||||
name: name.toLocaleLowerCase() + '\n' + name,
|
||||
};
|
||||
}),
|
||||
});
|
||||
let index = 0;
|
||||
let firstRun = true;
|
||||
|
@ -196,7 +170,7 @@ function showStyles(styles = [], matchUrlIds) {
|
|||
}
|
||||
|
||||
|
||||
function createStyleElement({style, name}) {
|
||||
function createStyleElement({style, name: nameLC}) {
|
||||
// query the sub-elements just once, then reuse the references
|
||||
if ((createStyleElement.parts || {}).newUI !== newUI.enabled) {
|
||||
const entry = template[`style${newUI.enabled ? 'Compact' : ''}`];
|
||||
|
@ -225,8 +199,9 @@ function createStyleElement({style, name}) {
|
|||
}
|
||||
const parts = createStyleElement.parts;
|
||||
const configurable = style.usercssData && style.usercssData.vars && Object.keys(style.usercssData.vars).length > 0;
|
||||
const name = style.customName || style.name;
|
||||
parts.checker.checked = style.enabled;
|
||||
parts.nameLink.textContent = tWordBreak(style.name);
|
||||
parts.nameLink.textContent = tWordBreak(name);
|
||||
parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id;
|
||||
parts.homepage.href = parts.homepage.title = style.url || '';
|
||||
if (!newUI.enabled) {
|
||||
|
@ -243,7 +218,7 @@ function createStyleElement({style, name}) {
|
|||
const entry = parts.entry.cloneNode(true);
|
||||
entry.id = ENTRY_ID_PREFIX_RAW + style.id;
|
||||
entry.styleId = style.id;
|
||||
entry.styleNameLowerCase = name || style.name.toLocaleLowerCase();
|
||||
entry.styleNameLowerCase = nameLC || name.toLocaleLowerCase() + '\n' + name;
|
||||
entry.styleMeta = style;
|
||||
entry.className = parts.entryClassBase + ' ' +
|
||||
(style.enabled ? 'enabled' : 'disabled') +
|
||||
|
@ -401,10 +376,11 @@ Object.assign(handleEvent, {
|
|||
const openWindow = left && shift && !ctrl;
|
||||
const openBackgroundTab = (middle && !shift) || (left && ctrl && !shift);
|
||||
const openForegroundTab = (middle && shift) || (left && ctrl && shift);
|
||||
const url = $('[href]', event.target.closest('.entry')).href;
|
||||
const entry = event.target.closest('.entry');
|
||||
const url = $('[href]', entry).href;
|
||||
if (openWindow || openBackgroundTab || openForegroundTab) {
|
||||
if (chrome.windows && openWindow) {
|
||||
chrome.windows.create(Object.assign(prefs.get('windowPosition'), {url}));
|
||||
API.openEditor({id: entry.styleId});
|
||||
} else {
|
||||
getOwnTab().then(({index}) => {
|
||||
openURL({
|
||||
|
@ -445,7 +421,7 @@ Object.assign(handleEvent, {
|
|||
animateElement(entry);
|
||||
messageBox({
|
||||
title: t('deleteStyleConfirm'),
|
||||
contents: entry.styleMeta.name,
|
||||
contents: entry.styleMeta.customName || entry.styleMeta.name,
|
||||
className: 'danger center',
|
||||
buttons: [t('confirmDelete'), t('confirmCancel')],
|
||||
})
|
||||
|
@ -489,7 +465,7 @@ Object.assign(handleEvent, {
|
|||
const y = Math.max(0, top);
|
||||
const first = document.elementFromPoint(x, y);
|
||||
const lastOffset = first.offsetTop + window.innerHeight;
|
||||
const numTargets = prefs.get('manage.newUI.targets');
|
||||
const numTargets = newUI.targets;
|
||||
let entry = first && first.closest('.entry') || installed.children[0];
|
||||
while (entry && entry.offsetTop <= lastOffset) {
|
||||
favicons.push(...$$('img', entry).slice(0, numTargets).filter(img => img.dataset.src));
|
||||
|
@ -541,7 +517,7 @@ function handleBulkChange() {
|
|||
const {id} = msg.style;
|
||||
if (msg.method === 'styleDeleted') {
|
||||
handleDelete(id);
|
||||
bulkChangeTime = performance.now();
|
||||
bulkChangeQueue.time = performance.now();
|
||||
} else {
|
||||
handleUpdateForId(id, msg);
|
||||
}
|
||||
|
@ -552,7 +528,7 @@ function handleBulkChange() {
|
|||
function handleUpdateForId(id, opts) {
|
||||
return API.getStyle(id, true).then(style => {
|
||||
handleUpdate(style, opts);
|
||||
bulkChangeTime = performance.now();
|
||||
bulkChangeQueue.time = performance.now();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -619,10 +595,8 @@ function switchUI({styleOnly} = {}) {
|
|||
const current = {};
|
||||
const changed = {};
|
||||
let someChanged = false;
|
||||
// ensure the global option is processed first
|
||||
for (const el of [$('#manage.newUI'), ...$$('[id^="manage.newUI."]')]) {
|
||||
const id = el.id.replace(/^manage\.newUI\.?/, '') || 'enabled';
|
||||
const value = el.type === 'checkbox' ? el.checked : Number(el.value);
|
||||
for (const id of newUI.ids) {
|
||||
const value = prefs.get(newUI.prefKeyForId(id));
|
||||
const valueChanged = value !== newUI[id] && (id === 'enabled' || current.enabled);
|
||||
current[id] = value;
|
||||
changed[id] = valueChanged;
|
||||
|
@ -642,13 +616,11 @@ function switchUI({styleOnly} = {}) {
|
|||
}
|
||||
` + (newUI.faviconsGray ? `
|
||||
.newUI .target img {
|
||||
-webkit-filter: grayscale(1);
|
||||
filter: grayscale(1);
|
||||
opacity: .25;
|
||||
}
|
||||
` : `
|
||||
.newUI .target img {
|
||||
-webkit-filter: none;
|
||||
filter: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
@ -730,6 +702,20 @@ function highlightEditedStyle() {
|
|||
}
|
||||
}
|
||||
|
||||
function waitForSelector(selector) {
|
||||
// TODO: if used in other places, move to dom.js
|
||||
// TODO: if used concurrently, rework to use just one observer internally
|
||||
return new Promise(resolve => {
|
||||
const mo = new MutationObserver(() => {
|
||||
const el = $(selector);
|
||||
if (el) {
|
||||
mo.disconnect();
|
||||
resolve(el);
|
||||
}
|
||||
});
|
||||
mo.observe(document, {childList: true, subtree: true});
|
||||
});
|
||||
}
|
||||
|
||||
function embedOptions() {
|
||||
let options = $('#stylus-embedded-options');
|
||||
|
|
|
@ -130,7 +130,7 @@ const sorter = (() => {
|
|||
const sorted = sort({
|
||||
styles: current.map(entry => ({
|
||||
entry,
|
||||
name: entry.styleNameLowerCase + '\n' + entry.styleMeta.name,
|
||||
name: entry.styleNameLowerCase,
|
||||
style: entry.styleMeta,
|
||||
}))
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "Stylus",
|
||||
"version": "1.5.13",
|
||||
"minimum_chrome_version": "49",
|
||||
"minimum_chrome_version": "55",
|
||||
"description": "__MSG_description__",
|
||||
"homepage_url": "https://add0n.com/stylus.html",
|
||||
"manifest_version": 2,
|
||||
|
@ -23,6 +23,9 @@
|
|||
"identity",
|
||||
"<all_urls>"
|
||||
],
|
||||
"optional_permissions": [
|
||||
"declarativeContent"
|
||||
],
|
||||
"background": {
|
||||
"scripts": [
|
||||
"js/polyfill.js",
|
||||
|
@ -38,6 +41,7 @@
|
|||
"vendor/semver-bundle/semver.js",
|
||||
"vendor/db-to-cloud/db-to-cloud.min.js",
|
||||
"vendor/uuid/uuid.min.js",
|
||||
"vendor/webext-launch-web-auth-flow/webext-launch-web-auth-flow.min.js",
|
||||
"background/token-manager.js",
|
||||
"background/sync.js",
|
||||
"background/content-scripts.js",
|
||||
|
@ -51,7 +55,9 @@
|
|||
"background/icon-manager.js",
|
||||
"background/background.js",
|
||||
"background/usercss-helper.js",
|
||||
"background/usercss-install-helper.js",
|
||||
"background/style-via-api.js",
|
||||
"background/style-via-xhr.js",
|
||||
"background/search-db.js",
|
||||
"background/update.js",
|
||||
"background/openusercss-api.js"
|
||||
|
|
|
@ -135,12 +135,7 @@
|
|||
}
|
||||
|
||||
.danger #message-box-buttons > button:not([data-focused-via-click]):first-child:focus {
|
||||
outline: red auto 1px;
|
||||
}
|
||||
|
||||
/* FF ignores color with 'auto' */
|
||||
.firefox .danger #message-box-buttons > button:not([data-focused-via-click]):first-child:focus {
|
||||
outline: red solid 1px;
|
||||
box-shadow: 0 0 0 1px red; /* Using box-shadow instead of the ugly outline in new Chrome */
|
||||
}
|
||||
|
||||
.non-windows #message-box-buttons {
|
||||
|
|
|
@ -62,28 +62,28 @@ function messageBox({
|
|||
resolveWith({button: this.buttonIndex});
|
||||
},
|
||||
key(event) {
|
||||
const {which, shiftKey, ctrlKey, altKey, metaKey, target} = event;
|
||||
if (shiftKey && which !== 9 || ctrlKey || altKey || metaKey) {
|
||||
const {key, shiftKey, ctrlKey, altKey, metaKey, target} = event;
|
||||
if (shiftKey && key !== 'Tab' || ctrlKey || altKey || metaKey) {
|
||||
return;
|
||||
}
|
||||
switch (which) {
|
||||
case 13:
|
||||
switch (key) {
|
||||
case 'Enter':
|
||||
if (target.closest(focusAccessibility.ELEMENTS.join(','))) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 27:
|
||||
case 'Escape':
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case 9:
|
||||
case 'Tab':
|
||||
moveFocus(messageBox.element, shiftKey ? -1 : 1);
|
||||
event.preventDefault();
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
resolveWith(which === 13 ? {enter: true} : {esc: true});
|
||||
resolveWith(key === 'Enter' ? {enter: true} : {esc: true});
|
||||
},
|
||||
scroll() {
|
||||
scrollTo(blockScroll.x, blockScroll.y);
|
||||
|
|
72
options.html
72
options.html
|
@ -41,6 +41,16 @@
|
|||
|
||||
<div id="options">
|
||||
|
||||
<div class="block">
|
||||
<h1 i18n-text="cm_theme"></h1>
|
||||
<div class="items">
|
||||
<label>
|
||||
<a i18n-text="optionsStylusThemes" target="_blank"
|
||||
href="https://33kk.github.io/uso-archive/?category=chrome-extension&search=Stylus"></a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<h1 i18n-text="optionsCustomizeIcon"></h1>
|
||||
<div class="items">
|
||||
|
@ -101,6 +111,13 @@
|
|||
<span></span>
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
<span i18n-text="popupOpenEditInPopup"></span>
|
||||
<span class="onoffswitch">
|
||||
<input type="checkbox" id="openEditInWindow.popup" class="slider">
|
||||
<span></span>
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
<span i18n-text="popupStylesFirst"></span>
|
||||
<span class="onoffswitch">
|
||||
|
@ -125,6 +142,45 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<h1 i18n-text="openManage"></h1>
|
||||
<div class="items">
|
||||
<label>
|
||||
<span i18n-text="manageNewUI"></span>
|
||||
<span class="onoffswitch">
|
||||
<input type="checkbox" id="manage.newUI" class="slider">
|
||||
<span></span>
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
<span i18n-text="manageFavicons">
|
||||
<a data-cmd="note"
|
||||
i18n-title="manageFaviconsHelp"
|
||||
href="#"
|
||||
class="svg-inline-wrapper"
|
||||
tabindex="0">
|
||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</a>
|
||||
</span>
|
||||
<span class="onoffswitch">
|
||||
<input type="checkbox" id="manage.newUI.favicons" class="slider">
|
||||
<span></span>
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
<span i18n-text="manageFaviconsGray"></span>
|
||||
<span class="onoffswitch">
|
||||
<input type="checkbox" id="manage.newUI.faviconsGray" class="slider">
|
||||
<span></span>
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
<span i18n-text="manageMaxTargets"></span>
|
||||
<input id="manage.newUI.targets" type="number" min="1" max="99">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block" id="updates">
|
||||
<h1 i18n-text="optionsCustomizeUpdate"></h1>
|
||||
<div class="items">
|
||||
|
@ -183,9 +239,25 @@
|
|||
</h1>
|
||||
</div>
|
||||
<div class="items">
|
||||
<label class="chromium-only">
|
||||
<span i18n-text="optionsAdvancedStyleViaXhr">
|
||||
<a data-cmd="note"
|
||||
i18n-title="optionsAdvancedStyleViaXhrNote"
|
||||
href="#"
|
||||
class="svg-inline-wrapper"
|
||||
tabindex="0">
|
||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</a>
|
||||
</span>
|
||||
<span class="onoffswitch">
|
||||
<input type="checkbox" id="styleViaXhr" class="slider">
|
||||
<span></span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="option-item">
|
||||
<span i18n-text="optionsAdvancedExposeIframes">
|
||||
<a data-cmd="note"
|
||||
i18n-data-title="optionsAdvancedExposeIframesNote"
|
||||
i18n-title="optionsAdvancedExposeIframesNote"
|
||||
href="#"
|
||||
class="svg-inline-wrapper"
|
||||
|
|
|
@ -3,9 +3,8 @@
|
|||
.onoffswitch {
|
||||
position: relative;
|
||||
margin: 1ex 0;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.onoffswitch input {
|
||||
|
@ -17,6 +16,7 @@
|
|||
bottom: -10px;
|
||||
left: -10px;
|
||||
width: calc(100% + 12px);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
#message-box .onoffswitch input {
|
||||
|
|
|
@ -343,10 +343,11 @@ html:not(.firefox):not(.opera) #updates {
|
|||
#message-box.note {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#message-box.note > div {
|
||||
max-width: calc(100% - 5rem);
|
||||
max-width: 40em;
|
||||
top: unset;
|
||||
right: unset;
|
||||
position: relative;
|
||||
|
|
|
@ -5,16 +5,9 @@
|
|||
'use strict';
|
||||
|
||||
setupLivePrefs();
|
||||
enforceInputRange($('#popupWidth'));
|
||||
$$('input[min], input[max]').forEach(enforceInputRange);
|
||||
setTimeout(splitLongTooltips);
|
||||
|
||||
// https://github.com/openstyles/stylus/issues/822
|
||||
if (!FIREFOX && CHROME >= 76 && CHROME <= 81) {
|
||||
const dropboxOption = $('option[value="dropbox"]');
|
||||
dropboxOption.disabled = true;
|
||||
dropboxOption.setAttribute('title', t('hostDisabled'));
|
||||
}
|
||||
|
||||
if (CHROME_HAS_BORDER_BUG) {
|
||||
const borderOption = $('.chrome-no-popup-border');
|
||||
if (borderOption) {
|
||||
|
@ -44,6 +37,27 @@ if (FIREFOX && 'update' in (chrome.commands || {})) {
|
|||
});
|
||||
}
|
||||
|
||||
if (CHROME) {
|
||||
// Show the option as disabled until the permission is actually granted
|
||||
const el = $('#styleViaXhr');
|
||||
el.addEventListener('click', () => {
|
||||
if (el.checked && !chrome.declarativeContent) {
|
||||
chrome.permissions.request({permissions: ['declarativeContent']}, ok => {
|
||||
if (chrome.runtime.lastError || !ok) {
|
||||
el.checked = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if (!chrome.declarativeContent) {
|
||||
prefs.initializing.then(() => {
|
||||
if (prefs.get('styleViaXhr')) {
|
||||
el.checked = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// actions
|
||||
$('#options-close-icon').onclick = () => {
|
||||
top.dispatchEvent(new CustomEvent('closeOptions'));
|
||||
|
@ -85,7 +99,7 @@ document.onclick = e => {
|
|||
e.preventDefault();
|
||||
messageBox({
|
||||
className: 'note',
|
||||
contents: target.title,
|
||||
contents: target.dataset.title,
|
||||
buttons: [t('confirmClose')],
|
||||
});
|
||||
}
|
||||
|
@ -216,6 +230,7 @@ function splitLongTooltips() {
|
|||
.map(s => s.replace(/(.{50,80}(?=.{40,}))\s+/g, '$1\n'))
|
||||
.join('\n');
|
||||
if (newTitle !== el.title) {
|
||||
el.dataset.title = el.title;
|
||||
el.title = newTitle;
|
||||
}
|
||||
}
|
||||
|
@ -281,7 +296,7 @@ function customizeHotkeys() {
|
|||
}
|
||||
|
||||
window.onkeydown = event => {
|
||||
if (event.keyCode === 27) {
|
||||
if (event.key === 'Escape') {
|
||||
top.dispatchEvent(new CustomEvent('closeOptions'));
|
||||
}
|
||||
};
|
||||
|
|
31
package-lock.json
generated
31
package-lock.json
generated
|
@ -1917,9 +1917,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"codemirror": {
|
||||
"version": "5.56.0",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.56.0.tgz",
|
||||
"integrity": "sha512-MfKVmYgifXjQpLSgpETuih7A7WTTIsxvKfSLGseTY5+qt0E1UD1wblZGM6WLenORo8sgmf+3X+WTe2WF7mufyw=="
|
||||
"version": "5.58.0",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.58.0.tgz",
|
||||
"integrity": "sha512-OUK+7EgaYnLyC0F09UWjckLWvviy02IDDGTW5Zmj60a3gdGnFtUM6rVsqrfl5+YSylQVQBNfAGG4KF7tQOb4/Q=="
|
||||
},
|
||||
"collection-visit": {
|
||||
"version": "1.0.0",
|
||||
|
@ -5065,9 +5065,9 @@
|
|||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
|
||||
"version": "4.17.19",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
|
||||
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.defaults": {
|
||||
|
@ -5324,9 +5324,9 @@
|
|||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
},
|
||||
"minipass": {
|
||||
|
@ -5425,14 +5425,6 @@
|
|||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
|
@ -7942,6 +7934,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"webext-launch-web-auth-flow": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/webext-launch-web-auth-flow/-/webext-launch-web-auth-flow-0.1.0.tgz",
|
||||
"integrity": "sha512-3W8ANT9/6uL6NX5SiaKQee439dfiS1NT8wSc+vmjly/2MmH7FBqGFBXLfBFw296w8OOqHNPnEdNcBkDGJQkDgg=="
|
||||
},
|
||||
"webext-tx-fix": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/webext-tx-fix/-/webext-tx-fix-0.3.3.tgz",
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"repository": "openstyles/stylus",
|
||||
"author": "Stylus Team",
|
||||
"dependencies": {
|
||||
"codemirror": "^5.56.0",
|
||||
"codemirror": "^5.58.0",
|
||||
"db-to-cloud": "^0.4.5",
|
||||
"jsonlint": "^1.6.3",
|
||||
"less-bundle": "github:openstyles/less-bundle#v0.1.0",
|
||||
|
@ -15,7 +15,8 @@
|
|||
"stylelint-bundle": "^8.0.0",
|
||||
"stylus-lang-bundle": "^0.54.5",
|
||||
"usercss-meta": "^0.9.0",
|
||||
"uuid": "^8.1.0"
|
||||
"uuid": "^8.1.0",
|
||||
"webext-launch-web-auth-flow": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"archiver": "^4.0.1",
|
||||
|
@ -31,7 +32,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"lint": "eslint \"**/*.js\" --cache",
|
||||
"test": "npm run lint && web-ext lint",
|
||||
"test": "npm run lint",
|
||||
"update-locales": "tx pull --all && webext-tx-fix",
|
||||
"update-transifex": "tx push -s",
|
||||
"build-vendor": "shx rm -rf vendor/* && node tools/build-vendor",
|
||||
|
|
27
popup.html
27
popup.html
|
@ -120,9 +120,7 @@
|
|||
<div class="search-result-actions">
|
||||
<button class="search-result-install hidden" i18n-text="installButton"></button>
|
||||
<button class="search-result-uninstall hidden" i18n-text="deleteStyleLabel"></button>
|
||||
<button class="search-result-customize hidden"
|
||||
i18n-text="configureStyle"
|
||||
i18n-title="configureStyleOnHomepage"></button>
|
||||
<button class="search-result-customize hidden" i18n-text="configureStyle"></button>
|
||||
</div>
|
||||
<dl class="search-result-meta">
|
||||
<div data-type="author">
|
||||
|
@ -182,8 +180,8 @@
|
|||
<script src="js/dom.js"></script>
|
||||
<script src="js/messaging.js"></script>
|
||||
<script src="js/localization.js"></script>
|
||||
<script src="js/prefs.js"></script>
|
||||
<script src="js/msg.js"></script>
|
||||
<script src="js/prefs.js"></script>
|
||||
<script src="content/style-injector.js"></script>
|
||||
<script src="content/apply.js"></script>
|
||||
|
||||
|
@ -254,6 +252,27 @@
|
|||
<div id="search-results-error" class="hidden"></div>
|
||||
<div id="search-results" class="hidden">
|
||||
<div class="search-results-nav" data-type="top"></div>
|
||||
<div id="search-params">
|
||||
<input id="search-query" type="search" i18n-placeholder="search"
|
||||
i18n-title="searchStyleQueryHint">
|
||||
<div class="select-resizer">
|
||||
<select id="search-order" i18n-title="sortStylesHelpTitle">
|
||||
<option value="n" i18n-text="genericTitle">
|
||||
<option value="u" i18n-text="searchResultUpdated">
|
||||
<option value="t" i18n-text="searchResultInstallCount">
|
||||
<option value="w" i18n-text="searchResultWeeklyCount">
|
||||
<option value="r" i18n-text="searchResultRating">
|
||||
</select>
|
||||
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
|
||||
</div>
|
||||
<label>
|
||||
<span class="checkbox-container">
|
||||
<input id="search-globals" type="checkbox" checked>
|
||||
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
|
||||
</span>
|
||||
<span i18n-text="searchGlobalStyles"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="search-results-list"></div>
|
||||
<div class="search-results-nav" data-type="bottom"></div>
|
||||
</div>
|
||||
|
|
|
@ -9,14 +9,13 @@ const hotkeys = (() => {
|
|||
let enabled = false;
|
||||
let ready = false;
|
||||
|
||||
window.addEventListener('showStyles:done', function _() {
|
||||
window.removeEventListener('showStyles:done', _);
|
||||
window.addEventListener('showStyles:done', () => {
|
||||
togglablesShown = true;
|
||||
togglables = getTogglables();
|
||||
ready = true;
|
||||
setState(true);
|
||||
initHotkeyInfo();
|
||||
});
|
||||
}, {once: true});
|
||||
|
||||
window.addEventListener('resize', adjustInfoPosition);
|
||||
|
||||
|
@ -33,44 +32,32 @@ const hotkeys = (() => {
|
|||
}
|
||||
|
||||
function onKeyDown(event) {
|
||||
if (event.ctrlKey || event.altKey || event.metaKey || !enabled) {
|
||||
if (event.ctrlKey || event.altKey || event.metaKey || !enabled ||
|
||||
/^(text|search)$/.test((document.activeElement || {}).type)) {
|
||||
return;
|
||||
}
|
||||
let entry;
|
||||
const {which: k, key, code} = event;
|
||||
let {key, code, shiftKey} = event;
|
||||
|
||||
if (code.startsWith('Digit') || code.startsWith('Numpad') && code.length === 7) {
|
||||
if (key >= '0' && key <= '9') {
|
||||
entry = entries[(Number(key) || 10) - 1];
|
||||
} else if (code >= 'Digit0' && code <= 'Digit9') {
|
||||
entry = entries[(Number(code.slice(-1)) || 10) - 1];
|
||||
|
||||
} else if (
|
||||
code === 'Backquote' || code === 'NumpadMultiply' ||
|
||||
key && (key === '`' || key === '*') ||
|
||||
k === 192 || k === 106) {
|
||||
} else if (key === '`' || key === '*' || code === 'Backquote' || code === 'NumpadMultiply') {
|
||||
invertTogglables();
|
||||
|
||||
} else if (
|
||||
code === 'NumpadSubtract' ||
|
||||
key && key === '-' ||
|
||||
k === 109) {
|
||||
} else if (key === '-' || code === 'NumpadSubtract') {
|
||||
toggleState(entries, 'enabled', false);
|
||||
|
||||
} else if (
|
||||
code === 'NumpadAdd' ||
|
||||
key && key === '+' ||
|
||||
k === 107) {
|
||||
} else if (key === '+' || code === 'NumpadAdd') {
|
||||
toggleState(entries, 'disabled', true);
|
||||
|
||||
} else if (
|
||||
// any single character
|
||||
key && key.length === 1 ||
|
||||
k >= 65 && k <= 90) {
|
||||
const letter = new RegExp(key ? '^' + key : '^\\x' + k.toString(16), 'i');
|
||||
entry = [...entries].find(entry => letter.test(entry.textContent));
|
||||
} else if (key.length === 1) {
|
||||
shiftKey = false; // typing ':' etc. needs Shift so we hide it here to avoid opening editor
|
||||
key = key.toLocaleLowerCase();
|
||||
entry = [...entries].find(e => e.innerText.toLocaleLowerCase().startsWith(key));
|
||||
}
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
const target = $(event.shiftKey ? '.style-edit-link' : '.checker', entry);
|
||||
const target = $(shiftKey ? '.style-edit-link' : '.checker', entry);
|
||||
target.dispatchEvent(new MouseEvent('click', {cancelable: true}));
|
||||
}
|
||||
|
||||
|
|
|
@ -342,11 +342,7 @@ a.configure[target="_blank"] .svg-icon.config {
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
#confirm button[data-cmd="ok"]:not([data-focused-via-click]):focus {
|
||||
outline: red auto 1px;
|
||||
}
|
||||
/* FF ignores color with 'auto' */
|
||||
.firefox #confirm button[data-cmd="ok"]:not([data-focused-via-click]):focus {
|
||||
outline: red solid 1px;
|
||||
box-shadow: 0 0 0 1px red; /* Using box-shadow instead of the ugly outline in new Chrome */
|
||||
}
|
||||
.menu-items-wrapper {
|
||||
width: 80%;
|
||||
|
|
116
popup/popup.js
116
popup/popup.js
|
@ -13,7 +13,8 @@ const handleEvent = {};
|
|||
|
||||
const ABOUT_BLANK = 'about:blank';
|
||||
const ENTRY_ID_PREFIX_RAW = 'style-';
|
||||
const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW;
|
||||
|
||||
$.entry = styleOrId => $(`#${ENTRY_ID_PREFIX_RAW}${styleOrId.id || styleOrId}`);
|
||||
|
||||
if (CHROME >= 66 && CHROME <= 69) { // Chrome 66-69 adds a gap, https://crbug.com/821143
|
||||
document.head.appendChild($create('style', 'html { overflow: overlay }'));
|
||||
|
@ -27,7 +28,7 @@ initTabUrls()
|
|||
onDOMready().then(() => initPopup(frames)),
|
||||
...frames
|
||||
.filter(f => f.url && !f.isDupe)
|
||||
.map(({url}) => API.getStylesByUrl(url).then(styles => ({styles, url}))),
|
||||
.map(({url}) => getStyleDataMerged(url).then(styles => ({styles, url}))),
|
||||
]))
|
||||
.then(([, ...results]) => {
|
||||
if (results[0]) {
|
||||
|
@ -53,17 +54,19 @@ if (CHROME_HAS_BORDER_BUG) {
|
|||
}
|
||||
|
||||
function onRuntimeMessage(msg) {
|
||||
if (!tabURL) return;
|
||||
let ready = Promise.resolve();
|
||||
switch (msg.method) {
|
||||
case 'styleAdded':
|
||||
case 'styleUpdated':
|
||||
if (msg.reason === 'editPreview' || msg.reason === 'editPreviewEnd') return;
|
||||
handleUpdate(msg);
|
||||
ready = handleUpdate(msg);
|
||||
break;
|
||||
case 'styleDeleted':
|
||||
handleDelete(msg.style.id);
|
||||
break;
|
||||
}
|
||||
dispatchEvent(new CustomEvent(msg.method, {detail: msg}));
|
||||
ready.then(() => dispatchEvent(new CustomEvent(msg.method, {detail: msg})));
|
||||
}
|
||||
|
||||
|
||||
|
@ -141,8 +144,7 @@ function initPopup(frames) {
|
|||
}
|
||||
|
||||
if (!tabURL) {
|
||||
document.body.classList.add('blocked');
|
||||
document.body.insertBefore(template.unavailableInfo, document.body.firstChild);
|
||||
blockPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -308,31 +310,37 @@ function sortStyles(entries) {
|
|||
return entries.sort(({styleMeta: a}, {styleMeta: b}) =>
|
||||
Boolean(a.frameUrl) - Boolean(b.frameUrl) ||
|
||||
enabledFirst && Boolean(b.enabled) - Boolean(a.enabled) ||
|
||||
a.name.localeCompare(b.name));
|
||||
(a.customName || a.name).localeCompare(b.customName || b.name));
|
||||
}
|
||||
|
||||
function showStyles(frameResults) {
|
||||
const entries = new Map();
|
||||
frameResults.forEach(({styles = [], url}, index) => {
|
||||
styles.forEach(style => {
|
||||
const {id} = style.data;
|
||||
const {id} = style;
|
||||
if (!entries.has(id)) {
|
||||
style.frameUrl = index === 0 ? '' : url;
|
||||
entries.set(id, createStyleElement(Object.assign(style.data, style)));
|
||||
entries.set(id, createStyleElement(style));
|
||||
}
|
||||
});
|
||||
});
|
||||
if (entries.size) {
|
||||
installed.append(...sortStyles([...entries.values()]));
|
||||
resortEntries([...entries.values()]);
|
||||
} else {
|
||||
installed.appendChild(template.noStyles.cloneNode(true));
|
||||
installed.appendChild(template.noStyles);
|
||||
}
|
||||
window.dispatchEvent(new Event('showStyles:done'));
|
||||
}
|
||||
|
||||
function resortEntries(entries) {
|
||||
// `entries` is specified only at startup, after that we respect the prefs
|
||||
if (entries || prefs.get('popup.autoResort')) {
|
||||
installed.append(...sortStyles(entries || $$('.entry', installed)));
|
||||
}
|
||||
}
|
||||
|
||||
function createStyleElement(style) {
|
||||
let entry = $(ENTRY_ID_PREFIX + style.id);
|
||||
let entry = $.entry(style);
|
||||
if (!entry) {
|
||||
entry = template.style.cloneNode(true);
|
||||
entry.setAttribute('style-id', style.id);
|
||||
|
@ -400,7 +408,7 @@ function createStyleElement(style) {
|
|||
$('.checker', entry).checked = style.enabled;
|
||||
|
||||
const styleName = $('.style-name', entry);
|
||||
styleName.lastChild.textContent = style.name;
|
||||
styleName.lastChild.textContent = style.customName || style.name;
|
||||
setTimeout(() => {
|
||||
styleName.title =
|
||||
entry.styleMeta.sloppy ? t('styleNotAppliedRegexpProblemTooltip') :
|
||||
|
@ -470,11 +478,7 @@ Object.assign(handleEvent, {
|
|||
event.stopPropagation();
|
||||
API
|
||||
.toggleStyle(handleEvent.getClickedStyleId(event), this.checked)
|
||||
.then(() => {
|
||||
if (prefs.get('popup.autoResort')) {
|
||||
installed.append(...sortStyles($$('.entry', installed)));
|
||||
}
|
||||
});
|
||||
.then(() => resortEntries());
|
||||
},
|
||||
|
||||
toggleExclude(event, type) {
|
||||
|
@ -504,16 +508,15 @@ Object.assign(handleEvent, {
|
|||
window.onkeydown = event => {
|
||||
const close = $('.menu-close', entry);
|
||||
const checkbox = $('.exclude-by-domain-checkbox', entry);
|
||||
const keyCode = event.keyCode || event.which;
|
||||
if (document.activeElement === close && (keyCode === 9) && !event.shiftKey) {
|
||||
if (document.activeElement === close && (event.key === 'Tab') && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
checkbox.focus();
|
||||
}
|
||||
if (document.activeElement === checkbox && (keyCode === 9) && event.shiftKey) {
|
||||
if (document.activeElement === checkbox && (event.key === 'Tab') && event.shiftKey) {
|
||||
event.preventDefault();
|
||||
close.focus();
|
||||
}
|
||||
if (keyCode === 27) {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
close.click();
|
||||
}
|
||||
|
@ -539,20 +542,20 @@ Object.assign(handleEvent, {
|
|||
const close = $('.menu-close', entry);
|
||||
const checkbox = $('.exclude-by-domain-checkbox', entry);
|
||||
const confirmActive = $('#confirm[data-display="true"]');
|
||||
const keyCode = event.keyCode || event.which;
|
||||
if (document.activeElement === cancel && (keyCode === 9)) {
|
||||
const {key} = event;
|
||||
if (document.activeElement === cancel && (key === 'Tab')) {
|
||||
event.preventDefault();
|
||||
affirm.focus();
|
||||
}
|
||||
if (document.activeElement === close && (keyCode === 9) && !event.shiftKey) {
|
||||
if (document.activeElement === close && (key === 'Tab') && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
checkbox.focus();
|
||||
}
|
||||
if (document.activeElement === checkbox && (keyCode === 9) && event.shiftKey) {
|
||||
if (document.activeElement === checkbox && (key === 'Tab') && event.shiftKey) {
|
||||
event.preventDefault();
|
||||
close.focus();
|
||||
}
|
||||
if (keyCode === 27) {
|
||||
if (key === 'Escape') {
|
||||
event.preventDefault();
|
||||
if (confirmActive) {
|
||||
box.dataset.display = false;
|
||||
|
@ -673,38 +676,25 @@ Object.assign(handleEvent, {
|
|||
});
|
||||
|
||||
|
||||
function handleUpdate({style, reason}) {
|
||||
if (!tabURL) return;
|
||||
|
||||
fetchStyle()
|
||||
.then(style => {
|
||||
if (!style) {
|
||||
return;
|
||||
}
|
||||
if ($(ENTRY_ID_PREFIX + style.id)) {
|
||||
createStyleElement(style);
|
||||
return;
|
||||
}
|
||||
document.body.classList.remove('blocked');
|
||||
$$.remove('.blocked-info, #no-styles');
|
||||
createStyleElement(style);
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
function fetchStyle() {
|
||||
if (reason === 'toggle' && $(ENTRY_ID_PREFIX + style.id)) {
|
||||
return Promise.resolve(style);
|
||||
}
|
||||
return API.getStylesByUrl(tabURL, style.id)
|
||||
.then(([result]) => result && Object.assign(result.data, result));
|
||||
async function handleUpdate({style, reason}) {
|
||||
if (reason !== 'toggle' || !$.entry(style)) {
|
||||
style = await getStyleDataMerged(tabURL, style.id);
|
||||
if (!style) return;
|
||||
}
|
||||
const el = createStyleElement(style);
|
||||
if (!el.parentNode) {
|
||||
installed.appendChild(el);
|
||||
blockPopup(false);
|
||||
}
|
||||
resortEntries();
|
||||
}
|
||||
|
||||
|
||||
function handleDelete(id) {
|
||||
$.remove(ENTRY_ID_PREFIX + id);
|
||||
if (!$('.entry')) {
|
||||
installed.appendChild(template.noStyles.cloneNode(true));
|
||||
const el = $.entry(id);
|
||||
if (el) {
|
||||
el.remove();
|
||||
if (!$('.entry')) installed.appendChild(template.noStyles);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -722,3 +712,21 @@ function waitForTabUrlFF(tab) {
|
|||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/* Merges the extra props from API into style data.
|
||||
* When `id` is specified returns a single object otherwise an array */
|
||||
async function getStyleDataMerged(url, id) {
|
||||
const styles = (await API.getStylesByUrl(url, id))
|
||||
.map(r => Object.assign(r.data, r));
|
||||
return id ? styles[0] : styles;
|
||||
}
|
||||
|
||||
function blockPopup(isBlocked = true) {
|
||||
document.body.classList.toggle('blocked', isBlocked);
|
||||
if (isBlocked) {
|
||||
document.body.prepend(template.unavailableInfo);
|
||||
} else {
|
||||
template.unavailableInfo.remove();
|
||||
template.noStyles.remove();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,21 +54,15 @@ body.search-results-shown {
|
|||
background-color: #fff;
|
||||
}
|
||||
|
||||
.search-result .lds-spinner {
|
||||
#search-results .lds-spinner {
|
||||
transform: scale(.5);
|
||||
filter: invert(1) drop-shadow(1px 1px 3px #000);
|
||||
}
|
||||
|
||||
.search-result-empty .lds-spinner {
|
||||
transform: scale(.5);
|
||||
#search-results .search-result-empty .lds-spinner {
|
||||
filter: opacity(.2);
|
||||
}
|
||||
|
||||
.search-result-fadein {
|
||||
animation: fadein 1s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.search-result-screenshot {
|
||||
height: 140px;
|
||||
width: 100%;
|
||||
|
@ -257,11 +251,27 @@ body.search-results-shown {
|
|||
padding-left: 16px;
|
||||
}
|
||||
|
||||
#search-params {
|
||||
display: flex;
|
||||
position: relative;
|
||||
margin-top: -.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#search-params > * {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
#search-query {
|
||||
min-width: 3em;
|
||||
margin-right: .5em;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
/* spinner: https://github.com/loadingio/css-spinner */
|
||||
.lds-spinner {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -62,6 +62,9 @@ const files = {
|
|||
],
|
||||
'uuid': [
|
||||
'dist/umd/uuidv4.min.js → uuid.min.js'
|
||||
],
|
||||
'webext-launch-web-auth-flow': [
|
||||
'dist/webext-launch-web-auth-flow.min.js → webext-launch-web-auth-flow.min.js'
|
||||
]
|
||||
};
|
||||
|
||||
|
|
34
tools/zip.js
34
tools/zip.js
|
@ -3,10 +3,11 @@
|
|||
|
||||
const fs = require('fs');
|
||||
const archiver = require('archiver');
|
||||
const manifest = require('../manifest.json');
|
||||
|
||||
function createZip() {
|
||||
const fileName = 'stylus.zip';
|
||||
const exclude = [
|
||||
function createZip({isFirefox} = {}) {
|
||||
const fileName = `stylus${isFirefox ? '-firefox' : ''}.zip`;
|
||||
const ignore = [
|
||||
'.*', // dot files/folders (glob, not regexp)
|
||||
'vendor/codemirror/lib/**', // get unmodified copy from node_modules
|
||||
'node_modules/**',
|
||||
|
@ -38,15 +39,30 @@ function createZip() {
|
|||
});
|
||||
|
||||
archive.pipe(file);
|
||||
archive.glob('**', {ignore: exclude});
|
||||
if (isFirefox) {
|
||||
const name = 'manifest.json';
|
||||
const keyOpt = 'optional_permissions';
|
||||
ignore.unshift(name);
|
||||
manifest[keyOpt] = manifest[keyOpt].filter(p => p !== 'declarativeContent');
|
||||
if (!manifest[keyOpt].length) {
|
||||
delete manifest[keyOpt];
|
||||
}
|
||||
archive.append(JSON.stringify(manifest, null, ' '), {name, stats: fs.lstatSync(name)});
|
||||
}
|
||||
archive.glob('**', {ignore});
|
||||
// Don't use modified codemirror.js (see "update-libraries.js")
|
||||
archive.directory('node_modules/codemirror/lib', 'vendor/codemirror/lib');
|
||||
archive.finalize();
|
||||
});
|
||||
}
|
||||
|
||||
createZip()
|
||||
.then(() => console.log('\x1b[32m%s\x1b[0m', 'Stylus zip complete'))
|
||||
.catch(err => {
|
||||
throw err;
|
||||
});
|
||||
(async () => {
|
||||
try {
|
||||
await createZip();
|
||||
await createZip({isFirefox: true});
|
||||
console.log('\x1b[32m%s\x1b[0m', 'Stylus zip complete');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -86,10 +86,7 @@
|
|||
border: 1px solid var(--main-border-color);
|
||||
background-color: var(--main-background-color);
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.12);
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
@ -139,8 +136,6 @@
|
|||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
-webkit-border-radius: 50%;
|
||||
-moz-border-radius: 50%;
|
||||
border-radius: 50%;
|
||||
left: -5px;
|
||||
top: -5px;
|
||||
|
@ -168,8 +163,6 @@
|
|||
-webkit-border-radius: 50%;
|
||||
-moz-border-radius: 50%;
|
||||
border-radius: 50%;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--input-border-color);
|
||||
}
|
||||
|
@ -191,8 +184,6 @@
|
|||
position: relative;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
background: linear-gradient(to right, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
|
||||
|
@ -212,8 +203,6 @@
|
|||
width: 100%;
|
||||
height: 10px;
|
||||
z-index: 2;
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAJElEQVQYV2NctWrVfwYkEBYWxojMZ6SDAmT7QGx0K1EcRBsFAADeG/3M/HteAAAAAElFTkSuQmCC");
|
||||
|
@ -239,9 +228,6 @@
|
|||
|
||||
.colorpicker-input-container {
|
||||
position: relative;
|
||||
-webkit-box-sizing: padding-box;
|
||||
-moz-box-sizing: padding-box;
|
||||
box-sizing: padding-box;
|
||||
}
|
||||
|
||||
.colorpicker-input-group {
|
||||
|
@ -263,8 +249,6 @@
|
|||
position: relative;
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
@ -295,10 +279,7 @@
|
|||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
box-sizing: border-box;
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
-o-user-select: text;
|
||||
user-select: text;
|
||||
border: 1px solid var(--input-border-color);
|
||||
background-color: var(--input-background-color);
|
||||
|
@ -306,7 +287,6 @@
|
|||
}
|
||||
|
||||
.colorpicker-theme-dark .colorpicker-input::-webkit-inner-spin-button {
|
||||
-webkit-filter: invert(1);
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
|
@ -377,3 +357,15 @@
|
|||
.colorpicker-format-change-button:hover {
|
||||
color: var(--label-color-hover);
|
||||
}
|
||||
|
||||
.colorpicker-palette:not(:empty) {
|
||||
padding: 0 8px 8px;
|
||||
min-height: 14px; /* same as padding-left in .colorview-swatch */
|
||||
max-height: 10vw;
|
||||
overflow: auto;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.colorpicker-palette .colorview-swatch {
|
||||
padding-bottom: 14px; /* same as padding-left in .colorview-swatch */
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
let $swatch;
|
||||
let $formatChangeButton;
|
||||
let $hexCode;
|
||||
let $palette;
|
||||
const $inputGroups = {};
|
||||
const $inputs = {};
|
||||
const $rgb = {};
|
||||
|
@ -164,6 +165,7 @@
|
|||
$formatChangeButton = $('format-change-button', {textContent: '↔'}),
|
||||
]}),
|
||||
]}),
|
||||
$palette = $('palette'),
|
||||
]});
|
||||
|
||||
$inputs.hex = [$hexCode];
|
||||
|
@ -221,6 +223,11 @@
|
|||
if (!isNaN(options.left) && !isNaN(options.top)) {
|
||||
reposition();
|
||||
}
|
||||
if (Array.isArray(options.palette)) {
|
||||
// Might need to clear a lot of elements so this is known to be faster than textContent = ''
|
||||
while ($palette.firstChild) $palette.firstChild.remove();
|
||||
$palette.append(...(options.palette));
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
|
@ -355,29 +362,29 @@
|
|||
}
|
||||
|
||||
function setFromKeyboard(event) {
|
||||
const {which, ctrlKey: ctrl, altKey: alt, shiftKey: shift, metaKey: meta} = event;
|
||||
switch (which) {
|
||||
case 9: // Tab
|
||||
case 33: // PgUp
|
||||
case 34: // PgDn
|
||||
const {key, ctrlKey: ctrl, altKey: alt, shiftKey: shift, metaKey: meta} = event;
|
||||
switch (key) {
|
||||
case 'Tab':
|
||||
case 'PageUp':
|
||||
case 'PageDown':
|
||||
if (!ctrl && !alt && !meta) {
|
||||
const el = document.activeElement;
|
||||
const inputs = $inputs[currentFormat];
|
||||
const lastInput = inputs[inputs.length - 1];
|
||||
if (which === 9 && shift && el === inputs[0]) {
|
||||
if (key === 'Tab' && shift && el === inputs[0]) {
|
||||
maybeFocus(lastInput);
|
||||
} else if (which === 9 && !shift && el === lastInput) {
|
||||
} else if (key === 'Tab' && !shift && el === lastInput) {
|
||||
maybeFocus(inputs[0]);
|
||||
} else if (which !== 9 && !shift) {
|
||||
setFromFormatElement({shift: which === 33 || shift});
|
||||
} else if (key !== 'Tab' && !shift) {
|
||||
setFromFormatElement({shift: key === 'PageUp' || shift});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
return;
|
||||
case 38: // Up
|
||||
case 40: // Down
|
||||
case 'ArrowUp':
|
||||
case 'ArrowDown':
|
||||
if (!event.metaKey &&
|
||||
document.activeElement.localName === 'input' &&
|
||||
document.activeElement.checkValidity()) {
|
||||
|
@ -389,8 +396,8 @@
|
|||
|
||||
function setFromKeyboardIncrement(event) {
|
||||
const el = document.activeElement;
|
||||
const {which, ctrlKey: ctrl, altKey: alt, shiftKey: shift} = event;
|
||||
const dir = which === 38 ? 1 : -1;
|
||||
const {key, ctrlKey: ctrl, altKey: alt, shiftKey: shift} = event;
|
||||
const dir = key === 'ArrowUp' ? 1 : -1;
|
||||
let value, newValue;
|
||||
if (currentFormat === 'hex') {
|
||||
value = el.value.trim();
|
||||
|
@ -454,7 +461,7 @@
|
|||
const newHSV = color.type === 'hsl' ?
|
||||
colorConverter.HSLtoHSV(color) :
|
||||
colorConverter.RGBtoHSV(color);
|
||||
if (Object.keys(newHSV).every(k => Math.abs(newHSV[k] - HSV[k]) < 1e-3)) {
|
||||
if (Object.entries(newHSV).every(([k, v]) => v === HSV[k] || Math.abs(v - HSV[k]) < 1e-3)) {
|
||||
return;
|
||||
}
|
||||
HSV = newHSV;
|
||||
|
@ -574,6 +581,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
/** @param {MouseEvent} e */
|
||||
function onPaletteClicked(e) {
|
||||
if (e.target !== e.currentTarget) {
|
||||
e.preventDefault();
|
||||
if (!e.button && setColor(e.target.__color)) {
|
||||
userActivity = performance.now();
|
||||
colorpickerCallback();
|
||||
} else if (e.button === 2 && options.paletteCallback) {
|
||||
options.paletteCallback(e.target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseUp(event) {
|
||||
releaseMouse(event, ['saturation', 'hue', 'opacity']);
|
||||
if (onMouseDown.outsideClick) {
|
||||
|
@ -617,9 +637,9 @@
|
|||
|
||||
function onKeyDown(e) {
|
||||
if (!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
||||
switch (e.which) {
|
||||
case 13:
|
||||
case 27:
|
||||
switch (e.key) {
|
||||
case 'Enter':
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
hide();
|
||||
|
@ -710,6 +730,8 @@
|
|||
$opacity.addEventListener('mousedown', onOpacityMouseDown);
|
||||
$hexLettercase.true.addEventListener('click', onHexLettercaseClicked);
|
||||
$hexLettercase.false.addEventListener('click', onHexLettercaseClicked);
|
||||
$palette.addEventListener('click', onPaletteClicked);
|
||||
$palette.addEventListener('contextmenu', onPaletteClicked);
|
||||
|
||||
stopSnoozing();
|
||||
if (!options.isShortCut) {
|
||||
|
@ -735,6 +757,8 @@
|
|||
$opacity.removeEventListener('mousedown', onOpacityMouseDown);
|
||||
$hexLettercase.true.removeEventListener('click', onHexLettercaseClicked);
|
||||
$hexLettercase.false.removeEventListener('click', onHexLettercaseClicked);
|
||||
$palette.removeEventListener('click', onPaletteClicked);
|
||||
$palette.removeEventListener('contextmenu', onPaletteClicked);
|
||||
releaseMouse();
|
||||
stopSnoozing();
|
||||
}
|
||||
|
|
|
@ -532,6 +532,8 @@
|
|||
color: data.color,
|
||||
prevColor: data.color || '',
|
||||
callback: popupOnChange,
|
||||
palette: makePalette(state),
|
||||
paletteCallback: el => paletteCallback(state, el),
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -551,6 +553,49 @@
|
|||
}
|
||||
}
|
||||
|
||||
function makePalette({cm, options}) {
|
||||
const palette = new Map();
|
||||
let i = 0;
|
||||
let nums;
|
||||
cm.eachLine(({markedSpans}) => {
|
||||
++i;
|
||||
if (!markedSpans) return;
|
||||
for (const {from, marker: m} of markedSpans) {
|
||||
if (from == null || m.className !== COLORVIEW_CLASS) continue;
|
||||
nums = palette.get(m.color);
|
||||
if (!nums) palette.set(m.color, (nums = []));
|
||||
nums.push(i);
|
||||
}
|
||||
});
|
||||
const res = [];
|
||||
if (palette.size > 1 || nums && nums.length > 1) {
|
||||
const old = new Map((options.popup.palette || []).map(el => [el.__color, el]));
|
||||
for (const [color, data] of palette) {
|
||||
res.push(old.get(color) || makePaletteSwatch(color, data));
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function makePaletteSwatch(color, nums) {
|
||||
const s = nums.join(', ');
|
||||
const el = document.createElement('div');
|
||||
el.className = COLORVIEW_SWATCH_CLASS;
|
||||
el.style.cssText = COLORVIEW_SWATCH_CSS + color;
|
||||
el.title = color + (!s ? '' : `\nLine: ${s.length > 50 ? s.replace(/([^,]+,\s){10}/g, '$&\n') : s}`);
|
||||
el.__color = color;
|
||||
return el;
|
||||
}
|
||||
|
||||
function paletteCallback({cm}, el) {
|
||||
const lines = el.title.split('\n')[1].match(/\d+/g).map(Number);
|
||||
const curLine = cm.getCursor().line + 1;
|
||||
const i = lines.indexOf(curLine) + 1;
|
||||
const pos = {line: (lines[i] || curLine) - 1, ch: 0};
|
||||
cm.scrollIntoView(pos, cm.defaultTextHeight());
|
||||
cm.setCursor(pos);
|
||||
}
|
||||
|
||||
//endregion
|
||||
//region Utility
|
||||
|
||||
|
|
|
@ -154,11 +154,12 @@ self.parserlib = (() => {
|
|||
'azimuth': '<azimuth>',
|
||||
|
||||
// B
|
||||
'backdrop-filter': '<filter-function-list> | none',
|
||||
'backface-visibility': 'visible | hidden',
|
||||
'background': '[ <bg-layer> , ]* <final-bg-layer>',
|
||||
'background-attachment': '<attachment>#',
|
||||
'background-blend-mode': '<blend-mode>',
|
||||
'background-clip': '<box>#',
|
||||
'background-clip': '[ <box> | text ]#',
|
||||
'background-color': '<color>',
|
||||
'background-image': '<bg-image>#',
|
||||
'background-origin': '<box>#',
|
||||
|
@ -176,38 +177,14 @@ self.parserlib = (() => {
|
|||
'bookmark-level': 'none | <integer>',
|
||||
'bookmark-state': 'open | closed',
|
||||
'bookmark-target': 'none | <uri> | attr()',
|
||||
'border': '<border-shorthand>',
|
||||
'border-block-color': '<color>{1,2}',
|
||||
'border-block-end': '<border-shorthand>',
|
||||
'border-block-end-color': '<color>',
|
||||
'border-block-end-style': '<border-style>',
|
||||
'border-block-end-width': '<border-width>',
|
||||
'border-block-start': '<border-shorthand>',
|
||||
'border-block-start-color': '<color>',
|
||||
'border-block-start-style': '<border-style>',
|
||||
'border-block-start-width': '<border-width>',
|
||||
'border-block-style': '<border-style>{1,2}',
|
||||
'border-block-width': '<border-width>{1,2}',
|
||||
'border-bottom': '<border-shorthand>',
|
||||
'border-bottom-color': '<color>',
|
||||
|
||||
'border-bottom-left-radius': '<x-one-radius>',
|
||||
'border-bottom-right-radius': '<x-one-radius>',
|
||||
'border-bottom-style': '<border-style>',
|
||||
'border-bottom-width': '<border-width>',
|
||||
'border-boundary': 'none | parent | display',
|
||||
'border-inline-color': '<color>{1,2}',
|
||||
'border-inline-end': '<border-shorthand>',
|
||||
'border-inline-end-color': '<color>',
|
||||
'border-inline-end-style': '<border-style>',
|
||||
'border-inline-end-width': '<border-width>',
|
||||
'border-inline-start': '<border-shorthand>',
|
||||
'border-inline-start-color': '<color>',
|
||||
'border-inline-start-style': '<border-style>',
|
||||
'border-inline-start-width': '<border-width>',
|
||||
'border-inline-style': '<border-style>{1,2}',
|
||||
'border-inline-width': '<border-width>{1,2}',
|
||||
'border-top-left-radius': '<x-one-radius>',
|
||||
'border-top-right-radius': '<x-one-radius>',
|
||||
|
||||
'border-boundary': 'none | parent | display',
|
||||
'border-collapse': 'collapse | separate',
|
||||
'border-color': '<color>{1,4}',
|
||||
'border-image': '[ none | <image> ] || <border-image-slice> ' +
|
||||
'[ / <border-image-width> | / <border-image-width>? / <border-image-outset> ]? || ' +
|
||||
'<border-image-repeat>',
|
||||
|
@ -216,24 +193,8 @@ self.parserlib = (() => {
|
|||
'border-image-slice': '<border-image-slice>',
|
||||
'border-image-source': '<image> | none',
|
||||
'border-image-width': '<border-image-width>',
|
||||
'border-left': '<border-shorthand>',
|
||||
'border-left-color': '<color>',
|
||||
'border-left-style': '<border-style>',
|
||||
'border-left-width': '<border-width>',
|
||||
'border-radius': '<border-radius>',
|
||||
'border-right': '<border-shorthand>',
|
||||
'border-right-color': '<color>',
|
||||
'border-right-style': '<border-style>',
|
||||
'border-right-width': '<border-width>',
|
||||
'border-spacing': '<length>{1,2}',
|
||||
'border-style': '<border-style>{1,4}',
|
||||
'border-top': '<border-shorthand>',
|
||||
'border-top-color': '<color>',
|
||||
'border-top-left-radius': '<x-one-radius>',
|
||||
'border-top-right-radius': '<x-one-radius>',
|
||||
'border-top-style': '<border-style>',
|
||||
'border-top-width': '<border-width>',
|
||||
'border-width': '<border-width>{1,4}',
|
||||
'bottom': '<width>',
|
||||
'box-decoration-break': 'slice | clone',
|
||||
'box-shadow': '<box-shadow>',
|
||||
|
@ -286,6 +247,7 @@ self.parserlib = (() => {
|
|||
'columns': 1,
|
||||
'contain': 'none | strict | content | [ size || layout || style || paint ]',
|
||||
'content': 'normal | none | <content-list> [ / <string> ]?',
|
||||
'content-visibility': 'visible | auto | hidden',
|
||||
'counter-increment': 1,
|
||||
'counter-reset': 1,
|
||||
'crop': 'rect() | inset-rect() | auto',
|
||||
|
@ -368,7 +330,7 @@ self.parserlib = (() => {
|
|||
'font-variant-ligatures': '<font-variant-ligatures> | normal | none',
|
||||
'font-variant-numeric': '<font-variant-numeric> | normal',
|
||||
'font-variant-position': 'normal | sub | super',
|
||||
'font-variation-settings': 'normal | [ <string> <number>]#',
|
||||
'font-variation-settings': 'normal | [ <string> <number> ]#',
|
||||
'font-weight': '<font-weight>',
|
||||
'-ms-flex-align': 'start | end | center | stretch | baseline',
|
||||
'-ms-flex-order': '<number>',
|
||||
|
@ -508,6 +470,7 @@ self.parserlib = (() => {
|
|||
'outline-style': '<border-style> | auto',
|
||||
'outline-width': '<border-width>',
|
||||
'overflow': '<overflow>{1,2}',
|
||||
'overflow-anchor': 'auto | none',
|
||||
'overflow-block': '<overflow>',
|
||||
'overflow-inline': '<overflow>',
|
||||
'overflow-style': 1,
|
||||
|
@ -578,6 +541,34 @@ self.parserlib = (() => {
|
|||
|
||||
// S
|
||||
'scale': 'none | <number>{1,3}',
|
||||
|
||||
'scroll-behavior': 'auto | smooth',
|
||||
'scroll-margin': '<length>{1,4}',
|
||||
'scroll-margin-bottom': '<length>',
|
||||
'scroll-margin-left': '<length>',
|
||||
'scroll-margin-right': '<length>',
|
||||
'scroll-margin-top': '<length>',
|
||||
'scroll-margin-block': '<length>{1,2}',
|
||||
'scroll-margin-block-end': '<length>',
|
||||
'scroll-margin-block-start': '<length>',
|
||||
'scroll-margin-inline': '<length>{1,2}',
|
||||
'scroll-margin-inline-end': '<length>',
|
||||
'scroll-margin-inline-start': '<length>',
|
||||
'scroll-padding': '<auto-length-pct>{1,4}',
|
||||
'scroll-padding-left': '<auto-length-pct>',
|
||||
'scroll-padding-right': '<auto-length-pct>',
|
||||
'scroll-padding-top': '<auto-length-pct>',
|
||||
'scroll-padding-bottom': '<auto-length-pct>',
|
||||
'scroll-padding-block': '<auto-length-pct>{1,2}',
|
||||
'scroll-padding-block-end': '<auto-length-pct>',
|
||||
'scroll-padding-block-start': '<auto-length-pct>',
|
||||
'scroll-padding-inline': '<auto-length-pct>{1,2}',
|
||||
'scroll-padding-inline-end': '<auto-length-pct>',
|
||||
'scroll-padding-inline-start': '<auto-length-pct>',
|
||||
'scroll-snap-align': '[ none | start | end | center ]{1,2}',
|
||||
'scroll-snap-stop': 'normal | always',
|
||||
'scroll-snap-type': 'none | [ x | y | block | inline | both ] [ mandatory | proximity ]?',
|
||||
|
||||
'scrollbar-color': 'auto | dark | light | <color>{2}',
|
||||
'scrollbar-width': 'auto | thin | none',
|
||||
'shape-inside': 'auto | outside-shape | [ <basic-shape> || shape-box ] | <image> | display',
|
||||
|
@ -689,6 +680,25 @@ self.parserlib = (() => {
|
|||
'-webkit-text-stroke-width': '<border-width>',
|
||||
};
|
||||
|
||||
for (const [k, reps] of Object.entries({
|
||||
'border': '{1,4}',
|
||||
'border-bottom': '',
|
||||
'border-left': '',
|
||||
'border-right': '',
|
||||
'border-top': '',
|
||||
'border-block': '{1,2}',
|
||||
'border-block-end': '',
|
||||
'border-block-start': '',
|
||||
'border-inline': '{1,2}',
|
||||
'border-inline-end': '',
|
||||
'border-inline-start': '',
|
||||
})) {
|
||||
Properties[k] = '<border-shorthand>';
|
||||
Properties[`${k}-color`] = '<color>' + reps;
|
||||
Properties[`${k}-style`] = '<border-style>' + reps;
|
||||
Properties[`${k}-width`] = '<border-width>' + reps;
|
||||
}
|
||||
|
||||
//endregion
|
||||
//region ValidationTypes - definitions
|
||||
|
||||
|
@ -710,6 +720,8 @@ self.parserlib = (() => {
|
|||
|
||||
'<attachment>': 'scroll | fixed | local',
|
||||
|
||||
'<auto-length-pct>': 'auto | <length> | <percentage>',
|
||||
|
||||
'<basic-shape>': 'inset() | circle() | ellipse() | polygon()',
|
||||
|
||||
'<bg-image>': '<image> | none',
|
||||
|
@ -2722,6 +2734,10 @@ self.parserlib = (() => {
|
|||
|
||||
constructor(input) {
|
||||
this._reader = new StringReader(input ? input.toString() : '');
|
||||
this.resetLT();
|
||||
}
|
||||
|
||||
resetLT() {
|
||||
// Token object for the last consumed token.
|
||||
this._token = null;
|
||||
// Lookahead token buffer.
|
||||
|
@ -3148,8 +3164,7 @@ self.parserlib = (() => {
|
|||
*/
|
||||
atRuleToken(first, pos) {
|
||||
this._reader.mark();
|
||||
const ident = this.readName();
|
||||
let rule = first + ident;
|
||||
let rule = first + this.readName();
|
||||
let tt = Tokens.type(lower(rule));
|
||||
// if it's not valid, use the first character only and reset the reader
|
||||
if (tt === Tokens.CHAR || tt === Tokens.UNKNOWN) {
|
||||
|
@ -3850,6 +3865,7 @@ self.parserlib = (() => {
|
|||
* @param {Boolean} [options.starHack] - allows IE6 star hack
|
||||
* @param {Boolean} [options.underscoreHack] - interprets leading underscores as IE6-7 for known properties
|
||||
* @param {Boolean} [options.ieFilters] - accepts IE < 8 filters instead of throwing syntax errors
|
||||
* @param {Boolean} [options.emptyDocument] - accepts @document without {} block produced by stylus-lang
|
||||
*/
|
||||
constructor(options) {
|
||||
super();
|
||||
|
@ -4005,24 +4021,19 @@ self.parserlib = (() => {
|
|||
|
||||
_supportsCondition() {
|
||||
const stream = this._tokenStream;
|
||||
if (stream.match(Tokens.IDENT)) {
|
||||
const ident = lower(stream._token.value);
|
||||
if (ident === 'not') {
|
||||
stream.mustMatch(Tokens.S);
|
||||
this._supportsConditionInParens();
|
||||
} else {
|
||||
stream.unget();
|
||||
}
|
||||
const next = stream.LT(1);
|
||||
if (next.type === Tokens.IDENT && lower(next.value) === 'not') {
|
||||
stream.get();
|
||||
stream.mustMatch(Tokens.S);
|
||||
this._supportsConditionInParens();
|
||||
} else {
|
||||
this._supportsConditionInParens();
|
||||
this._ws();
|
||||
while (stream.peek() === Tokens.IDENT) {
|
||||
const ident = lower(stream.LT(1).value);
|
||||
if (ident === 'and' || ident === 'or') {
|
||||
stream.mustMatch(Tokens.IDENT);
|
||||
stream.get();
|
||||
this._ws();
|
||||
this._supportsConditionInParens();
|
||||
this._ws();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4030,36 +4041,41 @@ self.parserlib = (() => {
|
|||
|
||||
_supportsConditionInParens() {
|
||||
const stream = this._tokenStream;
|
||||
if (stream.match(Tokens.LPAREN)) {
|
||||
const next = stream.LT(1);
|
||||
if (next.type === Tokens.LPAREN) {
|
||||
stream.get();
|
||||
this._ws();
|
||||
if (stream.match([Tokens.IDENT, Tokens.CUSTOM_PROP])) {
|
||||
// look ahead for not keyword,
|
||||
// if not given, continue with declaration condition.
|
||||
const ident = lower(stream._token.value);
|
||||
if (ident === 'not') {
|
||||
this._ws();
|
||||
const {type, value} = stream.LT(1);
|
||||
if (type === Tokens.IDENT || type === Tokens.CUSTOM_PROP) {
|
||||
if (lower(value) === 'not') {
|
||||
this._supportsCondition();
|
||||
stream.mustMatch(Tokens.RPAREN);
|
||||
} else {
|
||||
stream.unget();
|
||||
this._supportsDeclarationCondition(false);
|
||||
this._supportsDecl(false);
|
||||
}
|
||||
} else {
|
||||
this._supportsCondition();
|
||||
stream.mustMatch(Tokens.RPAREN);
|
||||
}
|
||||
} else if (next.type === Tokens.FUNCTION && lower(next.value) === 'selector(') {
|
||||
stream.get();
|
||||
this._ws();
|
||||
this._selector();
|
||||
stream.mustMatch(Tokens.RPAREN);
|
||||
} else {
|
||||
this._supportsDeclarationCondition();
|
||||
this._supportsDecl();
|
||||
}
|
||||
this._ws();
|
||||
}
|
||||
|
||||
_supportsDeclarationCondition(requireStartParen = true) {
|
||||
_supportsDecl(requireStartParen = true) {
|
||||
const stream = this._tokenStream;
|
||||
if (requireStartParen) {
|
||||
this._tokenStream.mustMatch(Tokens.LPAREN);
|
||||
stream.mustMatch(Tokens.LPAREN);
|
||||
}
|
||||
this._ws();
|
||||
this._declaration();
|
||||
this._tokenStream.mustMatch(Tokens.RPAREN);
|
||||
stream.mustMatch(Tokens.RPAREN);
|
||||
}
|
||||
|
||||
_media() {
|
||||
|
@ -4311,18 +4327,18 @@ self.parserlib = (() => {
|
|||
_document() {
|
||||
const stream = this._tokenStream;
|
||||
const functions = [];
|
||||
let prefix = '';
|
||||
|
||||
const start = stream.mustMatch(Tokens.DOCUMENT_SYM);
|
||||
if (/^@-([^-]+)-/.test(start.value)) {
|
||||
prefix = RegExp.$1;
|
||||
}
|
||||
|
||||
const prefix = start.value.split('-')[1] || '';
|
||||
do {
|
||||
this._ws();
|
||||
functions.push(this._documentFunction());
|
||||
} while (stream.match(Tokens.COMMA));
|
||||
|
||||
this._ws();
|
||||
if (this.options.emptyDocument && stream.peek() !== Tokens.LBRACE) {
|
||||
this.fire({type: 'emptydocument', functions, prefix}, start);
|
||||
return;
|
||||
}
|
||||
stream.mustMatch(Tokens.LBRACE);
|
||||
|
||||
this.fire({
|
||||
|
@ -4358,9 +4374,12 @@ self.parserlib = (() => {
|
|||
|
||||
_documentFunction() {
|
||||
const stream = this._tokenStream;
|
||||
return stream.match(Tokens.URI) ?
|
||||
new PropertyValuePart(stream._token) :
|
||||
this._function();
|
||||
if (stream.match(Tokens.URI)) {
|
||||
const res = new PropertyValuePart(stream._token);
|
||||
this._ws();
|
||||
return res;
|
||||
}
|
||||
return this._function();
|
||||
}
|
||||
|
||||
_operator(inFunction) {
|
||||
|
@ -5296,19 +5315,62 @@ self.parserlib = (() => {
|
|||
throw new SyntaxError('Unknown @ rule.', lt0);
|
||||
}
|
||||
|
||||
this.fire({
|
||||
type: 'error',
|
||||
error: null,
|
||||
message: 'Unknown @ rule: ' + lt0.value + '.',
|
||||
}, lt0);
|
||||
this._ws();
|
||||
const simpleValue =
|
||||
stream.match([Tokens.IDENT, Tokens.CUSTOM_PROP]) && SyntaxUnit.fromToken(stream._token) ||
|
||||
stream.peek() === Tokens.FUNCTION && this._function({asText: true}) ||
|
||||
this._unknownBlock([Tokens.LBRACKET, Tokens.LPAREN]);
|
||||
|
||||
// skip {} block
|
||||
let count = 0;
|
||||
do {
|
||||
const brace = stream.advance([Tokens.LBRACE, Tokens.RBRACE]);
|
||||
count += brace === Tokens.LBRACE ? 1 : -1;
|
||||
} while (count > 0 && !stream._reader.eof());
|
||||
if (count < 0) stream.unget();
|
||||
this._ws();
|
||||
const blockValue = this._unknownBlock();
|
||||
if (!blockValue) {
|
||||
stream.match(Tokens.SEMICOLON);
|
||||
}
|
||||
|
||||
this.fire({
|
||||
type: 'unknown-at-rule',
|
||||
name: lt0.value,
|
||||
simpleValue,
|
||||
blockValue,
|
||||
}, lt0);
|
||||
this._ws();
|
||||
}
|
||||
|
||||
_unknownBlock(canStartWith = [Tokens.LBRACE]) {
|
||||
const stream = this._tokenStream;
|
||||
if (!canStartWith.includes(stream.peek())) {
|
||||
return null;
|
||||
}
|
||||
stream.get();
|
||||
const start = stream._token;
|
||||
const reader = stream._reader;
|
||||
reader.mark();
|
||||
reader._cursor = start.offset;
|
||||
reader._line = start.startLine;
|
||||
reader._col = start.startCol;
|
||||
const value = [];
|
||||
const endings = [];
|
||||
let blockEnd;
|
||||
while (!reader.eof()) {
|
||||
const chunk = reader.readMatch(/[^{}()[\]]*[{}()[\]]?/y);
|
||||
const c = chunk.slice(-1);
|
||||
value.push(chunk);
|
||||
if (c === '{' || c === '(' || c === '[') {
|
||||
endings.push(blockEnd);
|
||||
blockEnd = c === '{' ? '}' : c === '(' ? ')' : ']';
|
||||
} else if (c === '}' || c === ')' || c === ']') {
|
||||
if (c !== blockEnd) {
|
||||
break;
|
||||
}
|
||||
blockEnd = endings.pop();
|
||||
if (!blockEnd) {
|
||||
stream.resetLT();
|
||||
return new SyntaxUnit(value.join(''), start);
|
||||
}
|
||||
}
|
||||
}
|
||||
reader.reset();
|
||||
return null;
|
||||
}
|
||||
|
||||
_unexpectedToken(token) {
|
||||
|
@ -5406,18 +5468,28 @@ self.parserlib = (() => {
|
|||
Object.assign(Parser.prototype, TYPES);
|
||||
Parser.prototype._readWhitespace = Parser.prototype._ws;
|
||||
|
||||
const symDocument = [Tokens.DOCUMENT_SYM, Parser.prototype._document];
|
||||
const symDocMisplaced = [Tokens.DOCUMENT_SYM, Parser.prototype._documentMisplaced];
|
||||
const symFontFace = [Tokens.FONT_FACE_SYM, Parser.prototype._fontFace];
|
||||
const symKeyframes = [Tokens.KEYFRAMES_SYM, Parser.prototype._keyframes];
|
||||
const symMedia = [Tokens.MEDIA_SYM, Parser.prototype._media];
|
||||
const symPage = [Tokens.PAGE_SYM, Parser.prototype._page];
|
||||
const symSupports = [Tokens.SUPPORTS_SYM, Parser.prototype._supports];
|
||||
const symUnknown = [Tokens.UNKNOWN_SYM, Parser.prototype._unknownSym];
|
||||
const symViewport = [Tokens.VIEWPORT_SYM, Parser.prototype._viewport];
|
||||
|
||||
Parser.ACTIONS = {
|
||||
|
||||
stylesheet: new Map([
|
||||
[Tokens.MEDIA_SYM, Parser.prototype._media],
|
||||
[Tokens.DOCUMENT_SYM, Parser.prototype._document],
|
||||
[Tokens.SUPPORTS_SYM, Parser.prototype._supports],
|
||||
[Tokens.PAGE_SYM, Parser.prototype._page],
|
||||
[Tokens.FONT_FACE_SYM, Parser.prototype._fontFace],
|
||||
[Tokens.KEYFRAMES_SYM, Parser.prototype._keyframes],
|
||||
[Tokens.VIEWPORT_SYM, Parser.prototype._viewport],
|
||||
symMedia,
|
||||
symDocument,
|
||||
symSupports,
|
||||
symPage,
|
||||
symFontFace,
|
||||
symKeyframes,
|
||||
symViewport,
|
||||
symUnknown,
|
||||
[Tokens.S, Parser.prototype._ws],
|
||||
[Tokens.UNKNOWN_SYM, Parser.prototype._unknownSym],
|
||||
]),
|
||||
|
||||
stylesheetMisplaced: new Map([
|
||||
|
@ -5427,31 +5499,34 @@ self.parserlib = (() => {
|
|||
]),
|
||||
|
||||
document: new Map([
|
||||
[Tokens.MEDIA_SYM, Parser.prototype._media],
|
||||
[Tokens.DOCUMENT_SYM, Parser.prototype._documentMisplaced],
|
||||
[Tokens.SUPPORTS_SYM, Parser.prototype._supports],
|
||||
[Tokens.PAGE_SYM, Parser.prototype._page],
|
||||
[Tokens.FONT_FACE_SYM, Parser.prototype._fontFace],
|
||||
[Tokens.VIEWPORT_SYM, Parser.prototype._viewport],
|
||||
[Tokens.KEYFRAMES_SYM, Parser.prototype._keyframes],
|
||||
symMedia,
|
||||
symDocMisplaced,
|
||||
symSupports,
|
||||
symPage,
|
||||
symFontFace,
|
||||
symViewport,
|
||||
symKeyframes,
|
||||
symUnknown,
|
||||
]),
|
||||
|
||||
supports: new Map([
|
||||
[Tokens.KEYFRAMES_SYM, Parser.prototype._keyframes],
|
||||
[Tokens.MEDIA_SYM, Parser.prototype._media],
|
||||
[Tokens.SUPPORTS_SYM, Parser.prototype._supports],
|
||||
[Tokens.DOCUMENT_SYM, Parser.prototype._documentMisplaced],
|
||||
[Tokens.VIEWPORT_SYM, Parser.prototype._viewport],
|
||||
symKeyframes,
|
||||
symMedia,
|
||||
symSupports,
|
||||
symDocMisplaced,
|
||||
symViewport,
|
||||
symUnknown,
|
||||
]),
|
||||
|
||||
media: new Map([
|
||||
[Tokens.KEYFRAMES_SYM, Parser.prototype._keyframes],
|
||||
[Tokens.MEDIA_SYM, Parser.prototype._media],
|
||||
[Tokens.DOCUMENT_SYM, Parser.prototype._documentMisplaced],
|
||||
[Tokens.SUPPORTS_SYM, Parser.prototype._supports],
|
||||
[Tokens.PAGE_SYM, Parser.prototype._page],
|
||||
[Tokens.FONT_FACE_SYM, Parser.prototype._fontFace],
|
||||
[Tokens.VIEWPORT_SYM, Parser.prototype._viewport],
|
||||
symKeyframes,
|
||||
symMedia,
|
||||
symDocMisplaced,
|
||||
symSupports,
|
||||
symPage,
|
||||
symFontFace,
|
||||
symViewport,
|
||||
symUnknown,
|
||||
]),
|
||||
|
||||
simpleSelectorSequence: new Map([
|
||||
|
|
2
vendor/codemirror/README.md
vendored
2
vendor/codemirror/README.md
vendored
|
@ -1,4 +1,4 @@
|
|||
## codemirror v5.56.0
|
||||
## codemirror v5.58.0
|
||||
|
||||
Following files are copied from npm (node_modules):
|
||||
|
||||
|
|
4
vendor/codemirror/addon/comment/comment.js
vendored
4
vendor/codemirror/addon/comment/comment.js
vendored
|
@ -13,7 +13,7 @@
|
|||
|
||||
var noOptions = {};
|
||||
var nonWS = /[^\s\u00a0]/;
|
||||
var Pos = CodeMirror.Pos;
|
||||
var Pos = CodeMirror.Pos, cmp = CodeMirror.cmpPos;
|
||||
|
||||
function firstNonWS(str) {
|
||||
var found = str.search(nonWS);
|
||||
|
@ -126,7 +126,9 @@
|
|||
if (i != end || lastLineHasText)
|
||||
self.replaceRange(lead + pad, Pos(i, 0));
|
||||
} else {
|
||||
var atCursor = cmp(self.getCursor("to"), to) == 0, empty = !self.somethingSelected()
|
||||
self.replaceRange(endString, to);
|
||||
if (atCursor) self.setSelection(empty ? to : self.getCursor("from"), to)
|
||||
self.replaceRange(startString, from);
|
||||
}
|
||||
});
|
||||
|
|
24
vendor/codemirror/addon/lint/lint.css
vendored
24
vendor/codemirror/addon/lint/lint.css
vendored
|
@ -25,22 +25,20 @@
|
|||
-ms-transition: opacity .4s;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning {
|
||||
.CodeMirror-lint-mark {
|
||||
background-position: left bottom;
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark-error {
|
||||
background-image:
|
||||
url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==")
|
||||
;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark-warning {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning {
|
||||
.CodeMirror-lint-mark-error {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker {
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
|
@ -51,20 +49,20 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-message-error, .CodeMirror-lint-message-warning {
|
||||
.CodeMirror-lint-message {
|
||||
padding-left: 18px;
|
||||
background-position: top left;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-multiple {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC");
|
||||
background-repeat: no-repeat;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user