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
tNodeList: false
tDocLoader: false
tWordBreak: false
# dom.js
onDOMready: false
onDOMscriptReady: false
scrollElementIntoView: false
enforceInputRange: false
animateElement: false

View File

@ -105,7 +105,11 @@
},
"configureStyle": {
"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": {
"message": "Check for update",

View File

@ -33,6 +33,7 @@
<script src="edit/beautify.js"></script>
<script src="edit/sections.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="vendor/codemirror/lib/codemirror.js"></script>
@ -67,8 +68,11 @@
<script src="vendor/codemirror/keymap/emacs.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/codemirror-editing-hooks.js"></script>
<script src="edit/codemirror-default.js"></script>
<link rel="stylesheet" href="/edit/codemirror-default.css">

View File

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

View File

@ -5,7 +5,8 @@ global save toggleStyle setupAutocomplete makeSectionVisible getSectionForChild
*/
'use strict';
onDOMready().then(() => {
onDOMscriptReady('/codemirror.js').then(() => {
CodeMirror.defaults.lint = linterConfig.getForCodeMirror();
const COMMANDS = {
@ -46,11 +47,16 @@ onDOMready().then(() => {
// cm.state.search for last used 'find'
let searchState;
// N.B. the event listener should be registered before setupLivePrefs()
$('#options').addEventListener('change', onOptionElementChanged);
buildOptionsElements();
setupLivePrefs();
rerouteHotkeys(true);
onDOMready().then(() => {
prefs.subscribe(['editor.keyMap'], showKeyInSaveButtonTooltip);
showKeyInSaveButtonTooltip();
// N.B. the event listener should be registered before setupLivePrefs()
$('#options').addEventListener('change', onOptionElementChanged);
buildOptionsElements();
rerouteHotkeys(true);
});
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 */
'use strict';
// eslint-disable-next-line no-var
var initColorpicker = () => {
onDOMscriptReady('/colorview.js').then(() => {
initOverlayHooks();
onDOMready().then(() => {
$('#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'], colorpickerOnDemand);
return prefs.get('editor.colorpicker') && colorpickerOnDemand(null, true);
function colorpickerOnDemand(id, enabled) {
return loadScript(enabled && scripts)
.then(() => setColorpickerOption(id, enabled));
}
prefs.subscribe(['editor.colorpicker'], setColorpickerOption);
setColorpickerOption(null, prefs.get('editor.colorpicker'));
function setColorpickerOption(id, enabled) {
const defaults = CodeMirror.defaults;
const keyName = prefs.get('editor.colorpicker.hotkey');
delete defaults.extraKeys[keyName];
defaults.colorpicker = enabled;
if (enabled) {
if (keyName) {
CodeMirror.commands.colorpicker = invokeColorpicker;
defaults.extraKeys = defaults.extraKeys || {};
defaults.extraKeys[keyName] = '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
editors.forEach(cm => cm.setOption('colorpicker', defaults.colorpicker));
@ -162,4 +156,4 @@ var initColorpicker = () => {
return style;
}
}
};
});

View File

@ -576,9 +576,6 @@ html:not(.usercss) .applies-to li:last-child .add-applies-to {
top: 1em;
margin: 1ex 0;
}
.firefox .beautify-options > label input {
top: 1px;
}
.beautify-options:after {
clear: both;
display: block;
@ -809,9 +806,3 @@ html:not(.usercss) .usercss-only,
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 mozParser createSourceEditor
global closeCurrentTab regExpTester messageBox
global initColorpicker
global initCollapsibles
global setupCodeMirror
global beautify
global initWithSectionStyle addSections removeSection getSectionsHashes
@ -17,8 +15,6 @@ let dirty = {};
// array of all CodeMirror instances
const editors = [];
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
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;
preinit();
window.onbeforeunload = beforeUnload;
chrome.runtime.onMessage.addListener(onRuntimeMessage);
preinit();
Promise.all([
initStyleData().then(style => {
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;
}),
initStyleData(),
onDOMready(),
onBackgroundReady(),
])
.then(([style]) => Promise.all([
style,
initColorpicker(),
initCollapsibles(),
initHooksCommon(),
]))
.then(([style]) => {
setupLivePrefs();
const usercss = isUsercss(style);
$('#heading').textContent = t(styleId ? 'editStyleHeading' : 'addStyleTitle');
$('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
$('#name').title = usercss ? t('usercssReplaceTemplateName') : '';
$('#beautify').onclick = beautify;
$('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true});
window.addEventListener('resize', () => debounce(rememberWindowSize, 100));
if (usercss) {
editor = createSourceEditor(style);
} else {
@ -99,30 +82,6 @@ function preinit() {
'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) {
queryTabs({currentWindow: true}).then(tabs => {
const windowId = tabs[0].windowId;
@ -151,7 +110,18 @@ function preinit() {
getOwnTab().then(tab => {
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) {
return;
}
@ -258,9 +228,20 @@ function initStyleData() {
)
],
});
return !id ?
Promise.resolve(createEmptyStyle()) :
getStylesSafe({id}).then(([style]) => style || createEmptyStyle());
return getStylesSafe({id: id || -1})
.then(([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() {
@ -293,40 +274,6 @@ function initHooks() {
}
// 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) {
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 {
position: absolute;
height: 8px;
@ -13,12 +49,11 @@ input[type="checkbox"]:not(.slider):checked + .svg-icon.checked {
}
input[type="checkbox"]:not(.slider) {
-webkit-appearance: none;
-moz-appearance: none;
position: absolute;
left: 0;
top: 0;
-moz-appearance: none;
-webkit-appearance: none;
pointer-events: none;
border: 1px solid hsl(0, 0%, 46%);
height: 12px;
width: 12px;
@ -63,17 +98,14 @@ select {
-moz-appearance: none;
-webkit-appearance: none;
height: 22px;
color: currentColor;
font: 400 13.3333px Arial;
color: #000;
background-color: transparent;
border: 1px solid hsl(0, 0%, 66%);
padding: 0 20px 0 6px;
transition: color .5s;
}
.firefox select {
padding: 0 20px 0 2px;
}
.select-resizer {
display: inline-flex!important;
cursor: default;
@ -86,7 +118,7 @@ select {
display: inline-flex;
height: 14px;
width: 14px;
fill: currentColor;
fill: #000;
position: absolute;
top: 4px;
right: 4px;
@ -102,11 +134,37 @@ select {
-moz-appearance: checkbox !important;
}
::-moz-focus-inner {
border: 0;
.firefox select {
font-size: 13px;
padding: 0 20px 0 2px;
line-height: 22px!important;
}
svg {
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(() => {
$.remove('#firefox-transitions-bug-suppressor');
initCollapsibles();
});
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'} = {}) {
const prefMap = {};
const elements = $$('details[data-pref]');
if (!elements.length) {
return;
}
for (const el of elements) {
const key = el.dataset.pref;

View File

@ -48,26 +48,14 @@ function tHTML(html, tag) {
function tNodeList(nodes) {
const PREFIX = 'i18n-';
for (let n = nodes.length; --n >= 0;) {
const node = nodes[n];
// skip non-ELEMENT_NODE
if (node.nodeType !== 1) {
if (node.nodeType !== Node.ELEMENT_NODE) {
continue;
}
if (node.localName === 'template') {
const elements = node.content.querySelectorAll('*');
tNodeList(elements);
template[node.dataset.id] = elements[0];
// compress inter-tag whitespace to reduce number of DOM nodes by 25%
const walker = document.createTreeWalker(elements[0], NodeFilter.SHOW_TEXT);
const toRemove = [];
while (walker.nextNode()) {
const textNode = walker.currentNode;
if (!textNode.nodeValue.trim()) {
toRemove.push(textNode);
}
}
toRemove.forEach(el => el.remove());
createTemplate(node);
continue;
}
for (let a = node.attributes.length; --a >= 0;) {
@ -78,26 +66,71 @@ function tNodeList(nodes) {
}
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':
node.insertBefore(document.createTextNode(value), node.firstChild);
break;
before = node.firstChild;
// fallthrough to text-append
case 'text-append':
node.appendChild(document.createTextNode(value));
toInsert = createText(value);
break;
case 'html':
// localized strings only allow having text nodes and links
node.textContent = '';
[...tHTML(value, 'div').childNodes]
.filter(a => a.nodeType === a.TEXT_NODE || a.tagName === 'A')
.forEach(n => node.appendChild(n));
case 'html': {
toInsert = createHtml(value);
break;
}
default:
node.setAttribute(type, value);
}
tDocLoader.pause();
if (toInsert) {
node.insertBefore(toInsert, before || null);
}
node.removeAttribute(name);
}
}
function createTemplate(node) {
const elements = node.content.querySelectorAll('*');
tNodeList(elements);
template[node.dataset.id] = elements[0];
// compress inter-tag whitespace to reduce number of DOM nodes by 25%
const walker = document.createTreeWalker(elements[0], NodeFilter.SHOW_TEXT);
const toRemove = [];
while (walker.nextNode()) {
const textNode = walker.currentNode;
if (!textNode.nodeValue.trim()) {
toRemove.push(textNode);
}
}
tDocLoader.pause();
toRemove.forEach(el => el.remove());
}
function createText(str) {
return document.createTextNode(tWordBreak(str));
}
function createHtml(value) {
// <a href=foo>bar</a> are the only recognizable HTML elements
const rx = /(?:<a\s([^>]*)>([^<]*)<\/a>)?([^<]*)/gi;
const bin = document.createDocumentFragment();
for (let m; (m = rx.exec(value)) && m[0];) {
const [, linkParams, linkText, nextText] = m;
if (linkText) {
const href = /\bhref\s*=\s*(\S+)/.exec(linkParams);
const a = bin.appendChild(document.createElement('a'));
a.href = href && href[1].replace(/^(["'])(.*)\1$/, '$2') || '';
a.appendChild(createText(linkText));
}
if (nextText) {
bin.appendChild(createText(nextText));
}
}
return bin;
}
}
@ -115,33 +148,50 @@ function tDocLoader() {
t.cache = {browserUIlanguage: UIlang};
localStorage.L10N = JSON.stringify(t.cache);
}
const cacheLength = Object.keys(t.cache).length;
// localize HEAD
tNodeList(document.getElementsByTagName('*'));
Object.assign(tDocLoader, {
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
const process = mutations => {
tNodeList(document.getElementsByTagName('*'));
tDocLoader.start();
document.addEventListener('DOMContentLoaded', onLoad);
function process(mutations) {
for (const mutation of mutations) {
tNodeList(mutation.addedNodes);
}
};
const observer = new MutationObserver(process);
const onLoad = () => {
tDocLoader.start();
}
function onLoad() {
tDocLoader.stop();
process(observer.takeRecords());
process(tDocLoader.observer.takeRecords());
if (cacheLength !== Object.keys(t.cache).length) {
localStorage.L10N = JSON.stringify(t.cache);
}
};
tDocLoader.start = () => {
observer.observe(document, {subtree: true, childList: true});
};
tDocLoader.stop = () => {
observer.disconnect();
document.removeEventListener('DOMContentLoaded', onLoad);
};
tDocLoader.start();
document.addEventListener('DOMContentLoaded', onLoad);
}
}
function tWordBreak(text) {
// adds soft hyphens every 10 characters to ensure the long words break before breaking the layout
return text.length <= 10 ? text : text.replace(/[\d\w\u0080-\uFFFF]{10}|((?!\s)\W){10}/g, '$&\u00AD');
}

View File

@ -44,3 +44,60 @@ var loadScript = (() => {
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>
</p>
<p id="add-style-wrapper">
<div id="add-style-wrapper">
<a href="edit.html">
<button id="add-style-label" i18n-text="addStyleLabel"></button>
</a>
@ -275,7 +275,7 @@
</svg>
</a>
</label>
</p>
</div>
<details id="options" data-pref="manage.options.expanded">

View File

@ -1,16 +1,15 @@
#stylus-popup #message-box-contents {
padding: .25rem .75rem;
#stylus-manage .config-dialog #message-box-contents {
padding: 2em 16px;
}
#stylus-popup .config-dialog #message-box-contents {
padding: 8px 16px;
}
#stylus-popup .config-body label {
padding: .5em 0;
}
#stylus-popup .config-body label > :first-child {
max-width: 140px;
min-width: 140px;
}
.config-heading {
float: right;
margin: -1.25rem 0 0 0;
@ -21,9 +20,10 @@
display: flex;
padding: .75em 0;
align-items: center;
margin-right: -16px; /* for .config-reset-icon */
}
.config-dialog .select-resizer {
.config-body .select-resizer {
position: static;
}
@ -39,25 +39,16 @@
border-top: 1px dotted #ccc;
}
.config-body label > :first-child {
margin-right: 8px;
flex-grow: 1;
.config-body .dirty {
font-style: italic;
}
.config-body label:not([disabled]) > :first-child {
cursor: default;
}
.config-dialog .dirty:after {
.config-body .dirty:after {
content: "*";
position: absolute;
left: 6px;
}
.config-dialog .dirty {
font-style: italic;
}
.config-body input,
.config-body select,
.config-body .onoffswitch {
@ -81,27 +72,77 @@
.config-body .onoffswitch {
height: auto;
margin: calc((2em - 12px) / 2) 0;
flex-grow: 0;
}
.config-body input[type="text"] {
padding-left: 0.25em;
}
.config-body label > :last-child {
.config-name {
flex-grow: 1;
margin-right: 1em;
}
.config-value {
box-sizing: border-box;
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;
}
.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 {
position: relative;
padding: 0 0 0 16px;
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,
.color-swatch {
width: var(--onoffswitch-width) !important;
@ -124,3 +165,12 @@
border: none !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';
function configDialog(style) {
const AUTOSAVE_DELAY = 500;
const data = style.usercssData;
const varsHash = deepCopy(data.vars) || {};
const varNames = Object.keys(varsHash);
@ -71,15 +72,17 @@ function configDialog(style) {
colorpicker.hide();
}
function onchange({target}) {
function onchange({target, justSaved = false}) {
// invoked after element's own onchange so 'va' contains the updated value
const va = target.va;
if (va) {
va.dirty = varsInitial[va.name] !== (isDefault(va) ? va.default : va.value);
if (prefs.get('config.autosave')) {
debounce(save);
} else {
target.closest('label').classList.toggle('dirty', va.dirty);
if (prefs.get('config.autosave') && !justSaved) {
debounce(save, 0, {anyChangeIsDirty: true});
return;
}
renderValueState(va);
if (!justSaved) {
updateButtons();
}
}
@ -92,8 +95,9 @@ function configDialog(style) {
buttons.close.textContent = t(someDirty ? 'confirmCancel' : 'confirmClose');
}
function save() {
if (!vars.length || !vars.some(va => va.dirty)) {
function save({anyChangeIsDirty = false} = {}) {
if (!vars.length ||
!vars.some(va => va.dirty || anyChangeIsDirty && va.value !== va.savedValue)) {
return;
}
style.enabled = true;
@ -117,10 +121,11 @@ function configDialog(style) {
!isDefault(va) &&
bgva.options.every(o => o.name !== va.value)) {
error = `'${va.value}' not in the updated '${va.type}' list`;
} else if (!va.dirty) {
} else if (!va.dirty && (!anyChangeIsDirty || va.value === va.savedValue)) {
continue;
} else {
styleVars[va.name].value = va.value;
va.savedValue = va.value;
numValid++;
continue;
}
@ -145,10 +150,16 @@ function configDialog(style) {
return BG.usercssHelper.save(style)
.then(saved => {
varsInitial = getInitialValues(deepCopy(saved.usercssData.vars));
vars.forEach(va => onchange({target: va.input}));
vars.forEach(va => onchange({target: va.input, justSaved: true}));
renderValues();
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() {
@ -164,12 +175,19 @@ function configDialog(style) {
}
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) {
let children;
switch (va.type) {
case 'color':
children = [
$create('.cm-colorview', [
$create('.cm-colorview.config-value', [
va.input = $create('.color-swatch', {
va,
onclick: showColorpicker
@ -180,13 +198,11 @@ function configDialog(style) {
case 'checkbox':
children = [
$create('span.onoffswitch', [
$create('span.onoffswitch.config-value', [
va.input = $create('input.slider', {
va,
type: 'checkbox',
onchange() {
va.value = va.input.checked ? '1' : '0';
},
onchange: updateVarOnChange,
}),
$create('span'),
]),
@ -198,12 +214,10 @@ function configDialog(style) {
case 'image':
// TODO: a image picker input?
children = [
$create('.select-resizer', [
$create('.select-resizer.config-value', [
va.input = $create('select', {
va,
onchange() {
va.value = this.value;
}
onchange: updateVarOnChange,
},
va.options.map(o =>
$create('option', {value: o.name}, o.label))),
@ -215,27 +229,43 @@ function configDialog(style) {
default:
children = [
va.input = $create('input', {
va.input = $create('input.config-value', {
va,
type: 'text',
oninput() {
va.value = this.value;
this.dispatchEvent(new Event('change', {bubbles: true}));
},
onchange: updateVarOnChange,
oninput: updateVarOnInput,
}),
];
break;
}
resetter = resetter.cloneNode(true);
resetter.va = va;
resetter.onclick = resetOnClick;
elements.push(
$create(`label.config-${va.type}`, [
$create('span', va.label),
$create('span.config-name', tWordBreak(va.label)),
...children,
resetter,
]));
}
}
function renderValues() {
for (const va of vars) {
function updateVarOnChange() {
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;
if (va.type === 'color') {
va.input.style.backgroundColor = value;
@ -247,9 +277,25 @@ function configDialog(style) {
} else {
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() {
window.removeEventListener('keydown', messageBox.listeners.key, true);
const box = $('#message-box-contents');
@ -287,13 +333,18 @@ function configDialog(style) {
const colorpicker = document.body.appendChild(
$create('.colorpicker-popup', {style: 'display: none!important'}));
const PADDING = 50;
const MIN_WIDTH = parseFloat(getComputedStyle(colorpicker).width) || 350;
const MIN_HEIGHT = 250;
const MIN_HEIGHT = 250 + PADDING;
colorpicker.remove();
width = Math.max(Math.min(width / 0.9 + 2, 800), MIN_WIDTH);
height = Math.max(Math.min(height / 0.9 + 2, 600), MIN_HEIGHT);
width = constrain(MIN_WIDTH, 800, width + PADDING);
height = constrain(MIN_HEIGHT, 600, height + PADDING);
document.body.style.setProperty('min-width', width + '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"] {
text-decoration: none;
margin-right: 8px;
}
#add-style-label {
margin-right: .25em;
margin-bottom: .25em;
#add-style-wrapper {
display: flex;
align-items: center;
padding-bottom: 1.5em;
}
#add-style-as-usercss-wrapper {
display: inline-flex;
margin-top: 3px;
}
#add-style-as-usercss-wrapper:not(:hover) input:not(:checked) ~ a svg {
fill: #aaa;
}
#usercss-wiki svg {
margin-top: -4px;
#add-style-as-usercss-wrapper #usercss-wiki {
position: absolute;
right: -20px;
top: -3px;
}
#installed {
@ -256,8 +261,7 @@ select {
}
/* collapsibles */
#header details:not(#filters),
#add-style-wrapper {
#header details:not(#filters) {
padding-bottom: .7em;
}
@ -825,8 +829,14 @@ input[id^="manage.newUI"] {
#search {
flex-grow: 1;
margin: 0.25rem 0 0;
padding-left: 0.25rem;
border-width: 1px;
background: #fff;
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 {

View File

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

View File

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

View File

@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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/onoffswitch.css">

View File

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

View File

@ -59,6 +59,8 @@
if (!mx || mx.token !== colorizeToken) {
CodeMirror.extendMode('css', {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();
}
if (value) {
registerHooks();
cm.state.colorpicker = new ColorMarker(cm, value);
}
});
// 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();
})();