manage: add new UI option

* align svg icons to pixel grid
* globalize enforceInputRange() and use it in manage
* 1 installed.onclick instead of 1000+ local ones
This commit is contained in:
tophf 2017-04-05 16:14:59 +03:00
parent efd3e9ef6c
commit e3b461a9e8
8 changed files with 489 additions and 101 deletions

View File

@ -57,7 +57,10 @@ globals:
isCheckbox: false isCheckbox: false
runTryCatch: false runTryCatch: false
tryRegExp: false tryRegExp: false
tryJSONparse: false
debounce: false
setupLivePrefs: false setupLivePrefs: false
enforceInputRange: false
getCodeMirrorThemes: false getCodeMirrorThemes: false
styleSectionsEqual: false styleSectionsEqual: false

View File

@ -282,6 +282,18 @@
"message": "Only edited styles", "message": "Only edited styles",
"description": "Checkbox to show only locally edited styles" "description": "Checkbox to show only locally edited styles"
}, },
"manageNewUI": {
"message": "New manage UI layout",
"description": "Label for the checkbox that toggles the new UI on manage page"
},
"manageFavicons": {
"message": "Favicons in applies-to column",
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
},
"manageMaxTargets": {
"message": "Number of applies-to items",
"description": "Label for the numeric input box to limit max number of applies-to targets in the new UI on manage page"
},
"manageText": { "manageText": {
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>", "message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
"description": "Help text on the manage page" "description": "Help text on the manage page"
@ -482,6 +494,10 @@
"message": "(Stylus can't affect this page.)", "message": "(Stylus can't affect this page.)",
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect" "description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
}, },
"toggleStyle": {
"message": "Toggle style",
"description": "Label for the checkbox to enable/disable a style"
},
"undo": { "undo": {
"message": "Undo", "message": "Undo",
"description": "Button label" "description": "Button label"

View File

@ -47,10 +47,9 @@ a:hover {
.svg-icon { .svg-icon {
cursor: pointer; cursor: pointer;
vertical-align: middle; vertical-align: middle;
margin-top: -4px;
transition: fill .5s; transition: fill .5s;
width: 16px; width: 20px;
height: 16px; height: 20px;
fill: #000; fill: #000;
} }
@ -58,11 +57,21 @@ a:hover {
fill: #666; fill: #666;
} }
.svg-icon.delete {
width: 16px;
height: 16px;
}
.homepage { .homepage {
margin-left: 0.1em; margin-left: 0.1em;
margin-right: 0.1em; margin-right: 0.1em;
} }
.homepage .svg-icon {
margin-top: -4px;
margin-left: .5ex;
}
.style-name { .style-name {
margin-top: .25em; margin-top: .25em;
word-break: break-word; word-break: break-word;
@ -101,7 +110,7 @@ a:hover {
margin-right: .25rem; margin-right: .25rem;
} }
.applies-to > :first-child { .applies-to label {
margin-right: .5ex; margin-right: .5ex;
} }
@ -109,8 +118,9 @@ a:hover {
background-color: rgba(128, 128, 128, .15); background-color: rgba(128, 128, 128, .15);
} }
.applies-to-extra { .applies-to-extra:not([open]) {
display: inline; display: inline;
margin-left: 1ex;
} }
summary { summary {
@ -142,6 +152,175 @@ summary {
display: none; display: none;
} }
/* compact layout */
.newUI {
display: table;
margin: .75rem 0 .75rem 0;
}
.newUI .disabled {
opacity: 1;
}
.newUI .disabled .style-name,
.newUI .disabled .applies-to {
opacity: .5;
}
.newUI .entry {
display: table-row;
}
.newUI .entry:nth-child(2n) {
background-color: rgba(128, 128, 128, 0.05);
}
.newUI .entry > * {
padding: .9rem 0 1rem;
margin: 0;
display: table-cell;
vertical-align: middle;
}
.newUI .checker {
position: relative;
top: 1px;
margin-right: 1ex;
}
.newUI .style-name {
font-size: 14px;
font-family: sans-serif;
text-indent: -2em;
padding-left: 3em;
padding-right: 30px;
}
.newUI .homepage .svg-icon {
position: absolute;
margin-top: 0;
margin-left: -28px;
}
.newUI .actions {
width: 60px;
height: 20px;
white-space: nowrap;
font-size: 0;
}
.newUI .actions > * {
margin: 0;
}
.newUI .actions .svg-icon {
margin-right: 8px;
}
.newUI .updater-icons > * {
transition: opacity 1s;
display: none;
}
.newUI .checking-update .check-update {
opacity: 0;
display: inline;
pointer-events: none;
}
.newUI .can-update .update,
.newUI .no-update:not(.update-problem) .up-to-date,
.newUI .no-update.update-problem .check-update,
.newUI .update-done .updated {
display: inline;
}
.update-problem .check-update svg {
fill: darkred;
}
.newUI .applies-to {
padding-top: .25rem;
padding-bottom: .25rem;
}
.newUI .targets {
overflow: hidden;
}
.newUI .applies-to.expanded .targets {
max-height: 100vh;
}
.newUI .target {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: calc(100vw - 280px - 60px - 25vw - 3rem);
box-sizing: border-box;
padding-right: 1rem;
line-height: 18px;
}
.newUI .applies-to .expander {
margin: 0;
cursor: pointer;
font-size: 3ex;
line-height: .5ex;
vertical-align: super;
letter-spacing: .1ex;
}
.newUI.has-favicons .target {
padding-left: 20px;
}
.newUI .target:hover {
background-color: inherit;
}
.newUI .target img {
width: 16px;
height: 16px;
vertical-align: sub;
margin-left: -20px;
margin-right: 4px;
transition: opacity .5s, filter .5s;
/* unprefixed since Chrome 53 */
-webkit-filter: grayscale(1);
filter: grayscale(1);
opacity: .25;
display: none;
}
.newUI.has-favicons .target img[src] {
display: inline;
}
.newUI .entry:hover .target img {
opacity: 1;
/* unprefixed since Chrome 53 */
-webkit-filter: grayscale(0);
filter: grayscale(0);
}
#newUIoptions label {
display: flex;
align-items: center;
margin-bottom: auto;
}
#newUIoptions input[type="number"] {
width: 3em;
margin-right: .5em;
}
input[id^="manage.newUI"] {
margin-left: 0;
}
/* Default, no update buttons */ /* Default, no update buttons */
.update, .update,
.check-update { .check-update {
@ -285,6 +464,15 @@ fieldset {
} }
} }
@keyframes fadein-25pct {
from {
opacity: 0;
}
to {
opacity: .25;
}
}
@media (max-width: 675px) { @media (max-width: 675px) {
#header { #header {
height: auto; height: auto;

View File

@ -6,10 +6,12 @@
<link rel="stylesheet" href="manage.css"> <link rel="stylesheet" href="manage.css">
<link rel="stylesheet" href="msgbox/msgbox.css"> <link rel="stylesheet" href="msgbox/msgbox.css">
<style id="style-overrides"></style>
<template data-id="style"> <template data-id="style">
<div class="entry"> <div class="entry">
<h2 class="style-name"><a class="style-name-link" href="edit.html?id="></a></h2> <h2 class="style-name"><a class="style-name-link" href="edit.html?id="></a></h2>
<p class="applies-to"><span></span></p> <p class="applies-to"><label i18n-text="appliesDisplay"></label></p>
<p class="actions"> <p class="actions">
<a class="style-edit-link" href="edit.html?id="> <a class="style-edit-link" href="edit.html?id=">
<button i18n-text="editStyleLabel"></button> <button i18n-text="editStyleLabel"></button>
@ -24,6 +26,26 @@
</div> </div>
</template> </template>
<template data-id="styleCompact">
<div class="entry">
<h2 class="style-name">
<input class="checker" type="checkbox" i18n-title="toggleStyle">
<a class="style-name-link" href="edit.html?id="></a>
</h2>
<p class="actions">
<span i18n-title="deleteStyleLabel"><svg class="svg-icon delete"><use xlink:href="#svg-icon-remove"/></svg></span>
<span class="updater-icons">
<span class="check-update" i18n-title="checkForUpdate"><svg class="svg-icon"><use xlink:href="#svg-icon-check"/></svg></span>
<span class="update" i18n-title="installUpdate"><svg class="svg-icon"><use xlink:href="#svg-icon-update"/></svg></span>
<span class="up-to-date" i18n-title="updateCheckSucceededNoUpdate"><svg class="svg-icon"><use xlink:href="#svg-icon-uptodate"/></svg></span>
<span class="updated" i18n-title="updateCompleted"><svg class="svg-icon"><use xlink:href="#svg-icon-uptodate"/></svg></span>
<span class="update-note"></span>
</span>
</p>
<div class="applies-to"><div class="targets"></div></div>
</div>
</template>
<template data-id="styleHomepage"> <template data-id="styleHomepage">
<a target="_blank" class="homepage"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a> <a target="_blank" class="homepage"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a>
</template> </template>
@ -46,6 +68,10 @@
</details> </details>
</template> </template>
<template data-id="expandAppliesTo">
<span class="expander">...</span>
</template>
<script src="health.js"></script> <script src="health.js"></script>
<script src="storage.js"></script> <script src="storage.js"></script>
<script src="messaging.js"></script> <script src="messaging.js"></script>
@ -85,6 +111,11 @@
</p> </p>
<div id="options"> <div id="options">
<h2 id="options-heading" i18n-text="optionsHeading"></h2> <h2 id="options-heading" i18n-text="optionsHeading"></h2>
<label><input id="manage.newUI" type="checkbox"><span i18n-text="manageNewUI"></span></label>
<div id="newUIoptions" class="hidden">
<label><input id="manage.newUI.favicons" type="checkbox"><span i18n-text="manageFavicons"></span></label>
<label><input id="manage.newUI.targets" type="number" min="1" max="99"><span i18n-text="manageMaxTargets"></span></label>
</div>
<p> <p>
<button id="manage-options-button" i18n-text="openOptionsManage"></button> <button id="manage-options-button" i18n-text="openOptionsManage"></button>
<button id="manage-shortcuts-button" i18n-text="openOptionsShortcuts"></button> <button id="manage-shortcuts-button" i18n-text="openOptionsShortcuts"></button>
@ -106,13 +137,39 @@
</div> </div>
<div id="installed"></div> <div id="installed"></div>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none"> <svg xmlns="http://www.w3.org/2000/svg" style="display: none !important;">
<symbol id="svg-icon-external-link" height="16" width="16" viewBox="0 0 8 8">
<path d="M0 0v8h8v-2h-1v1h-6v-6h1v-1h-2zm4 0l1.5 1.5-2.5 2.5 1 1 2.5-2.5 1.5 1.5v-4h-4z"></path> <symbol id="svg-icon-external-link" viewBox="0 0 20 20">
<polygon shape-rendering="crispEdges" points="3,3 3,17 17,17 17,13 15,13 15,15 5,15 5,5 7,5 7,3 "/>
<polygon points="10,3 12.5,5.5 8,10 10,12 14.5,7.5 17,10 17,3 "/>
</symbol> </symbol>
<symbol id="svg-icon-close" height="16" width="12" viewBox="0 0 12 16">
<symbol id="svg-icon-close" viewBox="0 0 12 16">
<path fill-rule="evenodd" d="M7.48 8l3.75 3.75-1.48 1.48L6 9.48l-3.75 3.75-1.48-1.48L4.52 8 .77 4.25l1.48-1.48L6 6.52l3.75-3.75 1.48 1.48z"></path> <path fill-rule="evenodd" d="M7.48 8l3.75 3.75-1.48 1.48L6 9.48l-3.75 3.75-1.48-1.48L4.52 8 .77 4.25l1.48-1.48L6 6.52l3.75-3.75 1.48 1.48z"></path>
</symbol> </symbol>
<symbol id="svg-icon-remove" viewBox="0 0 14 16">
<path shape-rendering="crispEdges" fill-rule="evenodd" d="M11 2H9c0-.55-.45-1-1-1H5c-.55 0-1 .45-1 1H2c-.55 0-1 .45-1 1v1c0 .55.45 1 1 1v9c0 .55.45 1 1 1h7c.55 0 1-.45 1-1V5c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 12H3V5h1v8h1V5h1v8h1V5h1v8h1V5h1v9zm1-10H2V3h9v1z">
</path>
</symbol>
<symbol id="svg-icon-check" viewBox="0 0 20 20">
<path d="M15,11c-0.5,2.3-2.5,4.1-5,4.1c-2.4,0-4.5-1.7-5-4.1H3.3c0.5,3.3,3.3,5.8,6.7,5.8s6.2-2.5,6.7-5.8H15z"/>
<path d="M10,3.2C6.6,3.2,3.8,5.7,3.3,9H5c0.5-2.3,2.5-4.1,5-4.1c2.4,0,4.5,1.7,5,4.1h1.7C16.2,5.7,13.4,3.2,10,3.2z"/>
<polygon shape-rendering="crispEdges" points="17,3 17,9 11,9 "/>
<polygon shape-rendering="crispEdges" points="9.1,11 3,11 3,17.1 "/>
<polygon points="19.9,18.6 15.1,13.8 13.8,15.1 18.6,19.9 "/>
</symbol>
<symbol id="svg-icon-update" viewBox="0 0 20 20">
<polygon points="16,8 12,8 12,3 8,3 8,8 4,8 10,14 "/>
<rect shape-rendering="crispEdges" x="4" y="15" width="12" height="2"/>
</symbol>
<symbol id="svg-icon-uptodate" viewBox="0 0 20 20">
<polygon shape-rendering="crispEdges" points="4,9 8,13 18,3 19,4 8,15 3,10 "/>
</symbol>
</svg> </svg>
<script src="manage.js"></script> <script src="manage.js"></script>

252
manage.js
View File

@ -2,11 +2,19 @@
'use strict'; 'use strict';
const installed = $('#installed'); const installed = $('#installed');
const TARGET_LABEL = t('appliesDisplay', '').trim(); const newUI = {
enabled: prefs.get('manage.newUI'),
favicons: prefs.get('manage.newUI.favicons'),
targets: prefs.get('manage.newUI.targets'),
};
const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps']; const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps'];
const TARGET_LIMIT = 10; const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain=';
const OWN_ICON = chrome.app.getDetails().icons['16'];
const handleEvent = {}; const handleEvent = {};
getStylesSafe() getStylesSafe()
.then(showStyles) .then(showStyles)
.then(initGlobalEvents); .then(initGlobalEvents);
@ -26,6 +34,7 @@ chrome.runtime.onMessage.addListener(msg => {
function initGlobalEvents() { function initGlobalEvents() {
installed.onclick = handleEvent.entryClicked;
$('#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;
@ -50,19 +59,28 @@ function initGlobalEvents() {
} }
}); });
for (const [className, checkbox] of [
['enabled-only', $('#manage.onlyEnabled')],
['edited-only', $('#manage.onlyEdited')],
]) {
// will be triggered by setupLivePrefs immediately
checkbox.onchange = () => installed.classList.toggle(className, checkbox.checked);
}
enforceInputRange($('#manage.newUI.favicons'));
setupLivePrefs([ setupLivePrefs([
'manage.onlyEnabled', 'manage.onlyEnabled',
'manage.onlyEdited', 'manage.onlyEdited',
'manage.newUI',
'manage.newUI.favicons',
'manage.newUI.targets',
]); ]);
[ $$('[id^="manage.newUI"]')
['enabled-only', $('#manage.onlyEnabled')], .forEach(el => (el.oninput = (el.onchange = switchUI)));
['edited-only', $('#manage.onlyEdited')],
] switchUI({styleOnly: true});
.forEach(([className, checkbox]) => {
checkbox.onchange = () => installed.classList.toggle(className, checkbox.checked);
checkbox.onchange();
});
} }
@ -98,7 +116,7 @@ function showStyles(styles = []) {
function createStyleElement({style, name}) { function createStyleElement({style, name}) {
const entry = template.style.cloneNode(true); const entry = template[`style${newUI.enabled ? 'Compact' : ''}`].cloneNode(true);
entry.classList.add(style.enabled ? 'enabled' : 'disabled'); entry.classList.add(style.enabled ? 'enabled' : 'disabled');
entry.setAttribute('style-id', style.id); entry.setAttribute('style-id', style.id);
entry.id = 'style-' + style.id; entry.id = 'style-' + style.id;
@ -118,69 +136,111 @@ function createStyleElement({style, name}) {
const styleNameEditLink = $('a', styleName); const styleNameEditLink = $('a', styleName);
styleNameEditLink.appendChild(document.createTextNode(style.name)); styleNameEditLink.appendChild(document.createTextNode(style.name));
styleNameEditLink.href = styleNameEditLink.getAttribute('href') + style.id; styleNameEditLink.href = styleNameEditLink.getAttribute('href') + style.id;
styleNameEditLink.onclick = handleEvent.edit;
if (style.url) { if (style.url) {
const homepage = template.styleHomepage.cloneNode(true); const homepage = Object.assign(template.styleHomepage.cloneNode(true), {
homepage.href = style.url; href: style.url,
homepage.onclick = handleEvent.external; title: style.url,
styleName.appendChild(document.createTextNode(' ')); });
if (newUI.enabled) {
const actions = $('.actions', entry);
actions.insertBefore(homepage, actions.firstChild);
} else {
styleName.appendChild(homepage); styleName.appendChild(homepage);
} }
}
const targets = new Map(TARGET_TYPES.map(t => [t, new Set()])); const appliesTo = $('.applies-to', entry);
const decorations = { const decorations = {
urlPrefixesAfter: '*', urlPrefixesAfter: '*',
regexpsBefore: '/', regexpsBefore: '/',
regexpsAfter: '/', regexpsAfter: '/',
}; };
for (const [name, target] of targets.entries()) { const showFavicons = newUI.enabled && newUI.favicons;
const maxTargets = newUI.enabled ? Number.MAX_VALUE : 10;
const displayed = new Set();
let container = newUI.enabled ? $('.targets', appliesTo) : appliesTo;
let numTargets = 0;
for (const type of TARGET_TYPES) {
for (const section of style.sections) { for (const section of style.sections) {
for (const targetValue of section[name] || []) { for (const targetValue of section[type] || []) {
target.add( if (displayed.has(targetValue)) {
(decorations[name + 'Before'] || '') + continue;
targetValue.trim() +
(decorations[name + 'After'] || ''));
} }
} displayed.add(targetValue);
} if (numTargets++ == maxTargets) {
const appliesTo = $('.applies-to', entry); container = appliesTo.appendChild(template.extraAppliesTo.cloneNode(true));
appliesTo.firstElementChild.textContent = TARGET_LABEL; } else if (numTargets > 1 && !newUI.enabled) {
const targetsList = Array.prototype.concat.apply([],
[...targets.values()].map(set => [...set.values()]));
if (!targetsList.length) {
appliesTo.appendChild(template.appliesToEverything.cloneNode(true));
entry.classList.add('global');
} else {
let index = 0;
let container = appliesTo;
for (const target of targetsList) {
if (index > 0) {
container.appendChild(template.appliesToSeparator.cloneNode(true)); container.appendChild(template.appliesToSeparator.cloneNode(true));
} }
if (++index == TARGET_LIMIT) { const element = template.appliesToTarget.cloneNode(true);
container = appliesTo.appendChild(template.extraAppliesTo.cloneNode(true)); if (showFavicons) {
let favicon = '';
if (type == 'domains') {
favicon = GET_FAVICON_URL + targetValue;
} else if (targetValue.startsWith('chrome-extension:')) {
favicon = OWN_ICON;
} else if (type != 'regexps') {
favicon = targetValue.match(/^.*?:\/\/([^/]+)/);
favicon = favicon ? GET_FAVICON_URL + favicon[1] : '';
} }
const item = template.appliesToTarget.cloneNode(true); if (favicon) {
item.textContent = target; element.appendChild(document.createElement('img')).dataset.src = favicon;
container.appendChild(item); debounce(handleEvent.loadFavicons);
} }
} }
element.appendChild(
document.createTextNode(
(decorations[type + 'Before'] || '') +
targetValue +
(decorations[type + 'After'] || '')));
container.appendChild(element);
}
}
}
if (!numTargets) {
appliesTo.appendChild(template.appliesToEverything.cloneNode(true));
entry.classList.add('global');
}
if (newUI.enabled) {
$('.checker', entry).checked = style.enabled;
if (numTargets > newUI.targets) {
appliesTo.appendChild(template.expandAppliesTo.cloneNode(true));
}
} else {
const editLink = $('.style-edit-link', entry); const editLink = $('.style-edit-link', entry);
editLink.href = editLink.getAttribute('href') + style.id; editLink.href = editLink.getAttribute('href') + style.id;
editLink.onclick = handleEvent.edit; }
$('.enable', entry).onclick = handleEvent.toggle;
$('.disable', entry).onclick = handleEvent.toggle;
$('.check-update', entry).onclick = handleEvent.check;
$('.update', entry).onclick = handleEvent.update;
$('.delete', entry).onclick = handleEvent.delete;
return entry; return entry;
} }
Object.assign(handleEvent, { Object.assign(handleEvent, {
ENTRY_ROUTES: {
'.checker, .enable, .disable': 'toggle',
'.style-name-link': 'edit',
'.homepage': 'external',
'.check-update': 'check',
'.update': 'update',
'.delete': 'delete',
'.applies-to .expander': 'expandTargets',
},
entryClicked(event) {
const target = event.target;
const entry = target.closest('.entry');
for (const selector in handleEvent.ENTRY_ROUTES) {
for (let el = target; el && el != entry; el = el.parentElement) {
if (el.matches(selector)) {
const handler = handleEvent.ENTRY_ROUTES[selector];
return handleEvent[handler].call(el, event, entry);
}
}
}
},
edit(event) { edit(event) {
if (event.altKey) { if (event.altKey) {
return; return;
@ -210,30 +270,28 @@ Object.assign(handleEvent, {
} }
}, },
toggle(event) { toggle(event, entry) {
enableStyle(getClickedStyleId(event), this.matches('.enable')) enableStyle(entry.styleId, this.matches('.enable') || this.checked)
.then(handleUpdate); .then(handleUpdate);
}, },
check(event) { check(event, entry) {
checkUpdate(getClickedStyleElement(event)); checkUpdate(entry);
}, },
update(event) { update(event, entry) {
const styleElement = getClickedStyleElement(event);
// update everything but name // update everything but name
saveStyle(Object.assign(styleElement.updatedCode, { saveStyle(Object.assign(entry.updatedCode, {
id: styleElement.styleId, id: entry.styleId,
name: null, name: null,
reason: 'update', reason: 'update',
})); }));
}, },
delete(event) { delete(event, entry) {
const styleElement = getClickedStyleElement(event); const id = entry.styleId;
const id = styleElement.styleId;
const {name} = cachedStyles.byId.get(id) || {}; const {name} = cachedStyles.byId.get(id) || {};
animateElement(styleElement, {className: 'highlight'}); animateElement(entry, {className: 'highlight'});
messageBox({ messageBox({
title: t('deleteStyleConfirm'), title: t('deleteStyleConfirm'),
contents: name, contents: name,
@ -251,10 +309,23 @@ Object.assign(handleEvent, {
openURL({url: event.target.closest('a').href}); openURL({url: event.target.closest('a').href});
event.preventDefault(); event.preventDefault();
}, },
expandTargets() {
this.closest('.applies-to').classList.toggle('expanded');
},
loadFavicons(container = installed) {
for (const img of container.getElementsByTagName('img')) {
if (img.dataset.src) {
img.src = img.dataset.src;
delete img.dataset.src;
}
}
}
}); });
function handleUpdate(style, {reason} = {}) { function handleUpdate(style, {reason, quiet} = {}) {
const element = createStyleElement({style}); const element = createStyleElement({style});
const oldElement = $('#style-' + style.id, installed); const oldElement = $('#style-' + style.id, installed);
if (oldElement) { if (oldElement) {
@ -269,9 +340,11 @@ function handleUpdate(style, {reason} = {}) {
} }
} }
installed.insertBefore(element, findNextElement(style)); installed.insertBefore(element, findNextElement(style));
if (!quiet) {
animateElement(element, {className: 'highlight'}); animateElement(element, {className: 'highlight'});
scrollElementIntoView(element); scrollElementIntoView(element);
} }
}
function handleDelete(id) { function handleDelete(id) {
@ -282,6 +355,38 @@ function handleDelete(id) {
} }
function switchUI({styleOnly} = {}) {
const enabled = $('#manage.newUI').checked;
const favicons = $('#manage.newUI.favicons').checked;
const targets = Number($('#manage.newUI.targets').value);
const stateToggled = newUI.enabled != enabled;
const targetsChanged = enabled && targets != newUI.targets;
const faviconsChanged = enabled && favicons != newUI.favicons;
const missingFavicons = enabled && favicons && !$('.applies-to img');
if (!styleOnly && !stateToggled && !targetsChanged && !faviconsChanged) {
return;
}
Object.assign(newUI, {enabled, favicons, targets});
installed.classList.toggle('newUI', enabled);
installed.classList.toggle('has-favicons', favicons);
$('#newUIoptions').classList.toggle('hidden', !enabled);
$('#style-overrides').textContent = `
.newUI .targets {
max-height: ${newUI.targets * 18}px;
}
`;
if (!styleOnly && (stateToggled || missingFavicons)) {
installed.innerHTML = '';
getStylesSafe().then(showStyles);
}
}
function applyUpdateAll() { function applyUpdateAll() {
const btnApply = $('#apply-all-updates'); const btnApply = $('#apply-all-updates');
btnApply.disabled = true; btnApply.disabled = true;
@ -310,8 +415,11 @@ function checkUpdateAll() {
Promise.all($$('[style-update-url]').map(checkUpdate)) Promise.all($$('[style-update-url]').map(checkUpdate))
.then(updatables => { .then(updatables => {
btnCheck.disabled = false; btnCheck.disabled = false;
if (updatables.includes(true)) { const numUpdatable = updatables.filter(u => u).length;
if (numUpdatable) {
btnApply.classList.remove('hidden'); btnApply.classList.remove('hidden');
btnApply.originalLabel = btnApply.originalLabel || btnApply.textContent;
btnApply.textContent = btnApply.originalLabel + ` (${numUpdatable})`;
} else { } else {
noUpdates.classList.remove('hidden'); noUpdates.classList.remove('hidden');
setTimeout(() => { setTimeout(() => {
@ -329,7 +437,8 @@ function checkUpdateAll() {
function checkUpdate(element) { function checkUpdate(element) {
$('.update-note', element).innerHTML = t('checkingForUpdate'); $('.update-note', element).innerHTML = t('checkingForUpdate');
element.classList.remove('checking-update', 'no-update', 'can-update'); $('.check-update', element).title = '';
element.classList.remove('checking-update', 'no-update', 'can-update', 'update-problem');
element.classList.add('checking-update'); element.classList.add('checking-update');
return new Updater(element).run(); // eslint-disable-line no-use-before-define return new Updater(element).run(); // eslint-disable-line no-use-before-define
} }
@ -337,12 +446,13 @@ function checkUpdate(element) {
class Updater { class Updater {
constructor(element) { constructor(element) {
const style = cachedStyles.byId.get(element.styleId);
Object.assign(this, { Object.assign(this, {
element, element,
id: element.styleId, id: style.id,
url: element.getAttribute('style-update-url'), url: style.updateUrl,
md5Url: element.getAttribute('style-md5-url'), md5Url: style.md5Url,
md5: element.getAttribute('style-original-md5'), md5: style.originalMd5,
}); });
} }
@ -357,7 +467,7 @@ class Updater {
md5 => (md5.length == 32 md5 => (md5.length == 32
? this.decideOnMd5(md5 != this.md5) ? this.decideOnMd5(md5 != this.md5)
: this.onFailure(-1)), : this.onFailure(-1)),
this.onFailure); status => this.onFailure(status));
} }
decideOnMd5(md5changed) { decideOnMd5(md5changed) {
@ -370,7 +480,7 @@ class Updater {
checkFullCode({forceUpdate = false} = {}) { checkFullCode({forceUpdate = false} = {}) {
return Updater.download(this.url).then( return Updater.download(this.url).then(
text => this.handleJson(forceUpdate, JSON.parse(text)), text => this.handleJson(forceUpdate, JSON.parse(text)),
this.onFailure); status => this.onFailure(status));
} }
handleJson(forceUpdate, json) { handleJson(forceUpdate, json) {
@ -400,7 +510,11 @@ class Updater {
$('.update-note', this.element).innerHTML = ''; $('.update-note', this.element).innerHTML = '';
} else { } else {
this.element.classList.add('no-update'); this.element.classList.add('no-update');
this.element.classList.toggle('update-problem', Boolean(message));
$('.update-note', this.element).innerHTML = message || t('updateCheckSucceededNoUpdate'); $('.update-note', this.element).innerHTML = message || t('updateCheckSucceededNoUpdate');
if (newUI.enabled) {
$('.check-update', this.element).title = message;
}
} }
} }

View File

@ -86,7 +86,12 @@
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
right: 3px; right: 3px;
top: 7px; top: 4px;
}
#message-box-close-icon svg {
width: 16px;
height: 16px;
} }
#message-box-contents { #message-box-contents {

View File

@ -10,20 +10,7 @@ setupLivePrefs([
'popupWidth', 'popupWidth',
'updateInterval', 'updateInterval',
]); ]);
enforceValueRange('popupWidth'); enforceInputRange($('#popupWidth'));
function enforceValueRange(id) {
const element = document.getElementById(id);
const min = Number(element.min);
const max = Number(element.max);
let value = Number(element.value);
if (value < min || value > max) {
value = Math.max(min, Math.min(max, value));
element.value = value;
}
element.onchange = element.onchange || (() => enforceValueRange(id));
return value | 0;
}
// overwrite the default URL if browser is Opera // overwrite the default URL if browser is Opera
$('[data-cmd="open-keyboard"]').href = configureCommands.url; $('[data-cmd="open-keyboard"]').href = configureCommands.url;

View File

@ -547,14 +547,14 @@ function tryJSONparse(jsonString) {
} }
function debounce(fn, ...args) { function debounce(fn, delay, ...args) {
const timers = debounce.timers = debounce.timers || new Map(); const timers = debounce.timers = debounce.timers || new Map();
debounce.run = debounce.run || ((fn, ...args) => { debounce.run = debounce.run || ((fn, ...args) => {
timers.delete(fn); timers.delete(fn);
fn(...args); fn(...args);
}); });
clearTimeout(timers.get(fn)); clearTimeout(timers.get(fn));
timers.set(fn, setTimeout(debounce.run, 0, fn, ...args)); timers.set(fn, setTimeout(debounce.run, delay, fn, ...args));
} }
@ -572,6 +572,9 @@ prefs = prefs || new function Prefs() {
'manage.onlyEnabled': false, // display only enabled styles 'manage.onlyEnabled': false, // display only enabled styles
'manage.onlyEdited': false, // display only styles created locally 'manage.onlyEdited': false, // display only styles created locally
'manage.newUI': true, // use the new compact layout
'manage.newUI.favicons': true, // show favicons for the sites in applies-to
'manage.newUI.targets': 3, // max number of applies-to targets visible: 0 = none
'editor.options': {}, // CodeMirror.defaults.* 'editor.options': {}, // CodeMirror.defaults.*
'editor.lineWrapping': true, // word wrap 'editor.lineWrapping': true, // word wrap
@ -755,6 +758,21 @@ function setupLivePrefs(IDs) {
} }
function enforceInputRange(element) {
const min = Number(element.min);
const max = Number(element.max);
const onChange = () => {
const value = Number(element.value);
if (value < min || value > max) {
element.value = Math.max(min, Math.min(max, value));
}
};
onChange();
element.addEventListener('change', onChange);
element.addEventListener('input', onChange);
}
function getCodeMirrorThemes(callback) { function getCodeMirrorThemes(callback) {
chrome.runtime.getPackageDirectoryEntry(function(rootDir) { chrome.runtime.getPackageDirectoryEntry(function(rootDir) {
rootDir.getDirectory('codemirror/theme', {create: false}, function(themeDir) { rootDir.getDirectory('codemirror/theme', {create: false}, function(themeDir) {