This commit is contained in:
derv82 2017-12-07 20:54:42 -08:00
commit cab2345bc6
22 changed files with 498 additions and 249 deletions

View File

@ -48,8 +48,10 @@ globals:
tHTML: false tHTML: false
tNodeList: false tNodeList: false
tDocLoader: false tDocLoader: false
tWordBreak: false
# dom.js # dom.js
onDOMready: false onDOMready: false
onDOMscriptReady: false
scrollElementIntoView: false scrollElementIntoView: false
enforceInputRange: false enforceInputRange: false
animateElement: false animateElement: false

View File

@ -105,7 +105,11 @@
}, },
"configureStyle": { "configureStyle": {
"message": "Configure", "message": "Configure",
"description": "Label for the button to configure userstyle" "description": "Label for the button to configure usercss userstyle"
},
"configureStyleOnHomepage": {
"message": "Configure on homepage",
"description": "Label for the button to configure userstyles.org userstyle"
}, },
"checkForUpdate": { "checkForUpdate": {
"message": "Check for update", "message": "Check for update",

View File

@ -33,6 +33,7 @@
<script src="edit/beautify.js"></script> <script src="edit/beautify.js"></script>
<script src="edit/sections.js"></script> <script src="edit/sections.js"></script>
<script src="edit/show-keymap-help.js"></script> <script src="edit/show-keymap-help.js"></script>
<script src="edit/codemirror-editing-hooks.js"></script>
<script src="edit/edit.js"></script> <script src="edit/edit.js"></script>
<script src="vendor/codemirror/lib/codemirror.js"></script> <script src="vendor/codemirror/lib/codemirror.js"></script>
@ -67,8 +68,11 @@
<script src="vendor/codemirror/keymap/emacs.js"></script> <script src="vendor/codemirror/keymap/emacs.js"></script>
<script src="vendor/codemirror/keymap/vim.js"></script> <script src="vendor/codemirror/keymap/vim.js"></script>
<link href="/vendor-overwrites/colorpicker/colorpicker.css" rel="stylesheet">
<script src="/vendor-overwrites/colorpicker/colorpicker.js"></script>
<script src="/vendor-overwrites/colorpicker/colorview.js"></script>
<script src="edit/match-highlighter-helper.js"></script> <script src="edit/match-highlighter-helper.js"></script>
<script src="edit/codemirror-editing-hooks.js"></script>
<script src="edit/codemirror-default.js"></script> <script src="edit/codemirror-default.js"></script>
<link rel="stylesheet" href="/edit/codemirror-default.css"> <link rel="stylesheet" href="/edit/codemirror-default.css">

View File

@ -25,12 +25,12 @@
styleActiveLine: true, styleActiveLine: true,
theme: 'default', theme: 'default',
keyMap: prefs.get('editor.keyMap'), keyMap: prefs.get('editor.keyMap'),
extraKeys: { extraKeys: Object.assign(CodeMirror.defaults.extraKeys || {}, {
// independent of current keyMap // independent of current keyMap
'Alt-Enter': 'toggleStyle', 'Alt-Enter': 'toggleStyle',
'Alt-PageDown': 'nextEditor', 'Alt-PageDown': 'nextEditor',
'Alt-PageUp': 'prevEditor' 'Alt-PageUp': 'prevEditor'
}, }),
maxHighlightLength: 100e3, maxHighlightLength: 100e3,
}; };

View File

@ -5,7 +5,8 @@ global save toggleStyle setupAutocomplete makeSectionVisible getSectionForChild
*/ */
'use strict'; 'use strict';
onDOMready().then(() => { onDOMscriptReady('/codemirror.js').then(() => {
CodeMirror.defaults.lint = linterConfig.getForCodeMirror(); CodeMirror.defaults.lint = linterConfig.getForCodeMirror();
const COMMANDS = { const COMMANDS = {
@ -46,11 +47,16 @@ onDOMready().then(() => {
// cm.state.search for last used 'find' // cm.state.search for last used 'find'
let searchState; let searchState;
onDOMready().then(() => {
prefs.subscribe(['editor.keyMap'], showKeyInSaveButtonTooltip);
showKeyInSaveButtonTooltip();
// N.B. the event listener should be registered before setupLivePrefs() // N.B. the event listener should be registered before setupLivePrefs()
$('#options').addEventListener('change', onOptionElementChanged); $('#options').addEventListener('change', onOptionElementChanged);
buildOptionsElements(); buildOptionsElements();
setupLivePrefs();
rerouteHotkeys(true); rerouteHotkeys(true);
});
return; return;
@ -677,4 +683,23 @@ onDOMready().then(() => {
}); });
}); });
} }
function showKeyInSaveButtonTooltip(prefName, value) {
$('#save-button').title = findKeyForCommand('save', value);
}
function findKeyForCommand(command, mapName = CodeMirror.defaults.keyMap) {
const map = CodeMirror.keyMap[mapName];
let key = Object.keys(map).find(k => map[k] === command);
if (key) {
return key;
}
for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) {
key = ft && findKeyForCommand(command, ft);
if (key) {
return key;
}
}
return '';
}
}); });

View File

