From b60c06f3c4d846ddad8f0d1abe4afac56900700a Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Mon, 9 Apr 2018 09:01:39 -0400 Subject: [PATCH] various improvements to the "Filter lists" pane --- src/3p-filters.html | 43 +++---- src/_locales/en/messages.json | 12 +- src/css/3p-filters.css | 81 +++++++------ src/js/3p-filters.js | 213 +++++++++++++++++++++++++--------- 4 files changed, 230 insertions(+), 119 deletions(-) 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 @@
-
-
-
    -
  • - - -
  •   -
  • -
    -
  • -
    -
-

+ +
+
+
    +
  • + + +
  •   +
  • +
    +
  • +
    +
+
    +
    • -
    -
    -

    - - - -

    + +
    +
    diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 0d77ec6ef..3cd998b0e 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -16,7 +16,7 @@ "description":"appears as tab name in dashboard" }, "3pPageName":{ - "message":"3rd-party filters", + "message":"Filter lists", "description":"appears as tab name in dashboard" }, "1pPageName":{ @@ -351,6 +351,10 @@ "message":"Apply changes", "description":"English: Apply changes" }, + "3pGroupDefault":{ + "message":"Local", + "description":"Header for the uBlock filters section in 'Filter lists pane'" + }, "3pGroupAds":{ "message":"Ads", "description":"English: Ads" @@ -379,8 +383,12 @@ "message":"Custom", "description":"English: Custom" }, + "3pImport":{ + "message":"Import...", + "description":"The label for the checkbox used to import external filter lists" + }, "3pExternalListsHint":{ - "message":"One URL per line. Lines prefixed with ! 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(); /******************************************************************************/