Add style exclusions. Closes #113

This commit is contained in:
Rob Garrison 2018-01-24 19:42:02 -06:00
parent 03d02ad405
commit 68dfa0153c
16 changed files with 608 additions and 17 deletions

View File

@ -205,6 +205,18 @@
"configOnChangeTooltip": { "configOnChangeTooltip": {
"message": "Autosave and apply changes automatically" "message": "Autosave and apply changes automatically"
}, },
"genericAdd": {
"message": "Add",
"description": "Used in various places to select/perform an add action."
},
"genericDelete": {
"message": "Delete",
"description": "Used in various places to select/perform a delete action."
},
"genericEdit": {
"message": "Edit",
"description": "Used in various places to select/perform an edit action."
},
"genericError": { "genericError": {
"message": "Error", "message": "Error",
"description": "Used in various places to indicate some error occurred." "description": "Used in various places to indicate some error occurred."
@ -359,6 +371,68 @@
"message": "Delete", "message": "Delete",
"description": "Label for the context menu item in the editor to delete selected text" "description": "Label for the context menu item in the editor to delete selected text"
}, },
"excludedDomain": {
"message": "Domain",
"description": "Label for a domain or subdomain portion of an URL used to exclude a style"
},
"excludedPrefix": {
"message": "Prefix",
"description": "Label for a full url with a subdirectory to be used as the beginning portion of a URL to match to exclude a style"
},
"exclusionsAddTitle": {
"message": "Add excluded page",
"description": "Title of popup to add an excluded page (URL)"
},
"exclusionsEditTitle": {
"message": "Edit excluded page(s)",
"description": "Title of popup to edit an excluded page (URL)"
},
"exclusionsEmpty": {
"message": "No exclusions",
"description": "Label shown when there are no global exclusions"
},
"exclusionsDeleteConfirmation": {
"message": "Are you sure you want to delete $number$ entries?",
"description": "Delete confirmation dialog message",
"placeholders": {
"number": {
"content": "$1"
}
}
},
"exclusionsHeader": {
"message": "Excluded Pages",
"description": "Title of user configurable lists of site urls to exclude per style"
},
"exclusionsHelp": {
"message": "Add one or more exclusions for each style. An exclusion is a string that will match a web location (URL). If a match is found, the given style (and all internal sections) will not be applied to that page. A list of exclusions is set separately from the userstyle so that it will not be effected when updating or editing the style itself.\n\nThis is useful because you can exclude websites that would be effected by a global style.\n\nThe exclusion string may contain wildcards (\"*\") to match any portion of the URL, e.g. \"forum.*.com\" will exclude the forum sub-domains of all dot-com top level domains.\n\nRegular expressions are not supported.",
"description": "Help text for user set style exclusions"
},
"exclusionsHelpTitle": {
"message": "Set Style Exclusions",
"description": "Header text for help modal"
},
"exclusionsInvalidUrl": {
"message": "Enter a unique and valid URL",
"description": "Text for an alert notifying the user that an entered URL is not unique or invalid"
},
"exclusionsPopupTip": {
"message": "Right-click to edit exclusions on this page",
"description": "Title on the checkbox in the popup to let the user know how to edit exclusions on the current page"
},
"exclusionsPrefix": {
"message": "Excluded on: ",
"description": "Prefix label added to the applies to column in the style manager"
},
"exclusionsStatus": {
"message": "$number$ sites",
"description": "Label added next to the Excluded Pages header when 'number' is not zero",
"placeholders": {
"number": {
"content": "$1"
}
}
},
"exportLabel": { "exportLabel": {
"message": "Export", "message": "Export",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)" "description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"

View File

@ -349,7 +349,6 @@ function saveStyle(style) {
} }
let existed; let existed;
let codeIsUpdated; let codeIsUpdated;
return maybeCalcDigest() return maybeCalcDigest()
.then(maybeImportFix) .then(maybeImportFix)
.then(decide); .then(decide);
@ -383,7 +382,9 @@ function saveStyle(style) {
if (reason === 'update-digest' && oldStyle.originalDigest === style.originalDigest) { if (reason === 'update-digest' && oldStyle.originalDigest === style.originalDigest) {
return style; return style;
} }
codeIsUpdated = !existed || 'sections' in style && !styleSectionsEqual(style, oldStyle); codeIsUpdated = !existed
|| 'sections' in style && !styleSectionsEqual(style, oldStyle)
|| reason === 'exclusionsUpdate';
style = Object.assign({installDate: Date.now()}, oldStyle, style); style = Object.assign({installDate: Date.now()}, oldStyle, style);
return write(style, store); return write(style, store);
}); });
@ -398,6 +399,7 @@ function saveStyle(style) {
url: null, url: null,
originalMd5: null, originalMd5: null,
installDate: Date.now(), installDate: Date.now(),
exclusions: {}
}, style); }, style);
return write(style); return write(style);
} }
@ -442,6 +444,11 @@ function deleteStyle({id, notify = true}) {
}); });
} }
function checkExclusions(matchUrl, exclusions = {}) {
const values = Object.values(exclusions);
return values.length &&
values.reduce((acc, exclude) => acc || tryRegExp(exclude).test(matchUrl), false);
}
function getApplicableSections({ function getApplicableSections({
style, style,
@ -456,7 +463,7 @@ function getApplicableSections({
// but the spec is outdated and doesn't account for SPA sites // but the spec is outdated and doesn't account for SPA sites
// so we only respect it in case of url("http://exact.url/without/hash") // so we only respect it in case of url("http://exact.url/without/hash")
}) { }) {
if (!skipUrlCheck && !URLS.supported(matchUrl)) { if (!skipUrlCheck && !URLS.supported(matchUrl) || checkExclusions(matchUrl, style.exclusions)) {
return []; return [];
} }
const sections = []; const sections = [];

View File

@ -24,6 +24,7 @@
<script src="js/localization.js"></script> <script src="js/localization.js"></script>
<script src="js/script-loader.js"></script> <script src="js/script-loader.js"></script>
<script src="js/storage-util.js"></script> <script src="js/storage-util.js"></script>
<script src="js/exclusions.js"></script>
<script src="content/apply.js"></script> <script src="content/apply.js"></script>
<script src="edit/lint.js"></script> <script src="edit/lint.js"></script>
<script src="edit/util.js"></script> <script src="edit/util.js"></script>
@ -399,6 +400,20 @@
</div> </div>
</div> </div>
</details> </details>
<details id="exclusions" data-pref="editor.exclusions.expanded">
<summary>
<h2 i18n-text="exclusionsHeader">: <span id="excluded-stats"></span></h2>
</summary>
<select id="excluded-list" class="style-contributor" size="4" multiple="true"></select>
<div id="excluded-wrap">
<button id="excluded-list-add" i18n-text="genericAdd"></button>
<button id="excluded-list-edit" i18n-text="genericEdit"></button>
<button id="excluded-list-delete" i18n-text="genericDelete"></button>
<a id="excluded-list-help" href="#" class="svg-inline-wrapper">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</div>
</details>
<details id="lint" class="hidden" data-pref="editor.lint.expanded"> <details id="lint" class="hidden" data-pref="editor.lint.expanded">
<summary> <summary>
<h2 i18n-text="linterIssues">: <span id="issue-count"></span> <h2 i18n-text="linterIssues">: <span id="issue-count"></span>

View File

@ -76,11 +76,53 @@ label {
min-height: 1.4rem; min-height: 1.4rem;
} }
#excluded-wrap {
align-items: center;
}
#exclusions h3 {
margin-top: 0;
}
#exclusions select {
width: 100%;
height: auto;
overflow: hidden;
margin-bottom: 10px;
padding: 0 6px;
}
#exclusions option {
overflow: hidden;
}
#excluded-pages:empty {
height: 1.4em;
border: none;
}
#excludedError {
position: absolute;
top: 7px;
color: red;
}
#excluded-stats:not(:empty) {
background-color: darkcyan;
border-color: darkcyan;
color: white;
font-size: 0.7rem;
font-weight: normal;
padding: 2px 5px;
position: relative;
top: -2px;
}
/* basic info */ /* basic info */
#basic-info { #basic-info {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
#name { #name, #excluded-input {
width: 100%; width: 100%;
} }
#basic-info-name { #basic-info-name {