@ -1,34 +1,23 @@
/* global CodeMirror loadScript editors showHelp */ /* global CodeMirror loadScript editors showHelp */
'use strict'; 'use strict';
// eslint-disable-next-line no-var onDOMscriptReady('/colorview.js').then(() => {
var initColorpicker = () => {
initOverlayHooks(); initOverlayHooks();
onDOMready().then(() => { onDOMready().then(() => {
$('#colorpicker-settings').onclick = configureColorpicker; $('#colorpicker-settings').onclick = configureColorpicker;
}); });
const scripts = [
'/vendor-overwrites/colorpicker/colorpicker.css',
'/vendor-overwrites/colorpicker/colorpicker.js',
'/vendor-overwrites/colorpicker/colorview.js',
];
prefs.subscribe(['editor.colorpicker.hotkey'], registerHotkey); prefs.subscribe(['editor.colorpicker.hotkey'], registerHotkey);
prefs.subscribe(['editor.colorpicker'], colorpickerOnDemand); prefs.subscribe(['editor.colorpicker'], setColorpickerOption);
return prefs.get('editor.colorpicker') && colorpickerOnDemand(null, true); setColorpickerOption(null, prefs.get('editor.colorpicker'));
function colorpickerOnDemand(id, enabled) {
return loadScript(enabled && scripts)
.then(() => setColorpickerOption(id, enabled));
}
function setColorpickerOption(id, enabled) { function setColorpickerOption(id, enabled) {
const defaults = CodeMirror.defaults; const defaults = CodeMirror.defaults;
const keyName = prefs.get('editor.colorpicker.hotkey'); const keyName = prefs.get('editor.colorpicker.hotkey');
delete defaults.extraKeys[keyName];
defaults.colorpicker = enabled; defaults.colorpicker = enabled;
if (enabled) { if (enabled) {
if (keyName) { if (keyName) {
CodeMirror.commands.colorpicker = invokeColorpicker; CodeMirror.commands.colorpicker = invokeColorpicker;
defaults.extraKeys = defaults.extraKeys || {};
defaults.extraKeys[keyName] = 'colorpicker'; defaults.extraKeys[keyName] = 'colorpicker';
} }
defaults.colorpicker = { defaults.colorpicker = {
@ -45,6 +34,11 @@ var initColorpicker = () => {
}, },
}, },
}; };
} else {
CodeMirror.modeExtensions.css.unregisterColorviewHooks();
if (defaults.extraKeys) {
delete defaults.extraKeys[keyName];
}
} }
// on page load runs before CodeMirror.setOption is defined // on page load runs before CodeMirror.setOption is defined
editors.forEach(cm => cm.setOption('colorpicker', defaults.colorpicker)); editors.forEach(cm => cm.setOption('colorpicker', defaults.colorpicker));
@ -162,4 +156,4 @@ var initColorpicker = () => {
return style; return style;
} }
} }
}; });

View File

@ -576,9 +576,6 @@ html:not(.usercss) .applies-to li:last-child .add-applies-to {
top: 1em; top: 1em;
margin: 1ex 0; margin: 1ex 0;
} }
.firefox .beautify-options > label input {
top: 1px;
}
.beautify-options:after { .beautify-options:after {
clear: both; clear: both;
display: block; display: block;
@ -809,9 +806,3 @@ html:not(.usercss) .usercss-only,
white-space: normal; white-space: normal;
} }
} }
@supports (-moz-appearance: none) {
#header button {
padding: 0 3px 2px;
}
}

View File

