diff options
Diffstat (limited to 'data/extensions/uBlock0@raymondhill.net/js/3p-filters.js')
-rw-r--r-- | data/extensions/uBlock0@raymondhill.net/js/3p-filters.js | 902 |
1 files changed, 902 insertions, 0 deletions
diff --git a/data/extensions/uBlock0@raymondhill.net/js/3p-filters.js b/data/extensions/uBlock0@raymondhill.net/js/3p-filters.js new file mode 100644 index 0000000..fed3154 --- /dev/null +++ b/data/extensions/uBlock0@raymondhill.net/js/3p-filters.js @@ -0,0 +1,902 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2014-present 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 + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock +*/ + +import { dom, qs$, qsa$ } from './dom.js'; +import { i18n, i18n$ } from './i18n.js'; +import { onBroadcast } from './broadcast.js'; + +/******************************************************************************/ + +const lastUpdateTemplateString = i18n$('3pLastUpdate'); +const obsoleteTemplateString = i18n$('3pExternalListObsolete'); +const reValidExternalList = /^[a-z-]+:\/\/(?:\S+\/\S*|\/\S+)/m; +const recentlyUpdated = 1 * 60 * 60 * 1000; // 1 hour + +let listsetDetails = {}; + +/******************************************************************************/ + +onBroadcast(msg => { + switch ( msg.what ) { + case 'assetUpdated': + updateAssetStatus(msg); + break; + case 'assetsUpdated': + dom.cl.remove(dom.body, 'updating'); + renderWidgets(); + break; + case 'staticFilteringDataChanged': + renderFilterLists(); + break; + default: + break; + } +}); + +/******************************************************************************/ + +const renderNumber = value => { + return value.toLocaleString(); +}; + +const listStatsTemplate = i18n$('3pListsOfBlockedHostsPerListStats'); + +const renderLeafStats = (used, total) => { + if ( isNaN(used) || isNaN(total) ) { return ''; } + return listStatsTemplate + .replace('{{used}}', renderNumber(used)) + .replace('{{total}}', renderNumber(total)); +}; + +const renderNodeStats = (used, total) => { + if ( isNaN(used) || isNaN(total) ) { return ''; } + return `${used.toLocaleString()}/${total.toLocaleString()}`; +}; + +const i18nGroupName = name => { + const groupname = i18n$('3pGroup' + name.charAt(0).toUpperCase() + name.slice(1)); + if ( groupname !== '' ) { return groupname; } + return `${name.charAt(0).toLocaleUpperCase}${name.slice(1)}`; +}; + +/******************************************************************************/ + +const renderFilterLists = ( ) => { + // Assemble a pretty list name if possible + const listNameFromListKey = listkey => { + const list = listsetDetails.current[listkey] || listsetDetails.available[listkey]; + const title = list && list.title || ''; + if ( title !== '' ) { return title; } + return listkey; + }; + + const initializeListEntry = (listDetails, listEntry) => { + const listkey = listEntry.dataset.key; + const groupkey = listDetails.group2 || listDetails.group; + const listEntryPrevious = + qs$(`[data-key="${groupkey}"] [data-key="${listkey}"]`); + if ( listEntryPrevious !== null ) { + if ( dom.cl.has(listEntryPrevious, 'checked') ) { + dom.cl.add(listEntry, 'checked'); + } + if ( dom.cl.has(listEntryPrevious, 'stickied') ) { + dom.cl.add(listEntry, 'stickied'); + } + if ( dom.cl.has(listEntryPrevious, 'toRemove') ) { + dom.cl.add(listEntry, 'toRemove'); + } + if ( dom.cl.has(listEntryPrevious, 'searchMatch') ) { + dom.cl.add(listEntry, 'searchMatch'); + } + } else { + dom.cl.toggle(listEntry, 'checked', listDetails.off !== true); + } + const on = dom.cl.has(listEntry, 'checked'); + dom.prop(qs$(listEntry, ':scope > .detailbar input'), 'checked', on); + let elem = qs$(listEntry, ':scope > .detailbar a.content'); + dom.attr(elem, 'href', 'asset-viewer.html?url=' + encodeURIComponent(listkey)); + dom.attr(elem, 'type', 'text/html'); + dom.cl.remove(listEntry, 'toRemove'); + if ( listDetails.supportName ) { + elem = qs$(listEntry, ':scope > .detailbar a.support'); + dom.attr(elem, 'href', listDetails.supportURL || '#'); + dom.attr(elem, 'title', listDetails.supportName); + } + if ( listDetails.external ) { + dom.cl.add(listEntry, 'external'); + } else { + dom.cl.remove(listEntry, 'external'); + } + if ( listDetails.instructionURL ) { + elem = qs$(listEntry, ':scope > .detailbar a.mustread'); + dom.attr(elem, 'href', listDetails.instructionURL || '#'); + } + dom.cl.toggle(listEntry, 'isDefault', + listDetails.isDefault === true || + listDetails.isImportant === true || + listkey === 'user-filters' + ); + elem = qs$(listEntry, '.leafstats'); + dom.text(elem, renderLeafStats(on ? listDetails.entryUsedCount : 0, listDetails.entryCount)); + // https://github.com/chrisaljoudi/uBlock/issues/104 + const asset = listsetDetails.cache[listkey] || {}; + const remoteURL = asset.remoteURL; + dom.cl.toggle(listEntry, 'unsecure', + typeof remoteURL === 'string' && remoteURL.lastIndexOf('http:', 0) === 0 + ); + dom.cl.toggle(listEntry, 'failed', asset.error !== undefined); + dom.cl.toggle(listEntry, 'obsolete', asset.obsolete === true); + const lastUpdateString = lastUpdateTemplateString.replace('{{ago}}', + i18n.renderElapsedTimeToString(asset.writeTime || 0) + ); + if ( asset.obsolete === true ) { + let title = obsoleteTemplateString; + if ( asset.cached && asset.writeTime !== 0 ) { + title += '\n' + lastUpdateString; + } + dom.attr(qs$(listEntry, ':scope > .detailbar .status.obsolete'), 'title', title); + } + if ( asset.cached === true ) { + dom.cl.add(listEntry, 'cached'); + dom.attr(qs$(listEntry, ':scope > .detailbar .status.cache'), 'title', lastUpdateString); + const timeSinceLastUpdate = Date.now() - asset.writeTime; + dom.cl.toggle(listEntry, 'recent', timeSinceLastUpdate < recentlyUpdated); + } else { + dom.cl.remove(listEntry, 'cached'); + } + }; + + const createListEntry = (listDetails, depth) => { + if ( listDetails.lists === undefined ) { + return dom.clone('#templates .listEntry[data-role="leaf"]'); + } + if ( depth !== 0 ) { + return dom.clone('#templates .listEntry[data-role="node"]'); + } + return dom.clone('#templates .listEntry[data-role="node"][data-parent="root"]'); + }; + + const createListEntries = (parentkey, listTree, depth = 0) => { + const listEntries = dom.clone('#templates .listEntries'); + const treeEntries = Object.entries(listTree); + if ( depth !== 0 ) { + const reEmojis = /\p{Emoji}+/gu; + treeEntries.sort((a ,b) => { + const ap = a[1].preferred === true; + const bp = b[1].preferred === true; + if ( ap !== bp ) { return ap ? -1 : 1; } + const as = (a[1].title || a[0]).replace(reEmojis, ''); + const bs = (b[1].title || b[0]).replace(reEmojis, ''); + return as.localeCompare(bs); + }); + } + for ( const [ listkey, listDetails ] of treeEntries ) { + const listEntry = createListEntry(listDetails, depth); + if ( dom.cl.has(dom.root, 'mobile') ) { + const leafStats = qs$(listEntry, '.leafstats'); + if ( leafStats ) { + listEntry.append(leafStats); + } + } + listEntry.dataset.key = listkey; + listEntry.dataset.parent = parentkey; + qs$(listEntry, ':scope > .detailbar .listname').append( + i18n.patchUnicodeFlags(listDetails.title) + ); + if ( listDetails.lists !== undefined ) { + listEntry.append(createListEntries(listEntry.dataset.key, listDetails.lists, depth+1)); + dom.cl.toggle(listEntry, 'expanded', listIsExpanded(listkey)); + updateListNode(listEntry); + } else { + initializeListEntry(listDetails, listEntry); + } + listEntries.append(listEntry); + } + return listEntries; + }; + + const onListsReceived = response => { + // Store in global variable + listsetDetails = response; + hashFromListsetDetails(); + + // Build list tree + const listTree = {}; + const groupKeys = [ + 'user', + 'default', + 'ads', + 'privacy', + 'malware', + 'multipurpose', + 'cookies', + 'social', + 'annoyances', + 'regions', + 'unknown', + 'custom' + ]; + for ( const key of groupKeys ) { + listTree[key] = { + title: i18nGroupName(key), + lists: {}, + }; + } + for ( const [ listkey, listDetails ] of Object.entries(response.available) ) { + let groupkey = listDetails.group2 || listDetails.group; + if ( Object.hasOwn(listTree, groupkey) === false ) { + groupkey = 'unknown'; + } + const groupDetails = listTree[groupkey]; + if ( listDetails.parent !== undefined ) { + let lists = groupDetails.lists; + for ( const parent of listDetails.parent.split('|') ) { + if ( lists[parent] === undefined ) { + lists[parent] = { title: parent, lists: {} }; + } + if ( listDetails.preferred === true ) { + lists[parent].preferred = true; + } + lists = lists[parent].lists; + } + lists[listkey] = listDetails; + } else { + listDetails.title = listNameFromListKey(listkey); + groupDetails.lists[listkey] = listDetails; + } + } + // https://github.com/uBlockOrigin/uBlock-issues/issues/3154#issuecomment-1975413427 + // Remove empty sections + for ( const groupkey of groupKeys ) { + const groupDetails = listTree[groupkey]; + if ( groupDetails === undefined ) { continue; } + if ( Object.keys(groupDetails.lists).length !== 0 ) { continue; } + delete listTree[groupkey]; + } + + const listEntries = createListEntries('root', listTree); + qs$('#lists .listEntries').replaceWith(listEntries); + + qs$('#autoUpdate').checked = listsetDetails.autoUpdate === true; + dom.text( + '#listsOfBlockedHostsPrompt', + i18n$('3pListsOfBlockedHostsPrompt') + .replace('{{netFilterCount}}', renderNumber(response.netFilterCount)) + .replace('{{cosmeticFilterCount}}', renderNumber(response.cosmeticFilterCount)) + ); + qs$('#parseCosmeticFilters').checked = + listsetDetails.parseCosmeticFilters === true; + qs$('#ignoreGenericCosmeticFilters').checked = + listsetDetails.ignoreGenericCosmeticFilters === true; + qs$('#suspendUntilListsAreLoaded').checked = + listsetDetails.suspendUntilListsAreLoaded === true; + + // https://github.com/gorhill/uBlock/issues/2394 + dom.cl.toggle(dom.body, 'updating', listsetDetails.isUpdating); + + renderWidgets(); + }; + + return vAPI.messaging.send('dashboard', { + what: 'getLists', + }).then(response => { + onListsReceived(response); + }); +}; + +/******************************************************************************/ + +const renderWidgets = ( ) => { + const updating = dom.cl.has(dom.body, 'updating'); + const hasObsolete = qs$('#lists .listEntry.checked.obsolete:not(.toRemove)') !== null; + dom.cl.toggle('#buttonApply', 'disabled', + filteringSettingsHash === hashFromCurrentFromSettings() + ); + dom.cl.toggle('#buttonUpdate', 'active', updating); + dom.cl.toggle('#buttonUpdate', 'disabled', + updating === false && hasObsolete === false + ); +}; + +/******************************************************************************/ + +const updateAssetStatus = details => { + const listEntry = qs$(`#lists .listEntry[data-key="${details.key}"]`); + if ( listEntry === null ) { return; } + dom.cl.toggle(listEntry, 'failed', !!details.failed); + dom.cl.toggle(listEntry, 'obsolete', !details.cached); + dom.cl.toggle(listEntry, 'cached', !!details.cached); + if ( details.cached ) { + dom.attr(qs$(listEntry, '.status.cache'), 'title', + lastUpdateTemplateString.replace('{{ago}}', i18n.renderElapsedTimeToString(Date.now())) + ); + dom.cl.add(listEntry, 'recent'); + } + updateAncestorListNodes(listEntry, ancestor => { + updateListNode(ancestor); + }); + renderWidgets(); +}; + +/******************************************************************************* + + Compute a hash from all the settings affecting how filter lists are loaded + in memory. + +**/ + +let filteringSettingsHash = ''; + +const hashFromListsetDetails = ( ) => { + const hashParts = [ + listsetDetails.parseCosmeticFilters === true, + listsetDetails.ignoreGenericCosmeticFilters === true, + ]; + const listHashes = []; + for ( const [ listkey, listDetails ] of Object.entries(listsetDetails.available) ) { + if ( listDetails.off === true ) { continue; } + listHashes.push(listkey); + } + hashParts.push( listHashes.sort().join(), '', false); + filteringSettingsHash = hashParts.join(); +}; + +const hashFromCurrentFromSettings = ( ) => { + const hashParts = [ + qs$('#parseCosmeticFilters').checked, + qs$('#ignoreGenericCosmeticFilters').checked, + ]; + const listHashes = []; + const listEntries = qsa$('#lists .listEntry[data-key]:not(.toRemove)'); + for ( const liEntry of listEntries ) { + if ( liEntry.dataset.role !== 'leaf' ) { continue; } + if ( dom.cl.has(liEntry, 'checked') === false ) { continue; } + listHashes.push(liEntry.dataset.key); + } + const textarea = qs$('#lists .listEntry[data-role="import"].expanded textarea'); + hashParts.push( + listHashes.sort().join(), + textarea !== null && textarea.value.trim() || '', + qs$('#lists .listEntry.toRemove') !== null + ); + return hashParts.join(); +}; + +/******************************************************************************/ + +const onListsetChanged = ev => { + const input = ev.target.closest('input'); + if ( input === null ) { return; } + toggleFilterList(input, input.checked, true); +}; + +dom.on('#lists', 'change', '.listEntry > .detailbar input', onListsetChanged); + +const toggleFilterList = (elem, on, ui = false) => { + const listEntry = elem.closest('.listEntry'); + if ( listEntry === null ) { return; } + if ( listEntry.dataset.parent === 'root' ) { return; } + const searchMode = dom.cl.has('#lists', 'searchMode'); + const input = qs$(listEntry, ':scope > .detailbar input'); + if ( on === undefined ) { + on = input.checked === false; + } + input.checked = on; + dom.cl.toggle(listEntry, 'checked', on); + dom.cl.toggle(listEntry, 'stickied', ui && !on && !searchMode); + // Select/unselect descendants. Twist: if in search-mode, select only + // search-matched descendants. + const childListEntries = searchMode + ? qsa$(listEntry, '.listEntry.searchMatch') + : qsa$(listEntry, '.listEntry'); + for ( const descendantList of childListEntries ) { + dom.cl.toggle(descendantList, 'checked', on); + qs$(descendantList, ':scope > .detailbar input').checked = on; + } + updateAncestorListNodes(listEntry, ancestor => { + updateListNode(ancestor); + }); + onFilteringSettingsChanged(); +}; + +const updateListNode = listNode => { + if ( listNode === null ) { return; } + if ( listNode.dataset.role !== 'node' ) { return; } + const checkedListLeaves = qsa$(listNode, '.listEntry[data-role="leaf"].checked'); + const allListLeaves = qsa$(listNode, '.listEntry[data-role="leaf"]'); + dom.text(qs$(listNode, '.nodestats'), + renderNodeStats(checkedListLeaves.length, allListLeaves.length) + ); + dom.cl.toggle(listNode, 'searchMatch', + qs$(listNode, ':scope > .listEntries > .listEntry.searchMatch') !== null + ); + if ( listNode.dataset.parent === 'root' ) { return; } + let usedFilterCount = 0; + let totalFilterCount = 0; + let isCached = false; + let isObsolete = false; + let latestWriteTime = 0; + let oldestWriteTime = Number.MAX_SAFE_INTEGER; + for ( const listLeaf of checkedListLeaves ) { + const listkey = listLeaf.dataset.key; + const listDetails = listsetDetails.available[listkey]; + usedFilterCount += listDetails.off ? 0 : listDetails.entryUsedCount || 0; + totalFilterCount += listDetails.entryCount || 0; + const assetCache = listsetDetails.cache[listkey] || {}; + isCached = isCached || dom.cl.has(listLeaf, 'cached'); + isObsolete = isObsolete || dom.cl.has(listLeaf, 'obsolete'); + latestWriteTime = Math.max(latestWriteTime, assetCache.writeTime || 0); + oldestWriteTime = Math.min(oldestWriteTime, assetCache.writeTime || Number.MAX_SAFE_INTEGER); + } + dom.cl.toggle(listNode, 'checked', checkedListLeaves.length !== 0); + dom.cl.toggle(qs$(listNode, ':scope > .detailbar .checkbox'), + 'partial', + checkedListLeaves.length !== allListLeaves.length + ); + dom.prop(qs$(listNode, ':scope > .detailbar input'), + 'checked', + checkedListLeaves.length !== 0 + ); + dom.text(qs$(listNode, '.leafstats'), + renderLeafStats(usedFilterCount, totalFilterCount) + ); + const firstLeaf = qs$(listNode, '.listEntry[data-role="leaf"]'); + if ( firstLeaf !== null ) { + dom.attr(qs$(listNode, ':scope > .detailbar a.support'), 'href', + dom.attr(qs$(firstLeaf, ':scope > .detailbar a.support'), 'href') || '#' + ); + dom.attr(qs$(listNode, ':scope > .detailbar a.mustread'), 'href', + dom.attr(qs$(firstLeaf, ':scope > .detailbar a.mustread'), 'href') || '#' + ); + } + dom.cl.toggle(listNode, 'cached', isCached); + dom.cl.toggle(listNode, 'obsolete', isObsolete); + if ( isCached ) { + dom.attr(qs$(listNode, ':scope > .detailbar .cache'), 'title', + lastUpdateTemplateString.replace('{{ago}}', i18n.renderElapsedTimeToString(latestWriteTime)) + ); + dom.cl.toggle(listNode, 'recent', (Date.now() - oldestWriteTime) < recentlyUpdated); + } + if ( qs$(listNode, '.listEntry.isDefault') !== null ) { + dom.cl.add(listNode, 'isDefault'); + } + if ( qs$(listNode, '.listEntry.stickied') !== null ) { + dom.cl.add(listNode, 'stickied'); + } +}; + +const updateAncestorListNodes = (listEntry, fn) => { + while ( listEntry !== null ) { + fn(listEntry); + listEntry = qs$(`.listEntry[data-key="${listEntry.dataset.parent}"]`); + } +}; + +/******************************************************************************/ + +const onFilteringSettingsChanged = ( ) => { + renderWidgets(); +}; + +dom.on('#parseCosmeticFilters', 'change', onFilteringSettingsChanged); +dom.on('#ignoreGenericCosmeticFilters', 'change', onFilteringSettingsChanged); +dom.on('#lists', 'input', '[data-role="import"] textarea', onFilteringSettingsChanged); + +/******************************************************************************/ + +const onRemoveExternalList = ev => { + const listEntry = ev.target.closest('[data-key]'); + if ( listEntry === null ) { return; } + dom.cl.toggle(listEntry, 'toRemove'); + renderWidgets(); +}; + +dom.on('#lists', 'click', '.listEntry .remove', onRemoveExternalList); + +/******************************************************************************/ + +const onPurgeClicked = ev => { + const liEntry = ev.target.closest('[data-key]'); + const listkey = liEntry.dataset.key || ''; + if ( listkey === '' ) { return; } + + const assetKeys = [ listkey ]; + for ( const listLeaf of qsa$(liEntry, '[data-role="leaf"]') ) { + assetKeys.push(listLeaf.dataset.key); + dom.cl.add(listLeaf, 'obsolete'); + dom.cl.remove(listLeaf, 'cached'); + } + + vAPI.messaging.send('dashboard', { + what: 'listsUpdateNow', + assetKeys, + preferOrigin: ev.shiftKey, + }); + + // If the cached version is purged, the installed version must be assumed + // to be obsolete. + // https://github.com/gorhill/uBlock/issues/1733 + // An external filter list must not be marked as obsolete, they will + // always be fetched anyways if there is no cached copy. + dom.cl.add(dom.body, 'updating'); + dom.cl.add(liEntry, 'obsolete'); + + if ( qs$(liEntry, 'input[type="checkbox"]').checked ) { + renderWidgets(); + } +}; + +dom.on('#lists', 'click', 'span.cache', onPurgeClicked); + +/******************************************************************************/ + +const selectFilterLists = async ( ) => { + // External filter lists to import + // Find stock list matching entries in lists to import + const toImport = (( ) => { + const textarea = qs$('#lists .listEntry[data-role="import"].expanded textarea'); + if ( textarea === null ) { return ''; } + const lists = listsetDetails.available; + const lines = textarea.value.split(/\s+/); + const after = []; + for ( const line of lines ) { + after.push(line); + if ( /^https?:\/\//.test(line) === false ) { continue; } + for ( const [ listkey, list ] of Object.entries(lists) ) { + if ( list.content !== 'filters' ) { continue; } + if ( list.contentURL === undefined ) { continue; } + if ( list.contentURL.includes(line) === false ) { continue; } + const groupkey = list.group2 || list.group; + const listEntry = qs$(`[data-key="${groupkey}"] [data-key="${listkey}"]`); + if ( listEntry === null ) { break; } + toggleFilterList(listEntry, true); + after.pop(); + break; + } + } + dom.cl.remove(textarea.closest('.expandable'), 'expanded'); + textarea.value = ''; + return after.join('\n'); + })(); + + // Cosmetic filtering switch + let checked = qs$('#parseCosmeticFilters').checked; + vAPI.messaging.send('dashboard', { + what: 'userSettings', + name: 'parseAllABPHideFilters', + value: checked, + }); + listsetDetails.parseCosmeticFilters = checked; + + checked = qs$('#ignoreGenericCosmeticFilters').checked; + vAPI.messaging.send('dashboard', { + what: 'userSettings', + name: 'ignoreGenericCosmeticFilters', + value: checked, + }); + listsetDetails.ignoreGenericCosmeticFilters = checked; + + // Filter lists to remove/select + const toSelect = []; + const toRemove = []; + for ( const liEntry of qsa$('#lists .listEntry[data-role="leaf"]') ) { + const listkey = liEntry.dataset.key; + if ( Object.hasOwn(listsetDetails.available, listkey) === false ) { + continue; + } + const listDetails = listsetDetails.available[listkey]; + if ( dom.cl.has(liEntry, 'toRemove') ) { + toRemove.push(listkey); + listDetails.off = true; + continue; + } + if ( dom.cl.has(liEntry, 'checked') ) { + toSelect.push(listkey); + listDetails.off = false; + } else { + listDetails.off = true; + } + } + + hashFromListsetDetails(); + + await vAPI.messaging.send('dashboard', { + what: 'applyFilterListSelection', + toSelect, + toImport, + toRemove, + }); +}; + +/******************************************************************************/ + +const buttonApplyHandler = async ( ) => { + await selectFilterLists(); + dom.cl.add(dom.body, 'working'); + dom.cl.remove('#lists .listEntry.stickied', 'stickied'); + renderWidgets(); + await vAPI.messaging.send('dashboard', { what: 'reloadAllFilters' }); + dom.cl.remove(dom.body, 'working'); +}; + +dom.on('#buttonApply', 'click', ( ) => { buttonApplyHandler(); }); + +/******************************************************************************/ + +const buttonUpdateHandler = async ( ) => { + dom.cl.remove('#lists .listEntry.stickied', 'stickied'); + await selectFilterLists(); + dom.cl.add(dom.body, 'updating'); + renderWidgets(); + vAPI.messaging.send('dashboard', { what: 'updateNow' }); +}; + +dom.on('#buttonUpdate', 'click', ( ) => { buttonUpdateHandler(); }); + +/******************************************************************************/ + +const userSettingCheckboxChanged = ( ) => { + const target = event.target; + vAPI.messaging.send('dashboard', { + what: 'userSettings', + name: target.id, + value: target.checked, + }); + listsetDetails[target.id] = target.checked; +}; + +dom.on('#autoUpdate', 'change', userSettingCheckboxChanged); +dom.on('#suspendUntilListsAreLoaded', 'change', userSettingCheckboxChanged); + +/******************************************************************************/ + +const searchFilterLists = ( ) => { + const pattern = dom.prop('.searchfield input', 'value') || ''; + dom.cl.toggle('#lists', 'searchMode', pattern !== ''); + if ( pattern === '' ) { return; } + const reflectSearchMatches = listEntry => { + if ( listEntry.dataset.role !== 'node' ) { return; } + dom.cl.toggle(listEntry, 'searchMatch', + qs$(listEntry, ':scope > .listEntries > .listEntry.searchMatch') !== null + ); + }; + const toI18n = tags => { + if ( tags === '' ) { return ''; } + return tags.toLowerCase().split(/\s+/).reduce((a, v) => { + let s = i18n$(v); + if ( s === '' ) { + s = i18nGroupName(v); + if ( s === '' ) { return a; } + } + return `${a} ${s}`.trim(); + }, ''); + }; + const re = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'); + for ( const listEntry of qsa$('#lists [data-role="leaf"]') ) { + const listkey = listEntry.dataset.key; + const listDetails = listsetDetails.available[listkey]; + if ( listDetails === undefined ) { continue; } + let haystack = perListHaystack.get(listDetails); + if ( haystack === undefined ) { + const groupkey = listDetails.group2 || listDetails.group || ''; + haystack = [ + listDetails.title, + groupkey, + i18nGroupName(groupkey), + listDetails.tags || '', + toI18n(listDetails.tags || ''), + ].join(' ').trim(); + perListHaystack.set(listDetails, haystack); + } + dom.cl.toggle(listEntry, 'searchMatch', re.test(haystack)); + updateAncestorListNodes(listEntry, reflectSearchMatches); + } +}; + +const perListHaystack = new WeakMap(); + +dom.on('.searchfield input', 'input', searchFilterLists); + +/******************************************************************************/ + +const expandedListSet = new Set([ + 'cookies', + 'social', +]); + +const listIsExpanded = which => { + return expandedListSet.has(which); +}; + +const applyListExpansion = listkeys => { + if ( listkeys === undefined ) { + listkeys = Array.from(expandedListSet); + } + expandedListSet.clear(); + dom.cl.remove('#lists [data-role="node"]', 'expanded'); + listkeys.forEach(which => { + expandedListSet.add(which); + dom.cl.add(`#lists [data-key="${which}"]`, 'expanded'); + }); +}; + +const toggleListExpansion = which => { + const isExpanded = expandedListSet.has(which); + if ( which === '*' ) { + if ( isExpanded ) { + expandedListSet.clear(); + dom.cl.remove('#lists .expandable', 'expanded'); + dom.cl.remove('#lists .stickied', 'stickied'); + } else { + expandedListSet.clear(); + expandedListSet.add('*'); + dom.cl.add('#lists .rootstats', 'expanded'); + for ( const expandable of qsa$('#lists > .listEntries .expandable') ) { + const listkey = expandable.dataset.key || ''; + if ( listkey === '' ) { continue; } + expandedListSet.add(listkey); + dom.cl.add(expandable, 'expanded'); + } + } + } else { + if ( isExpanded ) { + expandedListSet.delete(which); + const listNode = qs$(`#lists > .listEntries [data-key="${which}"]`); + dom.cl.remove(listNode, 'expanded'); + if ( listNode.dataset.parent === 'root' ) { + dom.cl.remove(qsa$(listNode, '.stickied'), 'stickied'); + } + } else { + expandedListSet.add(which); + dom.cl.add(`#lists > .listEntries [data-key="${which}"]`, 'expanded'); + } + } + vAPI.localStorage.setItem('expandedListSet', Array.from(expandedListSet)); + vAPI.localStorage.removeItem('hideUnusedFilterLists'); +}; + +dom.on('#listsOfBlockedHostsPrompt', 'click', ( ) => { + toggleListExpansion('*'); +}); + +dom.on('#lists', 'click', '.listExpander', ev => { + const expandable = ev.target.closest('.expandable'); + if ( expandable === null ) { return; } + const which = expandable.dataset.key; + if ( which !== undefined ) { + toggleListExpansion(which); + } else { + dom.cl.toggle(expandable, 'expanded'); + if ( expandable.dataset.role === 'import' ) { + onFilteringSettingsChanged(); + } + } + ev.preventDefault(); +}); + +dom.on('#lists', 'click', '[data-parent="root"] > .detailbar .listname', ev => { + const listEntry = ev.target.closest('.listEntry'); + if ( listEntry === null ) { return; } + const listkey = listEntry.dataset.key; + if ( listkey === undefined ) { return; } + toggleListExpansion(listkey); + ev.preventDefault(); +}); + +dom.on('#lists', 'click', '[data-role="import"] > .detailbar .listname', ev => { + const expandable = ev.target.closest('.listEntry'); + if ( expandable === null ) { return; } + dom.cl.toggle(expandable, 'expanded'); + ev.preventDefault(); +}); + +dom.on('#lists', 'click', '.listEntry > .detailbar .nodestats', ev => { + const listEntry = ev.target.closest('.listEntry'); + if ( listEntry === null ) { return; } + const listkey = listEntry.dataset.key; + if ( listkey === undefined ) { return; } + toggleListExpansion(listkey); + ev.preventDefault(); +}); + +// Initialize from saved state. +vAPI.localStorage.getItemAsync('expandedListSet').then(listkeys => { + if ( Array.isArray(listkeys) === false ) { return; } + applyListExpansion(listkeys); +}); + +/******************************************************************************/ + +// Cloud storage-related. + +self.cloud.onPush = function toCloudData() { + const bin = { + parseCosmeticFilters: qs$('#parseCosmeticFilters').checked, + ignoreGenericCosmeticFilters: qs$('#ignoreGenericCosmeticFilters').checked, + selectedLists: [] + }; + + const liEntries = qsa$('#lists .listEntry.checked[data-role="leaf"]'); + for ( const liEntry of liEntries ) { + bin.selectedLists.push(liEntry.dataset.key); + } + + return bin; +}; + +self.cloud.onPull = function fromCloudData(data, append) { + if ( typeof data !== 'object' || data === null ) { return; } + + let elem = qs$('#parseCosmeticFilters'); + let checked = data.parseCosmeticFilters === true || append && elem.checked; + elem.checked = listsetDetails.parseCosmeticFilters = checked; + + elem = qs$('#ignoreGenericCosmeticFilters'); + checked = data.ignoreGenericCosmeticFilters === true || append && elem.checked; + elem.checked = listsetDetails.ignoreGenericCosmeticFilters = checked; + + const selectedSet = new Set(data.selectedLists); + for ( const listEntry of qsa$('#lists .listEntry[data-role="leaf"]') ) { + const listkey = listEntry.dataset.key; + const mustEnable = selectedSet.has(listkey); + selectedSet.delete(listkey); + if ( mustEnable === false && append ) { continue; } + toggleFilterList(listEntry, mustEnable); + } + + // If there are URL-like list keys left in the selected set, import them. + for ( const listkey of selectedSet ) { + if ( reValidExternalList.test(listkey) ) { continue; } + selectedSet.delete(listkey); + } + if ( selectedSet.size !== 0 ) { + const textarea = qs$('#lists .listEntry[data-role="import"] textarea'); + const lines = append + ? textarea.value.split(/[\n\r]+/) + : []; + lines.push(...selectedSet); + if ( lines.length !== 0 ) { lines.push(''); } + textarea.value = lines.join('\n'); + dom.cl.toggle('#lists .listEntry[data-role="import"]', 'expanded', textarea.value !== ''); + } + + renderWidgets(); +}; + +/******************************************************************************/ + +self.wikilink = 'https://github.com/gorhill/uBlock/wiki/Dashboard:-Filter-lists'; + +self.hasUnsavedData = function() { + return hashFromCurrentFromSettings() !== filteringSettingsHash; +}; + +/******************************************************************************/ + +renderFilterLists().then(( ) => { + const buttonUpdate = qs$('#buttonUpdate'); + if ( dom.cl.has(buttonUpdate, 'active') ) { return; } + if ( dom.cl.has(buttonUpdate, 'disabled') ) { return; } + if ( listsetDetails.autoUpdate !== true ) { return; } + buttonUpdateHandler(); +}); + +/******************************************************************************/ |