Import styles: show report with Undo button
This commit is contained in:
parent
4bcfcb1503
commit
b1c19bdf3d
|
@ -1,9 +1,8 @@
|
||||||
/* globals getStyles, saveStyle, invalidateCache, refreshAllTabs, handleUpdate */
|
/* globals getStyles, saveStyle, invalidateCache, refreshAllTabs, handleUpdate */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var STYLISH_DUMP_FILE_EXT = '.txt';
|
const STYLISH_DUMP_FILE_EXT = '.txt';
|
||||||
var STYLISH_DUMPFILE_EXTENSION = '.json';
|
const STYLUS_BACKUP_FILE_EXT = '.json';
|
||||||
var STYLISH_DEFAULT_SAVE_NAME = 'stylus-mm-dd-yyyy' + STYLISH_DUMP_FILE_EXT;
|
|
||||||
|
|
||||||
function importFromFile({fileTypeFilter, file} = {}) {
|
function importFromFile({fileTypeFilter, file} = {}) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
|
@ -47,52 +46,184 @@ function importFromFile({fileTypeFilter, file} = {}) {
|
||||||
|
|
||||||
function importFromString(jsonString) {
|
function importFromString(jsonString) {
|
||||||
const json = runTryCatch(() => Array.from(JSON.parse(jsonString))) || [];
|
const json = runTryCatch(() => Array.from(JSON.parse(jsonString))) || [];
|
||||||
const numStyles = json.length;
|
const oldStyles = json.length && deepCopyStyles();
|
||||||
|
const oldStylesByName = json.length && new Map(
|
||||||
|
oldStyles.map(style => [style.name.trim(), style]));
|
||||||
|
const stats = {
|
||||||
|
added: {names: [], ids: [], legend: 'added'},
|
||||||
|
unchanged: {names: [], ids: [], legend: 'identical skipped'},
|
||||||
|
metaAndCode: {names: [], ids: [], legend: 'updated both meta info and code'},
|
||||||
|
metaOnly: {names: [], ids: [], legend: 'updated meta info'},
|
||||||
|
codeOnly: {names: [], ids: [], legend: 'updated code'},
|
||||||
|
invalid: {names: [], legend: 'invalid skipped'},
|
||||||
|
};
|
||||||
|
let index = 0;
|
||||||
|
return new Promise(proceed);
|
||||||
|
|
||||||
if (numStyles) {
|
function proceed(resolve) {
|
||||||
invalidateCache(true);
|
while (index < json.length) {
|
||||||
|
const item = json[index++];
|
||||||
|
if (!item || !item.name || !item.name.trim() || typeof item != 'object'
|
||||||
|
|| (item.sections && !(item.sections instanceof Array))) {
|
||||||
|
stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
item.name = item.name.trim();
|
||||||
return new Promise(resolve => {
|
const byId = (cachedStyles.byId.get(item.id) || {}).style;
|
||||||
proceed();
|
const byName = oldStylesByName.get(item.name);
|
||||||
function proceed() {
|
const oldStyle = byId && byId.name.trim() == item.name || !byName ? byId : byName;
|
||||||
const nextStyle = json.shift();
|
if (oldStyle == byName && byName) {
|
||||||
if (nextStyle) {
|
item.id = byName.id;
|
||||||
saveStyle(nextStyle, {notify: false}).then(style => {
|
}
|
||||||
|
const oldStyleKeys = oldStyle && Object.keys(oldStyle);
|
||||||
|
const metaEqual = oldStyleKeys &&
|
||||||
|
oldStyleKeys.length == Object.keys(item).length &&
|
||||||
|
oldStyleKeys.every(k => k == 'sections' || oldStyle[k] === item[k]);
|
||||||
|
const codeEqual = oldStyle && styleSectionsEqual(oldStyle, item);
|
||||||
|
if (metaEqual && codeEqual) {
|
||||||
|
stats.unchanged.names.push(oldStyle.name);
|
||||||
|
stats.unchanged.ids.push(oldStyle.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
saveStyle(item, {notify: false}).then(style => {
|
||||||
handleUpdate(style, {reason: 'import'});
|
handleUpdate(style, {reason: 'import'});
|
||||||
setTimeout(proceed, 0);
|
setTimeout(proceed, 0, resolve);
|
||||||
|
if (!oldStyle) {
|
||||||
|
stats.added.names.push(style.name);
|
||||||
|
stats.added.ids.push(style.id);
|
||||||
|
}
|
||||||
|
else if (!metaEqual && !codeEqual) {
|
||||||
|
stats.metaAndCode.names.push(reportNameChange(oldStyle, style));
|
||||||
|
stats.metaAndCode.ids.push(style.id);
|
||||||
|
}
|
||||||
|
else if (!codeEqual) {
|
||||||
|
stats.codeOnly.names.push(style.name);
|
||||||
|
stats.codeOnly.ids.push(style.id);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
stats.metaOnly.names.push(reportNameChange(oldStyle, style));
|
||||||
|
stats.metaOnly.ids.push(style.id);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
return;
|
||||||
refreshAllTabs().then(() => {
|
}
|
||||||
|
done(resolve);
|
||||||
|
}
|
||||||
|
|
||||||
|
function done(resolve) {
|
||||||
|
const numChanged = stats.metaAndCode.names.length +
|
||||||
|
stats.metaOnly.names.length +
|
||||||
|
stats.codeOnly.names.length +
|
||||||
|
stats.added.names.length;
|
||||||
|
Promise.resolve(numChanged && refreshAllTabs()).then(() => {
|
||||||
scrollTo(0, 0);
|
scrollTo(0, 0);
|
||||||
setTimeout(alert, 100, numStyles + ' styles installed/updated');
|
const report = Object.keys(stats)
|
||||||
resolve(numStyles);
|
.filter(kind => stats[kind].names.length)
|
||||||
|
.map(kind => `<details data-id="${kind}">
|
||||||
|
<summary><b>${stats[kind].names.length} ${stats[kind].legend}</b></summary>
|
||||||
|
<small>` + stats[kind].names.map((name, i) =>
|
||||||
|
`<div data-id="${stats[kind].ids[i]}">${name}</div>`).join('') + `
|
||||||
|
</small>
|
||||||
|
</details>`)
|
||||||
|
.join('');
|
||||||
|
const box = messageBox({
|
||||||
|
title: 'Finished importing styles',
|
||||||
|
contents: report || 'Nothing was changed.',
|
||||||
|
buttons: [t('confirmOK'), numChanged && t('undo')],
|
||||||
|
onclick: btnIndex => btnIndex == 1 && undo(),
|
||||||
});
|
});
|
||||||
}
|
bindClick(box);
|
||||||
}
|
resolve(numChanged);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateFileName() {
|
function undo() {
|
||||||
var today = new Date();
|
const oldStylesById = new Map(oldStyles.map(style => [style.id, style]));
|
||||||
var dd = '0' + today.getDate();
|
const newIds = [
|
||||||
var mm = '0' + (today.getMonth() + 1);
|
...stats.metaAndCode.ids,
|
||||||
var yyyy = today.getFullYear();
|
...stats.metaOnly.ids,
|
||||||
|
...stats.codeOnly.ids,
|
||||||
|
...stats.added.ids,
|
||||||
|
];
|
||||||
|
index = 0;
|
||||||
|
return new Promise(undoNextId)
|
||||||
|
.then(refreshAllTabs)
|
||||||
|
.then(() => messageBox({
|
||||||
|
title: 'Import has been undone',
|
||||||
|
contents: newIds.length + ' styles were reverted.',
|
||||||
|
buttons: [t('confirmOK')],
|
||||||
|
}));
|
||||||
|
function undoNextId(resolve) {
|
||||||
|
if (index == newIds.length) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = newIds[index++];
|
||||||
|
deleteStyle(id, {notify: false}).then(id => {
|
||||||
|
handleDelete(id);
|
||||||
|
const oldStyle = oldStylesById.get(id);
|
||||||
|
if (oldStyle) {
|
||||||
|
saveStyle(Object.assign(oldStyle, {reason: 'undoImport'}), {notify: false})
|
||||||
|
.then(handleUpdate)
|
||||||
|
.then(() => setTimeout(undoNextId, 0, resolve));
|
||||||
|
} else {
|
||||||
|
setTimeout(undoNextId, 0, resolve);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dd = dd.substr(-2);
|
function bindClick(box) {
|
||||||
mm = mm.substr(-2);
|
for (let block of [...box.querySelectorAll('details')]) {
|
||||||
|
if (block.dataset.id != 'invalid') {
|
||||||
|
block.style.cursor = 'pointer';
|
||||||
|
block.onclick = event => {
|
||||||
|
const styleElement = $(`[style-id="${event.target.dataset.id}"]`);
|
||||||
|
if (styleElement) {
|
||||||
|
scrollElementIntoView(styleElement);
|
||||||
|
highlightElement(styleElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
today = mm + '-' + dd + '-' + yyyy;
|
function deepCopyStyles() {
|
||||||
|
const clonedStyles = [];
|
||||||
|
for (let style of cachedStyles.list || []) {
|
||||||
|
style = Object.assign({}, style);
|
||||||
|
style.sections = style.sections.slice();
|
||||||
|
for (let i = 0, section; (section = style.sections[i]); i++) {
|
||||||
|
const copy = style.sections[i] = Object.assign({}, section);
|
||||||
|
for (let propName in copy) {
|
||||||
|
const prop = copy[propName];
|
||||||
|
if (prop instanceof Array) {
|
||||||
|
copy[propName] = prop.slice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clonedStyles.push(style);
|
||||||
|
}
|
||||||
|
return clonedStyles;
|
||||||
|
}
|
||||||
|
|
||||||
return 'stylus-' + today + STYLISH_DUMPFILE_EXTENSION;
|
function limitString(s, limit = 100) {
|
||||||
|
return s.length <= limit ? s : s.substr(0, limit) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportNameChange(oldStyle, newStyle) {
|
||||||
|
return newStyle.name != oldStyle.name
|
||||||
|
? oldStyle.name + ' —> ' + newStyle.name
|
||||||
|
: oldStyle.name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('file-all-styles').onclick = () => {
|
document.getElementById('file-all-styles').onclick = () => {
|
||||||
getStyles({}, function (styles) {
|
getStyles({}, function (styles) {
|
||||||
let text = JSON.stringify(styles, null, '\t');
|
const text = JSON.stringify(styles, null, '\t');
|
||||||
let fileName = generateFileName() || STYLISH_DEFAULT_SAVE_NAME;
|
const fileName = generateFileName();
|
||||||
|
|
||||||
let url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text);
|
const url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text);
|
||||||
// for long URLs; https://github.com/schomery/stylish-chrome/issues/13#issuecomment-284582600
|
// for long URLs; https://github.com/schomery/stylish-chrome/issues/13#issuecomment-284582600
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then(res => res.blob())
|
.then(res => res.blob())
|
||||||
|
@ -103,46 +234,51 @@ document.getElementById('file-all-styles').onclick = () => {
|
||||||
a.dispatchEvent(new MouseEvent('click'));
|
a.dispatchEvent(new MouseEvent('click'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function generateFileName() {
|
||||||
|
const today = new Date();
|
||||||
|
const dd = ('0' + today.getDate()).substr(-2);
|
||||||
|
const mm = ('0' + (today.getMonth() + 1)).substr(-2);
|
||||||
|
const yyyy = today.getFullYear();
|
||||||
|
return `stylus-${mm}-${dd}-${yyyy}${STYLUS_BACKUP_FILE_EXT}`;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
document.getElementById('unfile-all-styles').onclick = () => {
|
document.getElementById('unfile-all-styles').onclick = () => {
|
||||||
importFromFile({fileTypeFilter: STYLISH_DUMPFILE_EXTENSION});
|
importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT});
|
||||||
};
|
};
|
||||||
|
|
||||||
const dropTarget = Object.assign(document.body, {
|
Object.assign(document.body, {
|
||||||
ondragover: event => {
|
ondragover(event) {
|
||||||
const hasFiles = event.dataTransfer.types.includes('Files');
|
const hasFiles = event.dataTransfer.types.includes('Files');
|
||||||
event.dataTransfer.dropEffect = hasFiles || event.target.type == 'search' ? 'copy' : 'none';
|
event.dataTransfer.dropEffect = hasFiles || event.target.type == 'search' ? 'copy' : 'none';
|
||||||
dropTarget.classList.toggle('dropzone', hasFiles);
|
this.classList.toggle('dropzone', hasFiles);
|
||||||
if (hasFiles) {
|
if (hasFiles) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
clearTimeout(dropTarget.fadeoutTimer);
|
clearTimeout(this.fadeoutTimer);
|
||||||
dropTarget.classList.remove('fadeout');
|
this.classList.remove('fadeout');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ondragend: event => {
|
ondragend(event) {
|
||||||
dropTarget.classList.add('fadeout');
|
this.classList.add('fadeout');
|
||||||
// transitionend event may not fire if the user switched to another tab so we'll use a timer
|
this.addEventListener('animationend', function _() {
|
||||||
clearTimeout(dropTarget.fadeoutTimer);
|
this.removeEventListener('animationend', _);
|
||||||
dropTarget.fadeoutTimer = setTimeout(() => {
|
this.style.animationDuration = '';
|
||||||
dropTarget.classList.remove('dropzone', 'fadeout');
|
this.classList.remove('dropzone', 'fadeout');
|
||||||
}, 250);
|
});
|
||||||
},
|
},
|
||||||
ondragleave: event => {
|
ondragleave(event) {
|
||||||
// Chrome sets screen coords to 0 on Escape key pressed or mouse out of document bounds
|
// Chrome sets screen coords to 0 on Escape key pressed or mouse out of document bounds
|
||||||
if (!event.screenX && !event.screenX) {
|
if (!event.screenX && !event.screenX) {
|
||||||
dropTarget.ondragend();
|
this.ondragend();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ondrop: event => {
|
ondrop(event) {
|
||||||
|
this.ondragend();
|
||||||
if (event.dataTransfer.files.length) {
|
if (event.dataTransfer.files.length) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
importFromFile({file: event.dataTransfer.files[0]}).then(() => {
|
importFromFile({file: event.dataTransfer.files[0]});
|
||||||
dropTarget.classList.remove('dropzone');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
dropTarget.ondragend();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
18
manage.css
18
manage.css
|
@ -111,9 +111,12 @@ a.homepage {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.applies-to-extra summary {
|
summary {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.applies-to-extra summary {
|
||||||
list-style-type: none; /* for FF, allegedly */
|
list-style-type: none; /* for FF, allegedly */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,6 +258,19 @@ fieldset {
|
||||||
animation-fill-mode: both;
|
animation-fill-mode: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* post-import report */
|
||||||
|
#message-box details:not([data-id="invalid"]) div:hover {
|
||||||
|
background-color: rgba(128, 128, 128, .3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#message-box details:not(:last-child) {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#message-box details small div {
|
||||||
|
margin-left: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes fadein {
|
@keyframes fadein {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
||||||
<title i18n-text="manageTitle"></title>
|
<title i18n-text="manageTitle"></title>
|
||||||
<link href="manage.css" rel="stylesheet">
|
<link href="manage.css" rel="stylesheet">
|
||||||
|
<link href="msgbox/msgbox.css" rel="stylesheet">
|
||||||
|
|
||||||
<template data-id="style">
|
<template data-id="style">
|
||||||
<div class="entry">
|
<div class="entry">
|
||||||
|
@ -121,8 +122,8 @@
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<script src="manage.js"></script>
|
<script src="manage.js"></script>
|
||||||
<script src="openOptions.js"></script>
|
|
||||||
<script src="backup/fileSaveLoad.js"></script>
|
<script src="backup/fileSaveLoad.js"></script>
|
||||||
</body>
|
<script src="msgbox/msgbox.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
35
manage.js
35
manage.js
|
@ -28,6 +28,11 @@ function initGlobalEvents() {
|
||||||
$('#check-all-updates').onclick = checkUpdateAll;
|
$('#check-all-updates').onclick = checkUpdateAll;
|
||||||
$('#apply-all-updates').onclick = applyUpdateAll;
|
$('#apply-all-updates').onclick = applyUpdateAll;
|
||||||
$('#search').oninput = searchStyles;
|
$('#search').oninput = searchStyles;
|
||||||
|
$('#manage-options-button').onclick = () => chrome.runtime.openOptionsPage();
|
||||||
|
$('#manage-shortcuts-button').onclick = configureCommands.open;
|
||||||
|
$('#editor-styles-button').onclick = () => openURL({
|
||||||
|
url: 'https://userstyles.org/styles/browse/chrome-extension',
|
||||||
|
});
|
||||||
|
|
||||||
// focus search field on / key
|
// focus search field on / key
|
||||||
document.onkeypress = event => {
|
document.onkeypress = event => {
|
||||||
|
@ -233,11 +238,7 @@ class EntryOnClick {
|
||||||
function handleUpdate(style, {reason} = {}) {
|
function handleUpdate(style, {reason} = {}) {
|
||||||
const element = createStyleElement(style);
|
const element = createStyleElement(style);
|
||||||
const oldElement = $(`[style-id="${style.id}"]`, installed);
|
const oldElement = $(`[style-id="${style.id}"]`, installed);
|
||||||
element.addEventListener('animationend', function _() {
|
highlightElement(element);
|
||||||
element.removeEventListener('animationend', _);
|
|
||||||
element.classList.remove('highlight');
|
|
||||||
});
|
|
||||||
element.classList.add('highlight');
|
|
||||||
if (!oldElement) {
|
if (!oldElement) {
|
||||||
installed.appendChild(element);
|
installed.appendChild(element);
|
||||||
} else {
|
} else {
|
||||||
|
@ -247,11 +248,7 @@ function handleUpdate(style, {reason} = {}) {
|
||||||
$('.update-note', element).innerHTML = t('updateCompleted');
|
$('.update-note', element).innerHTML = t('updateCompleted');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// align to the top/bottom of the visible area if wasn't visible
|
scrollElementIntoView(element);
|
||||||
const bounds = element.getBoundingClientRect();
|
|
||||||
if (bounds.top < 0 || bounds.top > innerHeight - bounds.height) {
|
|
||||||
element.scrollIntoView(bounds.top < 0 );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -466,6 +463,24 @@ function getClickedStyleElement(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function scrollElementIntoView(element) {
|
||||||
|
// align to the top/bottom of the visible area if wasn't visible
|
||||||
|
const bounds = element.getBoundingClientRect();
|
||||||
|
if (bounds.top < 0 || bounds.top > innerHeight - bounds.height) {
|
||||||
|
element.scrollIntoView(bounds.top < 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function highlightElement(element) {
|
||||||
|
element.addEventListener('animationend', function _() {
|
||||||
|
element.removeEventListener('animationend', _);
|
||||||
|
element.classList.remove('highlight');
|
||||||
|
});
|
||||||
|
element.classList.add('highlight');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function rememberScrollPosition() {
|
function rememberScrollPosition() {
|
||||||
history.replaceState({scrollY}, document.title);
|
history.replaceState({scrollY}, document.title);
|
||||||
}
|
}
|
||||||
|
|
102
msgbox/msgbox.css
Normal file
102
msgbox/msgbox.css
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
#message-box {
|
||||||
|
top: 3rem;
|
||||||
|
right: 3rem;
|
||||||
|
min-width: 10rem;
|
||||||
|
max-width: 50vw;
|
||||||
|
min-height: 5rem;
|
||||||
|
max-height: 90vh;
|
||||||
|
position: fixed;
|
||||||
|
display: flex;
|
||||||
|
box-shadow: 5px 5px 50px rgba(0, 0, 0, 0.35);
|
||||||
|
animation: fadein .25s ease-in-out;
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 9999990;
|
||||||
|
}
|
||||||
|
|
||||||
|
#message-box > * {
|
||||||
|
z-index: 9999991;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#message-box.fadeout {
|
||||||
|
animation: fadeout .5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#message-box::before {
|
||||||
|
position: fixed;
|
||||||
|
content: "";
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, .25);
|
||||||
|
}
|
||||||
|
|
||||||
|
#message-box.big {
|
||||||
|
max-width: none;
|
||||||
|
left: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#message-box-title {
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: rgb(145, 208, 198);
|
||||||
|
padding: .75rem 50px .75rem .75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#message-box-title::before {
|
||||||
|
content: "";
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
padding: 0 32px 32px 0;
|
||||||
|
background: url(/32.png);
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#message-box-close-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border: 8px solid transparent;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
background: linear-gradient(-45deg, transparent 5px, black 5px, black 6px, transparent 6.5px) no-repeat, linear-gradient(45deg, transparent 5px, black 5px, black 6px, transparent 6.5px) no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
#message-box-contents {
|
||||||
|
overflow: auto;
|
||||||
|
padding: .75rem;
|
||||||
|
position: relative;
|
||||||
|
flex-grow: 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
#message-box-buttons {
|
||||||
|
padding: .75rem;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#message-box-buttons button:not(:last-child) {
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeout {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
64
msgbox/msgbox.js
Normal file
64
msgbox/msgbox.js
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function messageBox({title, contents, buttons, onclick}) {
|
||||||
|
// keep the same reference to be able to remove the listener later
|
||||||
|
messageBox.close = messageBox.close || close;
|
||||||
|
if (messageBox.element) {
|
||||||
|
messageBox.element.remove();
|
||||||
|
}
|
||||||
|
const id = 'message-box';
|
||||||
|
const putAs = typeof contents == 'string' ? 'innerHTML' : 'appendChild';
|
||||||
|
messageBox.element = $element({id, appendChild: [
|
||||||
|
$element({id: `${id}-title`, innerHTML: title}),
|
||||||
|
$element({id: `${id}-close-icon`, onclick: messageBox.close}),
|
||||||
|
$element({id: `${id}-contents`, [putAs]: contents}),
|
||||||
|
$element({id: `${id}-buttons`,
|
||||||
|
onclick: relayButtonClick,
|
||||||
|
appendChild: (buttons || []).map(textContent =>
|
||||||
|
textContent && $element({tag: 'button', textContent}))
|
||||||
|
}),
|
||||||
|
]});
|
||||||
|
show();
|
||||||
|
return messageBox.element;
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
document.body.appendChild(messageBox.element);
|
||||||
|
document.addEventListener('keydown', messageBox.close);
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(event) {
|
||||||
|
if ((!event
|
||||||
|
|| event.type == 'click'
|
||||||
|
|| event.keyCode == 27 && !event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey)
|
||||||
|
&& messageBox.element) {
|
||||||
|
const box = messageBox.element;
|
||||||
|
box.classList.add('fadeout');
|
||||||
|
box.addEventListener('animationend', function _() {
|
||||||
|
box.removeEventListener('animationend', _);
|
||||||
|
box.remove();
|
||||||
|
});
|
||||||
|
document.removeEventListener('keydown', messageBox.close);
|
||||||
|
$(`#${id}-buttons`).onclick = null;
|
||||||
|
messageBox.element = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function relayButtonClick(event) {
|
||||||
|
const button = event.target.closest('button');
|
||||||
|
if (button) {
|
||||||
|
close();
|
||||||
|
if (onclick) {
|
||||||
|
onclick([...this.children].indexOf(button));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function $element(opt) {
|
||||||
|
const element = document.createElement(opt.tag || 'div');
|
||||||
|
(opt.appendChild instanceof Array ? opt.appendChild : [opt.appendChild])
|
||||||
|
.forEach(child => child && element.appendChild(child));
|
||||||
|
delete opt.appendChild;
|
||||||
|
delete opt.tag;
|
||||||
|
return Object.assign(element, opt);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,20 +0,0 @@
|
||||||
/* globals configureCommands */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
document.querySelector('#manage-options-button').addEventListener("click", function() {
|
|
||||||
if (chrome.runtime.openOptionsPage) {
|
|
||||||
// Supported (Chrome 42+)
|
|
||||||
chrome.runtime.openOptionsPage();
|
|
||||||
} else {
|
|
||||||
// Fallback
|
|
||||||
window.open(chrome.runtime.getURL('options/index.html'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelector('#manage-shortcuts-button').addEventListener("click", configureCommands.open);
|
|
||||||
|
|
||||||
document.querySelector('#editor-styles-button').addEventListener("click", function() {
|
|
||||||
chrome.tabs.create({
|
|
||||||
'url': 'https://userstyles.org/styles/browse/chrome-extension'
|
|
||||||
});
|
|
||||||
});
|
|
25
storage.js
25
storage.js
|
@ -256,23 +256,28 @@ function saveStyle(style, {notify = true} = {}) {
|
||||||
const tx = db.transaction(['styles'], 'readwrite');
|
const tx = db.transaction(['styles'], 'readwrite');
|
||||||
const os = tx.objectStore('styles');
|
const os = tx.objectStore('styles');
|
||||||
|
|
||||||
|
const id = style.id !== undefined && style.id !== null ? Number(style.id) : null;
|
||||||
const reason = style.reason;
|
const reason = style.reason;
|
||||||
delete style.method;
|
delete style.method;
|
||||||
delete style.reason;
|
delete style.reason;
|
||||||
|
|
||||||
// Update
|
// Update
|
||||||
if (style.id) {
|
if (id != null) {
|
||||||
style.id = Number(style.id);
|
style.id = id;
|
||||||
os.get(style.id).onsuccess = eventGet => {
|
os.get(id).onsuccess = eventGet => {
|
||||||
|
const existed = !!eventGet.target.result;
|
||||||
const oldStyle = Object.assign({}, eventGet.target.result);
|
const oldStyle = Object.assign({}, eventGet.target.result);
|
||||||
const codeIsUpdated = 'sections' in style && !styleSectionsEqual(style, oldStyle);
|
const codeIsUpdated = 'sections' in style && !styleSectionsEqual(style, oldStyle);
|
||||||
style = Object.assign(oldStyle, style);
|
style = Object.assign(oldStyle, style);
|
||||||
addMissingStyleTargets(style);
|
addMissingStyleTargets(style);
|
||||||
os.put(style).onsuccess = eventPut => {
|
os.put(style).onsuccess = eventPut => {
|
||||||
style.id = style.id || eventPut.target.result;
|
style.id = style.id || eventPut.target.result;
|
||||||
invalidateCache(notify, {updated: style});
|
invalidateCache(notify, existed ? {updated: style} : {added: style});
|
||||||
if (notify) {
|
if (notify) {
|
||||||
notifyAllTabs({method: 'styleUpdated', style, codeIsUpdated, reason});
|
notifyAllTabs({
|
||||||
|
method: existed ? 'styleUpdated' : 'styleAdded',
|
||||||
|
style, codeIsUpdated, reason,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
resolve(style);
|
resolve(style);
|
||||||
};
|
};
|
||||||
|
@ -294,8 +299,10 @@ function saveStyle(style, {notify = true} = {}) {
|
||||||
os.add(style).onsuccess = event => {
|
os.add(style).onsuccess = event => {
|
||||||
// Give it the ID that was generated
|
// Give it the ID that was generated
|
||||||
style.id = event.target.result;
|
style.id = event.target.result;
|
||||||
invalidateCache(true, {added: style});
|
invalidateCache(notify, {added: style});
|
||||||
|
if (notify) {
|
||||||
notifyAllTabs({method: 'styleAdded', style, reason});
|
notifyAllTabs({method: 'styleAdded', style, reason});
|
||||||
|
}
|
||||||
resolve(style);
|
resolve(style);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -320,14 +327,16 @@ function enableStyle(id, enabled) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function deleteStyle(id) {
|
function deleteStyle(id, {notify = true} = {}) {
|
||||||
return new Promise(resolve =>
|
return new Promise(resolve =>
|
||||||
getDatabase(db => {
|
getDatabase(db => {
|
||||||
const tx = db.transaction(['styles'], 'readwrite');
|
const tx = db.transaction(['styles'], 'readwrite');
|
||||||
const os = tx.objectStore('styles');
|
const os = tx.objectStore('styles');
|
||||||
os.delete(Number(id)).onsuccess = event => {
|
os.delete(Number(id)).onsuccess = event => {
|
||||||
invalidateCache(true, {deletedId: id});
|
invalidateCache(notify, {deletedId: id});
|
||||||
|
if (notify) {
|
||||||
notifyAllTabs({method: 'styleDeleted', id});
|
notifyAllTabs({method: 'styleDeleted', id});
|
||||||
|
}
|
||||||
resolve(id);
|
resolve(id);
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
Loading…
Reference in New Issue
Block a user