summaryrefslogtreecommitdiffstats
path: root/src/js/epicker-ui.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 05:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 05:47:55 +0000
commit31d6ff6f931696850c348007241195ab3b2eddc7 (patch)
tree615cb1c57ce9f6611bad93326b9105098f379609 /src/js/epicker-ui.js
parentInitial commit. (diff)
downloadublock-origin-31d6ff6f931696850c348007241195ab3b2eddc7.tar.xz
ublock-origin-31d6ff6f931696850c348007241195ab3b2eddc7.zip
Adding upstream version 1.55.0+dfsg.upstream/1.55.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/js/epicker-ui.js')
-rw-r--r--src/js/epicker-ui.js900
1 files changed, 900 insertions, 0 deletions
diff --git a/src/js/epicker-ui.js b/src/js/epicker-ui.js
new file mode 100644
index 0000000..49fc116
--- /dev/null
+++ b/src/js/epicker-ui.js
@@ -0,0 +1,900 @@
+/*******************************************************************************
+
+ 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
+*/
+
+/* global CodeMirror */
+
+'use strict';
+
+import './codemirror/ubo-static-filtering.js';
+
+import { hostnameFromURI } from './uri-utils.js';
+import punycode from '../lib/punycode.js';
+import * as sfp from './static-filtering-parser.js';
+
+/******************************************************************************/
+/******************************************************************************/
+
+(( ) => {
+
+/******************************************************************************/
+
+if ( typeof vAPI !== 'object' ) { return; }
+
+const $id = id => document.getElementById(id);
+const $stor = selector => document.querySelector(selector);
+const $storAll = selector => document.querySelectorAll(selector);
+
+const pickerRoot = document.documentElement;
+const dialog = $stor('aside');
+let staticFilteringParser;
+
+const svgRoot = $stor('svg');
+const svgOcean = svgRoot.children[0];
+const svgIslands = svgRoot.children[1];
+const NoPaths = 'M0 0';
+
+const reCosmeticAnchor = /^#(\$|\?|\$\?)?#/;
+
+{
+ const url = new URL(self.location.href);
+ if ( url.searchParams.has('zap') ) {
+ pickerRoot.classList.add('zap');
+ }
+}
+
+const docURL = new URL(vAPI.getURL(''));
+
+let resultsetOpt;
+
+let netFilterCandidates = [];
+let cosmeticFilterCandidates = [];
+let computedCandidateSlot = 0;
+let computedCandidate = '';
+const computedSpecificityCandidates = new Map();
+let needBody = false;
+
+/******************************************************************************/
+
+const cmEditor = new CodeMirror(document.querySelector('.codeMirrorContainer'), {
+ autoCloseBrackets: true,
+ autofocus: true,
+ extraKeys: {
+ 'Ctrl-Space': 'autocomplete',
+ },
+ lineWrapping: true,
+ matchBrackets: true,
+ maxScanLines: 1,
+});
+
+vAPI.messaging.send('dashboard', {
+ what: 'getAutoCompleteDetails'
+}).then(hints => {
+ // For unknown reasons, `instanceof Object` does not work here in Firefox.
+ if ( hints instanceof Object === false ) { return; }
+ cmEditor.setOption('uboHints', hints);
+});
+
+/******************************************************************************/
+
+const rawFilterFromTextarea = function() {
+ const text = cmEditor.getValue();
+ const pos = text.indexOf('\n');
+ return pos === -1 ? text : text.slice(0, pos);
+};
+
+/******************************************************************************/
+
+const filterFromTextarea = function() {
+ const filter = rawFilterFromTextarea();
+ if ( filter === '' ) { return ''; }
+ const parser = staticFilteringParser;
+ parser.parse(filter);
+ if ( parser.isFilter() === false ) { return '!'; }
+ if ( parser.isExtendedFilter() ) {
+ if ( parser.isCosmeticFilter() === false ) { return '!'; }
+ } else if ( parser.isNetworkFilter() === false ) {
+ return '!';
+ }
+ return filter;
+};
+
+/******************************************************************************/
+
+const renderRange = function(id, value, invert = false) {
+ const input = $stor(`#${id} input`);
+ const max = parseInt(input.max, 10);
+ if ( typeof value !== 'number' ) {
+ value = parseInt(input.value, 10);
+ }
+ if ( invert ) {
+ value = max - value;
+ }
+ input.value = value;
+ const slider = $stor(`#${id} > span`);
+ const lside = slider.children[0];
+ const thumb = slider.children[1];
+ const sliderWidth = slider.offsetWidth;
+ const maxPercent = (sliderWidth - thumb.offsetWidth) / sliderWidth * 100;
+ const widthPercent = value / max * maxPercent;
+ lside.style.width = `${widthPercent}%`;
+};
+
+/******************************************************************************/
+
+const userFilterFromCandidate = function(filter) {
+ if ( filter === '' || filter === '!' ) { return; }
+
+ let hn = hostnameFromURI(docURL.href);
+ if ( hn.startsWith('xn--') ) {
+ hn = punycode.toUnicode(hn);
+ }
+
+ // Cosmetic filter?
+ if ( reCosmeticAnchor.test(filter) ) {
+ return hn + filter;
+ }
+
+ // Assume net filter
+ const opts = [];
+
+ // If no domain included in filter, we need domain option
+ if ( filter.startsWith('||') === false ) {
+ opts.push(`domain=${hn}`);
+ }
+
+ if ( resultsetOpt !== undefined ) {
+ opts.push(resultsetOpt);
+ }
+
+ if ( opts.length ) {
+ filter += '$' + opts.join(',');
+ }
+
+ return filter;
+};
+
+/******************************************************************************/
+
+const candidateFromFilterChoice = function(filterChoice) {
+ let { slot, filters } = filterChoice;
+ let filter = filters[slot];
+
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/47
+ for ( const elem of $storAll('#candidateFilters li') ) {
+ elem.classList.remove('active');
+ }
+
+ computedCandidateSlot = slot;
+ computedCandidate = '';
+
+ if ( filter === undefined ) { return ''; }
+
+ // For net filters there no such thing as a path
+ if ( filter.startsWith('##') === false ) {
+ $stor(`#netFilters li:nth-of-type(${slot+1})`)
+ .classList.add('active');
+ return filter;
+ }
+
+ // At this point, we have a cosmetic filter
+
+ $stor(`#cosmeticFilters li:nth-of-type(${slot+1})`)
+ .classList.add('active');
+
+ return cosmeticCandidatesFromFilterChoice(filterChoice);
+};
+
+/******************************************************************************/
+
+const cosmeticCandidatesFromFilterChoice = function(filterChoice) {
+ let { slot, filters } = filterChoice;
+
+ renderRange('resultsetDepth', slot, true);
+ renderRange('resultsetSpecificity');
+
+ if ( computedSpecificityCandidates.has(slot) ) {
+ onCandidatesOptimized({ slot });
+ return;
+ }
+
+ const specificities = [
+ 0b0000, // remove hierarchy; remove id, nth-of-type, attribute values
+ 0b0010, // remove hierarchy; remove id, nth-of-type
+ 0b0011, // remove hierarchy
+ 0b1000, // trim hierarchy; remove id, nth-of-type, attribute values
+ 0b1010, // trim hierarchy; remove id, nth-of-type
+ 0b1100, // remove id, nth-of-type, attribute values
+ 0b1110, // remove id, nth-of-type
+ 0b1111, // keep all = most specific
+ ];
+
+ const candidates = [];
+
+ let filter = filters[slot];
+
+ for ( const specificity of specificities ) {
+ // Return path: the target element, then all siblings prepended
+ const paths = [];
+ for ( let i = slot; i < filters.length; i++ ) {
+ filter = filters[i].slice(2);
+ // Remove id, nth-of-type
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/162
+ // Mind escaped periods: they do not denote a class identifier.
+ if ( (specificity & 0b0001) === 0 ) {
+ filter = filter.replace(/:nth-of-type\(\d+\)/, '');
+ if (
+ filter.charAt(0) === '#' && (
+ (specificity & 0b1000) === 0 || i === slot
+ )
+ ) {
+ const pos = filter.search(/[^\\]\./);
+ if ( pos !== -1 ) {
+ filter = filter.slice(pos + 1);
+ }
+ }
+ }
+ // Remove attribute values.
+ if ( (specificity & 0b0010) === 0 ) {
+ const match = /^\[([^^*$=]+)[\^*$]?=.+\]$/.exec(filter);
+ if ( match !== null ) {
+ filter = `[${match[1]}]`;
+ }
+ }
+ // Remove all classes when an id exists.
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/162
+ // Mind escaped periods: they do not denote a class identifier.
+ if ( filter.charAt(0) === '#' ) {
+ filter = filter.replace(/([^\\])\..+$/, '$1');
+ }
+ if ( paths.length !== 0 ) {
+ filter += ' > ';
+ }
+ paths.unshift(filter);
+ // Stop at any element with an id: these are unique in a web page
+ if ( (specificity & 0b1000) === 0 || filter.startsWith('#') ) {
+ break;
+ }
+ }
+
+ // Trim hierarchy: remove generic elements from path
+ if ( (specificity & 0b1100) === 0b1000 ) {
+ let i = 0;
+ while ( i < paths.length - 1 ) {
+ if ( /^[a-z0-9]+ > $/.test(paths[i+1]) ) {
+ if ( paths[i].endsWith(' > ') ) {
+ paths[i] = paths[i].slice(0, -2);
+ }
+ paths.splice(i + 1, 1);
+ } else {
+ i += 1;
+ }
+ }
+ }
+
+ if (
+ needBody &&
+ paths.length !== 0 &&
+ paths[0].startsWith('#') === false &&
+ paths[0].startsWith('body ') === false &&
+ (specificity & 0b1100) !== 0
+ ) {
+ paths.unshift('body > ');
+ }
+
+ candidates.push(paths);
+ }
+
+ pickerContentPort.postMessage({
+ what: 'optimizeCandidates',
+ candidates,
+ slot,
+ });
+};
+
+/******************************************************************************/
+
+const onCandidatesOptimized = function(details) {
+ $id('resultsetModifiers').classList.remove('hide');
+ const i = parseInt($stor('#resultsetSpecificity input').value, 10);
+ if ( Array.isArray(details.candidates) ) {
+ computedSpecificityCandidates.set(details.slot, details.candidates);
+ }
+ const candidates = computedSpecificityCandidates.get(details.slot);
+ computedCandidate = candidates[i];
+ cmEditor.setValue(computedCandidate);
+ cmEditor.clearHistory();
+ onCandidateChanged();
+};
+
+/******************************************************************************/
+
+const onSvgClicked = function(ev) {
+ // If zap mode, highlight element under mouse, this makes the zapper usable
+ // on touch screens.
+ if ( pickerRoot.classList.contains('zap') ) {
+ pickerContentPort.postMessage({
+ what: 'zapElementAtPoint',
+ mx: ev.clientX,
+ my: ev.clientY,
+ options: {
+ stay: ev.shiftKey || ev.type === 'touch',
+ highlight: ev.target !== svgIslands,
+ },
+ });
+ return;
+ }
+ // https://github.com/chrisaljoudi/uBlock/issues/810#issuecomment-74600694
+ // Unpause picker if:
+ // - click outside dialog AND
+ // - not in preview mode
+ if ( pickerRoot.classList.contains('paused') ) {
+ if ( pickerRoot.classList.contains('preview') === false ) {
+ unpausePicker();
+ }
+ return;
+ }
+ // Force dialog to always be visible when using a touch-driven device.
+ if ( ev.type === 'touch' ) {
+ pickerRoot.classList.add('show');
+ }
+ pickerContentPort.postMessage({
+ what: 'filterElementAtPoint',
+ mx: ev.clientX,
+ my: ev.clientY,
+ broad: ev.ctrlKey,
+ });
+};
+
+/*******************************************************************************
+
+ Swipe right:
+ If picker not paused: quit picker
+ If picker paused and dialog visible: hide dialog
+ If picker paused and dialog not visible: quit picker
+
+ Swipe left:
+ If picker paused and dialog not visible: show dialog
+
+*/
+
+const onSvgTouch = (( ) => {
+ let startX = 0, startY = 0;
+ let t0 = 0;
+ return ev => {
+ if ( ev.type === 'touchstart' ) {
+ startX = ev.touches[0].screenX;
+ startY = ev.touches[0].screenY;
+ t0 = ev.timeStamp;
+ return;
+ }
+ if ( startX === undefined ) { return; }
+ const stopX = ev.changedTouches[0].screenX;
+ const stopY = ev.changedTouches[0].screenY;
+ const angle = Math.abs(Math.atan2(stopY - startY, stopX - startX));
+ const distance = Math.sqrt(
+ Math.pow(stopX - startX, 2),
+ Math.pow(stopY - startY, 2)
+ );
+ // Interpret touch events as a tap if:
+ // - Swipe is not valid; and
+ // - The time between start and stop was less than 200ms.
+ const duration = ev.timeStamp - t0;
+ if ( distance < 32 && duration < 200 ) {
+ onSvgClicked({
+ type: 'touch',
+ target: ev.target,
+ clientX: ev.changedTouches[0].pageX,
+ clientY: ev.changedTouches[0].pageY,
+ });
+ ev.preventDefault();
+ return;
+ }
+ if ( distance < 64 ) { return; }
+ const angleUpperBound = Math.PI * 0.25 * 0.5;
+ const swipeRight = angle < angleUpperBound;
+ if ( swipeRight === false && angle < Math.PI - angleUpperBound ) {
+ return;
+ }
+ if ( ev.cancelable ) {
+ ev.preventDefault();
+ }
+ // Swipe left.
+ if ( swipeRight === false ) {
+ if ( pickerRoot.classList.contains('paused') ) {
+ pickerRoot.classList.remove('hide');
+ pickerRoot.classList.add('show');
+ }
+ return;
+ }
+ // Swipe right.
+ if (
+ pickerRoot.classList.contains('zap') &&
+ svgIslands.getAttribute('d') !== NoPaths
+ ) {
+ pickerContentPort.postMessage({
+ what: 'unhighlight'
+ });
+ return;
+ }
+ else if (
+ pickerRoot.classList.contains('paused') &&
+ pickerRoot.classList.contains('show')
+ ) {
+ pickerRoot.classList.remove('show');
+ pickerRoot.classList.add('hide');
+ return;
+ }
+ quitPicker();
+ };
+})();
+
+/******************************************************************************/
+
+const onCandidateChanged = function() {
+ const filter = filterFromTextarea();
+ const bad = filter === '!';
+ $stor('section').classList.toggle('invalidFilter', bad);
+ if ( bad ) {
+ $id('resultsetCount').textContent = 'E';
+ $id('create').setAttribute('disabled', '');
+ }
+ const text = rawFilterFromTextarea();
+ $id('resultsetModifiers').classList.toggle(
+ 'hide', text === '' || text !== computedCandidate
+ );
+ pickerContentPort.postMessage({
+ what: 'dialogSetFilter',
+ filter,
+ compiled: reCosmeticAnchor.test(filter)
+ ? staticFilteringParser.result.compiled
+ : undefined,
+ });
+};
+
+/******************************************************************************/
+
+const onPreviewClicked = function() {
+ const state = pickerRoot.classList.toggle('preview');
+ pickerContentPort.postMessage({
+ what: 'togglePreview',
+ state,
+ });
+};
+
+/******************************************************************************/
+
+const onCreateClicked = function() {
+ const candidate = filterFromTextarea();
+ const filter = userFilterFromCandidate(candidate);
+ if ( filter !== undefined ) {
+ vAPI.messaging.send('elementPicker', {
+ what: 'createUserFilter',
+ autoComment: true,
+ filters: filter,
+ docURL: docURL.href,
+ killCache: reCosmeticAnchor.test(candidate) === false,
+ });
+ }
+ pickerContentPort.postMessage({
+ what: 'dialogCreate',
+ filter: candidate,
+ compiled: reCosmeticAnchor.test(candidate)
+ ? staticFilteringParser.result.compiled
+ : undefined,
+ });
+};
+
+/******************************************************************************/
+
+const onPickClicked = function() {
+ unpausePicker();
+};
+
+/******************************************************************************/
+
+const onQuitClicked = function() {
+ quitPicker();
+};
+
+/******************************************************************************/
+
+const onDepthChanged = function() {
+ const input = $stor('#resultsetDepth input');
+ const max = parseInt(input.max, 10);
+ const value = parseInt(input.value, 10);
+ const text = candidateFromFilterChoice({
+ filters: cosmeticFilterCandidates,
+ slot: max - value,
+ });
+ if ( text === undefined ) { return; }
+ cmEditor.setValue(text);
+ cmEditor.clearHistory();
+ onCandidateChanged();
+};
+
+/******************************************************************************/
+
+const onSpecificityChanged = function() {
+ renderRange('resultsetSpecificity');
+ if ( rawFilterFromTextarea() !== computedCandidate ) { return; }
+ const depthInput = $stor('#resultsetDepth input');
+ const slot = parseInt(depthInput.max, 10) - parseInt(depthInput.value, 10);
+ const i = parseInt($stor('#resultsetSpecificity input').value, 10);
+ const candidates = computedSpecificityCandidates.get(slot);
+ computedCandidate = candidates[i];
+ cmEditor.setValue(computedCandidate);
+ cmEditor.clearHistory();
+ onCandidateChanged();
+};
+
+/******************************************************************************/
+
+const onCandidateClicked = function(ev) {
+ let li = ev.target.closest('li');
+ if ( li === null ) { return; }
+ const ul = li.closest('.changeFilter');
+ if ( ul === null ) { return; }
+ const choice = {
+ filters: Array.from(ul.querySelectorAll('li')).map(a => a.textContent),
+ slot: 0,
+ };
+ while ( li.previousElementSibling !== null ) {
+ li = li.previousElementSibling;
+ choice.slot += 1;
+ }
+ const text = candidateFromFilterChoice(choice);
+ if ( text === undefined ) { return; }
+ cmEditor.setValue(text);
+ cmEditor.clearHistory();
+ onCandidateChanged();
+};
+
+/******************************************************************************/
+
+const onKeyPressed = function(ev) {
+ // Delete
+ if (
+ (ev.key === 'Delete' || ev.key === 'Backspace') &&
+ pickerRoot.classList.contains('zap')
+ ) {
+ pickerContentPort.postMessage({
+ what: 'zapElementAtPoint',
+ options: { stay: true },
+ });
+ return;
+ }
+ // Esc
+ if ( ev.key === 'Escape' || ev.which === 27 ) {
+ onQuitClicked();
+ return;
+ }
+};
+
+/******************************************************************************/
+
+const onStartMoving = (( ) => {
+ let isTouch = false;
+ let mx0 = 0, my0 = 0;
+ let mx1 = 0, my1 = 0;
+ let r0 = 0, b0 = 0;
+ let rMax = 0, bMax = 0;
+ let timer;
+
+ const eatEvent = function(ev) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ };
+
+ const move = ( ) => {
+ timer = undefined;
+ const r1 = Math.min(Math.max(r0 - mx1 + mx0, 2), rMax);
+ const b1 = Math.min(Math.max(b0 - my1 + my0, 2), bMax);
+ dialog.style.setProperty('right', `${r1}px`);
+ dialog.style.setProperty('bottom', `${b1}px`);
+ };
+
+ const moveAsync = ev => {
+ if ( timer !== undefined ) { return; }
+ if ( isTouch ) {
+ const touch = ev.touches[0];
+ mx1 = touch.pageX;
+ my1 = touch.pageY;
+ } else {
+ mx1 = ev.pageX;
+ my1 = ev.pageY;
+ }
+ timer = self.requestAnimationFrame(move);
+ };
+
+ const stop = ev => {
+ if ( dialog.classList.contains('moving') === false ) { return; }
+ dialog.classList.remove('moving');
+ if ( isTouch ) {
+ self.removeEventListener('touchmove', moveAsync, { capture: true });
+ } else {
+ self.removeEventListener('mousemove', moveAsync, { capture: true });
+ }
+ eatEvent(ev);
+ };
+
+ return function(ev) {
+ const target = dialog.querySelector('#move');
+ if ( ev.target !== target ) { return; }
+ if ( dialog.classList.contains('moving') ) { return; }
+ isTouch = ev.type.startsWith('touch');
+ if ( isTouch ) {
+ const touch = ev.touches[0];
+ mx0 = touch.pageX;
+ my0 = touch.pageY;
+ } else {
+ mx0 = ev.pageX;
+ my0 = ev.pageY;
+ }
+ const style = self.getComputedStyle(dialog);
+ r0 = parseInt(style.right, 10);
+ b0 = parseInt(style.bottom, 10);
+ const rect = dialog.getBoundingClientRect();
+ rMax = pickerRoot.clientWidth - 2 - rect.width ;
+ bMax = pickerRoot.clientHeight - 2 - rect.height;
+ dialog.classList.add('moving');
+ if ( isTouch ) {
+ self.addEventListener('touchmove', moveAsync, { capture: true });
+ self.addEventListener('touchend', stop, { capture: true, once: true });
+ } else {
+ self.addEventListener('mousemove', moveAsync, { capture: true });
+ self.addEventListener('mouseup', stop, { capture: true, once: true });
+ }
+ eatEvent(ev);
+ };
+})();
+
+/******************************************************************************/
+
+const svgListening = (( ) => {
+ let on = false;
+ let timer;
+ let mx = 0, my = 0;
+
+ const onTimer = ( ) => {
+ timer = undefined;
+ pickerContentPort.postMessage({
+ what: 'highlightElementAtPoint',
+ mx,
+ my,
+ });
+ };
+
+ const onHover = ev => {
+ mx = ev.clientX;
+ my = ev.clientY;
+ if ( timer === undefined ) {
+ timer = self.requestAnimationFrame(onTimer);
+ }
+ };
+
+ return state => {
+ if ( state === on ) { return; }
+ on = state;
+ if ( on ) {
+ document.addEventListener('mousemove', onHover, { passive: true });
+ return;
+ }
+ document.removeEventListener('mousemove', onHover, { passive: true });
+ if ( timer !== undefined ) {
+ self.cancelAnimationFrame(timer);
+ timer = undefined;
+ }
+ };
+})();
+
+/******************************************************************************/
+
+// Create lists of candidate filters. This takes into account whether the
+// current mode is narrow or broad.
+
+const populateCandidates = function(candidates, selector) {
+
+ const root = dialog.querySelector(selector);
+ const ul = root.querySelector('ul');
+ while ( ul.firstChild !== null ) {
+ ul.firstChild.remove();
+ }
+ for ( let i = 0; i < candidates.length; i++ ) {
+ const li = document.createElement('li');
+ li.textContent = candidates[i];
+ ul.appendChild(li);
+ }
+ if ( candidates.length !== 0 ) {
+ root.style.removeProperty('display');
+ } else {
+ root.style.setProperty('display', 'none');
+ }
+};
+
+/******************************************************************************/
+
+const showDialog = function(details) {
+ pausePicker();
+
+ const { netFilters, cosmeticFilters, filter } = details;
+
+ netFilterCandidates = netFilters;
+
+ needBody =
+ cosmeticFilters.length !== 0 &&
+ cosmeticFilters[cosmeticFilters.length - 1] === '##body';
+ if ( needBody ) {
+ cosmeticFilters.pop();
+ }
+ cosmeticFilterCandidates = cosmeticFilters;
+
+ docURL.href = details.url;
+
+ populateCandidates(netFilters, '#netFilters');
+ populateCandidates(cosmeticFilters, '#cosmeticFilters');
+ computedSpecificityCandidates.clear();
+
+ const depthInput = $stor('#resultsetDepth input');
+ depthInput.max = cosmeticFilters.length - 1;
+ depthInput.value = depthInput.max;
+
+ dialog.querySelector('ul').style.display =
+ netFilters.length || cosmeticFilters.length ? '' : 'none';
+ $id('create').setAttribute('disabled', '');
+
+ // Auto-select a candidate filter
+
+ // 2020-09-01:
+ // In Firefox, `details instanceof Object` resolves to `false` despite
+ // `details` being a valid object. Consequently, falling back to use
+ // `typeof details`.
+ // This is an issue which surfaced when the element picker code was
+ // revisited to isolate the picker dialog DOM from the page DOM.
+ if ( typeof filter !== 'object' || filter === null ) {
+ cmEditor.setValue('');
+ return;
+ }
+
+ const filterChoice = {
+ filters: filter.filters,
+ slot: filter.slot,
+ };
+
+ const text = candidateFromFilterChoice(filterChoice);
+ if ( text === undefined ) { return; }
+ cmEditor.setValue(text);
+ onCandidateChanged();
+};
+
+/******************************************************************************/
+
+const pausePicker = function() {
+ pickerRoot.classList.add('paused');
+ svgListening(false);
+};
+
+/******************************************************************************/
+
+const unpausePicker = function() {
+ pickerRoot.classList.remove('paused', 'preview');
+ pickerContentPort.postMessage({
+ what: 'togglePreview',
+ state: false,
+ });
+ svgListening(true);
+};
+
+/******************************************************************************/
+
+const startPicker = function() {
+ self.addEventListener('keydown', onKeyPressed, true);
+ const svg = $stor('svg');
+ svg.addEventListener('click', onSvgClicked);
+ svg.addEventListener('touchstart', onSvgTouch);
+ svg.addEventListener('touchend', onSvgTouch);
+
+ unpausePicker();
+
+ if ( pickerRoot.classList.contains('zap') ) { return; }
+
+ cmEditor.on('changes', onCandidateChanged);
+
+ $id('preview').addEventListener('click', onPreviewClicked);
+ $id('create').addEventListener('click', onCreateClicked);
+ $id('pick').addEventListener('click', onPickClicked);
+ $id('quit').addEventListener('click', onQuitClicked);
+ $id('move').addEventListener('mousedown', onStartMoving);
+ $id('move').addEventListener('touchstart', onStartMoving);
+ $id('candidateFilters').addEventListener('click', onCandidateClicked);
+ $stor('#resultsetDepth input').addEventListener('input', onDepthChanged);
+ $stor('#resultsetSpecificity input').addEventListener('input', onSpecificityChanged);
+ staticFilteringParser = new sfp.AstFilterParser({
+ interactive: true,
+ nativeCssHas: vAPI.webextFlavor.env.includes('native_css_has'),
+ });
+};
+
+/******************************************************************************/
+
+const quitPicker = function() {
+ pickerContentPort.postMessage({ what: 'quitPicker' });
+ pickerContentPort.close();
+ pickerContentPort = undefined;
+};
+
+/******************************************************************************/
+
+const onPickerMessage = function(msg) {
+ switch ( msg.what ) {
+ case 'candidatesOptimized':
+ onCandidatesOptimized(msg);
+ break;
+ case 'showDialog':
+ showDialog(msg);
+ break;
+ case 'resultsetDetails': {
+ resultsetOpt = msg.opt;
+ $id('resultsetCount').textContent = msg.count;
+ if ( msg.count !== 0 ) {
+ $id('create').removeAttribute('disabled');
+ } else {
+ $id('create').setAttribute('disabled', '');
+ }
+ break;
+ }
+ case 'svgPaths': {
+ let { ocean, islands } = msg;
+ ocean += islands;
+ svgOcean.setAttribute('d', ocean);
+ svgIslands.setAttribute('d', islands || NoPaths);
+ break;
+ }
+ default:
+ break;
+ }
+};
+
+/******************************************************************************/
+
+// Wait for the content script to establish communication
+
+let pickerContentPort;
+
+globalThis.addEventListener('message', ev => {
+ const msg = ev.data || {};
+ if ( msg.what !== 'epickerStart' ) { return; }
+ if ( Array.isArray(ev.ports) === false ) { return; }
+ if ( ev.ports.length === 0 ) { return; }
+ pickerContentPort = ev.ports[0];
+ pickerContentPort.onmessage = ev => {
+ const msg = ev.data || {};
+ onPickerMessage(msg);
+ };
+ pickerContentPort.onmessageerror = ( ) => {
+ quitPicker();
+ };
+ startPicker();
+ pickerContentPort.postMessage({ what: 'start' });
+}, { once: true });
+
+/******************************************************************************/
+
+})();