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", "message": "Clone",
"description": "Used in various places for an action that clones something" "description": "Used in various places for an action that clones something"
}, },
"genericDescription": {
"message": "Description"
},
"genericDisabledLabel": { "genericDisabledLabel": {
"message": "Disabled", "message": "Disabled",
"description": "Used in various lists/options to indicate that something is 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.", "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." "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": { "readingStyles": {
"message": "Reading styles..." "message": "Reading styles..."
}, },
@ -1365,18 +1390,6 @@
"message": "Sections", "message": "Sections",
"description": "Header for the table of contents block listing style section names in the left panel of the classic editor" "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": { "shortcuts": {
"message": "Shortcuts", "message": "Shortcuts",
"description": "Go to shortcut configuration" "description": "Go to shortcut configuration"
@ -1493,6 +1506,9 @@
"message": "Mozilla Format", "message": "Mozilla Format",
"description": "Heading for the section with buttons to import/export Mozilla format of the style" "description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"styleName": {
"message": "Style name"
},
"styleNotAppliedRegexpProblemTooltip": { "styleNotAppliedRegexpProblemTooltip": {
"message": "Style was not applied due to its incorrect usage of 'regexp()'", "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" "description": "Tooltip in the popup for styles that were not applied at all"

View File

@ -6,6 +6,7 @@
/* global syncMan */ /* global syncMan */
/* global updateMan */ /* global updateMan */
/* global usercssMan */ /* global usercssMan */
/* global uswApi */
/* global /* global
FIREFOX FIREFOX
URLS URLS
@ -20,10 +21,26 @@
addAPI(/** @namespace API */ { 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, styles: styleMan,
sync: syncMan, sync: syncMan,
updater: updateMan, updater: updateMan,
usercss: usercssMan, usercss: usercssMan,
usw: uswApi,
/** @type {BackgroundWorker} */ /** @type {BackgroundWorker} */
worker: createWorker({url: '/background/background-worker'}), worker: createWorker({url: '/background/background-worker'}),

View File

@ -6,8 +6,6 @@
/* global prefs */ /* global prefs */
/* global tabMan */ /* global tabMan */
/* global usercssMan */ /* global usercssMan */
/* global tokenMan */
/* global retrieveStyleInformation uploadStyle */// usw-api.js
'use strict'; 'use strict';
/* /*
@ -63,7 +61,6 @@ const styleMan = (() => {
let ready = init(); let ready = init();
chrome.runtime.onConnect.addListener(handleLivePreview); chrome.runtime.onConnect.addListener(handleLivePreview);
chrome.runtime.onConnect.addListener(handlePublishingUSW);
//#endregion //#endregion
//#region Exports //#region Exports
@ -74,16 +71,21 @@ const styleMan = (() => {
async delete(id, reason) { async delete(id, reason) {
if (ready.then) await ready; if (ready.then) await ready;
const data = id2data(id); const data = id2data(id);
const {style, appliesTo} = data;
await db.exec('delete', id); await db.exec('delete', id);
if (reason !== 'sync') { 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); const cache = cachedStyleForUrl.get(url);
if (cache) delete cache.sections[id]; if (cache) delete cache.sections[id];
} }
dataMap.delete(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({ await msg.broadcast({
method: 'styleDeleted', method: 'styleDeleted',
style: {id}, style: {id},
@ -107,7 +109,7 @@ const styleMan = (() => {
if (ready.then) await ready; if (ready.then) await ready;
style = mergeWithMapped(style); style = mergeWithMapped(style);
style.updateDate = Date.now(); style.updateDate = Date.now();
return handleSave(await saveStyle(style), {reason: 'editSave'}); return saveStyle(style, {reason: 'editSave'});
}, },
/** @returns {Promise<?StyleObj>} */ /** @returns {Promise<?StyleObj>} */
@ -240,7 +242,7 @@ const styleMan = (() => {
if (url) style.url = style.installationUrl = url; if (url) style.url = style.installationUrl = url;
style.originalDigest = await calcStyleDigest(style); style.originalDigest = await calcStyleDigest(style);
// FIXME: update updateDate? what about usercss config? // FIXME: update updateDate? what about usercss config?
return handleSave(await saveStyle(style), {reason}); return saveStyle(style, {reason});
}, },
/** @returns {Promise<?StyleObj>} */ /** @returns {Promise<?StyleObj>} */
@ -268,11 +270,13 @@ const styleMan = (() => {
} }
}, },
save: saveStyle,
/** @returns {Promise<number>} style id */ /** @returns {Promise<number>} style id */
async toggle(id, enabled) { async toggle(id, enabled) {
if (ready.then) await ready; if (ready.then) await ready;
const style = Object.assign({}, id2style(id), {enabled}); const style = Object.assign({}, id2style(id), {enabled});
handleSave(await saveStyle(style), {reason: 'toggle', codeIsUpdated: false}); await saveStyle(style, {reason: 'toggle', codeIsUpdated: false});
return id; 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) { async function addIncludeExclude(type, id, rule) {
if (ready.then) await ready; if (ready.then) await ready;
const style = Object.assign({}, id2style(id)); const style = Object.assign({}, id2style(id));
@ -423,7 +368,7 @@ const styleMan = (() => {
throw new Error('The rule already exists'); throw new Error('The rule already exists');
} }
style[type] = list.concat([rule]); style[type] = list.concat([rule]);
return handleSave(await saveStyle(style), {reason: 'styleSettings'}); return saveStyle(style, {reason: 'styleSettings'});
} }
async function removeIncludeExclude(type, id, rule) { async function removeIncludeExclude(type, id, rule) {
@ -434,7 +379,7 @@ const styleMan = (() => {
return; return;
} }
style[type] = list.filter(r => r !== rule); 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) { function broadcastStyleUpdated(style, reason, method = 'styleUpdated', codeIsUpdated = true) {
@ -490,14 +435,14 @@ const styleMan = (() => {
style.id = newId; style.id = newId;
} }
uuidIndex.set(style._id, style.id); 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); beforeSave(style);
const newId = await db.exec('put', style); const newId = await db.exec('put', style);
afterSave(style, newId); afterSave(style, newId);
return style; return handleSave(style, handlingOptions);
} }
function handleSave(style, {reason, codeIsUpdated, broadcast = true}) { function handleSave(style, {reason, codeIsUpdated, broadcast = true}) {
@ -528,9 +473,7 @@ const styleMan = (() => {
async function init() { async function init() {
const styles = await db.exec('getAll') || []; const styles = await db.exec('getAll') || [];
const updated = styles.filter(style => const updated = styles.filter(fixOldStyleProps);
addMissingProps(style) +
addCustomName(style));
if (updated.length) { if (updated.length) {
await db.exec('putMany', updated); await db.exec('putMany', updated);
} }
@ -543,7 +486,7 @@ const styleMan = (() => {
bgReady._resolveStyles(); bgReady._resolveStyles();
} }
function addMissingProps(style) { function fixOldStyleProps(style) {
let res = 0; let res = 0;
for (const key in MISSING_PROPS) { for (const key in MISSING_PROPS) {
if (!style[key]) { if (!style[key]) {
@ -551,20 +494,15 @@ const styleMan = (() => {
res = 1; res = 1;
} }
} }
return res; /* Upgrade the old way of customizing local names */
}
/** Upgrades the old way of customizing local names */
function addCustomName(style) {
let res = 0;
const {originalName} = style; const {originalName} = style;
if (originalName) { if (originalName) {
res = 1;
if (originalName !== style.name) { if (originalName !== style.name) {
style.customName = style.name; style.customName = style.name;
style.name = originalName; style.name = originalName;
} }
delete style.originalName; delete style.originalName;
res = 1;
} }
return res; return res;
} }

View File

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

View File

@ -1,29 +1,119 @@
/* global API msg */// msg.js
/* global URLS */ // toolbox.js /* global URLS */ // toolbox.js
/* global tokenMan */
'use strict'; 'use strict';
/* exported retrieveStyleInformation */ const uswApi = (() => {
async function retrieveStyleInformation(token) {
return (await (await fetch(`${URLS.usw}api/style`, {
method: 'GET',
headers: new Headers({
'Authorization': `Bearer ${token}`,
}),
credentials: 'omit',
})).json()).data;
}
/* exported uploadStyle */ //#region Internals
async function uploadStyle(style) {
return (await (await fetch(`${URLS.usw}api/style/${style._usw.id}`, { class TokenHooks {
method: 'POST', constructor(id) {
headers: new Headers({ this.id = id;
'Authorization': `Bearer ${style._usw.token}`, }
'Content-Type': 'application/json', keyName(name) {
}), return `${name}/${this.id}`;
body: JSON.stringify({ }
code: style.sourceCode, query(query) {
}), return Object.assign(query, {vendor_data: this.id});
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') { if (location.pathname === '/api/oauth/style/new') {
const styleId = Number(new URLSearchParams(location.search).get('vendor_data')); const styleId = Number(new URLSearchParams(location.search).get('vendor_data'));
API.styles.get(styleId).then(style => { API.data.pop('usw' + styleId).then(data => {
style.sourceCode = style.tmpSourceCode; sendPostMessage({type: 'usw-fill-new-style', data});
sendPostMessage({type: 'usw-fill-new-style', data: style});
}); });
} }
} }

View File

@ -242,7 +242,7 @@
<body id="stylus-edit"> <body id="stylus-edit">
<div id="header"> <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"> <section id="basic-info">
<div id="basic-info-name"> <div id="basic-info-name">
<input id="name" class="style-contributor" spellcheck="false"> <input id="name" class="style-contributor" spellcheck="false">
@ -262,7 +262,7 @@
<input type="checkbox" id="enabled" class="style-contributor"> <input type="checkbox" id="enabled" class="style-contributor">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg> <svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label> </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"> <input type="checkbox" id="editor.livePreview">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg> <svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label> </label>
@ -392,11 +392,21 @@
</div> </div>
</div> </div>
</details> </details>
<details id="integration" data-pref="editor.integration.expanded" class="ignore-pref-if-compact"> <details id="publish" data-pref="editor.publish.expanded" class="ignore-pref-if-compact">
<summary><h2 i18n-text="integration"></h2></summary> <summary><h2 i18n-text="publish"></h2></summary>
<div> <div>
<button id="publish-style" i18n-text="uploadStyle"></button> <a id="usw-url" href="https://userstyles.world" target="_blank">&nbsp;</a>
<button id="revoke-link" i18n-text="revokeLink"></button> <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> </div>
</details> </details>
<details id="sections-list" data-pref="editor.toc.expanded" class="ignore-pref-if-compact"> <details id="sections-list" data-pref="editor.toc.expanded" class="ignore-pref-if-compact">

View File

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

View File

@ -7,6 +7,14 @@ body {
font: 12px arial,sans-serif; font: 12px arial,sans-serif;
} }
a {
color: #000;
transition: color .5s;
}
a:hover {
color: #666;
}
#global-progress { #global-progress {
position: fixed; position: fixed;
height: 4px; height: 4px;
@ -24,10 +32,17 @@ body {
opacity: 1; opacity: 1;
} }
html.is-new-style #preview-label,
html.is-new-style #publish,
.hidden { .hidden {
display: none !important; 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 ************/ /************ embedded popup for simple-window editor ************/
#popup-iframe { #popup-iframe {
@ -215,7 +230,9 @@ input:invalid {
margin-left: -13px; margin-left: -13px;
cursor: pointer; cursor: pointer;
} }
#header summary + * {
padding: .5rem 0;
}
#header summary h2 { #header summary h2 {
display: inline-block; display: inline-block;
border-bottom: 1px dotted transparent; 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 */ 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 { #header summary:hover h2 {
border-color: #bbb; border-color: #bbb;
} }
@ -244,6 +258,7 @@ input:invalid {
#header details { #header details {
margin-top: .5rem; margin-top: .5rem;
max-width: 100%;
} }
#actions > * { #actions > * {
@ -276,6 +291,81 @@ input:invalid {
#lint:not([open]) h2 { #lint:not([open]) h2 {
margin-bottom: 0; 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 */
#options [type="number"] { #options [type="number"] {
width: 3.5em; width: 3.5em;
@ -739,7 +829,6 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
#lint { #lint {
overflow: hidden; overflow: hidden;
margin: .5rem -1rem 0; margin: .5rem -1rem 0;
min-height: 30px;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
@ -758,7 +847,7 @@ body:not(.find-open) [data-match-highlight-count="1"] .CodeMirror-selection-high
text-indent: -2px; text-indent: -2px;
} }
#lint > .lint-scroll-container { #lint > .lint-scroll-container {
margin: 34px 10px 0; margin: 1rem 10px 0;
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
@ -954,7 +1043,7 @@ body.linter-disabled .hidden-unless-compact {
position: inherit; position: inherit;
border-right: none; border-right: none;
border-bottom: 1px dashed #AAA; border-bottom: 1px dashed #AAA;
padding: 0; padding: .5rem 1rem .5rem .5rem;
} }
.fixed-header { .fixed-header {
padding-top: var(--fixed-padding); padding-top: var(--fixed-padding);
@ -972,24 +1061,30 @@ body.linter-disabled .hidden-unless-compact {
.fixed-header #options { .fixed-header #options {
display: none !important; display: none !important;
} }
#header summary + *,
#lint > .lint-scroll-container {
margin-left: 1rem;
padding: .25rem 0 .5rem;
}
#actions { #actions {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
white-space: nowrap; white-space: nowrap;
padding: 0 1rem;
margin: 0; margin: 0;
box-sizing: border-box; box-sizing: border-box;
} }
#header input[type="checkbox"] { #header input[type="checkbox"] {
vertical-align: middle; vertical-align: middle;
} }
#header details {
margin: 0;
}
#heading, #heading,
h2 { h2 {
display: none; display: none;
} }
#basic-info { #basic-info {
padding: .5rem 1rem; margin-bottom: .5rem;
margin: 0;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -1006,22 +1101,17 @@ body.linter-disabled .hidden-unless-compact {
#options-wrapper { #options-wrapper {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
padding: .5rem 1rem 0;
box-sizing: border-box; box-sizing: border-box;
} }
#toc {
padding: .5rem 1rem;
}
#details-wrapper { #details-wrapper {
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
padding-bottom: .25rem;
} }
#options { #options[open] {
width: 100%; width: 100%;
} }
#sections-list[open] { #sections-list[open] {
height: 102px; max-height: 102px;
} }
#sections-list[open] #toc { #sections-list[open] #toc {
max-height: 60px; max-height: 60px;
@ -1029,13 +1119,16 @@ body.linter-disabled .hidden-unless-compact {
} }
#sections-list, #sections-list,
#lint { #lint {
width: 50%; max-width: 50%;
} }
.options-column { .options-column {
flex-grow: 1; flex-grow: 1;
padding-right: .5rem; padding-right: .5rem;
box-sizing: border-box; box-sizing: border-box;
} }
.options-column > .usercss-only {
margin-bottom: 0;
}
#options-wrapper .options-column:nth-child(2) { #options-wrapper .options-column:nth-child(2) {
margin-top: 0; margin-top: 0;
} }
@ -1054,8 +1147,9 @@ body.linter-disabled .hidden-unless-compact {
margin-left: 0; margin-left: 0;
padding-left: 4px; padding-left: 4px;
} }
#options h2 { #header summary h2 {
margin: 0 0 .5em; margin: 0;
padding: 0;
} }
.option label { .option label {
margin: 0; margin: 0;
@ -1069,7 +1163,8 @@ body.linter-disabled .hidden-unless-compact {
top: 0.2rem; top: 0.2rem;
} }
#lint > .lint-scroll-container { #lint > .lint-scroll-container {
margin: 26px 1rem 0; padding-top: 0;
margin-right: 0;
} }
#lint { #lint {
padding: 0; padding: 0;

