Add: user-frendly exclusions (#666)

* WIP: popup UI

* Fix: use simple menu toggle

* Add: inclusion/exclusion API

* Add: hook exclusion UI

* Fix: minor

* Fix: don't self-edit

* Icons and accessibility

* Icons and accessibility

* Fix: disable redundant exclude-by-url checkbox

* Disabled cursor and delete leftover code

* Generic menu button tooltip and tweak menu item cursors

* Generic menu button tooltip and tweak menu item cursors

* Generic menu button tooltip and tweak menu item cursors
This commit is contained in:
eight 2019-03-04 06:54:37 +08:00 committed by Rob Garrison
parent 1ff34fc449
commit cdc7f98150
6 changed files with 271 additions and 43 deletions

View File

@ -315,6 +315,15 @@
"message": "Enable",
"description": "Label for the button to enable a style"
},
"excludeStyleByDomainLabel": {
"message": "Exclude the current domain"
},
"excludeStyleByUrlLabel": {
"message": "Exclude the current URL"
},
"excludeStyleByUrlRedundant": {
"message": "The current URL is the domain page"
},
"exportLabel": {
"message": "Export",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
@ -1026,6 +1035,10 @@
"message": "Stylus failed to parse usercss:",
"description": "The error message to show when stylus failed to parse usercss"
},
"popupAutoResort": {
"message": "Resort styles in popup after toggling",
"description": "Label for the checkbox controlling popup resorting."
},
"popupBorders": {
"message": "Add white borders on the sides"
},
@ -1044,6 +1057,10 @@
"message": "Shift-click or right-click opens manager with styles applicable for current site",
"description": "Tooltip for the 'Manage' button in the popup."
},
"popupMenuButtonTooltip": {
"message": "Action menu",
"description": "Tooltip for menu button in popup."
},
"popupOpenEditInWindow": {
"message": "Open editor in a new window",
"description": "Label for the checkbox controlling 'edit' action behavior in the popup."
@ -1056,10 +1073,6 @@
"message": "Styles before commands",
"description": "Label for the checkbox controlling section order in the popup."
},
"popupAutoResort": {
"message": "Resort styles in popup after toggling",
"description": "Label for the checkbox controlling popup resorting."
},
"prefShowBadge": {
"message": "Number of styles active for the current site",
"description": "Label for the checkbox controlling toolbar badge text."

View File

@ -22,6 +22,11 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
styleExists: styleManager.styleExists,
toggleStyle: styleManager.toggleStyle,
addInclusion: styleManager.addInclusion,
removeInclusion: styleManager.removeInclusion,
addExclusion: styleManager.addExclusion,
removeExclusion: styleManager.removeExclusion,
getTabUrlPrefix() {
return this.sender.tab.url.match(/^([\w-]+:\/+[^/#]+)/)[1];
},

View File

@ -57,10 +57,13 @@ const styleManager = (() => {
importStyle,
importMany,
toggleStyle,
setStyleExclusions,
getAllStyles, // used by import-export
getStylesByUrl, // used by popup
styleExists,
addExclusion,
removeExclusion,
addInclusion,
removeInclusion
});
function handleLivePreviewConnections() {
@ -92,6 +95,11 @@ const styleManager = (() => {
});
}
function escapeRegExp(text) {
// https://github.com/lodash/lodash/blob/0843bd46ef805dd03c0c8d804630804f3ba0ca3c/lodash.js#L152
return text.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
}
function get(id, noCode = false) {
const data = styles.get(id).data;
return noCode ? getStyleWithNoCode(data) : data;
@ -182,10 +190,46 @@ const styleManager = (() => {
.then(newData => handleSave(newData, 'editSave'));
}
function setStyleExclusions(id, exclusions) {
const data = Object.assign({}, styles.get(id).data, {exclusions});
function addIncludeExclude(id, rule, type) {
const data = Object.assign({}, styles.get(id).data);
if (!data[type]) {
data[type] = [];
}
if (data[type].includes(rule)) {
throw new Error('The rule already exists');
}
data[type] = data[type].concat([rule]);
return saveStyle(data)
.then(newData => handleSave(newData, 'exclusions'));
.then(newData => handleSave(newData, 'styleSettings'));
}
function removeIncludeExclude(id, rule, type) {
const data = Object.assign({}, styles.get(id).data);
if (!data[type]) {
return;
}
if (!data[type].includes(rule)) {
return;
}
data[type] = data[type].filter(r => r !== rule);
return saveStyle(data)
.then(newData => handleSave(newData, 'styleSettings'));
}
function addExclusion(id, rule) {
return addIncludeExclude(id, rule, 'exclusions');
}
function removeExclusion(id, rule) {
return removeIncludeExclude(id, rule, 'exclusions');
}
function addInclusion(id, rule) {
return addIncludeExclude(id, rule, 'inclusions');
}
function removeInclusion(id, rule) {
return removeIncludeExclude(id, rule, 'inclusions');
}
function deleteStyle(id) {
@ -479,14 +523,7 @@ const styleManager = (() => {
}
function buildGlob(text) {
const prefix = text[0] === '^' ? '' : '\\b';
const suffix = text[text.length - 1] === '$' ? '' : '\\b';
return `${prefix}${escape(text)}${suffix}`;
function escape(text) {
// FIXME: using .* everywhere is slow
return text.replace(/[.*]/g, m => m === '.' ? '\\.' : '.*');
}
return '^' + escapeRegExp(text).replace(/\\\\\\\*|\\\*/g, m => m.length > 2 ? m : '.*') + '$';
}
function getDomain(url) {

View File

@ -24,6 +24,7 @@
<template data-id="style">
<div class="entry">
<div class="entry-content">
<div class="main-controls">
<label class="style-name">
<input class="checker" type="checkbox">
@ -39,10 +40,39 @@
<path fill-rule="evenodd" d="M0 12v3h3l8-8-3-3-8 8zm3 2H1v-2h1v1h1v1zm10.3-9.3L12 6 9 3l1.3-1.3a.996.996 0 0 1 1.41 0l1.59 1.59c.39.39.39 1.02 0 1.41z"/>
</svg>
</a>
<a href="#" class="delete" i18n-title="deleteStyleLabel" tabindex="0">
<a href="#" class="menu-button" i18n-title="popupMenuButtonTooltip" tabindex="0">
<svg class="svg-icon menu-button-icon" viewBox="0 0 3 16">
<path fill-rule="evenodd" d="M0 2.5a1.5 1.5 0 1 0 3 0 1.5 1.5 0 0 0-3 0zm0 5a1.5 1.5 0 1 0 3 0 1.5 1.5 0 0 0-3 0zM1.5 14a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
</svg>
</a>
</div>
</div>
<div class="menu">
<label class="menu-item exclude-by-domain button">
<div class="menu-icon">
<div class="checkbox-container">
<input type="checkbox" class="exclude-by-domain-checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</div>
</div>
<span i18n-text="excludeStyleByDomainLabel"></span>
</label>
<label class="menu-item exclude-by-url button">
<div class="menu-icon">
<div class="checkbox-container">
<input type="checkbox" class="exclude-by-url-checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</div>
</div>
<span i18n-text="excludeStyleByUrlLabel"></span>
</label>
<a href="#" class="menu-item delete">
<div class="menu-icon">
<svg class="svg-icon remove" viewBox="0 0 14 16">
<path 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"/>
</svg>
</div>
<span i18n-text="deleteStyleLabel"></span>
</a>
</div>
</div>
@ -240,8 +270,8 @@
<path d="M6.2,0C5.8,0,5.4,0.4,5.4,0.8v0.7C5,1.7,4.6,1.8,4.3,2L3.8,1.5C3.6,1.4,3.4,1.3,3.2,1.3S2.7,1.4,2.6,1.5L1.5,2.6c-0.3,0.3-0.3,0.9,0,1.2L2,4.3C1.8,4.6,1.7,5,1.5,5.4H0.8C0.4,5.4,0,5.8,0,6.2v1.5c0,0.5,0.4,0.8,0.8,0.8h0.7C1.7,9,1.8,9.4,2,9.7l-0.5,0.5c-0.3,0.3-0.3,0.8,0,1.2l1.1,1.1c0.3,0.3,0.9,0.3,1.2,0L4.3,12c0.4,0.2,0.8,0.4,1.2,0.5v0.7c0,0.5,0.4,0.8,0.8,0.8h1.5c0.5,0,0.8-0.4,0.8-0.8v-0.7C9,12.3,9.4,12.2,9.7,12l0.5,0.5c0.3,0.3,0.9,0.3,1.2,0l1.1-1.1c0.3-0.3,0.3-0.8,0-1.2L12,9.7c0.2-0.4,0.4-0.8,0.5-1.2h0.7c0.5,0,0.8-0.4,0.8-0.8V6.2c0-0.5-0.4-0.8-0.8-0.8h-0.7C12.3,5,12.2,4.6,12,4.3l0.5-0.5c0.3-0.3,0.3-0.9,0-1.2l-1.1-1.1c-0.2-0.2-0.4-0.2-0.6-0.2s-0.4,0.1-0.6,0.2L9.7,2C9.4,1.8,9,1.7,8.6,1.5V0.8C8.6,0.4,8.2,0,7.8,0L6.2,0z M6.8,0.8h0.4c0.2,0,0.4,0.2,0.4,0.4v1.2c0.8,0.1,1.6,0.4,2.3,0.9l0.8-0.8c0.2-0.2,0.4-0.2,0.6,0l0.3,0.3c0.2,0.2,0.2,0.4,0,0.6l-0.8,0.8c0.5,0.7,0.8,1.4,0.9,2.3h1.2c0.2,0,0.4,0.2,0.4,0.4v0.4c0,0.2-0.2,0.4-0.4,0.4h-1.2c-0.1,0.8-0.4,1.6-0.9,2.3l0.8,0.8c0.2,0.2,0.2,0.4,0,0.6l-0.3,0.3c-0.2,0.2-0.4,0.2-0.6,0l-0.8-0.8c-0.7,0.5-1.4,0.8-2.3,0.9v1.2c0,0.2-0.2,0.4-0.4,0.4H6.8c-0.2,0-0.4-0.2-0.4-0.4v-1.2c-0.8-0.1-1.6-0.4-2.3-0.9l-0.8,0.8c-0.2,0.2-0.4,0.2-0.6,0l-0.3-0.3c-0.2-0.2-0.2-0.4,0-0.6l0.8-0.8C2.8,9.2,2.5,8.4,2.4,7.6H1.2C1,7.6,0.8,7.4,0.8,7.2V6.8c0-0.2,0.2-0.4,0.4-0.4h1.2c0.1-0.8,0.4-1.6,0.9-2.3L2.5,3.3c-0.2-0.2-0.2-0.4,0-0.6l0.3-0.3c0.2-0.2,0.4-0.2,0.6,0l0.8,0.8c0.7-0.5,1.4-0.8,2.3-0.9V1.2C6.4,1,6.6,0.8,6.8,0.8L6.8,0.8z M7,3.6C5.1,3.6,3.6,5.1,3.6,7c0,0,0,0,0,0c0,1.9,1.5,3.4,3.4,3.4c1.9,0,3.4-1.5,3.4-3.4C10.4,5.1,8.9,3.6,7,3.6C7,3.6,7,3.6,7,3.6z M7,4.8c1.2,0,2.2,1,2.2,2.2c0,1.2-1,2.2-2.2,2.2c-1.2,0-2.2-1-2.2-2.2C4.8,5.8,5.8,4.8,7,4.8z"/>
</symbol>
<symbol id="svg-icon-config-uso" viewBox="0 0 14 14">
<path d="M2,3h4v2H4v6h6V9h2v4H2V3z M8,1h6v6l-2.2-2.2l-4,4L6.2,7.2l4-4L8,1z"/>
<symbol id="svg-icon-config-uso" viewBox="0 0 20 20">
<path d="M4,4h5v2H6v8h8v-3h2v5H4V4z M11,3h6v6l-2-2l-4,4L9,9l4-4L11,3z"/>
</symbol>
<symbol id="svg-icon-help" viewBox="0 0 14 16">

View File

@ -173,16 +173,17 @@ body.blocked > DIV {
}
/* entry */
.entry {
position: relative;
}
.entry-content {
display: flex;
align-items: center;
height: 26px;
padding: 0 14px 0 0;
position: relative;
}
html[style] .entry {
html[style] .entry-content {
padding: 0 16px 0 0;
}
@ -192,6 +193,7 @@ html[style] .entry {
.entry .actions {
display: inline-flex;
align-items: center;
}
.style-name {
@ -264,9 +266,34 @@ html[style*="border"] .entry:nth-child(11):before {
}
.entry .actions > * {
display: inline-block;
padding: 0 1px;
margin: 0 1px;
height: 26px;
width: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.entry .actions > .menu-button {
width: 14px;
}
.entry .actions > a.configure {
padding-right: 2px;
}
.entry .actions > a.configure[target="_blank"] {
width: 20px;
}
.svg-icon.config {
height: 14px;
width: 14px;
}
a.configure[target="_blank"] .svg-icon.config {
height: 20px;
width: 20px;
margin-top: 1px;
}
.not-applied .checker,
@ -286,6 +313,72 @@ html[style*="border"] .entry:nth-child(11):before {
color: darkred;
}
/* entry menu */
.entry .menu {
display: flex;
flex-direction: column;
top: 100%;
width: 100%;
z-index: 1;
box-sizing: border-box;
height: 0;
transition: height .25s ease-out, opacity .5s ease-in;
overflow: hidden;
opacity: 0;
}
.entry.menu-active .menu {
/* FIXME: avoid hard coded height */
height: 72px;
opacity: 1;
}
/* accessibility */
.menu-item {
display: none;
border: none;
align-items: center;
padding: 0 0 0 20px;
height: 24px;
background: none;
text-decoration: none;
}
.entry.menu-active.accessible-items .menu-item {
display: flex;
}
.entry .menu-item.delete {
cursor: pointer;
}
.entry .menu-item.delete:hover {
color: #000;
}
.entry .menu-item > span {
margin-top: 1px;
}
.entry .menu-item:hover,
.entry .menu-item:active {
background-color: rgba(0, 0, 0, 0.1);
transition: background-color .25s;
}
.entry .menu-icon {
width: 26px;
}
.entry .menu-icon > * {
display: block;
margin: 0 auto;
}
.entry .menu-item.disabled {
opacity: 0.5;
background-color: transparent;
cursor: help;
}
/* checkbox */
.checkbox-container {
position: relative;
display: inline-block;
width: 12px;
height: 12px;
}
.regexp-problem-indicator {
background-color: #d00;
width: 14px;

View File

@ -309,6 +309,11 @@ function createStyleElement(style) {
indicator.appendChild(document.createTextNode('!'));
indicator.onclick = handleEvent.indicator;
$('.main-controls', entry).appendChild(indicator);
$('.menu-button', entry).onclick = handleEvent.toggleMenu;
$('.exclude-by-domain-checkbox', entry).onchange = e => handleEvent.toggleExclude(e, 'domain');
$('.exclude-by-url-checkbox', entry).onchange = e => handleEvent.toggleExclude(e, 'url');
}
style = Object.assign(entry.styleMeta, style);
@ -329,9 +334,35 @@ function createStyleElement(style) {
entry.classList.toggle('not-applied', style.excluded || style.sloppy);
entry.classList.toggle('regexp-partial', style.sloppy);
$('.exclude-by-domain-checkbox', entry).checked = styleExcluded(style, 'domain');
const excludeByUrlCheckbox = $('.exclude-by-url-checkbox', entry);
const isRedundant = getExcludeRule('domain') === getExcludeRule('url');
excludeByUrlCheckbox.checked = !isRedundant && styleExcluded(style, 'url');
excludeByUrlCheckbox.disabled = isRedundant;
const excludeByUrlLabel = $('.exclude-by-url', entry);
excludeByUrlLabel.classList.toggle('disabled', isRedundant);
excludeByUrlLabel.title = isRedundant ?
chrome.i18n.getMessage('excludeStyleByUrlRedundant') : '';
return entry;
}
function styleExcluded({exclusions}, type) {
if (!exclusions) {
return false;
}
const rule = getExcludeRule(type);
return exclusions.includes(rule);
}
function getExcludeRule(type) {
if (type === 'domain') {
return new URL(tabURL).origin + '/*';
}
return tabURL + '*';
}
Object.assign(handleEvent, {
@ -356,10 +387,29 @@ Object.assign(handleEvent, {
.then(sortStylesInPlace);
},
toggleExclude(event, type) {
const entry = handleEvent.getClickedStyleElement(event);
if (event.target.checked) {
API.addExclusion(entry.styleMeta.id, getExcludeRule(type));
} else {
API.removeExclusion(entry.styleMeta.id, getExcludeRule(type));
}
},
toggleMenu(event) {
const entry = handleEvent.getClickedStyleElement(event);
entry.classList.toggle('menu-active');
setTimeout(() => {
entry.classList.toggle('accessible-items');
}, 250);
event.preventDefault();
},
delete(event) {
const entry = handleEvent.getClickedStyleElement(event);
const id = entry.styleId;
const box = $('#confirm');
const cancel = $('[data-cmd="cancel"]');
box.dataset.display = true;
box.style.cssText = '';
$('b', box).textContent = $('.style-name', entry).textContent;
@ -368,7 +418,7 @@ Object.assign(handleEvent, {
$('[data-cmd="cancel"]', box).onclick = () => confirm(false);
window.onkeydown = event => {
const keyCode = event.keyCode || event.which;
if (!event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey
if (document.activeElement !== cancel && !event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey
&& (keyCode === 13 || keyCode === 27)) {
event.preventDefault();
confirm(keyCode === 13);