diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 05:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 05:47:55 +0000 |
commit | 31d6ff6f931696850c348007241195ab3b2eddc7 (patch) | |
tree | 615cb1c57ce9f6611bad93326b9105098f379609 /src/js/epicker-ui.js | |
parent | Initial commit. (diff) | |
download | ublock-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.js | 900 |
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 }); + +/******************************************************************************/ + +})(); |