mirror of
https://github.com/gorhill/uBlock.git
synced 2024-11-02 00:42:45 +01:00
e33c0f9d9c
Added more evocative error messages to linter. Made the content of _My filters_ trusted by default in dev build.
925 lines
29 KiB
JavaScript
925 lines
29 KiB
JavaScript
/*******************************************************************************
|
|
|
|
uBlock Origin - a browser extension to block requests.
|
|
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 epickerId = (( ) => {
|
|
const url = new URL(self.location.href);
|
|
if ( url.searchParams.has('zap') ) {
|
|
pickerRoot.classList.add('zap');
|
|
}
|
|
return url.searchParams.get('epid');
|
|
})();
|
|
if ( epickerId === null ) { return; }
|
|
|
|
const docURL = new URL(vAPI.getURL(''));
|
|
|
|
let epickerConnectionId;
|
|
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);
|
|
}
|
|
|
|
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
|
|
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') ) {
|
|
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
|
|
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');
|
|
}
|
|
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
|
|
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
|
|
) {
|
|
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
|
|
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
|
|
);
|
|
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
|
|
what: 'dialogSetFilter',
|
|
filter,
|
|
compiled: reCosmeticAnchor.test(filter)
|
|
? staticFilteringParser.result.compiled
|
|
: undefined,
|
|
});
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
const onPreviewClicked = function() {
|
|
const state = pickerRoot.classList.toggle('preview');
|
|
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
|
|
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,
|
|
});
|
|
}
|
|
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
|
|
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')
|
|
) {
|
|
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
|
|
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;
|
|
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
|
|
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');
|
|
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
|
|
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() {
|
|
vAPI.MessagingConnection.sendTo(epickerConnectionId, { what: 'quitPicker' });
|
|
vAPI.MessagingConnection.disconnectFrom(epickerConnectionId);
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
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;
|
|
}
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
const onConnectionMessage = function(msg) {
|
|
switch ( msg.what ) {
|
|
case 'connectionBroken':
|
|
break;
|
|
case 'connectionMessage':
|
|
onPickerMessage(msg.payload);
|
|
break;
|
|
case 'connectionAccepted':
|
|
epickerConnectionId = msg.id;
|
|
startPicker();
|
|
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
|
|
what: 'start',
|
|
});
|
|
break;
|
|
}
|
|
};
|
|
|
|
vAPI.MessagingConnection.connectTo(
|
|
`epickerDialog-${epickerId}`,
|
|
`epicker-${epickerId}`,
|
|
onConnectionMessage
|
|
);
|
|
|
|
/******************************************************************************/
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*******************************************************************************
|
|
|
|
DO NOT:
|
|
- Remove the following code
|
|
- Add code beyond the following code
|
|
Reason:
|
|
- https://github.com/gorhill/uBlock/pull/3721
|
|
- uBO never uses the return value from injected content scripts
|
|
|
|
**/
|
|
|
|
void 0;
|