View File

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

View File

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

View File

@ -25,7 +25,7 @@ function SectionsEditor() {
updateHeader(); updateHeader();
rerouteHotkeys.toggle(true); // enabled initially because we don't always focus a CodeMirror 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'); container.classList.add('section-editor');
$('#to-mozilla').on('click', showMozillaFormat); $('#to-mozilla').on('click', showMozillaFormat);
$('#to-mozilla-help').on('click', showToMozillaHelp); $('#to-mozilla-help').on('click', showToMozillaHelp);
@ -54,6 +54,11 @@ function SectionsEditor() {
return `${t('sectionCode')} ${index + 1}`; return `${t('sectionCode')} ${index + 1}`;
}, },
getValue(asObject) {
const st = getModel();
return asObject ? st : MozDocMapper.styleToCss(st);
},
getSearchableInputs(cm) { getSearchableInputs(cm) {
const sec = sections.find(s => s.cm === cm); const sec = sections.find(s => s.cm === cm);
return sec ? sec.appliesTo.map(a => a.valueEl).filter(Boolean) : []; return sec ? sec.appliesTo.map(a => a.valueEl).filter(Boolean) : [];
@ -86,14 +91,13 @@ function SectionsEditor() {
await initSections(newStyle.sections, {replace: true}); await initSections(newStyle.sections, {replace: true});
} }
Object.assign(style, newStyle); Object.assign(style, newStyle);
editor.onStyleUpdated();
updateHeader(); updateHeader();
dirty.clear(); dirty.clear();
// Go from new style URL to edit style URL // Go from new style URL to edit style URL
if (location.href.indexOf('id=') === -1 && style.id) { if (style.id && !/[&?]id=/.test(location.search)) {
history.replaceState({}, document.title, 'edit.html?id=' + style.id); history.replaceState({}, document.title, `${location.pathname}?id=${style.id}`);
$('#heading').textContent = t('editStyleHeading');
} }
editor.livePreview.toggle(Boolean(style.id));
updateLivePreview(); updateLivePreview();
}, },
@ -323,7 +327,7 @@ function SectionsEditor() {
function showMozillaFormat() { function showMozillaFormat() {
const popup = showCodeMirrorPopup(t('styleToMozillaFormatTitle'), '', {readOnly: true}); const popup = showCodeMirrorPopup(t('styleToMozillaFormatTitle'), '', {readOnly: true});
popup.codebox.setValue(MozDocMapper.styleToCss(getModel())); popup.codebox.setValue(editor.getValue());
popup.codebox.execCommand('selectAll'); popup.codebox.execCommand('selectAll');
} }
@ -425,7 +429,7 @@ function SectionsEditor() {
editor.updateToc(); editor.updateToc();
} }
/** @returns {Style} */ /** @returns {StyleObj} */
function getModel() { function getModel() {
return Object.assign({}, style, { return Object.assign({}, style, {
sections: sections.filter(s => !s.removed).map(s => s.getModel()), 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 cm = cmFactory.create($('.single-editor'));
const sectionFinder = MozSectionFinder(cm); const sectionFinder = MozSectionFinder(cm);
const sectionWidget = MozSectionWidget(cm, sectionFinder); const sectionWidget = MozSectionWidget(cm, sectionFinder);
editor.livePreview.init(preprocess, style.id); editor.livePreview.init(preprocess);
createMetaCompiler(meta => { createMetaCompiler(meta => {
style.usercssData = meta; style.usercssData = meta;
style.name = meta.name; style.name = meta.name;
@ -48,6 +48,7 @@ function SourceEditor() {
closestVisible: () => cm, closestVisible: () => cm,
getEditors: () => [cm], getEditors: () => [cm],
getEditorTitle: () => '', getEditorTitle: () => '',
getValue: () => cm.getValue(),
getSearchableInputs: () => [], getSearchableInputs: () => [],
prevEditor: nextPrevSection.bind(null, -1), prevEditor: nextPrevSection.bind(null, -1),
nextEditor: nextPrevSection.bind(null, 1), nextEditor: nextPrevSection.bind(null, 1),
@ -241,9 +242,8 @@ function SourceEditor() {
} }
sessionStore.justEditedStyleId = newStyle.id; sessionStore.justEditedStyleId = newStyle.id;
Object.assign(style, newStyle); Object.assign(style, newStyle);
$('#preview-label').classList.remove('hidden'); editor.onStyleUpdated();
updateMeta(); 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 editor */
/* global t */// localization.js
'use strict'; 'use strict';
let uswPort; (() => {
//#region Main
function connectToPort() { const ERROR_TITLE = 'UserStyles.world ' + t('genericError');
if (!uswPort) { const PROGRESS = '#usw-progress';
uswPort = chrome.runtime.connect({name: 'link-style-usw'}); let spinnerTimer = 0;
uswPort.onDisconnect.addListener(err => { let prevCode = '';
throw err;
});
}
}
msg.onExtension(request => {
if (request.method === 'uswData' &&
request.style.id === editor.style.id) {
Object.assign(editor.style, request.style);
updateUI();
}
});
/* exported revokeLinking */ baseInit.ready.then(() => {
function revokeLinking() { updateUI();
connectToPort(); $('#usw-publish-style').onclick = disableWhileActive(publishStyle);
$('#usw-disconnect').onclick = disableWhileActive(disconnect);
});
uswPort.postMessage({reason: 'revoke', data: editor.style}); async function publishStyle() {
} const {id} = editor.style;
if (await API.data.has('usw' + id) &&
/* exported publishStyle */ !await messageBoxProxy.confirm(t('publishRetry'), 'danger', ERROR_TITLE)) {
function publishStyle() { return;
connectToPort(); }
const data = Object.assign(editor.style, {sourceCode: editor.getEditors()[0].getValue()}); const code = editor.getValue();
uswPort.postMessage({reason: 'publish', data}); 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);
/* exported updateUI */ $(PROGRESS).append(...failed && [
function updateUI(useStyle) { $create('div.error', {title}, res),
const style = useStyle || editor.style; $create('div', t('publishReconnect')),
if (style._usw && style._usw.token) { ] || [
$('#revoke-link').style = ''; $create(`span.${isDiff ? 'success' : 'unchanged'}`, {title}),
const linkInformation = $create('div', {id: 'link-info'}, [
$create('p', `Style name: ${style._usw.name}`),
$create('p', `Description: ${style._usw.description}`),
]); ]);
$remove('#link-info'); if (!failed) prevCode = code;
$('#integration').insertBefore(linkInformation, $('#integration').firstChild);
} else {
$('#revoke-link').style = 'display: none;';
$remove('#link-info');
} }
}
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%); fill: hsl(0, 0%, 50%);
} }
summary {
-moz-user-select: none;
user-select: none;
}
/* global stuff we use everywhere */ /* global stuff we use everywhere */
.hidden { .hidden {
display: none !important; display: none !important;

View File

@ -21,6 +21,7 @@
<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>
<link href="spinner.css" rel="stylesheet">
<link href="install-usercss/install-usercss.css" rel="stylesheet"> <link href="install-usercss/install-usercss.css" rel="stylesheet">
</head> </head>
<body id="stylus-install-usercss"> <body id="stylus-install-usercss">

View File

@ -297,93 +297,12 @@ label {
padding-left: 16px; padding-left: 16px;
position: relative; position: relative;
} }
/* spinner: https://github.com/loadingio/css-spinner */
@keyframes lds-spinner {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.lds-spinner { .lds-spinner {
position: absolute;
width: 200px;
height: 200px;
top: 50px; top: 50px;
left: 0;
right: 0;
margin: auto;
opacity: .2; opacity: .2;
transition: opacity .5s; transition: opacity .5s;
} animation: none;
.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);
}
} }
/************ reponsive layouts ************/ /************ reponsive layouts ************/

View File

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

View File

@ -13,6 +13,8 @@
moveFocus moveFocus
scrollElementIntoView scrollElementIntoView
setupLivePrefs setupLivePrefs
showSpinner
toggleDataset
waitForSheet 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 {string} selector - beware of $ quirks with `#dotted.id` that won't work with $$
* @param {Object} [opt] * @param {Object} [opt]

View File

@ -61,7 +61,7 @@
'editor.toc.expanded': true, // UI element state: expanded/collapsed 'editor.toc.expanded': true, // UI element state: expanded/collapsed
'editor.options.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.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.lineWrapping': true, // word wrap
'editor.smartIndent': true, // 'smart' indent 'editor.smartIndent': true, // 'smart' indent
'editor.indentWithTabs': false, // smart indent with tabs 'editor.indentWithTabs': false, // smart indent with tabs

View File

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

View File

@ -282,102 +282,3 @@ body.search-results-shown {
margin-right: .5em; margin-right: .5em;
flex: 1 1 0; 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 $entry tabURL */// popup.js
/* global API */// msg.js /* global API */// msg.js
/* global Events */ /* 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() { function next() {
displayedPage = Math.min(totalPages, displayedPage + 1); displayedPage = Math.min(totalPages, displayedPage + 1);
scrollToFirstResult = true; 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;
}