Merge pull request #1054 from tophf/custom-name

fix local name customization for usercss/legacy
This commit is contained in:
tophf 2020-10-22 15:07:33 +03:00 committed by GitHub
commit 7f15ae324d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 380 additions and 489 deletions

View File

@ -250,6 +250,13 @@
"message": "Copy to clipboard", "message": "Copy to clipboard",
"description": "Tooltip for elements which can be copied" "description": "Tooltip for elements which can be copied"
}, },
"customNameHint": {
"message": "Enter a custom name here to rename the style in UI without breaking its updates"
},
"customNameResetHint": {
"message": "Stop using customized name, switch to the style's own name",
"description": "Tooltip of 'x' button shown in editor when changing the name input of a) styles updated from a URL i.e. not locally created, b) UserCSS styles"
},
"dateInstalled": { "dateInstalled": {
"message": "Date installed", "message": "Date installed",
"description": "Option text for the user to sort the style by install date" "description": "Option text for the user to sort the style by install date"

View File

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

View File

@ -61,6 +61,8 @@ const styleManager = (() => {
username: '' username: ''
}; };
const DELETE_IF_NULL = ['id', 'customName'];
handleLivePreviewConnections(); handleLivePreviewConnections();
return Object.assign({ return Object.assign({
@ -387,8 +389,10 @@ const styleManager = (() => {
if (!style.name) { if (!style.name) {
throw new Error('style name is empty'); throw new Error('style name is empty');
} }
if (style.id == null) { for (const key of DELETE_IF_NULL) {
delete style.id; if (style[key] == null) {
delete style[key];
}
} }
if (!style._id) { if (!style._id) {
style._id = uuidv4(); style._id = uuidv4();
@ -569,6 +573,16 @@ const styleManager = (() => {
touched = true; touched = true;
} }
} }
// upgrade the old way of customizing local names
const {originalName} = style;
if (originalName) {
touched = true;
if (originalName !== style.name) {
style.customName = style.name;
style.name = originalName;
}
delete style.originalName;
}
return touched; return touched;
} }
} }

View File

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

View File

@ -18,6 +18,22 @@
} }
</style> </style>
<link id="cm-theme" rel="stylesheet">
<script src="js/polyfill.js"></script>
<script src="js/dom.js"></script>
<script src="js/messaging.js"></script>
<script src="js/msg.js"></script>
<script src="js/prefs.js"></script>
<script src="js/localization.js"></script>
<script src="js/script-loader.js"></script>
<script src="js/storage-util.js"></script>
<script src="content/style-injector.js"></script>
<script src="content/apply.js"></script>
<script src="edit/edit.js"></script> <!-- run it ASAP to send a request for the style -->
<link href="vendor/codemirror/lib/codemirror.css" rel="stylesheet"> <link href="vendor/codemirror/lib/codemirror.css" rel="stylesheet">
<script src="vendor/codemirror/lib/codemirror.js"></script> <script src="vendor/codemirror/lib/codemirror.js"></script>
@ -63,44 +79,27 @@
<script src="vendor-overwrites/codemirror-addon/match-highlighter.js"></script> <script src="vendor-overwrites/codemirror-addon/match-highlighter.js"></script>
<script src="js/polyfill.js"></script> <script src="msgbox/msgbox.js" async></script>
<script src="js/dom.js"></script>
<script src="js/messaging.js"></script>
<script src="js/prefs.js"></script>
<script src="js/localization.js"></script>
<script src="js/script-loader.js"></script>
<script src="js/storage-util.js"></script>
<script src="js/msg.js"></script>
<script src="js/worker-util.js"></script>
<script src="content/style-injector.js"></script>
<script src="content/apply.js"></script>
<link href="edit/global-search.css" rel="stylesheet">
<script src="edit/global-search.js"></script>
<link href="edit/codemirror-default.css" rel="stylesheet"> <link href="edit/codemirror-default.css" rel="stylesheet">
<script src="edit/codemirror-default.js"></script> <script src="edit/codemirror-default.js"></script>
<script src="edit/codemirror-factory.js"></script>
<script src="edit/util.js"></script> <script src="edit/util.js"></script>
<script src="edit/regexp-tester.js"></script> <script src="edit/regexp-tester.js"></script>
<script src="edit/live-preview.js"></script> <script src="edit/live-preview.js"></script>
<script src="edit/applies-to-line-widget.js"></script> <script src="edit/applies-to-line-widget.js"></script>
<script src="edit/reroute-hotkeys.js"></script> <script src="edit/reroute-hotkeys.js"></script>
<script src="edit/codemirror-factory.js"></script> <link href="edit/global-search.css" rel="stylesheet">
<script src="edit/global-search.js"></script>
<script src="edit/colorpicker-helper.js"></script> <script src="edit/colorpicker-helper.js"></script>
<script src="edit/beautify.js"></script> <script src="edit/beautify.js"></script>
<script src="edit/show-keymap-help.js"></script> <script src="edit/show-keymap-help.js"></script>
<script src="edit/codemirror-themes.js"></script> <script src="edit/codemirror-themes.js"></script>
<script src="edit/source-editor.js"></script> <script src="edit/source-editor.js"></script>
<script src="edit/sections-editor-section.js"></script> <script src="edit/sections-editor-section.js"></script>
<script src="edit/sections-editor.js"></script> <script src="edit/sections-editor.js"></script>
<script src="edit/edit.js"></script> <script src="js/worker-util.js"></script>
<script src="msgbox/msgbox.js" async></script>
<script src="edit/linter.js"></script> <script src="edit/linter.js"></script>
<script src="edit/linter-defaults.js"></script> <script src="edit/linter-defaults.js"></script>
<script src="edit/linter-engines.js"></script> <script src="edit/linter-engines.js"></script>
@ -109,8 +108,6 @@
<script src="edit/linter-report.js"></script> <script src="edit/linter-report.js"></script>
<script src="edit/linter-config-dialog.js"></script> <script src="edit/linter-config-dialog.js"></script>
<link id="cm-theme" rel="stylesheet">
<template data-id="appliesTo"> <template data-id="appliesTo">
<li class="applies-to-item"> <li class="applies-to-item">
<div class="select-resizer"> <div class="select-resizer">
@ -284,7 +281,13 @@
<h1 id="heading">&nbsp;</h1> <!-- nbsp allocates the actual height which prevents page shift --> <h1 id="heading">&nbsp;</h1> <!-- nbsp allocates the actual height which prevents page shift -->
<section id="basic-info"> <section id="basic-info">
<div id="basic-info-name"> <div id="basic-info-name">
<input id="name" class="style-contributor" spellcheck="false" required> <input id="name" class="style-contributor" spellcheck="false">
<a id="reset-name" href="#" i18n-title="customNameResetHint" tabindex="0" hidden>
<svg class="svg-icon" viewBox="0 0 20 20">
<polygon points="16.2,5.5 14.5,3.8 10,8.3 5.5,3.8 3.8,5.5 8.3,10 3.8,14.5
5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10 "/>
</svg>
</a>
<a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a> <a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a>
</div> </div>
<div id="basic-info-enabled"> <div id="basic-info-enabled">

View File

@ -315,7 +315,7 @@ CodeMirror.hint && (() => {
} }
// USO vars in usercss mode editor // USO vars in usercss mode editor
const vars = editor.getStyle().usercssData.vars; const vars = editor.style.usercssData.vars;
const list = vars ? const list = vars ?
Object.keys(vars).filter(name => name.startsWith(leftPart)) : []; Object.keys(vars).filter(name => name.startsWith(leftPart)) : [];
return { return {
@ -343,7 +343,7 @@ CodeMirror.hint && (() => {
string[start + 3] === '[' && string[start + 3] === '[' &&
string[pos - 3] === ']' && string[pos - 3] === ']' &&
string[pos - 4] === ']') { string[pos - 4] === ']') {
const vars = typeof editor !== 'undefined' && (editor.getStyle().usercssData || {}).vars; const vars = typeof editor !== 'undefined' && (editor.style.usercssData || {}).vars;
const name = vars && string.slice(start + 4, pos - 4); const name = vars && string.slice(start + 4, pos - 4);
if (vars && Object.hasOwnProperty.call(vars, name.endsWith('-rgb') ? name.slice(0, -4) : name)) { if (vars && Object.hasOwnProperty.call(vars, name.endsWith('-rgb') ? name.slice(0, -4) : name)) {
token[0] = USO_VALID_VAR; token[0] = USO_VALID_VAR;

View File

@ -88,6 +88,9 @@ label {
display: flex; display: flex;
align-items: center; align-items: center;
} }
#reset-name {
margin: 0 .25em 0 .5em;
}
#url { #url {
margin-left: 0.25rem; margin-left: 0.25rem;
} }
@ -610,7 +613,8 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
right: 4px; right: 4px;
top: .5em; top: .5em;
} }
#help-popup input[type="search"] { #help-popup input[type="search"],
#help-popup .CodeMirror {
margin: 3px; margin: 3px;
} }
@ -813,11 +817,6 @@ body.linter-disabled .hidden-unless-compact {
margin-top: .75rem; margin-top: .75rem;
} }
.usercss #name {
background-color: #eee;
color: #888;
}
.single-editor { .single-editor {
height: 100%; height: 100%;
} }