@ -3,8 +3,6 @@ global CodeMirror parserlib loadScript
global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter global CSSLint initLint linterConfig updateLintReport renderLintReport updateLinter
global mozParser createSourceEditor global mozParser createSourceEditor
global closeCurrentTab regExpTester messageBox global closeCurrentTab regExpTester messageBox
global initColorpicker
global initCollapsibles
global setupCodeMirror global setupCodeMirror
global beautify global beautify
global initWithSectionStyle addSections removeSection getSectionsHashes global initWithSectionStyle addSections removeSection getSectionsHashes
@ -17,8 +15,6 @@ let dirty = {};
// array of all CodeMirror instances // array of all CodeMirror instances
const editors = []; const editors = [];
let saveSizeOnClose; let saveSizeOnClose;
// use browser history back when 'back to manage' is clicked
let useHistoryBack;
// direct & reverse mapping of @-moz-document keywords and internal property names // direct & reverse mapping of @-moz-document keywords and internal property names
const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'}; const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'};
@ -26,40 +22,27 @@ const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'do
let editor; let editor;
preinit();
window.onbeforeunload = beforeUnload; window.onbeforeunload = beforeUnload;
chrome.runtime.onMessage.addListener(onRuntimeMessage); chrome.runtime.onMessage.addListener(onRuntimeMessage);
preinit();
Promise.all([ Promise.all([
initStyleData().then(style => { initStyleData(),
styleId = style.id;
sessionStorage.justEditedStyleId = styleId;
// we set "usercss" class on <html> when <body> is empty
// so there'll be no flickering of the elements that depend on it
if (isUsercss(style)) {
document.documentElement.classList.add('usercss');
}
// strip URL parameters when invoked for a non-existent id
if (!styleId) {
history.replaceState({}, document.title, location.pathname);
}
return style;
}),
onDOMready(), onDOMready(),
onBackgroundReady(),
]) ])
.then(([style]) => Promise.all([
style,
initColorpicker(),
initCollapsibles(),
initHooksCommon(),
]))
.then(([style]) => { .then(([style]) => {
setupLivePrefs();
const usercss = isUsercss(style); const usercss = isUsercss(style);
$('#heading').textContent = t(styleId ? 'editStyleHeading' : 'addStyleTitle'); $('#heading').textContent = t(styleId ? 'editStyleHeading' : 'addStyleTitle');
$('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName'); $('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
$('#name').title = usercss ? t('usercssReplaceTemplateName') : ''; $('#name').title = usercss ? t('usercssReplaceTemplateName') : '';
$('#beautify').onclick = beautify;
$('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true}); $('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true});
window.addEventListener('resize', () => debounce(rememberWindowSize, 100));
if (usercss) { if (usercss) {
editor = createSourceEditor(style); editor = createSourceEditor(style);
} else { } else {
@ -99,30 +82,6 @@ function preinit() {
'vendor/codemirror/theme/' + prefs.get('editor.theme') + '.css' 'vendor/codemirror/theme/' + prefs.get('editor.theme') + '.css'
})); }));
// forcefully break long labels in aligned options to prevent the entire block layout from breaking
onDOMready().then(() => new Promise(requestAnimationFrame)).then(() => {
const maxWidth2ndChild = $$('#options .aligned > :nth-child(2)')
.sort((a, b) => b.offsetWidth - a.offsetWidth)[0].offsetWidth;
const widthFor1stChild = $('#options').offsetWidth - maxWidth2ndChild;
if (widthFor1stChild > 50) {
for (const el of $$('#options .aligned > :nth-child(1)')) {
if (el.offsetWidth > widthFor1stChild) {
el.style.cssText = 'word-break: break-all; hyphens: auto;';
}
}
} else {
const width = $('#options').clientWidth;
document.head.appendChild($create('style', `
#options .aligned > nth-child(1) {
max-width: 70px;
}
#options .aligned > nth-child(2) {
max-width: ${width - 70}px;
}
`));
}
});
if (chrome.windows) { if (chrome.windows) {
queryTabs({currentWindow: true}).then(tabs => { queryTabs({currentWindow: true}).then(tabs => {
const windowId = tabs[0].windowId; const windowId = tabs[0].windowId;
@ -151,7 +110,18 @@ function preinit() {
getOwnTab().then(tab => { getOwnTab().then(tab => {
const ownTabId = tab.id; const ownTabId = tab.id;
useHistoryBack = sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href;
// 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) { if (!chrome.windows) {
return; return;
} }
@ -258,9 +228,20 @@ function initStyleData() {
) )
], ],
}); });
return !id ? return getStylesSafe({id: id || -1})
Promise.resolve(createEmptyStyle()) : .then(([style = createEmptyStyle()]) => {
getStylesSafe({id}).then(([style]) => style || createEmptyStyle()); styleId = sessionStorage.justEditedStyleId = style.id;
// we set "usercss" class on <html> when <body> is empty
// so there'll be no flickering of the elements that depend on it
if (isUsercss(style)) {
document.documentElement.classList.add('usercss');
}
// strip URL parameters when invoked for a non-existent id
if (!styleId) {
history.replaceState({}, document.title, location.pathname);
}
return style;
});
} }
function initHooks() { function initHooks() {
@ -293,40 +274,6 @@ function initHooks() {
} }
// common for usercss and classic // common for usercss and classic
function initHooksCommon() {
$('#cancel-button').addEventListener('click', goBackToManage);
$('#beautify').addEventListener('click', beautify);
prefs.subscribe(['editor.keyMap'], showKeyInSaveButtonTooltip);
showKeyInSaveButtonTooltip();
window.addEventListener('resize', () => debounce(rememberWindowSize, 100));
function goBackToManage(event) {
if (useHistoryBack) {
event.stopPropagation();
event.preventDefault();
history.back();
}
}
function showKeyInSaveButtonTooltip(prefName, value) {
$('#save-button').title = findKeyForCommand('save', value);
}
function findKeyForCommand(command, mapName = CodeMirror.defaults.keyMap) {
const map = CodeMirror.keyMap[mapName];
let key = Object.keys(map).find(k => map[k] === command);
if (key) {
return key;
}
for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) {
key = ft && findKeyForCommand(command, ft);
if (key) {
return key;
}
}
return '';
}
}
function onChange(event) { function onChange(event) {
const node = event.target; const node = event.target;

View File

@ -1,3 +1,39 @@
button {
-webkit-appearance: none;
-moz-appearance: none;
user-select: none;
padding: 2px 7px;
border: 1px solid hsl(0, 0%, 62%);
font: 400 13.3333px Arial;
color: #000;
background-color: hsl(0, 0%, 100%);
background: url(../images/button.png)repeat-x;
background-size: 100% 100%;
transition: background-color .25s, border-color .25s;
}
button:hover {
background-color: hsl(0, 0%, 95%);
border-color: hsl(0, 0%, 52%);
}
/* For some odd reason these hovers appear lighter than all other button hovers in every browser */
#message-box-buttons button:hover {
background-color: hsl(0, 0%, 90%);
border-color: hsl(0, 0%, 50%);
}
input:not([type]) {
background: #fff;
color: #000;
height: 22px;
min-height: 22px!important;
line-height: 22px;
padding: 0 3px;
font: 400 13.3333px Arial;
border: 1px solid hsl(0, 0%, 66%);
}
.svg-icon.checked { .svg-icon.checked {
position: absolute; position: absolute;
height: 8px; height: 8px;
@ -13,12 +49,11 @@ input[type="checkbox"]:not(.slider):checked + .svg-icon.checked {
} }
input[type="checkbox"]:not(.slider) { input[type="checkbox"]:not(.slider) {
-webkit-appearance: none;
-moz-appearance: none;
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
-moz-appearance: none;
-webkit-appearance: none;
pointer-events: none;
border: 1px solid hsl(0, 0%, 46%); border: 1px solid hsl(0, 0%, 46%);
height: 12px; height: 12px;
width: 12px; width: 12px;
@ -63,17 +98,14 @@ select {
-moz-appearance: none; -moz-appearance: none;
-webkit-appearance: none; -webkit-appearance: none;
height: 22px; height: 22px;
color: currentColor; font: 400 13.3333px Arial;
color: #000;
background-color: transparent; background-color: transparent;
border: 1px solid hsl(0, 0%, 66%); border: 1px solid hsl(0, 0%, 66%);
padding: 0 20px 0 6px; padding: 0 20px 0 6px;
transition: color .5s; transition: color .5s;
} }
.firefox select {
padding: 0 20px 0 2px;
}
.select-resizer { .select-resizer {
display: inline-flex!important; display: inline-flex!important;
cursor: default; cursor: default;
@ -86,7 +118,7 @@ select {
display: inline-flex; display: inline-flex;
height: 14px; height: 14px;
width: 14px; width: 14px;
fill: currentColor; fill: #000;
position: absolute; position: absolute;
top: 4px; top: 4px;
right: 4px; right: 4px;
@ -102,11 +134,37 @@ select {
-moz-appearance: checkbox !important; -moz-appearance: checkbox !important;
} }
::-moz-focus-inner { .firefox select {
border: 0; font-size: 13px;
padding: 0 20px 0 2px;
line-height: 22px!important;
} }
svg { svg {
transform: scale(1); /* de-blur */ transform: scale(1); /* de-blur */
} }
/* We can customize everything about number inputs except arrows. They're horrible in Linux FF, so we'll hide them unless hovered or focused. */
.firefox.non-windows input[type=number] {
-moz-appearance: textfield;
background: #fff;
color: #000;
border: 1px solid hsl(0, 0%, 66%);
}
.firefox.non-windows input[type="number"]:hover,
.firefox.non-windows input[type="number"]:focus {
-moz-appearance: number-input;
}
/* Firefox cannot handle fractions in font-size */
.firefox button:not(.install) {
font-size: 13px;
line-height: 13px;
padding: 3px 7px;
}
.firefox.moz-appearance-bug button:not(.install) {
padding: 2px 4px;
}
} }

BIN
images/button.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

View File

@ -55,6 +55,7 @@ $$.remove = (selector, base = document) => {
onDOMready().then(() => { onDOMready().then(() => {
$.remove('#firefox-transitions-bug-suppressor'); $.remove('#firefox-transitions-bug-suppressor');
initCollapsibles();
}); });
if (!chrome.app && chrome.windows) { if (!chrome.app && chrome.windows) {
@ -278,9 +279,13 @@ function $createLink(href = '', content) {
} }
// makes <details> with [data-pref] save/restore their state
function initCollapsibles({bindClickOn = 'h2'} = {}) { function initCollapsibles({bindClickOn = 'h2'} = {}) {
const prefMap = {}; const prefMap = {};
const elements = $$('details[data-pref]'); const elements = $$('details[data-pref]');
if (!elements.length) {
return;
}
for (const el of elements) { for (const el of elements) {
const key = el.dataset.pref; const key = el.dataset.pref;

View File

@ -48,13 +48,51 @@ function tHTML(html, tag) {
function tNodeList(nodes) { function tNodeList(nodes) {
const PREFIX = 'i18n-'; const PREFIX = 'i18n-';
for (let n = nodes.length; --n >= 0;) { for (let n = nodes.length; --n >= 0;) {
const node = nodes[n]; const node = nodes[n];
// skip non-ELEMENT_NODE if (node.nodeType !== Node.ELEMENT_NODE) {
if (node.nodeType !== 1) {
continue; continue;
} }
if (node.localName === 'template') { if (node.localName === 'template') {
createTemplate(node);
continue;
}
for (let a = node.attributes.length; --a >= 0;) {
const attr = node.attributes[a];
const name = attr.nodeName;
if (!name.startsWith(PREFIX)) {
continue;
}
const type = name.substr(PREFIX.length);
const value = t(attr.value);
let toInsert, before;
switch (type) {
case 'word-break':
// we already know that: hasWordBreak
break;
case 'text':
before = node.firstChild;
// fallthrough to text-append
case 'text-append':
toInsert = createText(value);
break;
case 'html': {
toInsert = createHtml(value);
break;
}
default:
node.setAttribute(type, value);
}
tDocLoader.pause();
if (toInsert) {
node.insertBefore(toInsert, before || null);
}
node.removeAttribute(name);
}
}
function createTemplate(node) {
const elements = node.content.querySelectorAll('*'); const elements = node.content.querySelectorAll('*');
tNodeList(elements); tNodeList(elements);
template[node.dataset.id] = elements[0]; template[node.dataset.id] = elements[0];
@ -67,37 +105,32 @@ function tNodeList(nodes) {
toRemove.push(textNode); toRemove.push(textNode);
} }
} }
tDocLoader.pause();
toRemove.forEach(el => el.remove()); toRemove.forEach(el => el.remove());
continue;
} }
for (let a = node.attributes.length; --a >= 0;) {
const attr = node.attributes[a]; function createText(str) {
const name = attr.nodeName; return document.createTextNode(tWordBreak(str));
if (!name.startsWith(PREFIX)) {
continue;
} }
const type = name.substr(PREFIX.length);
const value = t(attr.value); function createHtml(value) {
switch (type) { // <a href=foo>bar</a> are the only recognizable HTML elements
case 'text': const rx = /(?:<a\s([^>]*)>([^<]*)<\/a>)?([^<]*)/gi;
node.insertBefore(document.createTextNode(value), node.firstChild); const bin = document.createDocumentFragment();
break; for (let m; (m = rx.exec(value)) && m[0];) {
case 'text-append': const [, linkParams, linkText, nextText] = m;
node.appendChild(document.createTextNode(value)); if (linkText) {
break; const href = /\bhref\s*=\s*(\S+)/.exec(linkParams);
case 'html': const a = bin.appendChild(document.createElement('a'));
// localized strings only allow having text nodes and links a.href = href && href[1].replace(/^(["'])(.*)\1$/, '$2') || '';
node.textContent = ''; a.appendChild(createText(linkText));
[...tHTML(value, 'div').childNodes]
.filter(a => a.nodeType === a.TEXT_NODE || a.tagName === 'A')
.forEach(n => node.appendChild(n));
break;
default:
node.setAttribute(type, value);
} }
node.removeAttribute(name); if (nextText) {
bin.appendChild(createText(nextText));
} }
} }
return bin;
}
} }
@ -115,33 +148,50 @@ function tDocLoader() {
t.cache = {browserUIlanguage: UIlang}; t.cache = {browserUIlanguage: UIlang};
localStorage.L10N = JSON.stringify(t.cache); localStorage.L10N = JSON.stringify(t.cache);
} }
const cacheLength = Object.keys(t.cache).length; const cacheLength = Object.keys(t.cache).length;
// localize HEAD Object.assign(tDocLoader, {
tNodeList(document.getElementsByTagName('*')); observer: new MutationObserver(process),
start() {
if (!tDocLoader.observing) {
tDocLoader.observing = true;
tDocLoader.observer.observe(document, {subtree: true, childList: true});
}
},
stop() {
tDocLoader.pause();
document.removeEventListener('DOMContentLoaded', onLoad);
},
pause() {
if (tDocLoader.observing) {
tDocLoader.observing = false;
tDocLoader.observer.disconnect();
}
},
});
// localize BODY tNodeList(document.getElementsByTagName('*'));
const process = mutations => { tDocLoader.start();
document.addEventListener('DOMContentLoaded', onLoad);
function process(mutations) {
for (const mutation of mutations) { for (const mutation of mutations) {
tNodeList(mutation.addedNodes); tNodeList(mutation.addedNodes);
} }
}; tDocLoader.start();
const observer = new MutationObserver(process); }
const onLoad = () => {
function onLoad() {
tDocLoader.stop(); tDocLoader.stop();
process(observer.takeRecords()); process(tDocLoader.observer.takeRecords());
if (cacheLength !== Object.keys(t.cache).length) { if (cacheLength !== Object.keys(t.cache).length) {
localStorage.L10N = JSON.stringify(t.cache); localStorage.L10N = JSON.stringify(t.cache);
} }
}; }
tDocLoader.start = () => { }
observer.observe(document, {subtree: true, childList: true});
};
tDocLoader.stop = () => { function tWordBreak(text) {
observer.disconnect(); // adds soft hyphens every 10 characters to ensure the long words break before breaking the layout
document.removeEventListener('DOMContentLoaded', onLoad); return text.length <= 10 ? text : text.replace(/[\d\w\u0080-\uFFFF]{10}|((?!\s)\W){10}/g, '$&\u00AD');
};
tDocLoader.start();
document.addEventListener('DOMContentLoaded', onLoad);
} }

View File

@ -44,3 +44,60 @@ var loadScript = (() => {
return Promise.all(files.map(f => (typeof f === 'string' ? inject(f) : f))); return Promise.all(files.map(f => (typeof f === 'string' ? inject(f) : f)));
}; };
})(); })();
(() => {
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 = (src, 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(src);
if (listeners) {
listeners.push(resolve);
} else {
subscribers.set(src, [resolve]);
}
// no need to clear the timer since a resolved Promise won't reject anymore
setTimeout(reject, 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 [subscribedSrc, listeners] of subscribers.entries()) {
if (src.endsWith(subscribedSrc)) {
return {subscribedSrc, listeners};
}
}
}
function notifySubscribers(event) {
this.removeEventListener('load', notifySubscribers);
const {subscribedSrc, listeners = []} = getSubscribersForSrc(this.src) || {};
listeners.forEach(fn => fn(event));
subscribers.delete(subscribedSrc);
if (!subscribers.size) {
observer.disconnect();
observer = null;
subscribers = null;
}
}
})();

