diff --git a/src/3p-filters.html b/src/3p-filters.html index 1416251f0..21b3c6e71 100644 --- a/src/3p-filters.html +++ b/src/3p-filters.html @@ -13,32 +13,30 @@
!
will be ignored. Invalid URLs will be silently ignored.",
+ "message":"One URL per line. Invalid URLs will be silently ignored.",
"description":"Short information about how to use the textarea to import external filter lists by URL"
},
"3pExternalListObsolete":{
diff --git a/src/css/3p-filters.css b/src/css/3p-filters.css
index b03827b74..a148c55ee 100644
--- a/src/css/3p-filters.css
+++ b/src/css/3p-filters.css
@@ -3,8 +3,16 @@
100% { transform: rotate(360deg); -webkit-transform: rotate(360deg); }
}
ul {
- padding: 0;
list-style-type: none;
+ padding-left: 1em;
+ padding-right: 0;
+ }
+body[dir="rtl"] ul {
+ padding-left: 0;
+ padding-right: 1em;
+ }
+ul.root {
+ padding: 0;
}
#options li {
margin-bottom: 0.5em;
@@ -13,7 +21,7 @@ ul {
cursor: pointer;
}
#listsOfBlockedHostsPrompt:before {
- color: #aaa;
+ color: #888;
content: '\2212 ';
}
body.hideUnused #listsOfBlockedHostsPrompt:before {
@@ -21,53 +29,46 @@ body.hideUnused #listsOfBlockedHostsPrompt:before {
}
#lists {
margin: 0.5em 0 0 0;
- padding-left: 0.5em;
- padding-right: 0em;
- }
-body[dir="rtl"] #lists {
- padding-left: 0em;
- padding-right: 0.5em;
+ padding: 0;
}
#lists > li {
margin: 0.5em 0 0 0;
padding: 0;
list-style-type: none;
}
-#lists > .groupEntry > .geName {
+#lists > .groupEntry .geDetails {
cursor: pointer;
- font-size: 110%;
}
-#lists > .groupEntry > .geCount {
- font-size: 90%;
- }
-#lists > .groupEntry:not(:first-child) > .geName:before {
- color: #aaa;
+#lists > .groupEntry .geDetails:before {
+ color: #888;
content: '\2212 ';
}
-#lists > .groupEntry.collapsed > .geName:before {
- color: #aaa;
+#lists > .groupEntry.hideUnused .geDetails:before {
content: '+ ';
}
+#lists > .groupEntry .geName {
+ pointer-events: none;
+ }
+#lists > .groupEntry .geCount {
+ font-size: 90%;
+ pointer-events: none;
+ }
#lists > .groupEntry > ul {
margin: 0.25em 0 0 0;
- }
-#lists > .groupEntry.collapsed > ul {
- display: none;
+ padding-left: 1em;
}
li.listEntry {
- line-height: 150%;
margin: 0 auto 0 auto;
- margin-left: 2.5em;
- margin-right: 0;
- text-indent: -2em;
+ padding: 0.2em 0;
+ white-space: nowrap;
}
body[dir="rtl"] li.listEntry {
- margin-left: 0em;
- margin-right: 2.5em;
+ }
+li.listEntry.unused {
+ display: none;
}
li.listEntry > * {
margin-right: 0.5em;
- text-indent: 0;
unicode-bidi: embed;
}
li.listEntry.toRemove > input[type="checkbox"] {
@@ -83,6 +84,9 @@ li.listEntry > .fa {
opacity: 0.5;
vertical-align: baseline;
}
+li.listEntry > a.towiki {
+ display: inline-block;
+ }
li.listEntry > a.fa:hover {
opacity: 1;
}
@@ -164,17 +168,18 @@ body.updating li.listEntry.obsolete > input[type="checkbox"]:checked ~ span.upda
animation: spin 2s linear infinite;
display: inline-block;
}
-#externalListsDiv {
- margin: 1.5em auto 0 1.5em;
- }
-body[dir=rtl] #externalListsDiv {
- margin: 1.5em 1.5em 0 auto;
- }
-#externalLists {
+li.listEntry.toImport > input[type="checkbox"] ~ textarea {
+ border: 1px solid #ccc;
box-sizing: border-box;
- height: 8em;
- margin-top: 0.25em;
+ display: block;
+ font-size: smaller;
+ height: 6em;
+ margin-left: 2em;
+ resize: vertical;
+ visibility: hidden;
white-space: pre;
- width: 100%;
- word-wrap: normal;
+ width: calc(100% - 4em);
+ }
+li.listEntry.toImport > input[type="checkbox"]:checked ~ textarea {
+ visibility: visible;
}
diff --git a/src/js/3p-filters.js b/src/js/3p-filters.js
index baf128e87..85af98f10 100644
--- a/src/js/3p-filters.js
+++ b/src/js/3p-filters.js
@@ -1,7 +1,7 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
- Copyright (C) 2014-2017 Raymond Hill
+ Copyright (C) 2014-2018 Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -32,7 +32,8 @@
var listDetails = {},
filteringSettingsHash = '',
lastUpdateTemplateString = vAPI.i18n('3pLastUpdate'),
- reValidExternalList = /[a-z-]+:\/\/\S*\/\S+/;
+ reValidExternalList = /[a-z-]+:\/\/\S*\/\S+/,
+ hideUnusedSet = new Set();
/******************************************************************************/
@@ -69,7 +70,6 @@ var renderFilterLists = function(soft) {
listEntryTemplate = uDom('#templates .listEntry'),
listStatsTemplate = vAPI.i18n('3pListsOfBlockedHostsPerListStats'),
renderElapsedTimeToString = vAPI.i18n.renderElapsedTimeToString,
- hideUnusedLists = document.body.classList.contains('hideUnused'),
groupNames = new Map();
// Assemble a pretty list name if possible
@@ -80,16 +80,17 @@ var renderFilterLists = function(soft) {
return listTitle;
};
- var liFromListEntry = function(listKey, li) {
+ var liFromListEntry = function(listKey, li, hideUnused) {
var entry = listDetails.available[listKey],
elem;
if ( !li ) {
li = listEntryTemplate.clone().nodeAt(0);
}
+ var on = entry.off !== true;
if ( li.getAttribute('data-listkey') !== listKey ) {
li.setAttribute('data-listkey', listKey);
elem = li.querySelector('input[type="checkbox"]');
- elem.checked = entry.off !== true;
+ elem.checked = on;
elem = li.querySelector('a:nth-of-type(1)');
elem.setAttribute('href', 'asset-viewer.html?url=' + encodeURI(listKey));
elem.setAttribute('type', 'text/html');
@@ -115,18 +116,17 @@ var renderFilterLists = function(soft) {
} else {
li.classList.remove('mustread');
}
+ li.classList.toggle('unused', hideUnused && !on);
}
// https://github.com/gorhill/uBlock/issues/1429
if ( !soft ) {
- elem = li.querySelector('input[type="checkbox"]');
- elem.checked = entry.off !== true;
+ li.querySelector('input[type="checkbox"]').checked = on;
}
- li.style.setProperty('display', hideUnusedLists && entry.off === true ? 'none' : '');
elem = li.querySelector('span.counts');
var text = '';
if ( !isNaN(+entry.entryUsedCount) && !isNaN(+entry.entryCount) ) {
text = listStatsTemplate
- .replace('{{used}}', renderNumber(entry.off ? 0 : entry.entryUsedCount))
+ .replace('{{used}}', renderNumber(on ? entry.entryUsedCount : 0))
.replace('{{total}}', renderNumber(entry.entryCount));
}
elem.textContent = text;
@@ -157,14 +157,18 @@ var renderFilterLists = function(soft) {
var listEntryCountFromGroup = function(listKeys) {
if ( Array.isArray(listKeys) === false ) { return ''; }
- var count = 0;
+ var count = 0,
+ total = 0;
var i = listKeys.length;
while ( i-- ) {
if ( listDetails.available[listKeys[i]].off !== true ) {
count += 1;
}
+ total += 1;
}
- return count === 0 ? '' : '(' + count.toLocaleString() + ')';
+ return total !== 0 ?
+ '(' + count.toLocaleString() + '/' + total.toLocaleString() + ')' :
+ '';
};
var liFromListGroup = function(groupKey, listKeys) {
@@ -189,13 +193,19 @@ var renderFilterLists = function(soft) {
if ( liGroup.querySelector('.geName:empty') === null ) {
liGroup.querySelector('.geCount').textContent = listEntryCountFromGroup(listKeys);
}
+ var hideUnused = mustHideUnusedLists(groupKey);
+ liGroup.classList.toggle('hideUnused', hideUnused);
var ulGroup = liGroup.querySelector('.listEntries');
if ( !listKeys ) { return liGroup; }
listKeys.sort(function(a, b) {
return (listDetails.available[a].title || '').localeCompare(listDetails.available[b].title || '');
});
for ( var i = 0; i < listKeys.length; i++ ) {
- var liEntry = liFromListEntry(listKeys[i], ulGroup.children[i]);
+ var liEntry = liFromListEntry(
+ listKeys[i],
+ ulGroup.children[i],
+ hideUnused
+ );
if ( liEntry.parentElement === null ) {
ulGroup.appendChild(liEntry);
}
@@ -226,7 +236,10 @@ var renderFilterLists = function(soft) {
// Incremental rendering: this will allow us to easily discard unused
// DOM list entries.
- uDom('#lists .listEntries .listEntry').addClass('discard');
+ uDom('#lists .listEntries .listEntry[data-listkey]').addClass('discard');
+
+ // Remove import widget while we recreate list of lists.
+ var importWidget = uDom('.listEntry.toImport').detach();
// Visually split the filter lists in purpose-based groups
var ulLists = document.querySelector('#lists'),
@@ -242,6 +255,7 @@ var renderFilterLists = function(soft) {
'regions',
'custom'
];
+ document.body.classList.toggle('hideUnused', mustHideUnusedLists('*'));
for ( i = 0; i < groupKeys.length; i++ ) {
groupKey = groupKeys[i];
liGroup = liFromListGroup(groupKey, groups[groupKey]);
@@ -263,17 +277,28 @@ var renderFilterLists = function(soft) {
}
uDom('#lists .listEntries .listEntry.discard').remove();
- uDom('#autoUpdate').prop('checked', listDetails.autoUpdate === true);
- uDom('#listsOfBlockedHostsPrompt').text(
+
+ // Re-insert import widget.
+ uDom('[data-groupkey="custom"] .listEntries').append(importWidget);
+
+ uDom.nodeFromId('autoUpdate').checked = listDetails.autoUpdate === true;
+ uDom.nodeFromId('listsOfBlockedHostsPrompt').textContent =
vAPI.i18n('3pListsOfBlockedHostsPrompt')
- .replace('{{netFilterCount}}', renderNumber(details.netFilterCount))
- .replace('{{cosmeticFilterCount}}', renderNumber(details.cosmeticFilterCount))
- );
+ .replace(
+ '{{netFilterCount}}',
+ renderNumber(details.netFilterCount)
+ )
+ .replace(
+ '{{cosmeticFilterCount}}',
+ renderNumber(details.cosmeticFilterCount)
+ );
+ uDom.nodeFromId('parseCosmeticFilters').checked =
+ listDetails.parseCosmeticFilters === true;
+ uDom.nodeFromId('ignoreGenericCosmeticFilters').checked =
+ listDetails.ignoreGenericCosmeticFilters === true;
// Compute a hash of the settings so that we can keep track of changes
// affecting the loading of filter lists.
- uDom('#parseCosmeticFilters').prop('checked', listDetails.parseCosmeticFilters === true);
- uDom('#ignoreGenericCosmeticFilters').prop('checked', listDetails.ignoreGenericCosmeticFilters === true);
if ( !soft ) {
filteringSettingsHash = hashFromCurrentFromSettings();
}
@@ -286,12 +311,18 @@ var renderFilterLists = function(soft) {
/******************************************************************************/
var renderWidgets = function() {
- uDom('#buttonApply').toggleClass('disabled', filteringSettingsHash === hashFromCurrentFromSettings());
+ uDom('#buttonApply').toggleClass(
+ 'disabled',
+ filteringSettingsHash === hashFromCurrentFromSettings()
+ );
uDom('#buttonPurgeAll').toggleClass(
'disabled',
document.querySelector('#lists .listEntry.cached:not(.obsolete)') === null
);
- uDom('#buttonUpdate').toggleClass('disabled', document.querySelector('body:not(.updating) #lists .listEntry.obsolete > input[type="checkbox"]:checked') === null);
+ uDom('#buttonUpdate').toggleClass(
+ 'disabled',
+ document.querySelector('body:not(.updating) #lists .listEntry.obsolete > input[type="checkbox"]:checked') === null
+ );
};
/******************************************************************************/
@@ -323,8 +354,8 @@ var updateAssetStatus = function(details) {
var hashFromCurrentFromSettings = function() {
var hash = [
- document.getElementById('parseCosmeticFilters').checked,
- document.getElementById('ignoreGenericCosmeticFilters').checked
+ uDom.nodeFromId('parseCosmeticFilters').checked,
+ uDom.nodeFromId('ignoreGenericCosmeticFilters').checked
];
var listHash = [],
listEntries = document.querySelectorAll('#lists .listEntry[data-listkey]:not(.toRemove)'),
@@ -338,7 +369,8 @@ var hashFromCurrentFromSettings = function() {
}
hash.push(
listHash.sort().join(),
- reValidExternalList.test(document.getElementById('externalLists').value),
+ uDom.nodeFromId('importLists').checked &&
+ reValidExternalList.test(uDom.nodeFromId('externalLists').value),
document.querySelector('#lists .listEntry.toRemove') !== null
);
return hash.join();
@@ -424,6 +456,7 @@ var selectFilterLists = function(callback) {
var externalListsElem = document.getElementById('externalLists'),
toImport = externalListsElem.value.trim();
externalListsElem.value = '';
+ uDom.nodeFromId('importLists').checked = false;
messaging.send(
'dashboard',
@@ -490,36 +523,89 @@ var autoUpdateCheckboxChanged = function() {
/******************************************************************************/
-var toggleUnusedLists = function() {
- document.body.classList.toggle('hideUnused');
- var hide = document.body.classList.contains('hideUnused');
- uDom('#lists li.listEntry > input[type="checkbox"]:not(:checked)')
- .ancestors('li.listEntry[data-listkey]')
- .css('display', hide ? 'none' : '');
- vAPI.localStorage.setItem('hideUnusedFilterLists', hide ? '1' : '0');
+// Collapsing of unused lists.
+
+var mustHideUnusedLists = function(which) {
+ var hideAll = hideUnusedSet.has('*');
+ if ( which === '*' ) { return hideAll; }
+ return hideUnusedSet.has(which) !== hideAll;
};
-/******************************************************************************/
-
-var groupEntryClickHandler = function() {
- var li = uDom(this).ancestors('.groupEntry');
- li.toggleClass('collapsed');
- var key = 'collapseGroup' + li.nthOfType();
- if ( li.hasClass('collapsed') ) {
- vAPI.localStorage.setItem(key, 'y');
+var toggleHideUnusedLists = function(which) {
+ var groupSelector,
+ doesHideAll = hideUnusedSet.has('*'),
+ mustHide;
+ if ( which === '*' ) {
+ mustHide = doesHideAll === false;
+ groupSelector = '';
+ hideUnusedSet.clear();
+ if ( mustHide ) {
+ hideUnusedSet.add(which);
+ }
+ document.body.classList.toggle('hideUnused', mustHide);
+ uDom('.groupEntry[data-groupkey]').toggleClass('hideUnused', mustHide);
} else {
- vAPI.localStorage.removeItem(key);
+ var doesHide = hideUnusedSet.has(which);
+ if ( doesHide ) {
+ hideUnusedSet.delete(which);
+ } else {
+ hideUnusedSet.add(which);
+ }
+ mustHide = doesHide === doesHideAll;
+ groupSelector = '.groupEntry[data-groupkey="' + which + '"] ';
+ uDom(groupSelector).toggleClass('hideUnused', mustHide);
}
+ uDom(groupSelector + '.listEntry > input[type="checkbox"]:not(:checked)')
+ .ancestors('.listEntry[data-listkey]')
+ .toggleClass('unused', mustHide);
+ vAPI.localStorage.setItem(
+ 'hideUnusedFilterLists',
+ JSON.stringify(Array.from(hideUnusedSet))
+ );
};
+var revealHiddenUsedLists = function() {
+ uDom('#lists .listEntry.unused > input[type="checkbox"]:checked')
+ .ancestors('.listEntry[data-listkey]')
+ .removeClass('unused');
+};
+
+uDom('#listsOfBlockedHostsPrompt').on('click', function() {
+ toggleHideUnusedLists('*');
+});
+
+uDom('#lists').on('click', '.groupEntry[data-groupkey] > .geDetails', function(ev) {
+ toggleHideUnusedLists(
+ uDom(ev.target)
+ .ancestors('.groupEntry[data-groupkey]')
+ .attr('data-groupkey')
+ );
+});
+
+(function() {
+ var aa;
+ try {
+ var json = vAPI.localStorage.getItem('hideUnusedFilterLists');
+ if ( json !== null ) {
+ aa = JSON.parse(json);
+ }
+ } catch (ex) {
+ }
+ if ( Array.isArray(aa) === false ) {
+ aa = [ '*' ];
+ }
+ hideUnusedSet = new Set(aa);
+})();
+
/******************************************************************************/
+// Cloud-related.
+
var toCloudData = function() {
var bin = {
parseCosmeticFilters: uDom.nodeFromId('parseCosmeticFilters').checked,
ignoreGenericCosmeticFilters: uDom.nodeFromId('ignoreGenericCosmeticFilters').checked,
- selectedLists: [],
- externalLists: listDetails.externalLists
+ selectedLists: []
};
var liEntries = uDom('#lists .listEntry'), liEntry;
@@ -537,7 +623,7 @@ var toCloudData = function() {
var fromCloudData = function(data, append) {
if ( typeof data !== 'object' || data === null ) { return; }
- var elem, checked, i, n;
+ var elem, checked;
elem = uDom.nodeFromId('parseCosmeticFilters');
checked = data.parseCosmeticFilters === true || append && elem.checked;
@@ -549,19 +635,35 @@ var fromCloudData = function(data, append) {
var selectedSet = new Set(data.selectedLists),
listEntries = uDom('#lists .listEntry'),
- listEntry, listKey, input;
- for ( i = 0, n = listEntries.length; i < n; i++ ) {
+ listEntry, listKey;
+ for ( var i = 0, n = listEntries.length; i < n; i++ ) {
listEntry = listEntries.at(i);
listKey = listEntry.attr('data-listkey');
- input = listEntry.descendants('input').first();
+ var hasListKey = selectedSet.has(listKey);
+ selectedSet.delete(listKey);
+ var input = listEntry.descendants('input').first();
if ( append && input.prop('checked') ) { continue; }
- input.prop('checked', selectedSet.has(listKey) );
+ input.prop('checked', hasListKey);
}
- elem = uDom.nodeFromId('externalLists');
- if ( !append ) { elem.value = ''; }
- elem.value += data.externalLists || '';
+ // If there are URL-like list keys left in the selected set, import them.
+ for ( listKey of selectedSet ) {
+ if ( reValidExternalList.test(listKey) === false ) {
+ selectedSet.delete(listKey);
+ }
+ }
+ if ( selectedSet.size !== 0 ) {
+ elem = uDom.nodeFromId('externalLists');
+ if ( append ) {
+ if ( elem.value.trim() !== '' ) { elem.value += '\n'; }
+ } else {
+ elem.value = '';
+ }
+ elem.value += Array.from(selectedSet).join('\n');
+ uDom.nodeFromId('importLists').checked = true;
+ }
+ revealHiddenUsedLists();
renderWidgets();
};
@@ -570,24 +672,19 @@ self.cloud.onPull = fromCloudData;
/******************************************************************************/
-document.body.classList.toggle(
- 'hideUnused',
- vAPI.localStorage.getItem('hideUnusedFilterLists') === '1'
-);
-
uDom('#autoUpdate').on('change', autoUpdateCheckboxChanged);
uDom('#parseCosmeticFilters').on('change', onFilteringSettingsChanged);
uDom('#ignoreGenericCosmeticFilters').on('change', onFilteringSettingsChanged);
uDom('#buttonApply').on('click', buttonApplyHandler);
uDom('#buttonUpdate').on('click', buttonUpdateHandler);
uDom('#buttonPurgeAll').on('click', buttonPurgeAllHandler);
-uDom('#listsOfBlockedHostsPrompt').on('click', toggleUnusedLists);
-uDom('#lists').on('click', '.groupEntry > span', groupEntryClickHandler);
uDom('#lists').on('change', '.listEntry > input', onFilteringSettingsChanged);
uDom('#lists').on('click', '.listEntry > a.remove', onRemoveExternalList);
uDom('#lists').on('click', 'span.cache', onPurgeClicked);
uDom('#externalLists').on('input', onFilteringSettingsChanged);
+/******************************************************************************/
+
renderFilterLists();
/******************************************************************************/