View File

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

View File

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

View File

@ -1,41 +1,24 @@
/* global dirtyReporter showHelp toggleContextMenuDelete createSection /* global showHelp toggleContextMenuDelete createSection
CodeMirror linter createLivePreview showCodeMirrorPopup CodeMirror linter createLivePreview showCodeMirrorPopup
sectionsToMozFormat messageBox clipString sectionsToMozFormat messageBox clipString
rerouteHotkeys $ $$ $create t FIREFOX API $ $$ $create t FIREFOX API
debounce */ debounce */
/* exported createSectionsEditor */ /* exported createSectionsEditor */
'use strict'; 'use strict';
function createSectionsEditor({style, onTitleChanged}) { function createSectionsEditor(editorBase) {
const {style, dirty} = editorBase;
let INC_ID = 0; // an increment id that is used by various object to track the order let INC_ID = 0; // an increment id that is used by various object to track the order
const dirty = dirtyReporter();
const container = $('#sections'); const container = $('#sections');
const sections = []; const sections = [];
container.classList.add('section-editor'); container.classList.add('section-editor');
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(); updateHeader();
rerouteHotkeys(true);
$('#to-mozilla').addEventListener('click', showMozillaFormat); $('#to-mozilla').addEventListener('click', showMozillaFormat);
$('#to-mozilla-help').addEventListener('click', showToMozillaHelp); $('#to-mozilla-help').addEventListener('click', showToMozillaHelp);
$('#from-mozilla').addEventListener('click', () => showMozillaFormatImport()); $('#from-mozilla').addEventListener('click', () => showMozillaFormatImport());
$('#save-button').addEventListener('click', saveStyle);
document.addEventListener('wheel', scrollEntirePageOnCtrlShift, {passive: false}); document.addEventListener('wheel', scrollEntirePageOnCtrlShift, {passive: false});
CodeMirror.defaults.extraKeys['Shift-Ctrl-Wheel'] = 'scrollWindow'; CodeMirror.defaults.extraKeys['Shift-Ctrl-Wheel'] = 'scrollWindow';
@ -65,33 +48,30 @@ function createSectionsEditor({style, onTitleChanged}) {
let sectionOrder = ''; let sectionOrder = '';
let headerOffset; // in compact mode the header is at the top so it reduces the available height let headerOffset; // in compact mode the header is at the top so it reduces the available height
const initializing = initSections(style.sections.slice()); const ready = initSections(style.sections.slice());
const livePreview = createLivePreview(); const livePreview = createLivePreview();
livePreview.show(Boolean(style.id)); livePreview.show(Boolean(style.id));
return { return Object.assign({}, editorBase, {
ready: () => initializing, ready,
replaceStyle, replaceStyle,
dirty,
getStyle: () => style,
getEditors, getEditors,
scrollToEditor, scrollToEditor,
getStyleId: () => style.id,
getEditorTitle: cm => { getEditorTitle: cm => {
const index = sections.filter(s => !s.isRemoved()).findIndex(s => s.cm === cm); const index = sections.filter(s => !s.isRemoved()).findIndex(s => s.cm === cm);
return `${t('sectionCode')} ${index + 1}`; return `${t('sectionCode')} ${index + 1}`;
}, },
save: saveStyle, save,
toggleStyle,
nextEditor, nextEditor,
prevEditor, prevEditor,
closestVisible, closestVisible,
getSearchableInputs, getSearchableInputs,
}; updateLivePreview,
});
function fitToContent(section) { function fitToContent(section) {
const {cm, cm: {display: {wrapper, sizer}}} = section; const {el, cm, cm: {display: {wrapper, sizer}}} = section;
if (cm.display.renderedView) { if (cm.display.renderedView) {
resize(); resize();
} else { } else {
@ -104,7 +84,7 @@ function createSectionsEditor({style, onTitleChanged}) {
return; return;
} }
if (headerOffset == null) { if (headerOffset == null) {
headerOffset = wrapper.getBoundingClientRect().top; headerOffset = el.getBoundingClientRect().top;
} }
contentHeight += 9; // border & resize grip contentHeight += 9; // border & resize grip
cm.off('update', resize); cm.off('update', resize);
@ -115,16 +95,15 @@ function createSectionsEditor({style, onTitleChanged}) {
} }
function fitToAvailableSpace() { function fitToAvailableSpace() {
const available = const ch = container.offsetHeight;
Math.floor(container.offsetHeight - sections.reduce((h, s) => h + s.el.offsetHeight, 0)) || let available = ch - sections[sections.length - 1].el.getBoundingClientRect().bottom + headerOffset;
window.innerHeight - container.offsetHeight; if (available <= 1) available = window.innerHeight - ch - headerOffset;
if (available <= 0) { const delta = Math.floor(available / sections.length);
return; if (delta > 1) {
sections.forEach(({cm}) => {
cm.setSize(null, cm.display.wrapper.offsetHeight + delta);
});
} }
const cmHeights = sections.map(s => s.cm.getWrapperElement().offsetHeight);
sections.forEach((section, i) => {
section.cm.setSize(null, cmHeights[i] + Math.floor(available / sections.length));
});
} }
function genId() { function genId() {
@ -246,14 +225,6 @@ function createSectionsEditor({style, onTitleChanged}) {
return sections.filter(s => !s.isRemoved()).map(s => s.cm); return sections.filter(s => !s.isRemoved()).map(s => s.cm);
} }
function toggleStyle() {
const newValue = !style.enabled;
dirty.modify('enabled', style.enabled, newValue);
style.enabled = newValue;
enabledEl.checked = newValue;
updateLivePreview();
}
function nextEditor(cm, cycle = true) { function nextEditor(cm, cycle = true) {
if (!cycle && findLast(sections, s => !s.isRemoved()).cm === cm) { if (!cycle && findLast(sections, s => !s.isRemoved()).cm === cm) {
return; return;
@ -417,7 +388,7 @@ function createSectionsEditor({style, onTitleChanged}) {
} }
function validate() { function validate() {
if (!nameEl.reportValidity()) { if (!$('#name').reportValidity()) {
messageBox.alert(t('styleMissingName')); messageBox.alert(t('styleMissingName'));
return false; return false;
} }
@ -435,7 +406,7 @@ function createSectionsEditor({style, onTitleChanged}) {
return true; return true;
} }
function saveStyle() { function save() {
if (!dirty.isDirty()) { if (!dirty.isDirty()) {
return; return;
} }
@ -464,10 +435,10 @@ function createSectionsEditor({style, onTitleChanged}) {
} }
function updateHeader() { function updateHeader() {
nameEl.value = style.name || ''; $('#name').value = style.customName || style.name || '';
enabledEl.checked = style.enabled !== false; $('#enabled').checked = style.enabled !== false;
$('#url').href = style.url || ''; $('#url').href = style.url || '';
onTitleChanged(); editorBase.updateName();
} }
function updateLivePreview() { function updateLivePreview() {
@ -609,6 +580,7 @@ function createSectionsEditor({style, onTitleChanged}) {
} }
function replaceStyle(newStyle, codeIsUpdated) { function replaceStyle(newStyle, codeIsUpdated) {
dirty.clear('name');
// FIXME: avoid recreating all editors? // FIXME: avoid recreating all editors?
reinit().then(() => { reinit().then(() => {
Object.assign(style, newStyle); Object.assign(style, newStyle);

View File

@ -1,4 +1,4 @@
/* global dirtyReporter /* global
createAppliesToLineWidget messageBox createAppliesToLineWidget messageBox
sectionsToMozFormat sectionsToMozFormat
createMetaCompiler linter createLivePreview cmFactory $ $create API prefs t createMetaCompiler linter createLivePreview cmFactory $ $create API prefs t
@ -6,17 +6,14 @@
/* exported createSourceEditor */ /* exported createSourceEditor */
'use strict'; 'use strict';
function createSourceEditor({style, onTitleChanged}) { function createSourceEditor(editorBase) {
$('#name').disabled = true; const {style, dirty} = editorBase;
$('#save-button').disabled = true;
$('#mozilla-format-container').remove(); $('#mozilla-format-container').remove();
$('#save-button').onclick = save;
$('#header').addEventListener('wheel', headerOnScroll); $('#header').addEventListener('wheel', headerOnScroll);
$('#sections').textContent = ''; $('#sections').textContent = '';
$('#sections').appendChild($create('.single-editor')); $('#sections').appendChild($create('.single-editor'));
const dirty = dirtyReporter();
// normalize style // normalize style
if (!style.id) setupNewStyle(style); if (!style.id) setupNewStyle(style);
@ -28,13 +25,6 @@ function createSourceEditor({style, onTitleChanged}) {
const livePreview = createLivePreview(preprocess); const livePreview = createLivePreview(preprocess);
livePreview.show(Boolean(style.id)); livePreview.show(Boolean(style.id));
$('#enabled').onchange = function () {
const value = this.checked;
dirty.modify('enabled', style.enabled, value);
style.enabled = value;
updateLivePreview();
};
cm.on('changes', () => { cm.on('changes', () => {
dirty.modify('sourceGeneration', savedGeneration, cm.changeGeneration()); dirty.modify('sourceGeneration', savedGeneration, cm.changeGeneration());
updateLivePreview(); updateLivePreview();
@ -162,14 +152,15 @@ function createSourceEditor({style, onTitleChanged}) {
} }
function updateMeta() { function updateMeta() {
$('#name').value = style.name; $('#name').value = style.customName || style.name;
$('#enabled').checked = style.enabled; $('#enabled').checked = style.enabled;
$('#url').href = style.url; $('#url').href = style.url;
onTitleChanged(); editorBase.updateName();
return cm.setPreprocessor((style.usercssData || {}).preprocessor); return cm.setPreprocessor((style.usercssData || {}).preprocessor);
} }
function replaceStyle(newStyle, codeIsUpdated) { function replaceStyle(newStyle, codeIsUpdated) {
dirty.clear('name');
const sameCode = newStyle.sourceCode === cm.getValue(); const sameCode = newStyle.sourceCode === cm.getValue();
if (sameCode) { if (sameCode) {
savedGeneration = cm.changeGeneration(); savedGeneration = cm.changeGeneration();
@ -210,14 +201,6 @@ function createSourceEditor({style, onTitleChanged}) {
} }
} }
function toggleStyle() {
const value = !style.enabled;
dirty.modify('enabled', style.enabled, value);
style.enabled = value;
updateMeta();
$('#enabled').dispatchEvent(new Event('change', {bubbles: true}));
}
function save() { function save() {
if (!dirty.isDirty()) return; if (!dirty.isDirty()) return;
const code = cm.getValue(); const code = cm.getValue();
@ -226,6 +209,7 @@ function createSourceEditor({style, onTitleChanged}) {
id: style.id, id: style.id,
enabled: style.enabled, enabled: style.enabled,
sourceCode: code, sourceCode: code,
customName: style.customName,
})) }))
.then(replaceStyle) .then(replaceStyle)
.catch(err => { .catch(err => {
@ -372,19 +356,17 @@ function createSourceEditor({style, onTitleChanged}) {
(mode.helperType || ''); (mode.helperType || '');
} }
return { return Object.assign({}, editorBase, {
ready: Promise.resolve(),
replaceStyle, replaceStyle,
dirty,
getStyle: () => style,
getEditors: () => [cm], getEditors: () => [cm],
scrollToEditor: () => {}, scrollToEditor: () => {},
getStyleId: () => style.id,
getEditorTitle: () => '', getEditorTitle: () => '',
save, save,
toggleStyle,
prevEditor: cm => nextPrevMozDocument(cm, -1), prevEditor: cm => nextPrevMozDocument(cm, -1),
nextEditor: cm => nextPrevMozDocument(cm, 1), nextEditor: cm => nextPrevMozDocument(cm, 1),
closestVisible: () => cm, closestVisible: () => cm,
getSearchableInputs: () => [] getSearchableInputs: () => [],
}; updateLivePreview,
});
} }

View File

@ -243,7 +243,7 @@
(!dup ? (!dup ?
Promise.resolve(true) : Promise.resolve(true) :
messageBox.confirm(t('styleInstallOverwrite', [ messageBox.confirm(t('styleInstallOverwrite', [
data.name, data.name + (dup.customName ? ` (${dup.customName})` : ''),
dupData.version, dupData.version,
data.version, data.version,
])) ]))

View File

@ -33,7 +33,8 @@ self.msg = self.INJECTED === 1 ? self.msg : (() => {
onExtension, onExtension,
off, off,
RX_NO_RECEIVER, RX_NO_RECEIVER,
RX_PORT_CLOSED RX_PORT_CLOSED,
isBg,
}; };
function getBg() { function getBg() {

View File

@ -1,6 +1,8 @@
/* global promisifyChrome */ /* global promisifyChrome msg API */
'use strict'; 'use strict';
// Needs msg.js loaded first
self.prefs = self.INJECTED === 1 ? self.prefs : (() => { self.prefs = self.INJECTED === 1 ? self.prefs : (() => {
const defaults = { const defaults = {
'openEditInWindow': false, // new editor opens in a own browser window 'openEditInWindow': false, // new editor opens in a own browser window
@ -112,12 +114,11 @@ self.prefs = self.INJECTED === 1 ? self.prefs : (() => {
'storage.sync': ['get', 'set'], 'storage.sync': ['get', 'set'],
}); });
const initializing = browser.storage.sync.get('settings') const initializing = (
.then(result => { msg.isBg
if (result.settings) { ? browser.storage.sync.get('settings').then(res => res.settings)
setAll(result.settings, true); : API.getPrefs()
} ).then(res => res && setAll(res, true));
});
chrome.storage.onChanged.addListener((changes, area) => { chrome.storage.onChanged.addListener((changes, area) => {
if (area !== 'sync' || !changes.settings || !changes.settings.newValue) { if (area !== 'sync' || !changes.settings || !changes.settings.newValue) {

View File

@ -48,73 +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, {once: true});
}
}
}
}
function getSubscribersForSrc(src) {
for (const [suffix, listeners] of subscribers.entries()) {
if (src.endsWith(suffix)) {
return {suffix, listeners};
}
}
}
function notifySubscribers(event) {
for (let data; (data = getSubscribersForSrc(this.src));) {
data.listeners.forEach(fn => fn(event));
if (emptyAfterCleanup(data.suffix)) {
return;
}
}
}
function emptyAfterCleanup(suffix) {
if (!subscribers) {
return true;
}
subscribers.delete(suffix);
if (!subscribers.size) {
observer.disconnect();
observer = null;
subscribers = null;
return true;
}
}
})();

View File

@ -149,8 +149,8 @@
<script src="js/polyfill.js"></script> <script src="js/polyfill.js"></script>
<script src="js/dom.js"></script> <script src="js/dom.js"></script>
<script src="js/messaging.js"></script> <script src="js/messaging.js"></script>
<script src="js/prefs.js"></script>
<script src="js/msg.js"></script> <script src="js/msg.js"></script>
<script src="js/prefs.js"></script>
<script src="js/router.js"></script> <script src="js/router.js"></script>
<script src="content/style-injector.js"></script> <script src="content/style-injector.js"></script>
<script src="content/apply.js"></script> <script src="content/apply.js"></script>

View File

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

View File

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

View File

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

View File

@ -4,11 +4,10 @@ global messageBox getStyleWithNoCode
checkUpdate handleUpdateInstalled checkUpdate handleUpdateInstalled
objectDiff objectDiff
configDialog configDialog
sorter msg prefs API onDOMready $ $$ $create template setupLivePrefs sorter msg prefs API $ $$ $create template setupLivePrefs
t tWordBreak formatDate t tWordBreak formatDate
getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce
scrollElementIntoView CHROME VIVALDI router scrollElementIntoView CHROME VIVALDI router
bulkChangeTime:true bulkChangeQueue
*/ */
'use strict'; 'use strict';
@ -18,6 +17,8 @@ const ENTRY_ID_PREFIX_RAW = 'style-';
const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW; const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW;
const BULK_THROTTLE_MS = 100; const BULK_THROTTLE_MS = 100;
const bulkChangeQueue = [];
bulkChangeQueue.time = 0;
// define pref-mapped ids separately // define pref-mapped ids separately
const newUI = { const newUI = {
@ -49,18 +50,45 @@ Promise.all([
API.getAllStyles(true), API.getAllStyles(true),
// FIXME: integrate this into filter.js // FIXME: integrate this into filter.js
router.getSearch('search') && API.searchDB({query: router.getSearch('search')}), router.getSearch('search') && API.searchDB({query: router.getSearch('search')}),
Promise.all([ waitForSelector('#installed'), // needed to avoid flicker due to an extra frame and layout shift
onDOMready(), prefs.initializing
prefs.initializing, ]).then(([styles, ids, el]) => {
]) installed = el;
.then(() => { installed.onclick = handleEvent.entryClicked;
initGlobalEvents(); $('#manage-options-button').onclick = () => router.updateHash('#stylus-options');
if (!VIVALDI) { $('#sync-styles').onclick = () => router.updateHash('#stylus-options');
$$('#header select').forEach(el => el.adjustWidth()); $$('#header a[href^="http"]').forEach(a => (a.onclick = handleEvent.external));
} // show date installed & last update on hover
}), installed.addEventListener('mouseover', handleEvent.lazyAddEntryTitle);
]).then(args => { installed.addEventListener('mouseout', handleEvent.lazyAddEntryTitle);
showStyles(...args); 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); msg.onExtension(onRuntimeMessage);
@ -71,7 +99,7 @@ function onRuntimeMessage(msg) {
case 'styleAdded': case 'styleAdded':
case 'styleDeleted': case 'styleDeleted':
bulkChangeQueue.push(msg); bulkChangeQueue.push(msg);
if (performance.now() - bulkChangeTime < BULK_THROTTLE_MS) { if (performance.now() - bulkChangeQueue.time < BULK_THROTTLE_MS) {
debounce(handleBulkChange, BULK_THROTTLE_MS); debounce(handleBulkChange, BULK_THROTTLE_MS);
} else { } else {
handleBulkChange(); handleBulkChange();
@ -86,61 +114,16 @@ function onRuntimeMessage(msg) {
setTimeout(sorter.updateStripes, 0, {onlyWhenColumnsChanged: true}); setTimeout(sorter.updateStripes, 0, {onlyWhenColumnsChanged: true});
} }
function initGlobalEvents() {
installed = $('#installed');
installed.onclick = handleEvent.entryClicked;
$('#manage-options-button').onclick = () => router.updateHash('#stylus-options');
$('#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);
$$('[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', '');
}
};
});
// 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')}";
}
`));
}
function showStyles(styles = [], matchUrlIds) { function showStyles(styles = [], matchUrlIds) {
const sorted = sorter.sort({ const sorted = sorter.sort({
styles: styles.map(style => ({ styles: styles.map(style => {
style, const name = style.customName || style.name || '';
name: (style.name || '').toLocaleLowerCase() + '\n' + style.name, return {
})), style,
// sort case-insensitively the whole list then sort dupes like `Foo` and `foo` case-sensitively
name: name.toLocaleLowerCase() + '\n' + name,
};
}),
}); });
let index = 0; let index = 0;
let firstRun = true; let firstRun = true;
@ -187,7 +170,7 @@ function showStyles(styles = [], matchUrlIds) {
} }
function createStyleElement({style, name}) { function createStyleElement({style, name: nameLC}) {
// query the sub-elements just once, then reuse the references // query the sub-elements just once, then reuse the references
if ((createStyleElement.parts || {}).newUI !== newUI.enabled) { if ((createStyleElement.parts || {}).newUI !== newUI.enabled) {
const entry = template[`style${newUI.enabled ? 'Compact' : ''}`]; const entry = template[`style${newUI.enabled ? 'Compact' : ''}`];
@ -216,8 +199,9 @@ function createStyleElement({style, name}) {
} }
const parts = createStyleElement.parts; const parts = createStyleElement.parts;
const configurable = style.usercssData && style.usercssData.vars && Object.keys(style.usercssData.vars).length > 0; const configurable = style.usercssData && style.usercssData.vars && Object.keys(style.usercssData.vars).length > 0;
const name = style.customName || style.name;
parts.checker.checked = style.enabled; parts.checker.checked = style.enabled;
parts.nameLink.textContent = tWordBreak(style.name); parts.nameLink.textContent = tWordBreak(name);
parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id; parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id;
parts.homepage.href = parts.homepage.title = style.url || ''; parts.homepage.href = parts.homepage.title = style.url || '';
if (!newUI.enabled) { if (!newUI.enabled) {
@ -234,7 +218,7 @@ function createStyleElement({style, name}) {
const entry = parts.entry.cloneNode(true); const entry = parts.entry.cloneNode(true);
entry.id = ENTRY_ID_PREFIX_RAW + style.id; entry.id = ENTRY_ID_PREFIX_RAW + style.id;
entry.styleId = style.id; entry.styleId = style.id;
entry.styleNameLowerCase = name || style.name.toLocaleLowerCase(); entry.styleNameLowerCase = nameLC || name.toLocaleLowerCase() + '\n' + name;
entry.styleMeta = style; entry.styleMeta = style;
entry.className = parts.entryClassBase + ' ' + entry.className = parts.entryClassBase + ' ' +
(style.enabled ? 'enabled' : 'disabled') + (style.enabled ? 'enabled' : 'disabled') +
@ -437,7 +421,7 @@ Object.assign(handleEvent, {
animateElement(entry); animateElement(entry);
messageBox({ messageBox({
title: t('deleteStyleConfirm'), title: t('deleteStyleConfirm'),
contents: entry.styleMeta.name, contents: entry.styleMeta.customName || entry.styleMeta.name,
className: 'danger center', className: 'danger center',
buttons: [t('confirmDelete'), t('confirmCancel')], buttons: [t('confirmDelete'), t('confirmCancel')],
}) })
@ -533,7 +517,7 @@ function handleBulkChange() {
const {id} = msg.style; const {id} = msg.style;
if (msg.method === 'styleDeleted') { if (msg.method === 'styleDeleted') {
handleDelete(id); handleDelete(id);
bulkChangeTime = performance.now(); bulkChangeQueue.time = performance.now();
} else { } else {
handleUpdateForId(id, msg); handleUpdateForId(id, msg);
} }
@ -544,7 +528,7 @@ function handleBulkChange() {
function handleUpdateForId(id, opts) { function handleUpdateForId(id, opts) {
return API.getStyle(id, true).then(style => { return API.getStyle(id, true).then(style => {
handleUpdate(style, opts); handleUpdate(style, opts);
bulkChangeTime = performance.now(); bulkChangeQueue.time = performance.now();
}); });
} }
@ -718,6 +702,20 @@ function highlightEditedStyle() {
} }
} }
function waitForSelector(selector) {
// TODO: if used in other places, move to dom.js
// TODO: if used concurrently, rework to use just one observer internally
return new Promise(resolve => {
const mo = new MutationObserver(() => {
const el = $(selector);
if (el) {
mo.disconnect();
resolve(el);
}
});
mo.observe(document, {childList: true, subtree: true});
});
}
function embedOptions() { function embedOptions() {
let options = $('#stylus-embedded-options'); let options = $('#stylus-embedded-options');

View File

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

View File

@ -180,8 +180,8 @@
<script src="js/dom.js"></script> <script src="js/dom.js"></script>
<script src="js/messaging.js"></script> <script src="js/messaging.js"></script>
<script src="js/localization.js"></script> <script src="js/localization.js"></script>
<script src="js/prefs.js"></script>
<script src="js/msg.js"></script> <script src="js/msg.js"></script>
<script src="js/prefs.js"></script>
<script src="content/style-injector.js"></script> <script src="content/style-injector.js"></script>
<script src="content/apply.js"></script> <script src="content/apply.js"></script>

View File

@ -310,7 +310,7 @@ function sortStyles(entries) {
return entries.sort(({styleMeta: a}, {styleMeta: b}) => return entries.sort(({styleMeta: a}, {styleMeta: b}) =>
Boolean(a.frameUrl) - Boolean(b.frameUrl) || Boolean(a.frameUrl) - Boolean(b.frameUrl) ||
enabledFirst && Boolean(b.enabled) - Boolean(a.enabled) || enabledFirst && Boolean(b.enabled) - Boolean(a.enabled) ||
a.name.localeCompare(b.name)); (a.customName || a.name).localeCompare(b.customName || b.name));
} }
function showStyles(frameResults) { function showStyles(frameResults) {
@ -408,7 +408,7 @@ function createStyleElement(style) {
$('.checker', entry).checked = style.enabled; $('.checker', entry).checked = style.enabled;
const styleName = $('.style-name', entry); const styleName = $('.style-name', entry);
styleName.lastChild.textContent = style.name; styleName.lastChild.textContent = style.customName || style.name;
setTimeout(() => { setTimeout(() => {
styleName.title = entry.styleMeta.sloppy ? styleName.title = entry.styleMeta.sloppy ?
t('styleNotAppliedRegexpProblemTooltip') : t('styleNotAppliedRegexpProblemTooltip') :