View File

@ -259,7 +259,7 @@
<button id="check-all-updates-force" class="hidden" i18n-text="checkAllUpdatesForce"></button> <button id="check-all-updates-force" class="hidden" i18n-text="checkAllUpdatesForce"></button>
</p> </p>
<p id="add-style-wrapper"> <div id="add-style-wrapper">
<a href="edit.html"> <a href="edit.html">
<button id="add-style-label" i18n-text="addStyleLabel"></button> <button id="add-style-label" i18n-text="addStyleLabel"></button>
</a> </a>
@ -275,7 +275,7 @@
</svg> </svg>
</a> </a>
</label> </label>
</p> </div>
<details id="options" data-pref="manage.options.expanded"> <details id="options" data-pref="manage.options.expanded">

View File

@ -1,16 +1,15 @@
#stylus-popup #message-box-contents { #stylus-manage .config-dialog #message-box-contents {
padding: .25rem .75rem; padding: 2em 16px;
}
#stylus-popup .config-dialog #message-box-contents {
padding: 8px 16px;
} }
#stylus-popup .config-body label { #stylus-popup .config-body label {
padding: .5em 0; padding: .5em 0;
} }
#stylus-popup .config-body label > :first-child {
max-width: 140px;
min-width: 140px;
}
.config-heading { .config-heading {
float: right; float: right;
margin: -1.25rem 0 0 0; margin: -1.25rem 0 0 0;
@ -21,9 +20,10 @@
display: flex; display: flex;
padding: .75em 0; padding: .75em 0;
align-items: center; align-items: center;
margin-right: -16px; /* for .config-reset-icon */
} }
.config-dialog .select-resizer { .config-body .select-resizer {
position: static; position: static;
} }
@ -39,25 +39,16 @@
border-top: 1px dotted #ccc; border-top: 1px dotted #ccc;
} }
.config-body label > :first-child { .config-body .dirty {
margin-right: 8px; font-style: italic;
flex-grow: 1;
} }
.config-body label:not([disabled]) > :first-child { .config-body .dirty:after {
cursor: default;
}
.config-dialog .dirty:after {
content: "*"; content: "*";
position: absolute; position: absolute;
left: 6px; left: 6px;
} }
.config-dialog .dirty {
font-style: italic;
}
.config-body input, .config-body input,
.config-body select, .config-body select,
.config-body .onoffswitch { .config-body .onoffswitch {
@ -81,27 +72,77 @@
.config-body .onoffswitch { .config-body .onoffswitch {
height: auto; height: auto;
margin: calc((2em - 12px) / 2) 0; margin: calc((2em - 12px) / 2) 0;
flex-grow: 0;
} }
.config-body input[type="text"] { .config-body input[type="text"] {
padding-left: 0.25em; padding-left: 0.25em;
} }
.config-body label > :last-child { .config-name {
flex-grow: 1;
margin-right: 1em;
}
.config-value {
box-sizing: border-box; box-sizing: border-box;
flex-shrink: 0; flex-shrink: 0;
} }
.config-body label > :last-child:not(.onoffswitch):not(.select-resizer) > :not(:last-child) { .config-value:not(.onoffswitch):not(.select-resizer) > :not(:last-child) {
margin-right: 4px; margin-right: 4px;
} }
.config-body label:not(.nondefault) .config-reset-icon {
visibility: hidden;
}
.svg-icon.config-reset-icon {
/*position: absolute;*/
pointer-events: all !important;
cursor: pointer;
/*right: -7px;*/
fill: #aaa;
width: 16px;
height: 16px;
padding: 0 1px;
box-sizing: border-box;
flex-shrink: 0;
}
.svg-icon.config-reset-icon:hover {
fill: #666;
}
#config-autosave-wrapper { #config-autosave-wrapper {
position: relative; position: relative;
padding: 0 0 0 16px; padding: 0 0 0 16px;
display: inline-flex; display: inline-flex;
} }
#message-box-buttons {
position: relative;
}
.config-error {
position: absolute;
z-index: 99;
left: 0;
right: 0;
bottom: -1rem;
padding: 0 .75rem;
line-height: 24px;
height: 24px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
background-color: red;
color: white;
font-weight: bold;
text-shadow: 0.5px 0.5px 6px #400;
animation: fadein .5s;
}
.cm-colorview::before, .cm-colorview::before,
.color-swatch { .color-swatch {
width: var(--onoffswitch-width) !important; width: var(--onoffswitch-width) !important;
@ -124,3 +165,12 @@
border: none !important; border: none !important;
box-shadow: 3px 3px 50px rgba(0,0,0,.5) !important; box-shadow: 3px 3px 50px rgba(0,0,0,.5) !important;
} }
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

