Modify input method
This commit is contained in:
parent
9f75b69cd8
commit
2b4a1a5635
|
@ -205,18 +205,6 @@
|
|||
"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."
|
||||
|
@ -380,42 +368,29 @@
|
|||
"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"
|
||||
}
|
||||
}
|
||||
"message": "Add exclusion",
|
||||
"description": "Title of popup to add an excluded site or page (URL)"
|
||||
},
|
||||
"exclusionsHeader": {
|
||||
"message": "Excluded Pages",
|
||||
"message": "Excluded",
|
||||
"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.",
|
||||
"message": "Exclusion entries are only checked when a style is set to be applied to a page, and if an exclusion is found, the given style (and all internal sections) will not be applied to that page.\n\nThe list of exclusions is set separately from the userstyle so that it will not be effected when updating or editing the style itself. This is useful because you can exclude pages that would be otherwise be effected by a global style.\n\nAdd one or more exclusion entries for each style. An exclusion entry string contains a pattern that will match a web location (URL). This string may contain wildcards (\"*\") to match any portion of a URL, e.g. \"forum.*.com\" will exclude the forum sub-domains of all top level dot-com domains. Regular expressions are allowed, except `.` and `*` are altered, and are saved as a string so character classes must be doubly escaped (e.g. `\\w`).\n\nExcluded pages are automatically updated while typing; invalid entries will be removed on page reload!",
|
||||
"description": "Help text for user set style exclusions"
|
||||
},
|
||||
"exclusionsHelpTitle": {
|
||||
"message": "Set Style Exclusions",
|
||||
"description": "Header text for help modal"
|
||||
},
|
||||
"exclusionsInvalidUrl": {
|
||||
"exclusionsvalidateEntry": {
|
||||
"message": "Enter a unique and valid URL",
|
||||
"description": "Text for an alert notifying the user that an entered URL is not unique or invalid"
|
||||
},
|
||||
"exclusionsPopupTitle": {
|
||||
"message": "Exclude the site or page",
|
||||
"description": "Title of exclusion popup dialog within the extension popup"
|
||||
},
|
||||
"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"
|
||||
|
@ -425,7 +400,7 @@
|
|||
"description": "Prefix label added to the applies to column in the style manager"
|
||||
},
|
||||
"exclusionsStatus": {
|
||||
"message": "$number$ sites",
|
||||
"message": "$number$ pages",
|
||||
"description": "Label added next to the Excluded Pages header when 'number' is not zero",
|
||||
"placeholders": {
|
||||
"number": {
|
||||
|
|
|
@ -19,6 +19,7 @@ var cachedStyles = {
|
|||
byId: new Map(), // all styles indexed by id
|
||||
filters: new Map(), // filterStyles() parameters mapped to the returned results, 10k max
|
||||
regexps: new Map(), // compiled style regexps
|
||||
exclusions: new Map(), // compiled exclusion regexps
|
||||
urlDomains: new Map(), // getDomain() results for 100 last checked urls
|
||||
needTransitionPatch: new Map(), // FF bug workaround
|
||||
mutex: {
|
||||
|
@ -250,6 +251,7 @@ function filterStyles({
|
|||
strictRegexp,
|
||||
blankHash,
|
||||
cacheKey,
|
||||
omitCode,
|
||||
});
|
||||
}
|
||||
if (!omitCode) return styles;
|
||||
|
@ -275,6 +277,7 @@ function filterStylesInternal({
|
|||
strictRegexp,
|
||||
blankHash,
|
||||
cacheKey,
|
||||
omitCode,
|
||||
}) {
|
||||
if (matchUrl && !cachedStyles.urlDomains.has(matchUrl)) {
|
||||
cachedStyles.urlDomains.set(matchUrl, getDomains(matchUrl));
|
||||
|
@ -309,6 +312,7 @@ function filterStylesInternal({
|
|||
stopOnFirst: !asHash,
|
||||
skipUrlCheck: true,
|
||||
matchUrlBase,
|
||||
omitCode,
|
||||
});
|
||||
if (asHash) {
|
||||
if (sections.length) {
|
||||
|
@ -423,10 +427,9 @@ function saveStyle(style) {
|
|||
style.id = style.id || event.target.result;
|
||||
invalidateCache(existed ? {updated: style} : {added: style});
|
||||
if (notify) {
|
||||
notifyAllTabs({
|
||||
method: existed ? 'styleUpdated' : 'styleAdded',
|
||||
style, codeIsUpdated, reason,
|
||||
});
|
||||
const method = reason === 'exclusionsUpdate' ? reason :
|
||||
existed ? 'styleUpdated' : 'styleAdded';
|
||||
notifyAllTabs({method, style, codeIsUpdated, reason});
|
||||
}
|
||||
return style;
|
||||
}
|
||||
|
@ -444,12 +447,28 @@ 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 compileExclusionRegexps(exclusions) {
|
||||
exclusions.forEach(exclusion => {
|
||||
if (!cachedStyles.exclusions.get(exclusion)) {
|
||||
cachedStyles.exclusions.set(exclusion, tryRegExp(exclusion) || false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isPageExcluded(matchUrl, exclusions = {}) {
|
||||
const values = Object.values(exclusions);
|
||||
if (!values.length) {
|
||||
return false;
|
||||
}
|
||||
compileExclusionRegexps(values);
|
||||
return values.some(exclude => {
|
||||
const rx = cachedStyles.exclusions.get(exclude);
|
||||
return rx && rx.test(matchUrl);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function getApplicableSections({
|
||||
style,
|
||||
matchUrl,
|
||||
|
@ -458,12 +477,13 @@ function getApplicableSections({
|
|||
stopOnFirst,
|
||||
skipUrlCheck,
|
||||
matchUrlBase = matchUrl.includes('#') && matchUrl.split('#', 1)[0],
|
||||
omitCode,
|
||||
// as per spec the fragment portion is ignored in @-moz-document:
|
||||
// https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#url-of-doc
|
||||
// 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) || checkExclusions(matchUrl, style.exclusions)) {
|
||||
if (!skipUrlCheck && !URLS.supported(matchUrl) || omitCode !== false && isPageExcluded(matchUrl, style.exclusions)) {
|
||||
return [];
|
||||
}
|
||||
const sections = [];
|
||||
|
@ -642,6 +662,7 @@ function updateFiltersCache(style) {
|
|||
matchUrlBase,
|
||||
strictRegexp,
|
||||
skipUrlCheck: true,
|
||||
omitCode: false
|
||||
});
|
||||
if (sections.length) {
|
||||
styles[id] = sections;
|
||||
|
@ -830,8 +851,8 @@ function detectSloppyRegexps({matchUrl, ids}) {
|
|||
}
|
||||
}
|
||||
if (!hasRegExp) continue;
|
||||
const applied = getApplicableSections({style, matchUrl});
|
||||
const wannabe = getApplicableSections({style, matchUrl, strictRegexp: false});
|
||||
const applied = getApplicableSections({style, matchUrl, omitCode: false});
|
||||
const wannabe = getApplicableSections({style, matchUrl, omitCode: false, strictRegexp: false});
|
||||
results.push({
|
||||
id,
|
||||
applied,
|
||||
|
|
28
edit.html
28
edit.html
|
@ -172,6 +172,16 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<template data-id="exclusionEntry">
|
||||
<div class="exclusion-entry">
|
||||
<input class="exclusion-input" placeholder="*://*.google.com/*foo*">
|
||||
<button class="exclusion-add" i18n-title="exclusionsAddTitle">+</button>
|
||||
<button class="exclusion-delete" i18n-title="confirmDelete">
|
||||
<svg class="svg-icon"><use xlink:href="#svg-icon-delete"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template data-id="find">
|
||||
<div data-type="content">
|
||||
<div data-type="input-wrapper">
|
||||
|
@ -400,19 +410,15 @@
|
|||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<details id="exclusions" data-pref="editor.exclusions.expanded">
|
||||
<details id="exclusions">
|
||||
<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>
|
||||
<h2 i18n-text="exclusionsHeader"></h2>
|
||||
<a id="excluded-list-help" href="#" class="svg-inline-wrapper">
|
||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
<span id="excluded-stats"></span>
|
||||
</summary>
|
||||
<div id="excluded-wrap"></div>
|
||||
</details>
|
||||
<details id="lint" class="hidden" data-pref="editor.lint.expanded">
|
||||
<summary>
|
||||
|
@ -480,6 +486,10 @@
|
|||
<path fill-rule="evenodd" d="M0 3v2h8v-2h-8z"/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="svg-icon-delete" 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"/>
|
||||
</symbol>
|
||||
|
||||
</svg>
|
||||
|
||||
</body>
|
||||
|
|
|
@ -84,27 +84,22 @@ label {
|
|||
margin-top: 0;
|
||||
}
|
||||
|
||||
#exclusions select {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
padding: 0 6px;
|
||||
#exclusions .exclusion-input {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
#exclusions option {
|
||||
overflow: hidden;
|
||||
#exclusions button {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
#excluded-pages:empty {
|
||||
height: 1.4em;
|
||||
border: none;
|
||||
#exclusions .exclusion-input:invalid {
|
||||
border-color: red;
|
||||
}
|
||||
|
||||
#excludedError {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
color: red;
|
||||
#exclusions .exclusion-delete .svg-icon {
|
||||
pointer-events: none;
|
||||
vertical-align: text-top;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
#excluded-stats:not(:empty) {
|
||||
|
@ -443,7 +438,7 @@ body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar
|
|||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.applies-to li {
|
||||
.applies-to li, .exclusion-entry {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
list-style-type: none;
|
||||
|
|
|
@ -40,8 +40,6 @@ function createSourceEditor(style) {
|
|||
style.enabled = value;
|
||||
};
|
||||
|
||||
exclusions.onchange(dirty);
|
||||
|
||||
cm.on('changes', () => {
|
||||
dirty.modify('sourceGeneration', savedGeneration, cm.changeGeneration());
|
||||
updateLintReportIfEnabled(cm);
|
||||
|
@ -201,15 +199,18 @@ function createSourceEditor(style) {
|
|||
function save() {
|
||||
if (!dirty.isDirty()) return;
|
||||
const code = cm.getValue();
|
||||
style.exclusions = exclusions.get();
|
||||
exclusions.save(style, dirty);
|
||||
const exclusionList = exclusions.get();
|
||||
exclusions.save({
|
||||
id: style.id,
|
||||
exclusionList
|
||||
});
|
||||
return (
|
||||
API.saveUsercssUnsafe({
|
||||
id: style.id,
|
||||
reason: 'editSave',
|
||||
enabled: style.enabled,
|
||||
sourceCode: code,
|
||||
exclusions: style.exclusions
|
||||
exclusions: exclusionList
|
||||
}))
|
||||
.then(({style, errors}) => {
|
||||
replaceStyle(style);
|
||||
|
|
328
js/exclusions.js
328
js/exclusions.js
|
@ -1,190 +1,132 @@
|
|||
/*
|
||||
global messageBox resolveWith
|
||||
gloabl editor showHelp onChange
|
||||
gloabl editor showHelp getSectionsHashes
|
||||
global popupExclusions
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const exclusions = (() => {
|
||||
|
||||
// `\S*\*\S*` => `foo*`, `*bar`, `f*bar`
|
||||
// `\S+\.\S+` => `foo.bar`, `f.b`
|
||||
// see https://regex101.com/r/NUuwiu/2
|
||||
const validExclusionRegex = /^(\S*\*\S*|\S+\.\S+)$/;
|
||||
// ms to wait before validating user input
|
||||
const saveDelay = 250;
|
||||
|
||||
// get exclusions from a select element
|
||||
function get(options = {}) {
|
||||
const lists = {};
|
||||
const excluded = options.exclusions || getMultiOptions(options);
|
||||
excluded.forEach(list => {
|
||||
lists[list] = createRegExp(list);
|
||||
function get() {
|
||||
exclusions.list = {};
|
||||
$$('#excluded-wrap input').forEach(input => {
|
||||
const url = input.value;
|
||||
if (url && validExclusionRegex.test(url)) {
|
||||
exclusions.list[url] = createRegExp(url);
|
||||
}
|
||||
});
|
||||
return lists;
|
||||
return exclusions.list;
|
||||
}
|
||||
|
||||
function createRegExp(url) {
|
||||
// returning a regex string; Object.assign is used on style & doesn't save RegExp
|
||||
return url.replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&').replace(/[*]/g, '.+?');
|
||||
// Include boundaries to prevent `e.c` from matching `google.com`
|
||||
const prefix = url.startsWith('^') ? '' : '\\b';
|
||||
const suffix = url.endsWith('$') ? '' : '\\b';
|
||||
// Only escape `.`; alter `*`; all other regex allowed
|
||||
return `${prefix}${url.replace(/\./g, '\\.').replace(/\*/g, '.*?')}${suffix}`;
|
||||
}
|
||||
|
||||
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();
|
||||
function addExclusionEntry({container, value, insertAfter}) {
|
||||
const item = template.exclusionEntry.cloneNode(true);
|
||||
const input = $('input', item);
|
||||
const regex = validExclusionRegex.toString();
|
||||
input.value = value;
|
||||
input.setAttribute('pattern', regex.substring(1, regex.length - 1));
|
||||
if (insertAfter) {
|
||||
insertAfter.insertAdjacentElement('afterend', item);
|
||||
} else {
|
||||
// make regular userstyle dirty
|
||||
onChange({target: exclusions.select});
|
||||
container.appendChild(item);
|
||||
}
|
||||
input.focus();
|
||||
}
|
||||
|
||||
function populateList() {
|
||||
// List should never be empty - need to add an empty input
|
||||
const list = exclusions.list.length ? exclusions.list : [''];
|
||||
const block = $('#excluded-wrap');
|
||||
block.textContent = '';
|
||||
const container = document.createDocumentFragment();
|
||||
list.forEach(value => {
|
||||
addExclusionEntry({container, value});
|
||||
});
|
||||
block.appendChild(container);
|
||||
}
|
||||
|
||||
function validateEntry(input) {
|
||||
const lists = Object.keys(get());
|
||||
const url = input.value;
|
||||
const index = $$('.exclusion-entry input:valid').indexOf(input);
|
||||
// Generic URL globs; e.g. "https://test.com/*" & "*.test.com"
|
||||
return !(lists.includes(url) && lists.indexOf(url) !== index) &&
|
||||
validExclusionRegex.test(url);
|
||||
}
|
||||
|
||||
function updateList() {
|
||||
const list = get();
|
||||
const keys = Object.keys(list);
|
||||
if (exclusions.savedValue !== keys.join(',')) {
|
||||
exclusions.saveValue = keys.join(',');
|
||||
exclusions.list = list;
|
||||
}
|
||||
debounce(save, 100, {});
|
||||
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 deleteExclusions(entry) {
|
||||
if ($('#excluded-wrap').children.length === 1) {
|
||||
const input = $('.exclusion-input', entry);
|
||||
input.value = '';
|
||||
input.focus();
|
||||
} else {
|
||||
const nextFocus = entry.previousElementSibling || entry.nextElementSibling;
|
||||
entry.parentNode.removeChild(entry);
|
||||
if (nextFocus) {
|
||||
$('input', nextFocus).focus();
|
||||
}
|
||||
}
|
||||
updateList();
|
||||
}
|
||||
|
||||
function excludeAction(event) {
|
||||
event.preventDefault();
|
||||
const target = event.target;
|
||||
const entry = target.closest('.exclusion-entry');
|
||||
if (target.classList.contains('exclusion-add')) {
|
||||
addExclusionEntry({
|
||||
container: $('#excluded-wrap'),
|
||||
value: '',
|
||||
insertAfter: entry
|
||||
});
|
||||
} else if (target.classList.contains('exclusion-delete')) {
|
||||
deleteExclusions(entry);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleButtons(state = false) {
|
||||
const noSelection = exclusions.select.value === '';
|
||||
$('#excluded-list-edit').disabled = noSelection || state;
|
||||
$('#excluded-list-delete').disabled = noSelection || state;
|
||||
function excludeValidate(event) {
|
||||
const target = event.target;
|
||||
target.setCustomValidity('');
|
||||
target.title = '';
|
||||
if (target.matches(':valid')) {
|
||||
if (!validateEntry(target)) {
|
||||
target.setCustomValidity(t('exclusionsvalidateEntry'));
|
||||
target.title = t('exclusionsvalidateEntry');
|
||||
} else {
|
||||
updateList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
const total = Object.keys(exclusions.list).length;
|
||||
$('#excluded-stats').textContent = total ? t('exclusionsStatus', [total]) : '';
|
||||
}
|
||||
|
||||
function showExclusionHelp(event) {
|
||||
|
@ -193,49 +135,51 @@ const exclusions = (() => {
|
|||
}
|
||||
|
||||
function onRuntimeMessage(msg) {
|
||||
if (msg.method === 'styleUpdated' && msg.style && msg.style.exclusions && exclusions.select) {
|
||||
update(Object.keys(msg.style.exclusions));
|
||||
if (msg.method === 'exclusionsUpdate' && msg.style && msg.style.exclusions) {
|
||||
update({list: Object.keys(msg.style.exclusions), isUpdating: true});
|
||||
// update popup, if loaded
|
||||
if (typeof popupExclusions !== 'undefined') {
|
||||
popupExclusions.selectExclusions(msg.style.exclusions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function update(list = exclusions.list) {
|
||||
populateSelect(list);
|
||||
function update({list = exclusions.list, isUpdating}) {
|
||||
if (!isUpdating) {
|
||||
exclusions.list = list;
|
||||
populateList();
|
||||
}
|
||||
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 save({id, exclusionList = get()}) {
|
||||
// get last saved version
|
||||
API.getStyles({id: id || exclusions.id}).then(([style]) => {
|
||||
style.exclusions = exclusionList;
|
||||
style.reason = 'exclusionsUpdate';
|
||||
API.saveStyle(style);
|
||||
});
|
||||
}
|
||||
|
||||
function init(style) {
|
||||
const block = $('#exclusions');
|
||||
const list = Object.keys(style.exclusions || {});
|
||||
const size = list.length;
|
||||
exclusions.select = $('#excluded-list');
|
||||
exclusions.select.savedValue = String(size);
|
||||
exclusions.id = style.id;
|
||||
exclusions.savedValue = list.join(',');
|
||||
exclusions.list = list;
|
||||
update();
|
||||
if (size) {
|
||||
block.setAttribute('open', true);
|
||||
} else {
|
||||
block.removeAttribute('open');
|
||||
}
|
||||
update({});
|
||||
|
||||
$('#excluded-wrap').onclick = excludeAction;
|
||||
$('#excluded-wrap').oninput = event => debounce(excludeValidate, saveDelay, event);
|
||||
$('#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};
|
||||
return {init, get, update, save, createRegExp};
|
||||
})();
|
||||
|
|
|
@ -40,7 +40,6 @@ 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
|
||||
|
|
14
popup.html
14
popup.html
|
@ -165,7 +165,7 @@
|
|||
<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>
|
||||
<script src="popup/popup-exclusions.js"></script>
|
||||
</head>
|
||||
|
||||
<body id="stylus-popup">
|
||||
|
@ -181,6 +181,18 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="exclude">
|
||||
<div>
|
||||
<strong>Style's Name</strong>
|
||||
<span i18n-text="exclusionsPopupTitle"></span>
|
||||
<select id="popup-exclusions" size="0" multiple></select>
|
||||
<div>
|
||||
<button i18n-text="confirmOK" data-cmd="ok"></button>
|
||||
<button i18n-text="confirmCancel" data-cmd="cancel"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="installed">
|
||||
<aside id="hotkey-info" i18n-title="popupHotkeysTooltip"></aside>
|
||||
</div>
|
||||
|
|
|
@ -6,8 +6,6 @@ global exclusions
|
|||
|
||||
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 ['', '/*']
|
||||
|
@ -25,7 +23,8 @@ const popupExclusions = (() => {
|
|||
function processURL(url) {
|
||||
const results = [];
|
||||
const protocol = url.match(/\w+:\/\//);
|
||||
const parts = url.replace(/(\w+:\/\/|[#?].*$)/g, '').split('/');
|
||||
// remove ending '/', protocol, hash & search strings
|
||||
const parts = url.replace(/\/$/, '').replace(/(\w+:\/\/|[#?].*$)/g, '').split('/');
|
||||
const domain = parts[0].split('.');
|
||||
/*
|
||||
Domain: a.b.com
|
||||
|
@ -45,28 +44,42 @@ const popupExclusions = (() => {
|
|||
return results.reverse();
|
||||
}
|
||||
|
||||
function shortenURL(text) {
|
||||
const len = text.length;
|
||||
let prefix = '\u2026';
|
||||
// account for URL that end with a '/'
|
||||
let index = (text.endsWith('/') ? text.substring(0, len - 1) : text).lastIndexOf('/');
|
||||
if (index < 0 || len - index < 2) {
|
||||
index = 0;
|
||||
prefix = '';
|
||||
}
|
||||
return prefix + text.substring(index, len);
|
||||
}
|
||||
|
||||
function createOption(option) {
|
||||
// ["Domain/Prefix", "{url}"]
|
||||
return $create('option', {
|
||||
value: option[1],
|
||||
title: option[1],
|
||||
textContent: `${option[0]}: ${option[1]}`
|
||||
textContent: `${option[0]}: ${shortenURL(option[1])}`
|
||||
});
|
||||
}
|
||||
|
||||
function createPopupContent(url) {
|
||||
function getMultiOptions({select, selectedOnly} = {}) {
|
||||
return [...select.children].reduce((acc, opt) => {
|
||||
if (selectedOnly && opt.selected || !selectedOnly) {
|
||||
acc.push(opt.value);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function updatePopupContent(url) {
|
||||
const options = processURL(url);
|
||||
return [
|
||||
$create('h2', {textContent: t('exclusionsEditTitle')}),
|
||||
$create('select', {
|
||||
id: 'popup-exclusions',
|
||||
size: options.length,
|
||||
multiple: 'true',
|
||||
value: ''
|
||||
}, [
|
||||
...options.map(option => createOption(option))
|
||||
])
|
||||
];
|
||||
const renderBin = document.createDocumentFragment();
|
||||
options.map(option => renderBin.appendChild(createOption(option)));
|
||||
$('#popup-exclusions').textContent = '';
|
||||
$('#popup-exclusions').appendChild(renderBin);
|
||||
}
|
||||
|
||||
function getIframeURLs(style) {
|
||||
|
@ -75,62 +88,89 @@ const popupExclusions = (() => {
|
|||
chrome.webNavigation.getAllFrames({
|
||||
tabId: tab.id
|
||||
}, frames => {
|
||||
const urls = frames.reduce((acc, frame) => processURL(frame.url), []);
|
||||
const urls = frames.reduce((acc, frame) => [...acc, ...processURL(frame.url)], []);
|
||||
updateSelections(style, urls);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateSelections(style, newOptions = []) {
|
||||
const select = $('select', messageBox.element);
|
||||
const exclusions = Object.keys(style.exclusions || {});
|
||||
if (newOptions.length) {
|
||||
const currentOptions = [...select.children].map(opt => opt.value);
|
||||
newOptions.forEach(opt => {
|
||||
if (!currentOptions.includes(opt[1])) {
|
||||
select.appendChild(createOption(opt));
|
||||
}
|
||||
});
|
||||
select.size = select.children.length;
|
||||
}
|
||||
function selectExclusions(exclusions) {
|
||||
const select = $('#exclude select');
|
||||
const excludes = Object.keys(exclusions || {});
|
||||
[...select.children].forEach(option => {
|
||||
if (exclusionExists(exclusions, option.value).length) {
|
||||
if (exclusionExists(excludes, option.value).length) {
|
||||
option.selected = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
updateSelections(style);
|
||||
getIframeURLs(style);
|
||||
$('#message-box-buttons button', messageBox.element).onclick = function () {
|
||||
handlePopupSave(style, this);
|
||||
};
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
document.body.style.minWidth = '';
|
||||
document.body.style.minHeight = '';
|
||||
});
|
||||
return msgBox;
|
||||
function updateSelections(style, newOptions = []) {
|
||||
const wrap = $('#exclude');
|
||||
const select = $('select', wrap);
|
||||
if (newOptions.length) {
|
||||
const currentOptions = [...select.children].map(opt => opt.value);
|
||||
newOptions.forEach(opt => {
|
||||
if (!currentOptions.includes(opt[1])) {
|
||||
select.appendChild(createOption(opt));
|
||||
// newOptions may have duplicates (e.g. multiple iframes from same source)
|
||||
currentOptions.push(opt[1]);
|
||||
}
|
||||
});
|
||||
select.size = select.children.length;
|
||||
// hide select, then calculate & adjust height
|
||||
select.style.height = '0';
|
||||
document.body.style.height = `${select.scrollHeight + wrap.offsetHeight}px`;
|
||||
select.style.height = '';
|
||||
}
|
||||
selectExclusions(style.exclusions);
|
||||
}
|
||||
|
||||
function handlePopupSave(style, button) {
|
||||
function isExcluded(matchUrl, exclusions = {}) {
|
||||
const values = Object.values(exclusions);
|
||||
return values.length && values.some(exclude => tryRegExp(exclude).test(matchUrl));
|
||||
}
|
||||
|
||||
function openPopupDialog(entry, tabURL) {
|
||||
const style = entry.styleMeta;
|
||||
updateSelections(style, updatePopupContent(tabURL));
|
||||
getIframeURLs(style);
|
||||
const box = $('#exclude');
|
||||
box.dataset.display = true;
|
||||
box.style.cssText = '';
|
||||
$('strong', box).textContent = style.name;
|
||||
$('[data-cmd="ok"]', box).focus();
|
||||
$('[data-cmd="ok"]', box).onclick = () => confirm(true);
|
||||
$('[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
|
||||
&& (keyCode === 13 || keyCode === 27)) {
|
||||
event.preventDefault();
|
||||
confirm(keyCode === 13);
|
||||
}
|
||||
};
|
||||
function confirm(ok) {
|
||||
window.onkeydown = null;
|
||||
animateElement(box, {
|
||||
className: 'lights-on',
|
||||
onComplete: () => (box.dataset.display = false),
|
||||
});
|
||||
if (ok) {
|
||||
handlePopupSave(style);
|
||||
entry.styleMeta = style;
|
||||
entry.classList.toggle('excluded', isExcluded(tabURL, style.exclusions));
|
||||
document.body.style.height = '';
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function handlePopupSave(style) {
|
||||
const current = Object.keys(style.exclusions);
|
||||
const select = $('#popup-exclusions', messageBox.element);
|
||||
const all = exclusions.getMultiOptions({select});
|
||||
const selected = exclusions.getMultiOptions({select, selectedOnly: true});
|
||||
const all = getMultiOptions({select});
|
||||
const selected = getMultiOptions({select, selectedOnly: true});
|
||||
// Add exclusions
|
||||
selected.forEach(value => {
|
||||
let exists = exclusionExists(current, value);
|
||||
|
@ -151,10 +191,12 @@ const popupExclusions = (() => {
|
|||
delete style.exclusions[value + ending];
|
||||
});
|
||||
});
|
||||
exclusions.save(style);
|
||||
messageBox.listeners.button.apply(button);
|
||||
exclusions.save({
|
||||
id: style.id,
|
||||
exclusionList: style.exclusions
|
||||
});
|
||||
}
|
||||
|
||||
return {openPopupDialog};
|
||||
return {openPopupDialog, selectExclusions, isExcluded};
|
||||
|
||||
})();
|
||||
|
|
|
@ -117,11 +117,15 @@ body > div:not(#installed):not(#message-box):not(.colorpicker-popup) {
|
|||
margin-right: .5em;
|
||||
}
|
||||
|
||||
#popup-exclusions-title {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
#popup-exclusions {
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
max-width: 95%;
|
||||
padding: 0 6px;
|
||||
overflow-y: auto;
|
||||
max-width: 98%;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
#popup-exclusions option {
|
||||
|
@ -245,6 +249,10 @@ html[style] .entry {
|
|||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.entry.excluded .style-name {
|
||||
color: #d22;
|
||||
}
|
||||
|
||||
.entry:nth-child(even) {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
@ -490,7 +498,7 @@ body.blocked .actions > .main-controls {
|
|||
|
||||
/* confirm */
|
||||
|
||||
#confirm {
|
||||
#confirm, #exclude {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2147483647;
|
||||
|
@ -507,21 +515,26 @@ body.blocked .actions > .main-controls {
|
|||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
#confirm.lights-on {
|
||||
#confirm.lights-on,
|
||||
#exclude.lights-on {
|
||||
animation: lights-on .25s ease-in-out;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
#confirm.lights-on,
|
||||
#confirm.lights-on > div {
|
||||
#confirm.lights-on > div,
|
||||
#exclude.lights-on,
|
||||
#exclude.lights-on > div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#confirm[data-display=true] {
|
||||
#confirm[data-display=true],
|
||||
#exclude[data-display=true] {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#confirm > div {
|
||||
#confirm > div,
|
||||
#exclude > div {
|
||||
width: 80%;
|
||||
max-height: 80%;
|
||||
min-height: 6em;
|
||||
|
@ -536,16 +549,23 @@ body.blocked .actions > .main-controls {
|
|||
padding-bottom: .5em;
|
||||
}
|
||||
|
||||
#confirm > div > div {
|
||||
#exclude select {
|
||||
margin: .5em 0;
|
||||
}
|
||||
|
||||
#confirm > div > div,
|
||||
#exclude > div > div {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.non-windows #confirm > div > div {
|
||||
.non-windows #confirm > div > div,
|
||||
.non-windows #exclude > div > div {
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#confirm > button {
|
||||
#confirm > button,
|
||||
#exclude > button {
|
||||
/* add a gap between buttons both for horizontal
|
||||
or vertical (when the label is wide) layout */
|
||||
margin: 0 .25em .25em 0;
|
||||
|
|
|
@ -253,12 +253,15 @@ function createStyleElement({
|
|||
container = installed,
|
||||
}) {
|
||||
const entry = template.style.cloneNode(true);
|
||||
const excluded = popupExclusions.isExcluded(tabURL, style.exclusions);
|
||||
entry.setAttribute('style-id', style.id);
|
||||
Object.assign(entry, {
|
||||
id: ENTRY_ID_PREFIX_RAW + style.id,
|
||||
styleId: style.id,
|
||||
styleIsUsercss: Boolean(style.usercssData),
|
||||
className: entry.className + ' ' + (style.enabled ? 'enabled' : 'disabled'),
|
||||
className: entry.className + ' ' +
|
||||
(style.enabled ? 'enabled' : 'disabled') +
|
||||
(excluded ? ' excluded' : ''),
|
||||
onmousedown: handleEvent.maybeEdit,
|
||||
styleMeta: style
|
||||
});
|
||||
|
@ -466,11 +469,9 @@ Object.assign(handleEvent, {
|
|||
const entry = event.target.closest('.entry');
|
||||
if (!chkbox.eventHandled) {
|
||||
chkbox.eventHandled = true;
|
||||
const style = entry.styleMeta;
|
||||
popupExclusions
|
||||
.openPopupDialog(style, tabURL)
|
||||
.openPopupDialog(entry, tabURL)
|
||||
.then(() => {
|
||||
entry.styleMeta = style;
|
||||
chkbox.eventHandled = null;
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user