Add style exclusions. Closes #113
This commit is contained in:
parent
03d02ad405
commit
68dfa0153c
|
@ -205,6 +205,18 @@
|
|||
"configOnChangeTooltip": {
|
||||
"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": {
|
||||
"message": "Error",
|
||||
"description": "Used in various places to indicate some error occurred."
|
||||
|
@ -359,6 +371,68 @@
|
|||
"message": "Delete",
|
||||
"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": {
|
||||
"message": "Export",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
|
|
|
@ -349,7 +349,6 @@ function saveStyle(style) {
|
|||
}
|
||||
let existed;
|
||||
let codeIsUpdated;
|
||||
|
||||
return maybeCalcDigest()
|
||||
.then(maybeImportFix)
|
||||
.then(decide);
|
||||
|
@ -383,7 +382,9 @@ function saveStyle(style) {
|
|||
if (reason === 'update-digest' && oldStyle.originalDigest === style.originalDigest) {
|
||||
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);
|
||||
return write(style, store);
|
||||
});
|
||||
|
@ -398,6 +399,7 @@ function saveStyle(style) {
|
|||
url: null,
|
||||
originalMd5: null,
|
||||
installDate: Date.now(),
|
||||
exclusions: {}
|
||||
}, 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({
|
||||
style,
|
||||
|
@ -456,7 +463,7 @@ function getApplicableSections({
|
|||
// 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")
|
||||
}) {
|
||||
if (!skipUrlCheck && !URLS.supported(matchUrl)) {
|
||||
if (!skipUrlCheck && !URLS.supported(matchUrl) || checkExclusions(matchUrl, style.exclusions)) {
|
||||
return [];
|
||||
}
|
||||
const sections = [];
|
||||
|
|
15
edit.html
15
edit.html
|
@ -24,6 +24,7 @@
|
|||
<script src="js/localization.js"></script>
|
||||
<script src="js/script-loader.js"></script>
|
||||
<script src="js/storage-util.js"></script>
|
||||
<script src="js/exclusions.js"></script>
|
||||
<script src="content/apply.js"></script>
|
||||
<script src="edit/lint.js"></script>
|
||||
<script src="edit/util.js"></script>
|
||||
|
@ -399,6 +400,20 @@
|
|||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<summary>
|
||||
<h2 i18n-text="linterIssues">: <span id="issue-count"></span>
|
||||
|
|
|
@ -76,11 +76,53 @@ label {
|
|||
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 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
#name {
|
||||
#name, #excluded-input {
|
||||
width: 100%;
|
||||
}
|
||||
#basic-info-name {
|
||||
|
|
18
edit/edit.js
18
edit/edit.js
|
@ -7,6 +7,7 @@ global setupCodeMirror
|
|||
global beautify
|
||||
global initWithSectionStyle addSections removeSection getSectionsHashes
|
||||
global sectionsToMozFormat
|
||||
global exclusions
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
@ -45,6 +46,7 @@ Promise.all([
|
|||
$('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true});
|
||||
window.addEventListener('resize', () => debounce(rememberWindowSize, 100));
|
||||
|
||||
exclusions.init(style);
|
||||
if (usercss) {
|
||||
editor = createSourceEditor(style);
|
||||
} else {
|
||||
|
@ -161,7 +163,7 @@ function onRuntimeMessage(request) {
|
|||
request.reason !== 'config') {
|
||||
// code-less style from notifyAllTabs
|
||||
const {sections, id} = request.style;
|
||||
((sections[0] || {}).code === null
|
||||
((sections && sections[0] || {}).code === null
|
||||
? API.getStyles({id})
|
||||
: Promise.resolve([request.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) {
|
||||
const node = event.target;
|
||||
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);
|
||||
} else {
|
||||
// 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
|
||||
delete node.savedValue;
|
||||
}
|
||||
|
@ -314,7 +321,7 @@ function setCleanItem(node, isClean) {
|
|||
if (node.CodeMirror) {
|
||||
node.savedValue = node.CodeMirror.changeGeneration();
|
||||
} else {
|
||||
node.savedValue = node.type === 'checkbox' ? node.checked : node.value;
|
||||
node.savedValue = node.type === 'checkbox' ? node.checked : getNodeValue(node);
|
||||
}
|
||||
} else {
|
||||
dirty[node.id] = true;
|
||||
|
@ -357,7 +364,8 @@ function save() {
|
|||
name: $('#name').value.trim(),
|
||||
enabled: $('#enabled').checked,
|
||||
reason: 'editSave',
|
||||
sections: getSectionsHashes()
|
||||
sections: getSectionsHashes(),
|
||||
exclusions: exclusions.get()
|
||||
})
|
||||
.then(style => {
|
||||
styleId = style.id;
|
||||
|
|
|
@ -4,6 +4,7 @@ global CodeMirror dirtyReporter
|
|||
global updateLintReportIfEnabled initLint linterConfig updateLinter
|
||||
global createAppliesToLineWidget messageBox
|
||||
global sectionsToMozFormat
|
||||
global exclusions
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
@ -39,6 +40,8 @@ function createSourceEditor(style) {
|
|||
style.enabled = value;
|
||||
};
|
||||
|
||||
exclusions.onchange(dirty);
|
||||
|
||||
cm.on('changes', () => {
|
||||
dirty.modify('sourceGeneration', savedGeneration, cm.changeGeneration());
|
||||
updateLintReportIfEnabled(cm);
|
||||
|
@ -198,12 +201,15 @@ function createSourceEditor(style) {
|
|||
function save() {
|
||||
if (!dirty.isDirty()) return;
|
||||
const code = cm.getValue();
|
||||
style.exclusions = exclusions.get();
|
||||
exclusions.save(style, dirty);
|
||||
return (
|
||||
API.saveUsercssUnsafe({
|
||||
id: style.id,
|
||||
reason: 'editSave',
|
||||
enabled: style.enabled,
|
||||
sourceCode: code,
|
||||
exclusions: style.exclusions
|
||||
}))
|
||||
.then(({style, errors}) => {
|
||||
replaceStyle(style);
|
||||
|
|
241
js/exclusions.js
Normal file
241
js/exclusions.js
Normal 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};
|
||||
})();
|
|
@ -40,6 +40,7 @@ var prefs = new function Prefs() {
|
|||
|
||||
'editor.options': {}, // CodeMirror.defaults.*
|
||||
'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.lineWrapping': true, // word wrap
|
||||
'editor.smartIndent': true, // 'smart' indent
|
||||
|
|
|
@ -146,6 +146,13 @@
|
|||
</details>
|
||||
</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/messaging.js"></script>
|
||||
<script src="js/prefs.js"></script>
|
||||
|
|
|
@ -180,6 +180,7 @@ select {
|
|||
}
|
||||
|
||||
.applies-to,
|
||||
.excluded-on,
|
||||
.actions {
|
||||
padding-left: 15px;
|
||||
margin-bottom: 0;
|
||||
|
@ -643,6 +644,14 @@ select {
|
|||
line-height: 18px;
|
||||
}
|
||||
|
||||
.target[data-type="exclusions"] {
|
||||
color: #d22;
|
||||
}
|
||||
|
||||
.newUI .target[data-type="exclusions"] img {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.newUI .applies-to .expander {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
|
|
|
@ -25,7 +25,7 @@ const newUI = {
|
|||
newUI.renderClass();
|
||||
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 OWN_ICON = chrome.runtime.getManifest().icons['16'];
|
||||
|
||||
|
@ -121,7 +121,7 @@ function showStyles(styles = [], matchUrlIds) {
|
|||
const sorted = sorter.sort({
|
||||
styles: styles.map(style => ({
|
||||
style,
|
||||
name: style.name.toLocaleLowerCase() + '\n' + style.name,
|
||||
name: (style.name || '').toLocaleLowerCase() + '\n' + style.name,
|
||||
})),
|
||||
});
|
||||
let index = 0;
|
||||
|
@ -255,8 +255,16 @@ function createStyleTargetsElement({entry, style, iconsOnly}) {
|
|||
let numTargets = 0;
|
||||
const displayed = new Set();
|
||||
for (const type of TARGET_TYPES) {
|
||||
for (const section of style.sections) {
|
||||
for (const targetValue of section[type] || []) {
|
||||
const isExcluded = type === 'exclusions';
|
||||
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)) {
|
||||
continue;
|
||||
}
|
||||
|
@ -336,7 +344,7 @@ function getFaviconImgSrc(container = installed) {
|
|||
favicon = GET_FAVICON_URL + targetValue;
|
||||
} else if (targetValue.includes('chrome-extension:') || targetValue.includes('moz-extension:')) {
|
||||
favicon = OWN_ICON;
|
||||
} else if (type === 'regexps') {
|
||||
} else if (type === 'regexps' || type === 'exclusions') {
|
||||
favicon = targetValue
|
||||
.replace(regexpRemoveNegativeLookAhead, '')
|
||||
.replace(regexpReplaceExtraCharacters, '')
|
||||
|
@ -630,6 +638,9 @@ function switchUI({styleOnly} = {}) {
|
|||
.newUI .targets {
|
||||
max-height: ${newUI.targets * 18}px;
|
||||
}
|
||||
.newUI .target[data-type="exclusions"]:before {
|
||||
content: '${t('exclusionsPrefix')}';
|
||||
}
|
||||
` + (newUI.faviconsGray ? `
|
||||
.newUI .target img {
|
||||
-webkit-filter: grayscale(1);
|
||||
|
|
|
@ -40,7 +40,8 @@
|
|||
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;
|
||||
}
|
||||
|
||||
|
@ -55,6 +56,10 @@
|
|||
text-align: left;
|
||||
}
|
||||
|
||||
#message-box-contents h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#message-box-title {
|
||||
font-weight: bold;
|
||||
background-color: rgb(145, 208, 198);
|
||||
|
|
|
@ -164,6 +164,8 @@
|
|||
<script src="popup/hotkeys.js"></script>
|
||||
<script src="js/script-loader.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>
|
||||
|
||||
<body id="stylus-popup">
|
||||
|
|
123
popup/popup-exclusions.js
Normal file
123
popup/popup-exclusions.js
Normal 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};
|
||||
|
||||
})();
|
|
@ -89,7 +89,6 @@ body > div:not(#installed):not(#message-box):not(.colorpicker-popup) {
|
|||
position: absolute;
|
||||
top: 7px;
|
||||
left: var(--outer-padding);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#disable-all-wrapper {
|
||||
|
@ -118,6 +117,17 @@ body > div:not(#installed):not(#message-box):not(.colorpicker-popup) {
|
|||
margin-right: .5em;
|
||||
}
|
||||
|
||||
#popup-exclusions {
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
max-width: 95%;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
#popup-exclusions option {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.checker {
|
||||
display: inline;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
/* global configDialog hotkeys */
|
||||
/*
|
||||
global configDialog hotkeys
|
||||
global popupExclusions
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
|
@ -257,13 +260,16 @@ function createStyleElement({
|
|||
styleIsUsercss: Boolean(style.usercssData),
|
||||
className: entry.className + ' ' + (style.enabled ? 'enabled' : 'disabled'),
|
||||
onmousedown: handleEvent.maybeEdit,
|
||||
styleMeta: style
|
||||
});
|
||||
|
||||
const checkbox = $('.checker', entry);
|
||||
Object.assign(checkbox, {
|
||||
id: ENTRY_ID_PREFIX_RAW + style.id,
|
||||
title: t('exclusionsPopupTip'),
|
||||
checked: style.enabled,
|
||||
onclick: handleEvent.toggle,
|
||||
oncontextmenu: handleEvent.openExcludeMenu
|
||||
});
|
||||
|
||||
const editLink = $('.style-edit-link', entry);
|
||||
|
@ -328,6 +334,7 @@ Object.assign(handleEvent, {
|
|||
},
|
||||
|
||||
toggle(event) {
|
||||
event.stopPropagation();
|
||||
API.saveStyle({
|
||||
id: handleEvent.getClickedStyleId(event),
|
||||
enabled: this.matches('.enable') || this.checked,
|
||||
|
@ -410,6 +417,12 @@ Object.assign(handleEvent, {
|
|||
event.button === 2)) {
|
||||
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
|
||||
if (event.target.matches('.entry, .style-name, .style-edit-link')) {
|
||||
this.onmouseup = () => $('.style-edit-link', this).click();
|
||||
|
@ -445,6 +458,23 @@ Object.assign(handleEvent, {
|
|||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user