View File

@ -2,6 +2,7 @@
'use strict'; 'use strict';
function configDialog(style) { function configDialog(style) {
const AUTOSAVE_DELAY = 500;
const data = style.usercssData; const data = style.usercssData;
const varsHash = deepCopy(data.vars) || {}; const varsHash = deepCopy(data.vars) || {};
const varNames = Object.keys(varsHash); const varNames = Object.keys(varsHash);
@ -71,15 +72,17 @@ function configDialog(style) {
colorpicker.hide(); colorpicker.hide();
} }
function onchange({target}) { function onchange({target, justSaved = false}) {
// invoked after element's own onchange so 'va' contains the updated value // invoked after element's own onchange so 'va' contains the updated value
const va = target.va; const va = target.va;
if (va) { if (va) {
va.dirty = varsInitial[va.name] !== (isDefault(va) ? va.default : va.value); va.dirty = varsInitial[va.name] !== (isDefault(va) ? va.default : va.value);
if (prefs.get('config.autosave')) { if (prefs.get('config.autosave') && !justSaved) {
debounce(save); debounce(save, 0, {anyChangeIsDirty: true});
} else { return;
target.closest('label').classList.toggle('dirty', va.dirty); }
renderValueState(va);
if (!justSaved) {
updateButtons(); updateButtons();
} }
} }
@ -92,8 +95,9 @@ function configDialog(style) {
buttons.close.textContent = t(someDirty ? 'confirmCancel' : 'confirmClose'); buttons.close.textContent = t(someDirty ? 'confirmCancel' : 'confirmClose');
} }
function save() { function save({anyChangeIsDirty = false} = {}) {
if (!vars.length || !vars.some(va => va.dirty)) { if (!vars.length ||
!vars.some(va => va.dirty || anyChangeIsDirty && va.value !== va.savedValue)) {
return; return;
} }
style.enabled = true; style.enabled = true;
@ -117,10 +121,11 @@ function configDialog(style) {
!isDefault(va) && !isDefault(va) &&
bgva.options.every(o => o.name !== va.value)) { bgva.options.every(o => o.name !== va.value)) {
error = `'${va.value}' not in the updated '${va.type}' list`; error = `'${va.value}' not in the updated '${va.type}' list`;
} else if (!va.dirty) { } else if (!va.dirty && (!anyChangeIsDirty || va.value === va.savedValue)) {
continue; continue;
} else { } else {
styleVars[va.name].value = va.value; styleVars[va.name].value = va.value;
va.savedValue = va.value;
numValid++; numValid++;
continue; continue;
} }
@ -145,10 +150,16 @@ function configDialog(style) {
return BG.usercssHelper.save(style) return BG.usercssHelper.save(style)
.then(saved => { .then(saved => {
varsInitial = getInitialValues(deepCopy(saved.usercssData.vars)); varsInitial = getInitialValues(deepCopy(saved.usercssData.vars));
vars.forEach(va => onchange({target: va.input})); vars.forEach(va => onchange({target: va.input, justSaved: true}));
renderValues();
updateButtons(); updateButtons();
$.remove('.config-error');
}) })
.catch(errors => onhide() + messageBox.alert(Array.isArray(errors) ? errors.join('\n') : errors)); .catch(errors => {
const el = $('.config-error', messageBox.element) ||
$('#message-box-buttons').insertAdjacentElement('afterbegin', $create('.config-error'));
el.textContent = el.title = Array.isArray(errors) ? errors.join('\n') : errors;
});
} }
function useDefault() { function useDefault() {
@ -164,12 +175,19 @@ function configDialog(style) {
} }
function buildConfigForm() { function buildConfigForm() {
let resetter = $create('SVG:svg.svg-icon.config-reset-icon', {viewBox: '0 0 20 20'}, [
$create('SVG:title', t('genericResetLabel')),
$create('SVG:polygon', {
points: '16.2,5.5 14.5,3.8 10,8.3 5.5,3.8 3.8,5.5 8.3,10 3.8,14.5 ' +
'5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10',
}),
]);
for (const va of vars) { for (const va of vars) {
let children; let children;
switch (va.type) { switch (va.type) {
case 'color': case 'color':
children = [ children = [
$create('.cm-colorview', [ $create('.cm-colorview.config-value', [
va.input = $create('.color-swatch', { va.input = $create('.color-swatch', {
va, va,
onclick: showColorpicker onclick: showColorpicker
@ -180,13 +198,11 @@ function configDialog(style) {
case 'checkbox': case 'checkbox':
children = [ children = [
$create('span.onoffswitch', [ $create('span.onoffswitch.config-value', [
va.input = $create('input.slider', { va.input = $create('input.slider', {
va, va,
type: 'checkbox', type: 'checkbox',
onchange() { onchange: updateVarOnChange,
va.value = va.input.checked ? '1' : '0';
},
}), }),
$create('span'), $create('span'),
]), ]),
@ -198,12 +214,10 @@ function configDialog(style) {
case 'image': case 'image':
// TODO: a image picker input? // TODO: a image picker input?
children = [ children = [
$create('.select-resizer', [ $create('.select-resizer.config-value', [
va.input = $create('select', { va.input = $create('select', {
va, va,
onchange() { onchange: updateVarOnChange,
va.value = this.value;
}
}, },
va.options.map(o => va.options.map(o =>
$create('option', {value: o.name}, o.label))), $create('option', {value: o.name}, o.label))),
@ -215,27 +229,43 @@ function configDialog(style) {
default: default:
children = [ children = [
va.input = $create('input', { va.input = $create('input.config-value', {
va, va,
type: 'text', type: 'text',
oninput() { onchange: updateVarOnChange,
va.value = this.value; oninput: updateVarOnInput,
this.dispatchEvent(new Event('change', {bubbles: true}));
},
}), }),
]; ];
break; break;
} }
resetter = resetter.cloneNode(true);
resetter.va = va;
resetter.onclick = resetOnClick;
elements.push( elements.push(
$create(`label.config-${va.type}`, [ $create(`label.config-${va.type}`, [
$create('span', va.label), $create('span.config-name', tWordBreak(va.label)),
...children, ...children,
resetter,
])); ]));
} }
} }
function renderValues() { function updateVarOnChange() {
for (const va of vars) { this.va.value = this.value;
}
function updateVarOnInput(event, debounced = false) {
if (debounced) {
event.target.dispatchEvent(new Event('change', {bubbles: true}));
} else {
debounce(updateVarOnInput, AUTOSAVE_DELAY, event, true);
}
}
function renderValues(varsToRender = vars) {
for (const va of varsToRender) {
const value = isDefault(va) ? va.default : va.value; const value = isDefault(va) ? va.default : va.value;
if (va.type === 'color') { if (va.type === 'color') {
va.input.style.backgroundColor = value; va.input.style.backgroundColor = value;
@ -247,8 +277,24 @@ function configDialog(style) {
} else { } else {
va.input.value = value; va.input.value = value;
} }
if (!prefs.get('config.autosave')) {
renderValueState(va);
} }
} }
}
function renderValueState(va) {
const el = va.input.closest('label');
el.classList.toggle('dirty', Boolean(va.dirty));
el.classList.toggle('nondefault', !isDefault(va));
}
function resetOnClick(event) {
event.preventDefault();
this.va.value = null;
renderValues([this.va]);
onchange({target: this.va.input});
}
function showColorpicker() { function showColorpicker() {
window.removeEventListener('keydown', messageBox.listeners.key, true); window.removeEventListener('keydown', messageBox.listeners.key, true);
@ -287,13 +333,18 @@ function configDialog(style) {
const colorpicker = document.body.appendChild( const colorpicker = document.body.appendChild(
$create('.colorpicker-popup', {style: 'display: none!important'})); $create('.colorpicker-popup', {style: 'display: none!important'}));
const PADDING = 50;
const MIN_WIDTH = parseFloat(getComputedStyle(colorpicker).width) || 350; const MIN_WIDTH = parseFloat(getComputedStyle(colorpicker).width) || 350;
const MIN_HEIGHT = 250; const MIN_HEIGHT = 250 + PADDING;
colorpicker.remove(); colorpicker.remove();
width = Math.max(Math.min(width / 0.9 + 2, 800), MIN_WIDTH); width = constrain(MIN_WIDTH, 800, width + PADDING);
height = Math.max(Math.min(height / 0.9 + 2, 600), MIN_HEIGHT); height = constrain(MIN_HEIGHT, 600, height + PADDING);
document.body.style.setProperty('min-width', width + 'px', 'important'); document.body.style.setProperty('min-width', width + 'px', 'important');
document.body.style.setProperty('min-height', height + 'px', 'important'); document.body.style.setProperty('min-height', height + 'px', 'important');
} }
function constrain(min, max, value) {
return value < min ? min : value > max ? max : value;
}
} }

View File

@ -84,23 +84,28 @@ select {
#header a[href^="edit"] { #header a[href^="edit"] {
text-decoration: none; text-decoration: none;
margin-right: 8px;
} }
#add-style-label { #add-style-wrapper {
margin-right: .25em; display: flex;
margin-bottom: .25em; align-items: center;
padding-bottom: 1.5em;
} }
#add-style-as-usercss-wrapper { #add-style-as-usercss-wrapper {
display: inline-flex; display: inline-flex;
margin-top: 3px;
} }
#add-style-as-usercss-wrapper:not(:hover) input:not(:checked) ~ a svg { #add-style-as-usercss-wrapper:not(:hover) input:not(:checked) ~ a svg {
fill: #aaa; fill: #aaa;
} }
#usercss-wiki svg { #add-style-as-usercss-wrapper #usercss-wiki {
margin-top: -4px; position: absolute;
right: -20px;
top: -3px;
} }
#installed { #installed {
@ -256,8 +261,7 @@ select {
} }
/* collapsibles */ /* collapsibles */
#header details:not(#filters), #header details:not(#filters) {
#add-style-wrapper {
padding-bottom: .7em; padding-bottom: .7em;
} }
@ -825,8 +829,14 @@ input[id^="manage.newUI"] {
#search { #search {
flex-grow: 1; flex-grow: 1;
margin: 0.25rem 0 0; margin: 0.25rem 0 0;
padding-left: 0.25rem; background: #fff;
border-width: 1px; height: 20px;
box-sizing: border-box;
padding: 3px 3px 3px 4px;
font: 400 12px Arial;
color: #000;
border: 1px solid hsl(0, 0%, 66%);
border-radius: 0.25rem;
} }
#search-wrapper .info { #search-wrapper .info {

View File

@ -3,7 +3,6 @@
/* global checkUpdate, handleUpdateInstalled */ /* global checkUpdate, handleUpdateInstalled */
/* global objectDiff */ /* global objectDiff */
/* global configDialog */ /* global configDialog */
/* global initCollapsibles */
'use strict'; 'use strict';
let installed; let installed;
@ -83,9 +82,6 @@ function initGlobalEvents() {
// N.B. triggers existing onchange listeners // N.B. triggers existing onchange listeners
setupLivePrefs(); setupLivePrefs();
// the options block
initCollapsibles();
$$('[id^="manage.newUI"]') $$('[id^="manage.newUI"]')
.forEach(el => (el.oninput = (el.onchange = switchUI))); .forEach(el => (el.oninput = (el.onchange = switchUI)));
@ -179,7 +175,7 @@ function createStyleElement({style, name}) {
} }
const parts = createStyleElement.parts; const parts = createStyleElement.parts;
parts.checker.checked = style.enabled; parts.checker.checked = style.enabled;
parts.nameLink.textContent = style.name; parts.nameLink.textContent = tWordBreak(style.name);
parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id; parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id;
parts.homepage.href = parts.homepage.title = style.url || ''; parts.homepage.href = parts.homepage.title = style.url || '';

View File

@ -77,6 +77,6 @@
"default_locale": "en", "default_locale": "en",
"options_ui": { "options_ui": {
"page": "options.html", "page": "options.html",
"chrome_style": true "chrome_style": false
} }
} }

View File

@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title i18n-text-append="optionsHeading">Stylus </title> <title i18n-text-append="optionsHeading">Stylus </title>
<link rel="stylesheet" href="global.css">
<link rel="stylesheet" href="options/options.css"> <link rel="stylesheet" href="options/options.css">
<link rel="stylesheet" href="options/onoffswitch.css"> <link rel="stylesheet" href="options/onoffswitch.css">

View File

@ -274,6 +274,7 @@ function createStyleElement({
if (!style.usercssData && style.updateUrl && style.updateUrl.includes('?') && style.url) { if (!style.usercssData && style.updateUrl && style.updateUrl.includes('?') && style.url) {
config.href = style.url; config.href = style.url;
config.target = '_blank'; config.target = '_blank';
config.title = t('configureStyleOnHomepage');
$('use', config).attributes['xlink:href'].nodeValue = '#svg-icon-config-uso'; $('use', config).attributes['xlink:href'].nodeValue = '#svg-icon-config-uso';
} else if (!style.usercssData || !Object.keys(style.usercssData.vars || {}).length) { } else if (!style.usercssData || !Object.keys(style.usercssData.vars || {}).length) {
config.style.display = 'none'; config.style.display = 'none';

View File

@ -59,6 +59,8 @@
if (!mx || mx.token !== colorizeToken) { if (!mx || mx.token !== colorizeToken) {
CodeMirror.extendMode('css', {token: colorizeToken}); CodeMirror.extendMode('css', {token: colorizeToken});
CodeMirror.extendMode('stylus', {token: colorizeToken}); CodeMirror.extendMode('stylus', {token: colorizeToken});
CodeMirror.modeExtensions.css.registerColorviewHooks = registerHooks;
CodeMirror.modeExtensions.css.unregisterColorviewHooks = unregisterHooks;
} }
} }
@ -498,11 +500,12 @@
cm.state.colorpicker.destroy(); cm.state.colorpicker.destroy();
} }
if (value) { if (value) {
registerHooks();
cm.state.colorpicker = new ColorMarker(cm, value); cm.state.colorpicker = new ColorMarker(cm, value);
} }
}); });
// initial runMode is performed by CodeMirror before setting our option // initial runMode is performed by CodeMirror before setting our option
// so we register the hooks right away - not a problem as our js is loaded on demand // so we register the hooks right away (the cost of always loading colorview is ~1ms for >200ms)
registerHooks(); registerHooks();
})(); })();