View File

@ -7,6 +7,7 @@ global setupCodeMirror
global beautify global beautify
global initWithSectionStyle addSections removeSection getSectionsHashes global initWithSectionStyle addSections removeSection getSectionsHashes
global sectionsToMozFormat global sectionsToMozFormat
global exclusions
*/ */
'use strict'; 'use strict';
@ -45,6 +46,7 @@ Promise.all([
$('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true}); $('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true});
window.addEventListener('resize', () => debounce(rememberWindowSize, 100)); window.addEventListener('resize', () => debounce(rememberWindowSize, 100));
exclusions.init(style);
if (usercss) { if (usercss) {
editor = createSourceEditor(style); editor = createSourceEditor(style);
} else { } else {
@ -161,7 +163,7 @@ function onRuntimeMessage(request) {
request.reason !== 'config') { request.reason !== 'config') {
// code-less style from notifyAllTabs // code-less style from notifyAllTabs
const {sections, id} = request.style; const {sections, id} = request.style;
((sections[0] || {}).code === null ((sections && sections[0] || {}).code === null
? API.getStyles({id}) ? API.getStyles({id})
: Promise.resolve([request.style]) : Promise.resolve([request.style])
).then(([style]) => { ).then(([style]) => {
@ -284,14 +286,19 @@ function initHooks() {
} }
} }
function getNodeValue(node) {
// return length of exclusions; or the node value
return node.id === 'excluded-list' ? node.children.length.toString() : node.value;
}
function onChange(event) { function onChange(event) {
const node = event.target; const node = event.target;
if ('savedValue' in node) { if ('savedValue' in node) {
const currentValue = node.type === 'checkbox' ? node.checked : node.value; const currentValue = node.type === 'checkbox' ? node.checked : getNodeValue(node);
setCleanItem(node, node.savedValue === currentValue); setCleanItem(node, node.savedValue === currentValue);
} else { } else {
// the manually added section's applies-to is dirty only when the value is non-empty // the manually added section's applies-to is dirty only when the value is non-empty
setCleanItem(node, node.localName !== 'input' || !node.value.trim()); setCleanItem(node, node.localName !== 'input' || !getNodeValue(node).trim());
// only valid when actually saved // only valid when actually saved
delete node.savedValue; delete node.savedValue;
} }
@ -314,7 +321,7 @@ function setCleanItem(node, isClean) {
if (node.CodeMirror) { if (node.CodeMirror) {
node.savedValue = node.CodeMirror.changeGeneration(); node.savedValue = node.CodeMirror.changeGeneration();
} else { } else {
node.savedValue = node.type === 'checkbox' ? node.checked : node.value; node.savedValue = node.type === 'checkbox' ? node.checked : getNodeValue(node);
} }
} else { } else {
dirty[node.id] = true; dirty[node.id] = true;
@ -357,7 +364,8 @@ function save() {
name: $('#name').value.trim(), name: $('#name').value.trim(),
enabled: $('#enabled').checked, enabled: $('#enabled').checked,
reason: 'editSave', reason: 'editSave',
sections: getSectionsHashes() sections: getSectionsHashes(),
exclusions: exclusions.get()
}) })
.then(style => { .then(style => {
styleId = style.id; styleId = style.id;

View File

@ -4,6 +4,7 @@ global CodeMirror dirtyReporter
global updateLintReportIfEnabled initLint linterConfig updateLinter global updateLintReportIfEnabled initLint linterConfig updateLinter
global createAppliesToLineWidget messageBox global createAppliesToLineWidget messageBox
global sectionsToMozFormat global sectionsToMozFormat
global exclusions
*/ */
'use strict'; 'use strict';
@ -39,6 +40,8 @@ function createSourceEditor(style) {
style.enabled = value; style.enabled = value;
}; };
exclusions.onchange(dirty);
cm.on('changes', () => { cm.on('changes', () => {
dirty.modify('sourceGeneration', savedGeneration, cm.changeGeneration()); dirty.modify('sourceGeneration', savedGeneration, cm.changeGeneration());
updateLintReportIfEnabled(cm); updateLintReportIfEnabled(cm);
@ -198,12 +201,15 @@ function createSourceEditor(style) {
function save() { function save() {
if (!dirty.isDirty()) return; if (!dirty.isDirty()) return;
const code = cm.getValue(); const code = cm.getValue();
style.exclusions = exclusions.get();
exclusions.save(style, dirty);
return ( return (
API.saveUsercssUnsafe({ API.saveUsercssUnsafe({
id: style.id, id: style.id,
reason: 'editSave', reason: 'editSave',
enabled: style.enabled, enabled: style.enabled,
sourceCode: code, sourceCode: code,
exclusions: style.exclusions
})) }))
.then(({style, errors}) => { .then(({style, errors}) => {
replaceStyle(style); replaceStyle(style);

241
js/exclusions.js Normal file
View File

@ -0,0 +1,241 @@
/*
global messageBox resolveWith
gloabl editor showHelp onChange
*/
'use strict';
const exclusions = (() => {
// get exclusions from a select element
function get(options = {}) {
const lists = {};
const excluded = options.exclusions || getMultiOptions(options);
excluded.forEach(list => {
lists[list] = createRegExp(list);
});
return lists;
}
function createRegExp(url) {
// returning a regex string; Object.assign is used on style & doesn't save RegExp
return url.replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&').replace(/[*]/g, '.+?');
}
function getMultiOptions({select, selectedOnly, elements} = {}) {
return [...(select || exclusions.select).children].reduce((acc, opt) => {
if (selectedOnly && opt.selected) {
acc.push(elements ? opt : opt.value);
} else if (!selectedOnly) {
acc.push(elements ? opt : opt.value);
}
return acc;
}, []);
}
function populateSelect(options = []) {
exclusions.select.textContent = '';
const option = $create('option');
options.forEach(value => {
const opt = option.cloneNode();
opt.value = value;
opt.textContent = value;
opt.title = value;
exclusions.select.appendChild(opt);
});
exclusions.lastValue = exclusions.select.textContent;
}
function openInputDialog({title, callback, value = ''}) {
messageBox({
title,
className: 'center',
contents: [
$create('div', {id: 'excludedError', textContent: '\xa0\xa0'}),
$create('input', {type: 'text', id: 'excluded-input', value})
],
buttons: [t('confirmOK'), t('confirmCancel')]
});
setTimeout(() => {
const btn = $('#message-box-buttons button', messageBox.element);
// not using onkeyup here because pressing enter to activate add/edit
// button fires onkeyup here when user releases the key
$('#excluded-input').onkeydown = event => {
if (event.which === 13) {
event.preventDefault();
callback.apply(btn);
}
};
btn.onclick = callback;
}, 1);
}
function validateURL(url) {
const lists = getMultiOptions();
// Generic URL globs; e.g. "https://test.com/*" & "*.test.com"
return !lists.includes(url) && /^(?:https?:\/\/)?([\w*]+\.)+[\w*./-]+/.test(url);
}
function addExclusion() {
openInputDialog({
title: t('exclusionsAddTitle'),
callback: function () {
const value = $('#excluded-input').value;
if (value && validateURL(value)) {
exclusions.select.appendChild($create('option', {value, innerText: value}));
done();
messageBox.listeners.button.apply(this);
} else {
const errorBox = $('#excludedError', messageBox.element);
errorBox.textContent = t('exclusionsInvalidUrl');
setTimeout(() => {
errorBox.textContent = '';
}, 5000);
}
}
});
}
function editExclusion() {
const value = exclusions.select.value;
if (value) {
openInputDialog({
title: t('exclusionsAddTitle'),
value,
callback: function () {
const newValue = $('#excluded-input').value;
// only edit the first selected option
const option = getMultiOptions({selectedOnly: true, elements: true})[0];
if (newValue && validateURL(newValue) && option) {
option.value = newValue;
option.textContent = newValue;
option.title = newValue;
if (newValue !== value) {
// make it dirty!
exclusions.select.savedValue = '';
}
done();
messageBox.listeners.button.apply(this);
} else {
const errorBox = $('#excludedError', messageBox.element);
errorBox.textContent = t('exclusionsInvalidUrl');
setTimeout(() => {
errorBox.textContent = '';
}, 5000);
}
}
});
}
}
function deleteExclusions() {
const entries = getMultiOptions({selectedOnly: true, elements: true});
if (entries.length) {
messageBox
.confirm(t('exclusionsDeleteConfirmation', [entries.length]))
.then(ok => {
if (ok) {
entries.forEach(el => exclusions.select.removeChild(el));
done();
}
});
}
}
function excludeAction(event) {
const target = event.target;
if (target.id && target.id.startsWith('excluded-list-')) {
// class "excluded-list-(add/edit/delete)" -> ['excluded', 'list', 'add']
const type = target.id.split('-').pop();
switch (type) {
case 'add':
addExclusion();
break;
case 'edit':
editExclusion();
break;
case 'delete':
deleteExclusions();
break;
}
}
}
function done() {
if (editor) {
// make usercss dirty
exclusions.select.onchange();
} else {
// make regular userstyle dirty
onChange({target: exclusions.select});
}
updateStats();
}
function updateStats() {
if (exclusions.select) {
const excludedTotal = exclusions.select.children.length;
const state = excludedTotal === 0;
exclusions.select.setAttribute('size', excludedTotal || 1);
$('#excluded-stats').textContent = state ? '' : t('exclusionsStatus', [excludedTotal]);
toggleButtons(state);
}
}
function toggleButtons(state = false) {
const noSelection = exclusions.select.value === '';
$('#excluded-list-edit').disabled = noSelection || state;
$('#excluded-list-delete').disabled = noSelection || state;
}
function showExclusionHelp(event) {
event.preventDefault();
showHelp(t('exclusionsHelpTitle'), t('exclusionsHelp').replace(/\n/g, '<br>'), 'info');
}
function onRuntimeMessage(msg) {
if (msg.method === 'styleUpdated' && msg.style && msg.style.exclusions && exclusions.select) {
update(Object.keys(msg.style.exclusions));
}
}
function update(list = exclusions.list) {
populateSelect(list);
updateStats();
}
function onchange(dirty) {
exclusions.select.onchange = function () {
dirty.modify('exclusions', exclusions.lastValue, exclusions.select.textContent);
};
}
function save(style, dirty) {
style.reason = 'exclusionsUpdate';
API.saveStyle(style);
if (dirty) {
dirty.clear('exclusions');
}
}
function init(style) {
const list = Object.keys(style.exclusions || {});
const size = list.length;
exclusions.select = $('#excluded-list');
exclusions.select.savedValue = String(size);
exclusions.list = list;
update();
$('#excluded-wrap').onclick = excludeAction;
$('#excluded-list-help').onclick = showExclusionHelp;
// Disable Edit & Delete buttons if nothing selected
exclusions.select.onclick = () => toggleButtons();
document.head.appendChild($create('style', `
#excluded-list:empty:after {
content: "${t('exclusionsEmpty')}";
}
`));
chrome.runtime.onMessage.addListener(onRuntimeMessage);
}
return {init, get, update, onchange, save, createRegExp, getMultiOptions};
})();

View File

@ -40,6 +40,7 @@ var prefs = new function Prefs() {
'editor.options': {}, // CodeMirror.defaults.* 'editor.options': {}, // CodeMirror.defaults.*
'editor.options.expanded': true, // UI element state: expanded/collapsed 'editor.options.expanded': true, // UI element state: expanded/collapsed
'editor.exclusions.expanded': false, // UI element state: expanded/collapsed
'editor.lint.expanded': true, // UI element state: expanded/collapsed 'editor.lint.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

View File

@ -146,6 +146,13 @@
</details> </details>
</template> </template>
<template data-id="excludedOn">
<p class="excluded-on">
<label i18n-text="exclusionsPrefix"></label>
<span class="targets"></span>
</p>
</template>
<script src="js/dom.js"></script> <script src="js/dom.js"></script>
<script src="js/messaging.js"></script> <script src="js/messaging.js"></script>
<script src="js/prefs.js"></script> <script src="js/prefs.js"></script>

View File

@ -180,6 +180,7 @@ select {
} }
.applies-to, .applies-to,
.excluded-on,
.actions { .actions {
padding-left: 15px; padding-left: 15px;
margin-bottom: 0; margin-bottom: 0;
@ -643,6 +644,14 @@ select {
line-height: 18px; line-height: 18px;
} }
.target[data-type="exclusions"] {
color: #d22;
}
.newUI .target[data-type="exclusions"] img {
float: left;
}
.newUI .applies-to .expander { .newUI .applies-to .expander {
margin: 0; margin: 0;
cursor: pointer; cursor: pointer;

View File

@ -25,7 +25,7 @@ const newUI = {
newUI.renderClass(); newUI.renderClass();
requestAnimationFrame(usePrefsDuringPageLoad); requestAnimationFrame(usePrefsDuringPageLoad);
const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps']; const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps', 'exclusions'];
const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain='; const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain=';
const OWN_ICON = chrome.runtime.getManifest().icons['16']; const OWN_ICON = chrome.runtime.getManifest().icons['16'];
@ -121,7 +121,7 @@ function showStyles(styles = [], matchUrlIds) {
const sorted = sorter.sort({ const sorted = sorter.sort({
styles: styles.map(style => ({ styles: styles.map(style => ({
style, style,
name: style.name.toLocaleLowerCase() + '\n' + style.name, name: (style.name || '').toLocaleLowerCase() + '\n' + style.name,
})), })),
}); });
let index = 0; let index = 0;
@ -255,8 +255,16 @@ function createStyleTargetsElement({entry, style, iconsOnly}) {
let numTargets = 0; let numTargets = 0;
const displayed = new Set(); const displayed = new Set();
for (const type of TARGET_TYPES) { for (const type of TARGET_TYPES) {
for (const section of style.sections) { const isExcluded = type === 'exclusions';
for (const targetValue of section[type] || []) { const sections = isExcluded ? [''] : style.sections;
if (isExcluded && !newUI.enabled && Object.keys(style.exclusions || {}).length > 0) {
$('.applies-to', entry).insertAdjacentElement('afterend', template.excludedOn.cloneNode(true));
container = $('.excluded-on .targets', entry);
numTargets = 1;
}
for (const section of sections) {
const target = isExcluded ? Object.keys(style.exclusions || {}) : section[type] || [];
for (const targetValue of target) {
if (displayed.has(targetValue)) { if (displayed.has(targetValue)) {
continue; continue;
} }
@ -336,7 +344,7 @@ function getFaviconImgSrc(container = installed) {
favicon = GET_FAVICON_URL + targetValue; favicon = GET_FAVICON_URL + targetValue;
} else if (targetValue.includes('chrome-extension:') || targetValue.includes('moz-extension:')) { } else if (targetValue.includes('chrome-extension:') || targetValue.includes('moz-extension:')) {
favicon = OWN_ICON; favicon = OWN_ICON;
} else if (type === 'regexps') { } else if (type === 'regexps' || type === 'exclusions') {
favicon = targetValue favicon = targetValue
.replace(regexpRemoveNegativeLookAhead, '') .replace(regexpRemoveNegativeLookAhead, '')
.replace(regexpReplaceExtraCharacters, '') .replace(regexpReplaceExtraCharacters, '')
@ -630,6 +638,9 @@ function switchUI({styleOnly} = {}) {
.newUI .targets { .newUI .targets {
max-height: ${newUI.targets * 18}px; max-height: ${newUI.targets * 18}px;
} }
.newUI .target[data-type="exclusions"]:before {
content: '${t('exclusionsPrefix')}';
}
` + (newUI.faviconsGray ? ` ` + (newUI.faviconsGray ? `
.newUI .target img { .newUI .target img {
-webkit-filter: grayscale(1); -webkit-filter: grayscale(1);

View File

@ -40,7 +40,8 @@
text-align: center; text-align: center;
} }
#message-box.center #message-box-contents pre { #message-box.center #message-box-contents pre,
#message-box.center.content-left #message-box-contents {
text-align: left; text-align: left;
} }
@ -55,6 +56,10 @@
text-align: left; text-align: left;
} }
#message-box-contents h2 {
margin-top: 0;
}
#message-box-title { #message-box-title {
font-weight: bold; font-weight: bold;
background-color: rgb(145, 208, 198); background-color: rgb(145, 208, 198);

View File

@ -164,6 +164,8 @@
<script src="popup/hotkeys.js"></script> <script src="popup/hotkeys.js"></script>
<script src="js/script-loader.js" async></script> <script src="js/script-loader.js" async></script>
<script src="js/storage-util.js" async></script> <script src="js/storage-util.js" async></script>
<script src="js/exclusions.js" async></script>
<script src="popup/popup-exclusions.js" async></script>
</head> </head>
<body id="stylus-popup"> <body id="stylus-popup">

123
popup/popup-exclusions.js Normal file
View File

@ -0,0 +1,123 @@
/*
global messageBox
global exclusions
*/
'use strict';
const popupExclusions = (() => {
const popupWidth = '400px';
// return matches on url ending to prevent duplicates in the exclusion list
// e.g. http://test.com and http://test.com/* are equivalent
// this function would return ['', '/*']
function exclusionExists(array, value) {
const match = [];
['', '*', '/', '/*'].forEach(ending => {
if (array.includes(value + ending)) {
match.push(ending);
}
});
return match;
}
/* Modal in Popup.html */
function createPopupContent(url) {
const results = [];
const protocol = url.match(/\w+:\/\//);
const parts = url.replace(/(\w+:\/\/|[#?].*$)/g, '').split('/');
const domain = parts[0].split('.');
/*
Domain: a.b.com
Domain: b.com
Prefix: https://a.b.com
Prefix: https://a.b.com/current
Prefix: https://a.b.com/current/page
*/
while (parts.length > 1) {
results.push([t('excludedPrefix'), protocol + parts.join('/')]);
parts.pop();
}
while (domain.length > 1) {
results.push([t('excludedDomain'), domain.join('.')]);
domain.shift();
}
return [
$create('h2', {textContent: t('exclusionsEditTitle')}),
$create('select', {
id: 'popup-exclusions',
size: results.length,
multiple: 'true',
value: ''
}, [
...results.reverse().map(link => $create('option', {
value: link[1],
title: link[1],
textContent: `${link[0]}: ${link[1]}`
}))
])
];
}
function openPopupDialog(style, tabURL) {
const msgBox = messageBox({
title: style.name,
className: 'center content-left',
contents: createPopupContent(tabURL),
buttons: [t('confirmOK'), t('confirmCancel')],
onshow: box => {
const contents = box.firstElementChild;
contents.style = `max-width: calc(${popupWidth} - 20px); max-height: none;`;
document.body.style.minWidth = popupWidth;
document.body.style.minHeight = popupWidth;
const select = $('select', messageBox.element);
const exclusions = Object.keys(style.exclusions || {});
[...select.children].forEach(option => {
if (exclusionExists(exclusions, option.value).length) {
option.selected = true;
}
}, []);
$('#message-box-buttons button', messageBox.element).onclick = function () {
handlePopupSave(style, this);
};
}
})
.then(() => {
document.body.style.minWidth = '';
document.body.style.minHeight = '';
});
return msgBox;
}
function handlePopupSave(style, button) {
const current = Object.keys(style.exclusions);
const select = $('#popup-exclusions', messageBox.element);
const all = exclusions.getMultiOptions({select});
const selected = exclusions.getMultiOptions({select, selectedOnly: true});
// Add exclusions
selected.forEach(value => {
let exists = exclusionExists(current, value);
if (!exists.length) {
style.exclusions[value] = exclusions.createRegExp(value);
exists = [''];
}
exists.forEach(ending => {
const index = all.indexOf(value + ending);
if (index > -1) {
all.splice(index, 1);
}
});
});
// Remove exclusions (unselected in popup modal)
all.forEach(value => {
exclusionExists(current, value).forEach(ending => {
delete style.exclusions[value + ending];
});
});
exclusions.save(style);
messageBox.listeners.button.apply(button);
}
return {openPopupDialog};
})();

View File

@ -89,7 +89,6 @@ body > div:not(#installed):not(#message-box):not(.colorpicker-popup) {
position: absolute; position: absolute;
top: 7px; top: 7px;
left: var(--outer-padding); left: var(--outer-padding);
pointer-events: none;
} }
#disable-all-wrapper { #disable-all-wrapper {
@ -118,6 +117,17 @@ body > div:not(#installed):not(#message-box):not(.colorpicker-popup) {
margin-right: .5em; margin-right: .5em;
} }
#popup-exclusions {
height: auto;
overflow: hidden;
max-width: 95%;
padding: 0 6px;
}
#popup-exclusions option {
overflow: hidden;
}
.checker { .checker {
display: inline; display: inline;
} }

View File

@ -1,4 +1,7 @@
/* global configDialog hotkeys */ /*
global configDialog hotkeys
global popupExclusions
*/
'use strict'; 'use strict';
@ -257,13 +260,16 @@ function createStyleElement({
styleIsUsercss: Boolean(style.usercssData), styleIsUsercss: Boolean(style.usercssData),
className: entry.className + ' ' + (style.enabled ? 'enabled' : 'disabled'), className: entry.className + ' ' + (style.enabled ? 'enabled' : 'disabled'),
onmousedown: handleEvent.maybeEdit, onmousedown: handleEvent.maybeEdit,
styleMeta: style
}); });
const checkbox = $('.checker', entry); const checkbox = $('.checker', entry);
Object.assign(checkbox, { Object.assign(checkbox, {
id: ENTRY_ID_PREFIX_RAW + style.id, id: ENTRY_ID_PREFIX_RAW + style.id,
title: t('exclusionsPopupTip'),
checked: style.enabled, checked: style.enabled,
onclick: handleEvent.toggle, onclick: handleEvent.toggle,
oncontextmenu: handleEvent.openExcludeMenu
}); });
const editLink = $('.style-edit-link', entry); const editLink = $('.style-edit-link', entry);
@ -328,6 +334,7 @@ Object.assign(handleEvent, {
}, },
toggle(event) { toggle(event) {
event.stopPropagation();
API.saveStyle({ API.saveStyle({
id: handleEvent.getClickedStyleId(event), id: handleEvent.getClickedStyleId(event),
enabled: this.matches('.enable') || this.checked, enabled: this.matches('.enable') || this.checked,
@ -410,6 +417,12 @@ Object.assign(handleEvent, {
event.button === 2)) { event.button === 2)) {
return; return;
} }
// open exclude page config dialog on right-click
if (event.target.classList.contains('checker')) {
this.oncontextmenu = handleEvent.openExcludeMenu;
event.preventDefault();
return;
}
// open an editor on middleclick // open an editor on middleclick
if (event.target.matches('.entry, .style-name, .style-edit-link')) { if (event.target.matches('.entry, .style-name, .style-edit-link')) {
this.onmouseup = () => $('.style-edit-link', this).click(); this.onmouseup = () => $('.style-edit-link', this).click();
@ -445,6 +458,23 @@ Object.assign(handleEvent, {
handleEvent.openURLandHide.call(this, event); handleEvent.openURLandHide.call(this, event);
} }
}, },
openExcludeMenu(event) {
event.preventDefault();
event.stopPropagation();
const chkbox = this;
const entry = event.target.closest('.entry');
if (!chkbox.eventHandled) {
chkbox.eventHandled = true;
const style = entry.styleMeta;
popupExclusions
.openPopupDialog(style, tabURL)
.then(() => {
entry.styleMeta = style;
chkbox.eventHandled = null;
});
}
}
}); });