tidy up USW-related UI and code (#1285)

* shortened the title to "Publish" and fixed the compact mode
* made collapsed <details> share the same line in compact mode
* made hard-coded strings localizable
* IIFE modules instead of generically named globals
* unified and sorted the names of localized messages
* adjusted spacing of header items
* center auth popup to current window

Co-authored-by: Gusted <williamzijl7@hotmail.com>
This commit is contained in:
tophf 2021-07-30 15:44:06 +03:00 committed by GitHub
parent 23d86c53a7
commit 6650a37194
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 593 additions and 454 deletions

View File

@ -452,6 +452,9 @@
"message": "Clone",
"description": "Used in various places for an action that clones something"
},
"genericDescription": {
"message": "Description"
},
"genericDisabledLabel": {
"message": "Disabled",
"description": "Used in various lists/options to indicate that something is disabled"
@ -1242,6 +1245,28 @@
"message": "Temporarily applies the changes without saving.\nSave the style to make the changes permanent.",
"description": "Tooltip for the checkbox in style editor to enable live preview while editing."
},
"publish": {
"message": "Publish",
"description": "Header for the section to link the style with userStyles.world"
},
"publishPush": {
"message": "Push update",
"description": "The 'Publish style' button's new name when a connection is established"
},
"publishReconnect": {
"message": "Try disconnecting then publish again"
},
"publishRetry": {
"message": "Stylus is still trying to publish this style, but you can retry if you see no authentication activity or popups. Retry now?"
},
"publishStyle": {
"message": "Publish style",
"description": "Publish the current style to userstyles.world"
},
"publishUsw": {
"message": "Using <userstyles.world>",
"description": "Name of the link to https://userstyles.world in the editor"
},
"readingStyles": {
"message": "Reading styles..."
},
@ -1365,18 +1390,6 @@
"message": "Sections",
"description": "Header for the table of contents block listing style section names in the left panel of the classic editor"
},
"integration": {
"message": "UserStyles.world integration",
"description": "Header for the section to link the style with userStyles.world"
},
"uploadStyle": {
"message": "Publish style",
"description": "Publish the current style to userstyles.world"
},
"revokeLink": {
"message": "Revoke link",
"description": "Revoke current link of style with userstyles.world"
},
"shortcuts": {
"message": "Shortcuts",
"description": "Go to shortcut configuration"
@ -1493,6 +1506,9 @@
"message": "Mozilla Format",
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
},
"styleName": {
"message": "Style name"
},
"styleNotAppliedRegexpProblemTooltip": {
"message": "Style was not applied due to its incorrect usage of 'regexp()'",
"description": "Tooltip in the popup for styles that were not applied at all"

View File

@ -6,6 +6,7 @@
/* global syncMan */
/* global updateMan */
/* global usercssMan */
/* global uswApi */
/* global
FIREFOX
URLS
@ -20,10 +21,26 @@
addAPI(/** @namespace API */ {
/** Temporary storage for data needed elsewhere e.g. in a content script */
data: ((data = {}) => ({
del: key => delete data[key],
get: key => data[key],
has: key => key in data,
pop: key => {
const val = data[key];
delete data[key];
return val;
},
set: (key, val) => {
data[key] = val;
},
}))(),
styles: styleMan,
sync: syncMan,
updater: updateMan,
usercss: usercssMan,
usw: uswApi,
/** @type {BackgroundWorker} */
worker: createWorker({url: '/background/background-worker'}),

View File

@ -6,8 +6,6 @@
/* global prefs */
/* global tabMan */
/* global usercssMan */
/* global tokenMan */
/* global retrieveStyleInformation uploadStyle */// usw-api.js
'use strict';
/*
@ -63,7 +61,6 @@ const styleMan = (() => {
let ready = init();
chrome.runtime.onConnect.addListener(handleLivePreview);
chrome.runtime.onConnect.addListener(handlePublishingUSW);
//#endregion
//#region Exports
@ -74,16 +71,21 @@ const styleMan = (() => {
async delete(id, reason) {
if (ready.then) await ready;
const data = id2data(id);
const {style, appliesTo} = data;
await db.exec('delete', id);
if (reason !== 'sync') {
API.sync.delete(data.style._id, Date.now());
API.sync.delete(style._id, Date.now());
}
for (const url of data.appliesTo) {
for (const url of appliesTo) {
const cache = cachedStyleForUrl.get(url);
if (cache) delete cache.sections[id];
}
dataMap.delete(id);
uuidIndex.delete(data.style._id);
uuidIndex.delete(style._id);
if (style._usw && style._usw.token) {
// Must be called after the style is deleted from dataMap
API.usw.revoke(id);
}
await msg.broadcast({
method: 'styleDeleted',
style: {id},
@ -107,7 +109,7 @@ const styleMan = (() => {
if (ready.then) await ready;
style = mergeWithMapped(style);
style.updateDate = Date.now();
return handleSave(await saveStyle(style), {reason: 'editSave'});
return saveStyle(style, {reason: 'editSave'});
},
/** @returns {Promise<?StyleObj>} */
@ -240,7 +242,7 @@ const styleMan = (() => {
if (url) style.url = style.installationUrl = url;
style.originalDigest = await calcStyleDigest(style);
// FIXME: update updateDate? what about usercss config?
return handleSave(await saveStyle(style), {reason});
return saveStyle(style, {reason});
},
/** @returns {Promise<?StyleObj>} */
@ -268,11 +270,13 @@ const styleMan = (() => {
}
},
save: saveStyle,
/** @returns {Promise<number>} style id */
async toggle(id, enabled) {
if (ready.then) await ready;
const style = Object.assign({}, id2style(id), {enabled});
handleSave(await saveStyle(style), {reason: 'toggle', codeIsUpdated: false});
await saveStyle(style, {reason: 'toggle', codeIsUpdated: false});
return id;
},
@ -356,65 +360,6 @@ const styleMan = (() => {
});
}
function handlePublishingUSW(port) {
if (port.name !== 'link-style-usw') {
return;
}
port.onMessage.addListener(async incData => {
const {data: style, reason} = incData;
if (!style.id) {
return;
}
switch (reason) {
case 'revoke':
await tokenMan.revokeToken('userstylesworld', style.id);
style._usw = {};
handleSave(await saveStyle(style), {reason: 'success-revoke', codeIsUpdated: true});
break;
case 'publish': {
if (!style._usw || !style._usw.token) {
for (const {style: someStyle} of dataMap.values()) {
if (someStyle._id === style._id) {
someStyle.tmpSourceCode = style.sourceCode;
let metadata = {};
try {
const {metadata: tmpMetadata} = await API.worker.parseUsercssMeta(style.sourceCode);
metadata = tmpMetadata;
} catch (err) {
console.log(err);
}
someStyle.metadata = metadata;
} else {
delete someStyle.tmpSourceCode;
delete someStyle.metadata;
}
handleSave(await saveStyle(someStyle), {broadcast: false});
}
style._usw = {
token: await tokenMan.getToken('userstylesworld', true, style.id),
};
delete style.tmpSourceCode;
delete style.metadata;
for (const [k, v] of Object.entries(await retrieveStyleInformation(style._usw.token))) {
style._usw[k] = v;
}
handleSave(await saveStyle(style), {reason: 'success-publishing', codeIsUpdated: true});
}
const returnResult = await uploadStyle(style);
// USw prefix errors with `Error:`.
if (returnResult.startsWith('Error:')) {
style._usw.publishingError = returnResult;
handleSave(await saveStyle(style), {reason: 'publishing-failed', codeIsUpdated: true});
}
break;
}
}
});
}
async function addIncludeExclude(type, id, rule) {
if (ready.then) await ready;
const style = Object.assign({}, id2style(id));
@ -423,7 +368,7 @@ const styleMan = (() => {
throw new Error('The rule already exists');
}
style[type] = list.concat([rule]);
return handleSave(await saveStyle(style), {reason: 'styleSettings'});
return saveStyle(style, {reason: 'styleSettings'});
}
async function removeIncludeExclude(type, id, rule) {
@ -434,7 +379,7 @@ const styleMan = (() => {
return;
}
style[type] = list.filter(r => r !== rule);
return handleSave(await saveStyle(style), {reason: 'styleSettings'});
return saveStyle(style, {reason: 'styleSettings'});
}
function broadcastStyleUpdated(style, reason, method = 'styleUpdated', codeIsUpdated = true) {
@ -490,14 +435,14 @@ const styleMan = (() => {
style.id = newId;
}
uuidIndex.set(style._id, style.id);
API.sync.put(style._id, style._rev, style._usw);
API.sync.put(style._id, style._rev);
}
async function saveStyle(style) {
async function saveStyle(style, handlingOptions) {
beforeSave(style);
const newId = await db.exec('put', style);
afterSave(style, newId);
return style;
return handleSave(style, handlingOptions);
}
function handleSave(style, {reason, codeIsUpdated, broadcast = true}) {
@ -528,9 +473,7 @@ const styleMan = (() => {
async function init() {
const styles = await db.exec('getAll') || [];
const updated = styles.filter(style =>
addMissingProps(style) +
addCustomName(style));
const updated = styles.filter(fixOldStyleProps);
if (updated.length) {
await db.exec('putMany', updated);
}
@ -543,7 +486,7 @@ const styleMan = (() => {
bgReady._resolveStyles();
}
function addMissingProps(style) {
function fixOldStyleProps(style) {
let res = 0;
for (const key in MISSING_PROPS) {
if (!style[key]) {
@ -551,20 +494,15 @@ const styleMan = (() => {
res = 1;
}
}
return res;
}
/** Upgrades the old way of customizing local names */
function addCustomName(style) {
let res = 0;
/* Upgrade the old way of customizing local names */
const {originalName} = style;
if (originalName) {
res = 1;
if (originalName !== style.name) {
style.customName = style.name;
style.name = originalName;
}
delete style.originalName;
res = 1;
}
return res;
}

View File

@ -64,11 +64,12 @@ const tokenMan = (() => {
return {
buildKeys(name, styleId) {
buildKeys(name, hooks) {
const prefix = `secure/token/${hooks ? hooks.keyName(name) : name}/`;
const k = {
TOKEN: `secure/token/${name}/${styleId ? `${styleId}/` : ''}token`,
EXPIRE: `secure/token/${name}/${styleId ? `${styleId}/` : ''}expire`,
REFRESH: `secure/token/${name}/${styleId ? `${styleId}/` : ''}refresh`,
TOKEN: `${prefix}token`,
EXPIRE: `${prefix}expire`,
REFRESH: `${prefix}refresh`,
};
k.LIST = Object.values(k);
return k;
@ -78,8 +79,8 @@ const tokenMan = (() => {
return AUTH[name].clientId;
},
async getToken(name, interactive, styleId) {
const k = tokenMan.buildKeys(name, styleId);
async getToken(name, interactive, hooks) {
const k = tokenMan.buildKeys(name, hooks);
const obj = await chromeLocal.get(k.LIST);
if (obj[k.TOKEN]) {
if (!obj[k.EXPIRE] || Date.now() < obj[k.EXPIRE]) {
@ -92,13 +93,12 @@ const tokenMan = (() => {
if (!interactive) {
throw new Error(`Invalid token: ${name}`);
}
const accessToken = authUser(name, k, interactive, styleId ? {vendor_data: styleId} : {});
return accessToken;
return authUser(k, name, interactive, hooks);
},
async revokeToken(name, styleId) {
async revokeToken(name, hooks) {
const provider = AUTH[name];
const k = tokenMan.buildKeys(name, styleId);
const k = tokenMan.buildKeys(name, hooks);
if (provider.revoke) {
try {
const token = await chromeLocal.getValue(k.TOKEN);
@ -133,17 +133,17 @@ const tokenMan = (() => {
return handleTokenResult(result, k);
}
async function authUser(name, k, interactive = false, extraQuery = {}) {
async function authUser(keys, name, interactive = false, hooks = null) {
await require(['/vendor/webext-launch-web-auth-flow/webext-launch-web-auth-flow.min']);
/* global webextLaunchWebAuthFlow */
const provider = AUTH[name];
const state = Math.random().toFixed(8).slice(2);
const query = Object.assign(extraQuery, {
const query = {
response_type: provider.flow,
client_id: provider.clientId,
redirect_uri: provider.redirect_uri || chrome.identity.getRedirectURL(),
state,
});
};
if (provider.scopes) {
query.scope = provider.scopes.join(' ');
}
@ -153,17 +153,25 @@ const tokenMan = (() => {
if (alwaysUseTab == null) {
alwaysUseTab = await detectVivaldiWebRequestBug();
}
if (hooks) hooks.query(query);
const url = `${provider.authURL}?${new URLSearchParams(query)}`;
const width = Math.min(screen.availWidth - 100, 800);
const height = Math.min(screen.availHeight - 100, 800);
const wnd = await browser.windows.getLastFocused();
const finalUrl = await webextLaunchWebAuthFlow({
url,
alwaysUseTab,
interactive,
redirect_uri: query.redirect_uri,
windowOptions: {
windowOptions: Object.assign({
state: 'normal',
width: Math.min(screen.width - 100, 800),
height: Math.min(screen.height - 100, 800),
},
width,
height,
}, wnd.state !== 'minimized' && {
// Center the popup to the current window
top: Math.ceil(wnd.top + (wnd.height - width) / 2),
left: Math.ceil(wnd.left + (wnd.width - width) / 2),
}),
});
const params = new URLSearchParams(
provider.flow === 'token' ?
@ -194,7 +202,7 @@ const tokenMan = (() => {
}
result = await postQuery(provider.tokenURL, body);
}
return handleTokenResult(result, k);
return handleTokenResult(result, keys);
}
async function handleTokenResult(result, k) {

View File

@ -1,29 +1,119 @@
/* global API msg */// msg.js
/* global URLS */ // toolbox.js
/* global tokenMan */
'use strict';
/* exported retrieveStyleInformation */
async function retrieveStyleInformation(token) {
return (await (await fetch(`${URLS.usw}api/style`, {
method: 'GET',
headers: new Headers({
'Authorization': `Bearer ${token}`,
}),
credentials: 'omit',
})).json()).data;
const uswApi = (() => {
//#region Internals
class TokenHooks {
constructor(id) {
this.id = id;
}
keyName(name) {
return `${name}/${this.id}`;
}
query(query) {
return Object.assign(query, {vendor_data: this.id});
}
}
/* exported uploadStyle */
async function uploadStyle(style) {
return (await (await fetch(`${URLS.usw}api/style/${style._usw.id}`, {
method: 'POST',
headers: new Headers({
'Authorization': `Bearer ${style._usw.token}`,
'Content-Type': 'application/json',
}),
body: JSON.stringify({
code: style.sourceCode,
}),
credentials: 'omit',
})).json()).data;
function fakeUsercssHeader(style) {
const {name, _usw: u = {}} = style;
const meta = Object.entries({
'@name': u.name || name || '?',
'@version': // Same as USO-archive version: YYYYMMDD.hh.mm
new Date().toISOString().replace(/^(\d+)-(\d+)-(\d+)T(\d+):(\d+).+/, '$1$2$3.$4.$5'),
'@namespace': u.namespace !== '?' && u.namespace ||
u.username && `userstyles.world/user/${u.username}` ||
'?',
'@description': u.description,
'@author': u.username,
'@license': u.license,
});
const maxKeyLen = meta.reduce((res, [k]) => Math.max(res, k.length), 0);
return [
'/* ==UserStyle==',
...meta.map(([k, v]) => `${k}${' '.repeat(maxKeyLen - k.length + 2)}${v || ''}`),
'==/UserStyle== */',
].join('\n') + '\n\n';
}
async function linkStyle(style, sourceCode) {
const {id} = style;
const metadata = await API.worker.parseUsercssMeta(sourceCode).catch(console.warn) || {};
const uswData = Object.assign({}, style, {metadata, sourceCode});
API.data.set('usw' + id, uswData);
const token = await tokenMan.getToken('userstylesworld', true, new TokenHooks(id));
const info = await uswFetch('style', token);
const data = style._usw = Object.assign({token}, info);
style.url = style.url || data.homepage || `${URLS.usw}style/${data.id}`;
await uswSave(style);
return data;
}
async function uswFetch(path, token, opts) {
opts = Object.assign({credentials: 'omit'}, opts);
opts.headers = Object.assign({Authorization: `Bearer ${token}`}, opts.headers);
return (await (await fetch(`${URLS.usw}api/${path}`, opts)).json()).data;
}
/** Uses a custom method when broadcasting and avoids needlessly sending the entire style */
async function uswSave(style) {
const {id, _usw} = style;
await API.styles.save(style, {broadcast: false});
msg.broadcastExtension({method: 'uswData', style: {id, _usw}});
}
//#endregion
//#region Exports
return {
/**
* @param {number} id
* @param {string} sourceCode
* @return {Promise<string>}
*/
async publish(id, sourceCode) {
const style = await API.styles.get(id);
const data = (style._usw || {}).token
? style._usw
: await linkStyle(style, sourceCode);
const header = style.usercssData ? '' : fakeUsercssHeader(style);
return uswFetch(`style/${data.id}`, data.token, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({code: header + sourceCode}),
});
},
/**
* @param {number} id
* @return {Promise<void>}
*/
async revoke(id) {
await tokenMan.revokeToken('userstylesworld', new TokenHooks(id));
const style = await API.styles.get(id);
if (style) {
style._usw = {};
await uswSave(style);
}
},
};
//#endregion
})();
/* Doing this outside so we don't break IDE's recognition of the exported methods in IIFE */
for (const [k, fn] of Object.entries(uswApi)) {
uswApi[k] = async (id, ...args) => {
API.data.set('usw' + id, true);
try {
/* Awaiting inside `try` so that `finally` runs when done */
return await fn(id, ...args);
} finally {
API.data.del('usw' + id);
}
};
}

View File

@ -19,9 +19,8 @@
if (location.pathname === '/api/oauth/style/new') {
const styleId = Number(new URLSearchParams(location.search).get('vendor_data'));
API.styles.get(styleId).then(style => {
style.sourceCode = style.tmpSourceCode;
sendPostMessage({type: 'usw-fill-new-style', data: style});
API.data.pop('usw' + styleId).then(data => {
sendPostMessage({type: 'usw-fill-new-style', data});
});
}
}

View File

@ -242,7 +242,7 @@
<body id="stylus-edit">
<div id="header">
<h1 id="heading">&nbsp;</h1> <!-- nbsp allocates the actual height which prevents page shift -->
<h1 id="heading" i18n-data-edit="editStyleHeading" i18n-data-add="addStyleTitle"></h1>
<section id="basic-info">
<div id="basic-info-name">
<input id="name" class="style-contributor" spellcheck="false">
@ -262,7 +262,7 @@
<input type="checkbox" id="enabled" class="style-contributor">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
<label id="preview-label" i18n-text="previewLabel" i18n-title="previewTooltip" class="hidden">
<label id="preview-label" i18n-text="previewLabel" i18n-title="previewTooltip">
<input type="checkbox" id="editor.livePreview">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
@ -392,11 +392,21 @@
</div>
</div>
</details>
<details id="integration" data-pref="editor.integration.expanded" class="ignore-pref-if-compact">
<summary><h2 i18n-text="integration"></h2></summary>
<details id="publish" data-pref="editor.publish.expanded" class="ignore-pref-if-compact">
<summary><h2 i18n-text="publish"></h2></summary>
<div>
<button id="publish-style" i18n-text="uploadStyle"></button>
<button id="revoke-link" i18n-text="revokeLink"></button>
<a id="usw-url" href="https://userstyles.world" target="_blank">&nbsp;</a>
<div id="usw-link-info">
<dl><dt i18n-text="styleName"></dt><dd data-usw="name"></dd></dl>
<dl><dt i18n-text="genericDescription"></dt><dd data-usw="description"></dd></dl>
</div>
<div>
<button id="usw-publish-style"
i18n-data-publish="publishStyle"
i18n-data-push="publishPush"></button>
<button id="usw-disconnect" i18n-text="optionsSyncDisconnect"></button>
<span id="usw-progress"></span>
</div>
</div>
</details>
<details id="sections-list" data-pref="editor.toc.expanded" class="ignore-pref-if-compact">

View File

@ -21,6 +21,7 @@
* @namespace Editor
*/
const editor = {
style: null,
dirty: DirtyReporter(),
isUsercss: false,
isWindowed: false,
@ -34,6 +35,10 @@ const editor = {
previewDelay: 200, // Chrome devtools uses 200
scrollInfo: null,
onStyleUpdated() {
document.documentElement.classList.toggle('is-new-style', !editor.style.id);
},
updateTitle(isDirty = editor.dirty.isDirty()) {
const {customName, name} = editor.style;
document.title = `${
@ -84,6 +89,7 @@ const baseInit = (() => {
// switching the mode here to show the correct page ASAP, usually before DOMContentLoaded
editor.isUsercss = Boolean(style.usercssData || !style.id && prefs.get('newStyleAsUsercss'));
editor.style = style;
editor.onStyleUpdated();
editor.updateTitle(false);
document.documentElement.classList.toggle('usercss', editor.isUsercss);
sessionStore.justEditedStyleId = style.id || '';
@ -132,8 +138,7 @@ baseInit.domReady.then(() => {
document.body.classList.remove('compact-layout', 'fixed-header');
window.off('scroll', fixedHeader);
}
for (const type of ['options', 'toc', 'lint']) {
const el = $(`details[data-pref="editor.${type}.expanded"]`);
for (const el of $$('details[data-pref]')) {
el.open = compact ? false : prefs.get(el.dataset.pref);
}
}
@ -161,9 +166,6 @@ baseInit.ready.then(() => {
initThemeElement();
setupLivePrefs();
$('#heading').textContent = t(editor.style.id ? 'editStyleHeading' : 'addStyleTitle');
$('#preview-label').classList.toggle('hidden', !editor.style.id);
require(Object.values(editor.lazyKeymaps), () => {
initKeymapElement();
prefs.subscribe('editor.keyMap', showHotkeyInTooltip, {runNow: true});

View File

@ -7,6 +7,14 @@ body {
font: 12px arial,sans-serif;
}
a {
color: #000;
transition: color .5s;
}
a:hover {
color: #666;
}
#global-progress {
position: fixed;
height: 4px;
@ -24,10 +32,17 @@ body {
opacity: 1;
}
html.is-new-style #preview-label,
html.is-new-style #publish,
.hidden {
display: none !important;
}
html.is-new-style #heading::after {
content: attr(data-add);
}
html:not(.is-new-style) #heading::after {
content: attr(data-edit);
}
/************ embedded popup for simple-window editor ************/
#popup-iframe {
@ -215,7 +230,9 @@ input:invalid {
margin-left: -13px;
cursor: pointer;
}
#header summary + * {
padding: .5rem 0;
}
#header summary h2 {
display: inline-block;
border-bottom: 1px dotted transparent;
@ -225,9 +242,6 @@ input:invalid {
padding-left: 13px; /* clicking directly on details-marker doesn't set pref so we cover it with h2 */
}
#options-wrapper {
padding: .5rem 0;
}
#header summary:hover h2 {
border-color: #bbb;
}
@ -244,6 +258,7 @@ input:invalid {
#header details {
margin-top: .5rem;
max-width: 100%;
}
#actions > * {
@ -276,6 +291,81 @@ input:invalid {
#lint:not([open]) h2 {
margin-bottom: 0;
}
#publish > div > * {
margin-top: .75em;
}
#publish a:visited {
margin-top: .75em;
}
#publish[data-connected] summary::marker,
#publish[data-connected] h2 {
color: hsl(180, 100%, 20%);
}
#publish:not([data-connected]) #usw-link-info,
#publish:not([data-connected]) #usw-disconnect {
display: none;
}
#publish[data-connected] #usw-publish-style::after {
content: attr(data-push);
}
#publish:not([data-connected]) #usw-publish-style::after {
content: attr(data-publish);
}
#usw-link-info dl {
margin: 0;
display: flex;
}
#usw-link-info dt {
flex-shrink: 0;
}
#usw-link-info dt::after {
content: ":"
}
#usw-link-info dt,
#usw-link-info dd {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#usw-link-info dd {
margin-left: .5em;
}
#usw-link-info dd[data-usw="name"] {
font-weight: bold;
}
#usw-progress {
position: relative;
vertical-align: top;
}
#usw-progress .success,
#usw-progress .unchanged {
font-size: 150%;
font-weight: bold;
position: absolute;
margin-left: .25em;
}
#usw-progress .success {
margin-top: -.25em;
}
#usw-progress .success::after {
content: '\2713'; /* checkmark */
}
#usw-progress .unchanged::after {
content: '=';
}
#usw-progress .error {
display: block;
margin-top: .5em;
color: red;
}
#usw-progress .error + div {
font-size: smaller;
}
#usw-progress .lds-spinner {
transform: scale(0.125);
transform-origin: 0 10px;
}
/* options */
#options [type="number"] {
width: 3.5em;
@ -739,7 +829,6 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
#lint {
overflow: hidden;
margin: .5rem -1rem 0;
min-height: 30px;
padding: 0;
box-sizing: border-box;
display: flex;
@ -758,7 +847,7 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
text-indent: -2px;
}
#lint > .lint-scroll-container {
margin: 34px 10px 0;
margin: 1rem 10px 0;
position: absolute;
top: 0;
bottom: 0;
@ -954,7 +1043,7 @@ body.linter-disabled .hidden-unless-compact {
position: inherit;
border-right: none;
border-bottom: 1px dashed #AAA;
padding: 0;
padding: .5rem 1rem .5rem .5rem;
}
.fixed-header {
padding-top: var(--fixed-padding);
@ -972,24 +1061,30 @@ body.linter-disabled .hidden-unless-compact {
.fixed-header #options {
display: none !important;
}
#header summary + *,
#lint > .lint-scroll-container {
margin-left: 1rem;
padding: .25rem 0 .5rem;
}
#actions {
display: flex;
flex-wrap: wrap;
white-space: nowrap;
padding: 0 1rem;
margin: 0;
box-sizing: border-box;
}
#header input[type="checkbox"] {
vertical-align: middle;
}
#header details {
margin: 0;
}
#heading,
h2 {
display: none;
}
#basic-info {
padding: .5rem 1rem;
margin: 0;
margin-bottom: .5rem;
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
@ -1006,22 +1101,17 @@ body.linter-disabled .hidden-unless-compact {
#options-wrapper {
display: flex;
flex-wrap: wrap;
padding: .5rem 1rem 0;
box-sizing: border-box;
}
#toc {
padding: .5rem 1rem;
}
#details-wrapper {
flex-direction: row;
flex-wrap: wrap;
padding-bottom: .25rem;
}
#options {
#options[open] {
width: 100%;
}
#sections-list[open] {
height: 102px;
max-height: 102px;
}
#sections-list[open] #toc {
max-height: 60px;
@ -1029,13 +1119,16 @@ body.linter-disabled .hidden-unless-compact {
}
#sections-list,
#lint {
width: 50%;
max-width: 50%;
}
.options-column {
flex-grow: 1;
padding-right: .5rem;
box-sizing: border-box;
}
.options-column > .usercss-only {
margin-bottom: 0;
}
#options-wrapper .options-column:nth-child(2) {
margin-top: 0;
}
@ -1054,8 +1147,9 @@ body.linter-disabled .hidden-unless-compact {
margin-left: 0;
padding-left: 4px;
}
#options h2 {
margin: 0 0 .5em;
#header summary h2 {
margin: 0;
padding: 0;
}
.option label {
margin: 0;
@ -1069,7 +1163,8 @@ body.linter-disabled .hidden-unless-compact {
top: 0.2rem;
}
#lint > .lint-scroll-container {
margin: 26px 1rem 0;
padding-top: 0;
margin-right: 0;
}
#lint {
padding: 0;

View File

@ -11,7 +11,6 @@
/* global linterMan */
/* global prefs */
/* global t */// localization.js
/* global updateUI revokeLinking publishStyle */// usw-integration.js
'use strict';
//#region init
@ -19,7 +18,6 @@
baseInit.ready.then(async () => {
await waitForSheet();
(editor.isUsercss ? SourceEditor : SectionsEditor)();
updateUI();
await editor.ready;
editor.ready = true;
editor.dirty.onChange(editor.updateDirty);
@ -48,33 +46,29 @@ baseInit.ready.then(async () => {
require(['/edit/linter-dialogs'], () => linterMan.showLintConfig());
$('#lint-help').onclick = () =>
require(['/edit/linter-dialogs'], () => linterMan.showLintHelp());
$('#revoke-link').onclick = () => revokeLinking();
$('#publish-style').onclick = () => publishStyle();
require([
'/edit/autocomplete',
'/edit/global-search',
]);
});
//#endregion
//#region events
const IGNORE_UPDATE_REASONS = [
'editPreview',
'editPreviewEnd',
'editSave',
'config',
];
msg.onExtension(request => {
const {style} = request;
switch (request.method) {
case 'styleUpdated':
if (editor.style.id === style.id) {
if (!['editPreview', 'editPreviewEnd', 'editSave', 'config'].includes(request.reason)) {
if (editor.style.id === style.id && !IGNORE_UPDATE_REASONS.includes(request.reason)) {
Promise.resolve(request.codeIsUpdated === false ? style : API.styles.get(style.id))
.then(newStyle => {
editor.replaceStyle(newStyle, request.codeIsUpdated);
if (['success-publishing', 'success-revoke'].includes(request.reason)) {
updateUI(newStyle);
}
if (request.reason === 'publishing-failed') {
messageBoxProxy.alert(newStyle._usw.publishingError, 'pre',
'UserStyles.world: ' + t('genericError'));
}
});
}
.then(newStyle => editor.replaceStyle(newStyle, request.codeIsUpdated));
}
break;
case 'styleDeleted':
@ -262,15 +256,11 @@ editor.livePreview = (() => {
/**
* @param {Function} [fn] - preprocessor
* @param {boolean} [show]
*/
init(fn, show) {
init(fn) {
preprocess = fn;
if (show != null) toggle(show);
},
toggle,
update(newData) {
data = newData;
if (!port) {
@ -290,10 +280,6 @@ editor.livePreview = (() => {
});
}
function toggle(state) {
$('#preview-label').classList.toggle('hidden', !state);
}
async function updatePreviewer(data) {
const errorContainer = $('#preview-errors');
try {

View File

@ -1,4 +1,4 @@
/* global $ $$ $create $remove focusAccessibility */// dom.js
/* global $ $$ $create $remove focusAccessibility toggleDataset */// dom.js
/* global CodeMirror */
/* global chromeLocal */// storage-util.js
/* global colorMimicry */
@ -876,15 +876,6 @@
}
function toggleDataset(el, prop, state) {
if (state) {
el.dataset[prop] = '';
} else {
delete el.dataset[prop];
}
}
function saveWindowScrollPos() {
state.scrollX = window.scrollX;
state.scrollY = window.scrollY;

View File

@ -25,7 +25,7 @@ function SectionsEditor() {
updateHeader();
rerouteHotkeys.toggle(true); // enabled initially because we don't always focus a CodeMirror
editor.livePreview.init(null, style.id);
editor.livePreview.init();
container.classList.add('section-editor');
$('#to-mozilla').on('click', showMozillaFormat);
$('#to-mozilla-help').on('click', showToMozillaHelp);
@ -54,6 +54,11 @@ function SectionsEditor() {
return `${t('sectionCode')} ${index + 1}`;
},
getValue(asObject) {
const st = getModel();
return asObject ? st : MozDocMapper.styleToCss(st);
},
getSearchableInputs(cm) {
const sec = sections.find(s => s.cm === cm);
return sec ? sec.appliesTo.map(a => a.valueEl).filter(Boolean) : [];
@ -86,14 +91,13 @@ function SectionsEditor() {
await initSections(newStyle.sections, {replace: true});
}
Object.assign(style, newStyle);
editor.onStyleUpdated();
updateHeader();
dirty.clear();
// Go from new style URL to edit style URL
if (location.href.indexOf('id=') === -1 && style.id) {
history.replaceState({}, document.title, 'edit.html?id=' + style.id);
$('#heading').textContent = t('editStyleHeading');
if (style.id && !/[&?]id=/.test(location.search)) {
history.replaceState({}, document.title, `${location.pathname}?id=${style.id}`);
}
editor.livePreview.toggle(Boolean(style.id));
updateLivePreview();
},
@ -323,7 +327,7 @@ function SectionsEditor() {
function showMozillaFormat() {
const popup = showCodeMirrorPopup(t('styleToMozillaFormatTitle'), '', {readOnly: true});
popup.codebox.setValue(MozDocMapper.styleToCss(getModel()));
popup.codebox.setValue(editor.getValue());
popup.codebox.execCommand('selectAll');
}
@ -425,7 +429,7 @@ function SectionsEditor() {
editor.updateToc();
}
/** @returns {Style} */
/** @returns {StyleObj} */
function getModel() {
return Object.assign({}, style, {
sections: sections.filter(s => !s.removed).map(s => s.getModel()),

View File

@ -30,7 +30,7 @@ function SourceEditor() {
const cm = cmFactory.create($('.single-editor'));
const sectionFinder = MozSectionFinder(cm);
const sectionWidget = MozSectionWidget(cm, sectionFinder);
editor.livePreview.init(preprocess, style.id);
editor.livePreview.init(preprocess);
createMetaCompiler(meta => {
style.usercssData = meta;
style.name = meta.name;
@ -48,6 +48,7 @@ function SourceEditor() {
closestVisible: () => cm,
getEditors: () => [cm],
getEditorTitle: () => '',
getValue: () => cm.getValue(),
getSearchableInputs: () => [],
prevEditor: nextPrevSection.bind(null, -1),
nextEditor: nextPrevSection.bind(null, 1),
@ -241,9 +242,8 @@ function SourceEditor() {
}
sessionStore.justEditedStyleId = newStyle.id;
Object.assign(style, newStyle);
$('#preview-label').classList.remove('hidden');
editor.onStyleUpdated();
updateMeta();
editor.livePreview.toggle(Boolean(style.id));
}
}

View File

@ -1,49 +1,97 @@
/* global $ $create $remove */// dom.js
/* global $ $create $remove messageBoxProxy showSpinner toggleDataset */// dom.js
/* global API msg */// msg.js
/* global URLS */// toolbox.js
/* global baseInit */
/* global editor */
/* global t */// localization.js
'use strict';
let uswPort;
(() => {
//#region Main
function connectToPort() {
if (!uswPort) {
uswPort = chrome.runtime.connect({name: 'link-style-usw'});
uswPort.onDisconnect.addListener(err => {
throw err;
const ERROR_TITLE = 'UserStyles.world ' + t('genericError');
const PROGRESS = '#usw-progress';
let spinnerTimer = 0;
let prevCode = '';
msg.onExtension(request => {
if (request.method === 'uswData' &&
request.style.id === editor.style.id) {
Object.assign(editor.style, request.style);
updateUI();
}
});
baseInit.ready.then(() => {
updateUI();
$('#usw-publish-style').onclick = disableWhileActive(publishStyle);
$('#usw-disconnect').onclick = disableWhileActive(disconnect);
});
async function publishStyle() {
const {id} = editor.style;
if (await API.data.has('usw' + id) &&
!await messageBoxProxy.confirm(t('publishRetry'), 'danger', ERROR_TITLE)) {
return;
}
}
/* exported revokeLinking */
function revokeLinking() {
connectToPort();
uswPort.postMessage({reason: 'revoke', data: editor.style});
}
/* exported publishStyle */
function publishStyle() {
connectToPort();
const data = Object.assign(editor.style, {sourceCode: editor.getEditors()[0].getValue()});
uswPort.postMessage({reason: 'publish', data});
}
/* exported updateUI */
function updateUI(useStyle) {
const style = useStyle || editor.style;
if (style._usw && style._usw.token) {
$('#revoke-link').style = '';
const linkInformation = $create('div', {id: 'link-info'}, [
$create('p', `Style name: ${style._usw.name}`),
$create('p', `Description: ${style._usw.description}`),
const code = editor.getValue();
const isDiff = code !== prevCode;
const res = isDiff ? await API.usw.publish(id, code) : t('importReportUnchanged');
const title = `${new Date().toLocaleString()}\n${res}`;
const failed = /^Error:/.test(res);
$(PROGRESS).append(...failed && [
$create('div.error', {title}, res),
$create('div', t('publishReconnect')),
] || [
$create(`span.${isDiff ? 'success' : 'unchanged'}`, {title}),
]);
$remove('#link-info');
$('#integration').insertBefore(linkInformation, $('#integration').firstChild);
} else {
$('#revoke-link').style = 'display: none;';
$remove('#link-info');
if (!failed) prevCode = code;
}
async function disconnect() {
await API.usw.revoke(editor.style.id);
prevCode = null; // to allow the next publishStyle to upload style
}
function updateUI(style = editor.style) {
const usw = style._usw || {};
const section = $('#publish');
toggleDataset(section, 'connected', usw.token);
for (const type of ['name', 'description']) {
const el = $(`dd[data-usw="${type}"]`, section);
el.textContent = el.title = usw[type] || '';
}
const elUrl = $('#usw-url');
elUrl.href = `${URLS.usw}${usw.id ? `style/${usw.id}` : ''}`;
elUrl.textContent = t('publishUsw').replace(/<(.+)>/, `$1${usw.id ? `#${usw.id}` : ''}`);
}
//#endregion
//#region Utility
function disableWhileActive(fn) {
/** @this {Element} */
return async function () {
this.disabled = true;
timerOn();
await fn().catch(console.error);
timerOff();
this.disabled = false;
};
}
function timerOn() {
if (!spinnerTimer) {
$(PROGRESS).textContent = '';
spinnerTimer = setTimeout(showSpinner, 250, PROGRESS);
}
}
function timerOff() {
$remove(`${PROGRESS} .lds-spinner`);
clearTimeout(spinnerTimer);
spinnerTimer = 0;
}
//#endregion
})();

View File

@ -236,6 +236,11 @@ select[disabled] + .select-arrow {
fill: hsl(0, 0%, 50%);
}
summary {
-moz-user-select: none;
user-select: none;
}
/* global stuff we use everywhere */
.hidden {
display: none !important;

View File

@ -21,6 +21,7 @@
<script src="content/style-injector.js"></script>
<script src="content/apply.js"></script>
<link href="spinner.css" rel="stylesheet">
<link href="install-usercss/install-usercss.css" rel="stylesheet">
</head>
<body id="stylus-install-usercss">

View File

@ -297,93 +297,12 @@ label {
padding-left: 16px;
position: relative;
}
/* spinner: https://github.com/loadingio/css-spinner */
@keyframes lds-spinner {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.lds-spinner {
position: absolute;
width: 200px;
height: 200px;
top: 50px;
left: 0;
right: 0;
margin: auto;
opacity: .2;
transition: opacity .5s;
}
.lds-spinner div {
left: 94px;
top: 23px;
position: absolute;
animation: lds-spinner linear 1s infinite;
background: currentColor;
width: 12px;
height: 34px;
border-radius: 20%;
transform-origin: 6px 77px;
}
.lds-spinner div:nth-child(1) {
transform: rotate(0deg);
animation-delay: -0.916666666666667s;
}
.lds-spinner div:nth-child(2) {
transform: rotate(30deg);
animation-delay: -0.833333333333333s;
}
.lds-spinner div:nth-child(3) {
transform: rotate(60deg);
animation-delay: -0.75s;
}
.lds-spinner div:nth-child(4) {
transform: rotate(90deg);
animation-delay: -0.666666666666667s;
}
.lds-spinner div:nth-child(5) {
transform: rotate(120deg);
animation-delay: -0.583333333333333s;
}
.lds-spinner div:nth-child(6) {
transform: rotate(150deg);
animation-delay: -0.5s;
}
.lds-spinner div:nth-child(7) {
transform: rotate(180deg);
animation-delay: -0.416666666666667s;
}
.lds-spinner div:nth-child(8) {
transform: rotate(210deg);
animation-delay: -0.333333333333333s;
}
.lds-spinner div:nth-child(9) {
transform: rotate(240deg);
animation-delay: -0.25s;
}
.lds-spinner div:nth-child(10) {
transform: rotate(270deg);
animation-delay: -0.166666666666667s;
}
.lds-spinner div:nth-child(11) {
transform: rotate(300deg);
animation-delay: -0.083333333333333s;
}
.lds-spinner div:nth-child(12) {
transform: rotate(330deg);
animation-delay: 0s;
}
@keyframes load3 {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
animation: none;
}
/************ reponsive layouts ************/

View File

@ -22,7 +22,7 @@ document.on('visibilitychange', () => {
});
setTimeout(() => {
if (!installed) {
if (!cm) {
$('#header').appendChild($create('.lds-spinner',
new Array(12).fill($create('div')).map(e => e.cloneNode())));
}

View File

@ -13,6 +13,8 @@
moveFocus
scrollElementIntoView
setupLivePrefs
showSpinner
toggleDataset
waitForSheet
*/
@ -325,6 +327,22 @@ function setupLivePrefs(ids = prefs.knownKeys.filter(id => $('#' + id))) {
}
}
/** @param {string|Node} parent - selector or DOM node */
async function showSpinner(parent) {
await require(['/spinner.css']);
parent = parent instanceof Node ? parent : $(parent);
parent.appendChild($create('.lds-spinner',
new Array(12).fill($create('div')).map(e => e.cloneNode())));
}
function toggleDataset(el, prop, state) {
if (state) {
el.dataset[prop] = '';
} else {
delete el.dataset[prop];
}
}
/**
* @param {string} selector - beware of $ quirks with `#dotted.id` that won't work with $$
* @param {Object} [opt]

View File

@ -61,7 +61,7 @@
'editor.toc.expanded': true, // UI element state: expanded/collapsed
'editor.options.expanded': true, // UI element state: expanded/collapsed
'editor.lint.expanded': true, // UI element state: expanded/collapsed
'editor.integration.expanded': true, // UI element state expanded/collapsed
'editor.publish.expanded': true, // UI element state expanded/collapsed
'editor.lineWrapping': true, // word wrap
'editor.smartIndent': true, // 'smart' indent
'editor.indentWithTabs': false, // smart indent with tabs

View File

@ -44,10 +44,10 @@
"background/sync-manager.js",
"background/tab-manager.js",
"background/token-manager.js",
"background/usw-api.js",
"background/update-manager.js",
"background/usercss-install-helper.js",
"background/usercss-manager.js",
"background/usw-api.js",
"background/style-manager.js",
"background/background.js"

View File

@ -282,102 +282,3 @@ body.search-results-shown {
margin-right: .5em;
flex: 1 1 0;
}
/* spinner: https://github.com/loadingio/css-spinner */
.lds-spinner {
-moz-user-select: none;
user-select: none;
pointer-events: none;
position: absolute;
top: 0;
left: 0;
right: 0;
width: 200px; /* don't change! use "transform: scale(.75)" */
height: 200px; /* don't change! use "transform: scale(.75)" */
margin: auto;
animation: lds-spinner 1s reverse;
animation-fill-mode: both;
}
@keyframes lds-spinner {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.lds-spinner div {
left: 94px;
top: 23px;
position: absolute;
animation: lds-spinner linear 1s infinite;
animation-direction: reverse;
background: currentColor;
width: 12px;
height: 34px;
border-radius: 20%;
transform-origin: 6px 77px;
}
.lds-spinner div:nth-child(1) {
transform: rotate(0deg);
animation-delay: -0.916666666666667s;
}
.lds-spinner div:nth-child(2) {
transform: rotate(30deg);
animation-delay: -0.833333333333333s;
}
.lds-spinner div:nth-child(3) {
transform: rotate(60deg);
animation-delay: -0.75s;
}
.lds-spinner div:nth-child(4) {
transform: rotate(90deg);
animation-delay: -0.666666666666667s;
}
.lds-spinner div:nth-child(5) {
transform: rotate(120deg);
animation-delay: -0.583333333333333s;
}
.lds-spinner div:nth-child(6) {
transform: rotate(150deg);
animation-delay: -0.5s;
}
.lds-spinner div:nth-child(7) {
transform: rotate(180deg);
animation-delay: -0.416666666666667s;
}
.lds-spinner div:nth-child(8) {
transform: rotate(210deg);
animation-delay: -0.333333333333333s;
}
.lds-spinner div:nth-child(9) {
transform: rotate(240deg);
animation-delay: -0.25s;
}
.lds-spinner div:nth-child(10) {
transform: rotate(270deg);
animation-delay: -0.166666666666667s;
}
.lds-spinner div:nth-child(11) {
transform: rotate(300deg);
animation-delay: -0.083333333333333s;
}
.lds-spinner div:nth-child(12) {
transform: rotate(330deg);
animation-delay: 0s;
}

View File

@ -1,4 +1,4 @@
/* global $ $$ $create $remove */// dom.js
/* global $ $$ $create $remove showSpinner */// dom.js
/* global $entry tabURL */// popup.js
/* global API */// msg.js
/* global Events */
@ -153,13 +153,6 @@
});
}
function showSpinner(parent) {
parent = parent instanceof Node ? parent : $(parent);
parent.appendChild($create('.lds-spinner',
new Array(12).fill($create('div')).map(e => e.cloneNode())));
}
function next() {
displayedPage = Math.min(totalPages, displayedPage + 1);
scrollToFirstResult = true;

98
spinner.css Normal file
View File

@ -0,0 +1,98 @@
/* spinner: https://github.com/loadingio/css-spinner */
.lds-spinner {
-moz-user-select: none;
user-select: none;
pointer-events: none;
position: absolute;
top: 0;
left: 0;
right: 0;
width: 200px; /* don't change! use "transform: scale(.75)" */
height: 200px; /* don't change! use "transform: scale(.75)" */
margin: auto;
animation: lds-spinner 1s reverse;
animation-fill-mode: both;
}
@keyframes lds-spinner {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.lds-spinner div {
left: 94px;
top: 23px;
position: absolute;
animation: lds-spinner linear 1s infinite;
animation-direction: reverse;
background: currentColor;
width: 12px;
height: 34px;
border-radius: 20%;
transform-origin: 6px 77px;
}
.lds-spinner div:nth-child(1) {
transform: rotate(0deg);
animation-delay: -0.916666666666667s;
}
.lds-spinner div:nth-child(2) {
transform: rotate(30deg);
animation-delay: -0.833333333333333s;
}
.lds-spinner div:nth-child(3) {
transform: rotate(60deg);
animation-delay: -0.75s;
}
.lds-spinner div:nth-child(4) {
transform: rotate(90deg);
animation-delay: -0.666666666666667s;
}
.lds-spinner div:nth-child(5) {
transform: rotate(120deg);
animation-delay: -0.583333333333333s;
}
.lds-spinner div:nth-child(6) {
transform: rotate(150deg);
animation-delay: -0.5s;
}
.lds-spinner div:nth-child(7) {
transform: rotate(180deg);
animation-delay: -0.416666666666667s;
}
.lds-spinner div:nth-child(8) {
transform: rotate(210deg);
animation-delay: -0.333333333333333s;
}
.lds-spinner div:nth-child(9) {
transform: rotate(240deg);
animation-delay: -0.25s;
}
.lds-spinner div:nth-child(10) {
transform: rotate(270deg);
animation-delay: -0.166666666666667s;
}
.lds-spinner div:nth-child(11) {
transform: rotate(300deg);
animation-delay: -0.083333333333333s;
}
.lds-spinner div:nth-child(12) {
transform: rotate(330deg);
animation-delay: 0s;
}