mirror of https://github.com/gorhill/uBlock.git
Isolate element picker dialog from page content world
Related issues: - https://github.com/gorhill/uBlock/issues/3497 - https://github.com/uBlockOrigin/uBlock-issues/issues/1215 To solve above issues, the element picker's dialog is now isolated from the page content in which it is embedded. The highly interactive, mouse-driven part of the element picker is still visible by the page content.
This commit is contained in:
parent
43dba2bd0e
commit
9eb455ab5e
|
@ -102,8 +102,14 @@ vAPI.messaging = {
|
|||
},
|
||||
disconnectListenerBound: null,
|
||||
|
||||
// 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.
|
||||
messageListener: function(details) {
|
||||
if ( details instanceof Object === false ) { return; }
|
||||
if ( typeof details !== 'object' || details === null ) { return; }
|
||||
|
||||
// Response to specific message previously sent
|
||||
if ( details.msgId !== undefined ) {
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
html#ublock0-epicker,
|
||||
#ublock0-epicker body {
|
||||
background: transparent;
|
||||
color: black;
|
||||
cursor: not-allowed;
|
||||
font: 12px sans-serif;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
width: 100vw;
|
||||
}
|
||||
#ublock0-epicker :focus {
|
||||
outline: none;
|
||||
}
|
||||
#ublock0-epicker ul,
|
||||
#ublock0-epicker li,
|
||||
#ublock0-epicker div {
|
||||
display: block;
|
||||
}
|
||||
#ublock0-epicker #toolbar {
|
||||
cursor: grab;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
#ublock0-epicker aside.moving #toolbar {
|
||||
cursor: grabbing;
|
||||
}
|
||||
#ublock0-epicker ul {
|
||||
margin: 0.25em 0 0 0;
|
||||
}
|
||||
#ublock0-epicker button {
|
||||
background-color: #ccc;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: none;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
#ublock0-epicker button:disabled {
|
||||
color: #999;
|
||||
background-color: #ccc;
|
||||
}
|
||||
#ublock0-epicker button:not(:disabled):hover {
|
||||
opacity: 1;
|
||||
}
|
||||
#ublock0-epicker #create:not(:disabled) {
|
||||
background-color: hsl(36, 100%, 83%);
|
||||
border-color: hsl(36, 50%, 60%);
|
||||
}
|
||||
#ublock0-epicker #preview {
|
||||
float: left;
|
||||
}
|
||||
#ublock0-epicker body.preview #preview {
|
||||
background-color: hsl(204, 100%, 83%);
|
||||
border-color: hsl(204, 50%, 60%);
|
||||
}
|
||||
#ublock0-epicker section {
|
||||
border: 0;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
#ublock0-epicker section > div:first-child {
|
||||
border: 1px solid #aaa;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
}
|
||||
#ublock0-epicker section.invalidFilter > div:first-child {
|
||||
border-color: red;
|
||||
}
|
||||
#ublock0-epicker section > div:first-child > textarea {
|
||||
background-color: #fff;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
color: #000;
|
||||
font: 11px monospace;
|
||||
height: 8em;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 2px;
|
||||
resize: none;
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
}
|
||||
#ublock0-epicker #resultsetCount {
|
||||
background-color: #aaa;
|
||||
bottom: 0;
|
||||
color: white;
|
||||
padding: 2px 4px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
#ublock0-epicker section.invalidFilter #resultsetCount {
|
||||
background-color: red;
|
||||
}
|
||||
#ublock0-epicker section > div:first-child + div {
|
||||
direction: ltr;
|
||||
margin: 2px 0;
|
||||
text-align: right;
|
||||
}
|
||||
#ublock0-epicker ul {
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
}
|
||||
#ublock0-epicker #candidateFilters {
|
||||
max-height: 16em;
|
||||
overflow-y: auto;
|
||||
}
|
||||
#ublock0-epicker #candidateFilters > li:first-of-type {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
#ublock0-epicker .changeFilter > li > span:nth-of-type(1) {
|
||||
font-weight: bold;
|
||||
}
|
||||
#ublock0-epicker .changeFilter > li > span:nth-of-type(2) {
|
||||
font-size: smaller;
|
||||
color: gray;
|
||||
}
|
||||
#ublock0-epicker #candidateFilters .changeFilter {
|
||||
list-style-type: none;
|
||||
margin: 0 0 0 1em;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
}
|
||||
#ublock0-epicker #candidateFilters .changeFilter li {
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
direction: ltr;
|
||||
font: 11px monospace;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#ublock0-epicker #candidateFilters .changeFilter li.active {
|
||||
border: 1px dotted gray;
|
||||
}
|
||||
#ublock0-epicker #candidateFilters .changeFilter li:hover {
|
||||
background-color: white;
|
||||
}
|
||||
#ublock0-epicker aside {
|
||||
background-color: #eee;
|
||||
border: 1px solid #aaa;
|
||||
bottom: 4px;
|
||||
box-sizing: border-box;
|
||||
cursor: default;
|
||||
min-width: 24em;
|
||||
padding: 4px;
|
||||
position: fixed;
|
||||
right: 4px;
|
||||
width: calc(40% - 4px);
|
||||
}
|
||||
/**
|
||||
https://github.com/gorhill/uBlock/issues/3449
|
||||
https://github.com/uBlockOrigin/uBlock-issues/issues/55
|
||||
**/
|
||||
@keyframes startDialog {
|
||||
0% { opacity: 1.0; }
|
||||
60% { opacity: 1.0; }
|
||||
100% { opacity: 0.1; }
|
||||
}
|
||||
#ublock0-epicker body.paused > aside {
|
||||
opacity: 0.1;
|
||||
visibility: visible;
|
||||
z-index: 100;
|
||||
}
|
||||
#ublock0-epicker body.paused > aside:not(:hover):not(.show) {
|
||||
animation-duration: 1.6s;
|
||||
animation-name: startDialog;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
#ublock0-epicker body.paused > aside:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
#ublock0-epicker body.paused > aside.show {
|
||||
opacity: 1;
|
||||
}
|
||||
#ublock0-epicker body.paused > aside.hide {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
html#ublock0-epicker,
|
||||
#ublock0-epicker body {
|
||||
background: transparent !important;
|
||||
box-sizing: border-box !important;
|
||||
color: black !important;
|
||||
font: 12px sans-serif !important;
|
||||
height: 100vh !important;
|
||||
margin: 0 !important;
|
||||
overflow: hidden !important;
|
||||
position: fixed !important;
|
||||
width: 100vw !important;
|
||||
}
|
||||
#ublock0-epicker :focus {
|
||||
outline: none !important;
|
||||
}
|
||||
#ublock0-epicker svg {
|
||||
cursor: crosshair !important;
|
||||
box-sizing: border-box;
|
||||
height: 100% !important;
|
||||
left: 0 !important;
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
#ublock0-epicker .paused > svg {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
#ublock0-epicker svg > path:first-child {
|
||||
fill: rgba(0,0,0,0.5) !important;
|
||||
fill-rule: evenodd !important;
|
||||
}
|
||||
#ublock0-epicker svg > path + path {
|
||||
stroke: #F00 !important;
|
||||
stroke-width: 0.5px !important;
|
||||
fill: rgba(255,63,63,0.20) !important;
|
||||
}
|
||||
#ublock0-epicker body.zap svg > path + path {
|
||||
stroke: #FF0 !important;
|
||||
stroke-width: 0.5px !important;
|
||||
fill: rgba(255,255,63,0.20) !important;
|
||||
}
|
||||
#ublock0-epicker body.preview svg > path {
|
||||
fill: rgba(0,0,0,0.10) !important;
|
||||
}
|
||||
#ublock0-epicker body.preview svg > path + path {
|
||||
stroke: none !important;
|
||||
}
|
||||
#ublock0-epicker body > iframe {
|
||||
border: 0 !important;
|
||||
box-sizing: border-box !important;
|
||||
display: none !important;
|
||||
height: 100% !important;
|
||||
left: 0 !important;
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
#ublock0-epicker body.paused > iframe {
|
||||
display: initial !important;
|
||||
}
|
250
src/epicker.html
250
src/epicker.html
|
@ -1,250 +0,0 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>uBlock Origin Element Picker</title>
|
||||
<style>
|
||||
html#ublock0-epicker,
|
||||
#ublock0-epicker body {
|
||||
background: transparent !important;
|
||||
color: black !important;
|
||||
font: 12px sans-serif !important;
|
||||
height: 100% !important;
|
||||
margin: 0 !important;
|
||||
overflow: hidden !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
#ublock0-epicker :focus {
|
||||
outline: none !important;
|
||||
}
|
||||
#ublock0-epicker ul,
|
||||
#ublock0-epicker li,
|
||||
#ublock0-epicker div {
|
||||
display: block !important;
|
||||
}
|
||||
#ublock0-epicker #toolbar {
|
||||
cursor: grab;
|
||||
display: flex !important;
|
||||
justify-content: space-between;
|
||||
}
|
||||
#ublock0-epicker aside.moving #toolbar {
|
||||
cursor: grabbing;
|
||||
}
|
||||
#ublock0-epicker ul {
|
||||
margin: 0.25em 0 0 0 !important;
|
||||
}
|
||||
#ublock0-epicker button {
|
||||
background-color: #ccc !important;
|
||||
border: 1px solid #aaa !important;
|
||||
border-radius: 3px !important;
|
||||
box-sizing: border-box !important;
|
||||
box-shadow: none !important;
|
||||
color: #000 !important;
|
||||
cursor: pointer !important;
|
||||
opacity: 0.7 !important;
|
||||
padding: 4px 6px !important;
|
||||
}
|
||||
#ublock0-epicker button:disabled {
|
||||
color: #999 !important;
|
||||
background-color: #ccc !important;
|
||||
}
|
||||
#ublock0-epicker button:not(:disabled):hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
#ublock0-epicker #create:not(:disabled) {
|
||||
background-color: hsl(36, 100%, 83%) !important;
|
||||
border-color: hsl(36, 50%, 60%) !important;
|
||||
}
|
||||
#ublock0-epicker #preview {
|
||||
float: left !important;
|
||||
}
|
||||
#ublock0-epicker body.preview #preview {
|
||||
background-color: hsl(204, 100%, 83%) !important;
|
||||
border-color: hsl(204, 50%, 60%) !important;
|
||||
}
|
||||
#ublock0-epicker section {
|
||||
border: 0 !important;
|
||||
box-sizing: border-box !important;
|
||||
display: inline-block !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
#ublock0-epicker section > div:first-child {
|
||||
border: 1px solid #aaa !important;
|
||||
margin: 0 !important;
|
||||
position: relative !important;
|
||||
}
|
||||
#ublock0-epicker section.invalidFilter > div:first-child {
|
||||
border-color: red !important;
|
||||
}
|
||||
#ublock0-epicker section > div:first-child > textarea {
|
||||
background-color: #fff !important;
|
||||
border: none !important;
|
||||
box-sizing: border-box !important;
|
||||
color: #000 !important;
|
||||
font: 11px monospace !important;
|
||||
height: 8em !important;
|
||||
margin: 0 !important;
|
||||
overflow: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
padding: 2px !important;
|
||||
resize: none !important;
|
||||
width: 100% !important;
|
||||
word-break: break-all !important;
|
||||
}
|
||||
#ublock0-epicker #resultsetCount {
|
||||
background-color: #aaa !important;
|
||||
bottom: 0 !important;
|
||||
color: white !important;
|
||||
padding: 2px 4px !important;
|
||||
position: absolute !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
#ublock0-epicker section.invalidFilter #resultsetCount {
|
||||
background-color: red !important;
|
||||
}
|
||||
#ublock0-epicker section > div:first-child + div {
|
||||
direction: ltr !important;
|
||||
margin: 2px 0 !important;
|
||||
text-align: right !important;
|
||||
}
|
||||
#ublock0-epicker ul {
|
||||
padding: 0 !important;
|
||||
list-style-type: none !important;
|
||||
text-align: left !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
#ublock0-epicker #candidateFilters {
|
||||
max-height: 16em !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
#ublock0-epicker #candidateFilters > li:first-of-type {
|
||||
margin-bottom: 0.5em !important;
|
||||
}
|
||||
#ublock0-epicker .changeFilter > li > span:nth-of-type(1) {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
#ublock0-epicker .changeFilter > li > span:nth-of-type(2) {
|
||||
font-size: smaller !important;
|
||||
color: gray !important;
|
||||
}
|
||||
#ublock0-epicker #candidateFilters .changeFilter {
|
||||
list-style-type: none !important;
|
||||
margin: 0 0 0 1em !important;
|
||||
overflow: hidden !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
#ublock0-epicker #candidateFilters .changeFilter li {
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer !important;
|
||||
direction: ltr !important;
|
||||
font: 11px monospace !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
#ublock0-epicker #candidateFilters .changeFilter li.active {
|
||||
border: 1px dotted gray;
|
||||
}
|
||||
#ublock0-epicker #candidateFilters .changeFilter li:hover {
|
||||
background-color: white !important;
|
||||
}
|
||||
#ublock0-epicker svg {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
cursor: crosshair !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
#ublock0-epicker .paused > svg {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
#ublock0-epicker svg > path:first-child {
|
||||
fill: rgba(0,0,0,0.5) !important;
|
||||
fill-rule: evenodd !important;
|
||||
}
|
||||
#ublock0-epicker svg > path + path {
|
||||
stroke: #F00 !important;
|
||||
stroke-width: 0.5px !important;
|
||||
fill: rgba(255,63,63,0.20) !important;
|
||||
}
|
||||
#ublock0-epicker body.zap svg > path + path {
|
||||
stroke: #FF0 !important;
|
||||
stroke-width: 0.5px !important;
|
||||
fill: rgba(255,255,63,0.20) !important;
|
||||
}
|
||||
#ublock0-epicker body.preview svg > path {
|
||||
fill: rgba(0,0,0,0.10) !important;
|
||||
}
|
||||
#ublock0-epicker body.preview svg > path + path {
|
||||
stroke: none !important;
|
||||
}
|
||||
#ublock0-epicker aside {
|
||||
background-color: #eee !important;
|
||||
border: 1px solid #aaa !important;
|
||||
bottom: 4px !important;
|
||||
box-sizing: border-box !important;
|
||||
min-width: 24em !important;
|
||||
padding: 4px !important;
|
||||
position: fixed !important;
|
||||
right: 4px !important;
|
||||
visibility: hidden !important;
|
||||
width: calc(40% - 4px) !important;
|
||||
}
|
||||
#ublock0-epicker body.paused > aside {
|
||||
opacity: 0.1;
|
||||
visibility: visible !important;
|
||||
z-index: 100 !important;
|
||||
}
|
||||
/**
|
||||
https://github.com/gorhill/uBlock/issues/3449
|
||||
https://github.com/uBlockOrigin/uBlock-issues/issues/55
|
||||
**/
|
||||
@keyframes startDialog {
|
||||
0% { opacity: 1.0; }
|
||||
60% { opacity: 1.0; }
|
||||
100% { opacity: 0.1; }
|
||||
}
|
||||
#ublock0-epicker body.paused > aside:not(:hover):not(.show) {
|
||||
animation-duration: 1.6s !important;
|
||||
animation-name: startDialog !important;
|
||||
animation-timing-function: linear !important;
|
||||
}
|
||||
#ublock0-epicker body.paused > aside:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
#ublock0-epicker body.paused > aside.show {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
#ublock0-epicker body.paused > aside.hide {
|
||||
opacity: 0.1 !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body direction="{{bidi_dir}}">
|
||||
<svg><path d></path><path d></path></svg>
|
||||
<aside>
|
||||
<section>
|
||||
<div>
|
||||
<textarea lang="en" dir="ltr" spellcheck="false"></textarea>
|
||||
<div id="resultsetCount"></div>
|
||||
</div>
|
||||
<div id="toolbar">
|
||||
<div>
|
||||
<button id="preview" type="button">{{preview}}</button>
|
||||
</div>
|
||||
<div>
|
||||
<button id="create" type="button" disabled>{{create}}</button>
|
||||
<button id="pick" type="button">{{pick}}</button>
|
||||
<button id="quit" type="button">{{quit}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<ul id="candidateFilters">
|
||||
<li id="netFilters">
|
||||
<span>{{netFilters}}</span><ul lang="en" class="changeFilter"></ul>
|
||||
</li>
|
||||
<li id="cosmeticFilters">
|
||||
<span>{{cosmeticFilters}}</span> <span>{{cosmeticFiltersHint}}</span>
|
||||
<ul lang="en" class="changeFilter"></ul>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</body>
|
|
@ -0,0 +1,499 @@
|
|||
/*******************************************************************************
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/******************************************************************************/
|
||||
/******************************************************************************/
|
||||
|
||||
(( ) => {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
if ( typeof vAPI !== 'object' ) { return; }
|
||||
|
||||
const epickerId = (( ) => {
|
||||
const url = new URL(self.location.href);
|
||||
return url.searchParams.get('epid');
|
||||
})();
|
||||
if ( epickerId === null ) { return; }
|
||||
|
||||
let epickerConnectionId;
|
||||
let filterHostname = '';
|
||||
let filterOrigin = '';
|
||||
let filterResultset = [];
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const $id = id => document.getElementById(id);
|
||||
const $stor = selector => document.querySelector(selector);
|
||||
const $storAll = selector => document.querySelectorAll(selector);
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const filterFromTextarea = function() {
|
||||
const s = taCandidate.value.trim();
|
||||
if ( s === '' ) { return ''; }
|
||||
const pos = s.indexOf('\n');
|
||||
const filter = pos === -1 ? s.trim() : s.slice(0, pos).trim();
|
||||
staticFilteringParser.analyze(filter);
|
||||
staticFilteringParser.analyzeExtra();
|
||||
return staticFilteringParser.shouldDiscard() ? '!' : filter;
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const userFilterFromCandidate = function(filter) {
|
||||
if ( filter === '' || filter === '!' ) { return; }
|
||||
|
||||
// Cosmetic filter?
|
||||
if ( filter.startsWith('##') ) {
|
||||
return filterHostname + filter;
|
||||
}
|
||||
|
||||
// Assume net filter
|
||||
const opts = [];
|
||||
|
||||
// If no domain included in filter, we need domain option
|
||||
if ( filter.startsWith('||') === false ) {
|
||||
opts.push(`domain=${filterHostname}`);
|
||||
}
|
||||
|
||||
if ( filterResultset.length !== 0 ) {
|
||||
const item = filterResultset[0];
|
||||
if ( item.opts ) {
|
||||
opts.push(item.opts);
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
// Modifier means "target broadly". Hence:
|
||||
// - Do not compute exact path.
|
||||
// - Discard narrowing directives.
|
||||
// - Remove the id if one or more classes exist
|
||||
// TODO: should remove tag name too? ¯\_(ツ)_/¯
|
||||
if ( filterChoice.modifier ) {
|
||||
filter = filter.replace(/:nth-of-type\(\d+\)/, '');
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/162
|
||||
// Mind escaped periods: they do not denote a class identifier.
|
||||
if ( filter.charAt(2) === '#' ) {
|
||||
const pos = filter.search(/[^\\]\./);
|
||||
if ( pos !== -1 ) {
|
||||
filter = '##' + filter.slice(pos + 1);
|
||||
}
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
// Return path: the target element, then all siblings prepended
|
||||
let selector = '', joiner = '';
|
||||
for ( ; slot < filters.length; slot++ ) {
|
||||
filter = filters[slot];
|
||||
// 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(2) === '#' ) {
|
||||
filter = filter.replace(/([^\\])\..+$/, '$1');
|
||||
}
|
||||
selector = filter.slice(2) + joiner + selector;
|
||||
// Stop at any element with an id: these are unique in a web page
|
||||
if ( filter.startsWith('###') ) { break; }
|
||||
// Stop if current selector matches only one element on the page
|
||||
if ( document.querySelectorAll(selector).length === 1 ) { break; }
|
||||
joiner = ' > ';
|
||||
}
|
||||
|
||||
// https://github.com/gorhill/uBlock/issues/2519
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/17
|
||||
if (
|
||||
slot === filters.length &&
|
||||
selector.startsWith('body > ') === false &&
|
||||
document.querySelectorAll(selector).length > 1
|
||||
) {
|
||||
selector = 'body > ' + selector;
|
||||
}
|
||||
|
||||
return '##' + selector;
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onCandidateChanged = function() {
|
||||
const filter = filterFromTextarea();
|
||||
const bad = filter === '!';
|
||||
$stor('section').classList.toggle('invalidFilter', bad);
|
||||
$id('create').disabled = bad;
|
||||
if ( bad ) {
|
||||
$id('resultsetCount').textContent = 'E';
|
||||
$id('create').setAttribute('disabled', '');
|
||||
}
|
||||
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
|
||||
what: 'dialogSetFilter',
|
||||
filter,
|
||||
compiled: filter.startsWith('##')
|
||||
? staticFilteringParser.result.compiled
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onPreviewClicked = function() {
|
||||
const state = pickerBody.classList.toggle('preview');
|
||||
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
|
||||
what: 'dialogPreview',
|
||||
state,
|
||||
});
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onCreateClicked = function() {
|
||||
const candidate = filterFromTextarea();
|
||||
const filter = userFilterFromCandidate(candidate);
|
||||
if ( filter !== undefined ) {
|
||||
vAPI.messaging.send('elementPicker', {
|
||||
what: 'createUserFilter',
|
||||
autoComment: true,
|
||||
filters: filter,
|
||||
origin: filterOrigin,
|
||||
pageDomain: filterHostname,
|
||||
killCache: /^#[$?]?#/.test(candidate) === false,
|
||||
});
|
||||
}
|
||||
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
|
||||
what: 'dialogCreate',
|
||||
filter: candidate,
|
||||
compiled: candidate.startsWith('##')
|
||||
? staticFilteringParser.result.compiled
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onPickClicked = function(ev) {
|
||||
if (
|
||||
(ev instanceof MouseEvent) &&
|
||||
(ev.type === 'mousedown') &&
|
||||
(ev.which !== 1 || ev.target !== document.body)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
pickerBody.classList.remove('paused');
|
||||
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
|
||||
what: 'dialogPick'
|
||||
});
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onQuitClicked = function() {
|
||||
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
|
||||
what: 'dialogQuit'
|
||||
});
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onCandidateClicked = function(ev) {
|
||||
let li = ev.target.closest('li');
|
||||
const ul = li.closest('.changeFilter');
|
||||
if ( ul === null ) { return; }
|
||||
const choice = {
|
||||
filters: Array.from(ul.querySelectorAll('li')).map(a => a.textContent),
|
||||
slot: 0,
|
||||
modifier: ev.ctrlKey || ev.metaKey
|
||||
};
|
||||
while ( li.previousElementSibling !== null ) {
|
||||
li = li.previousElementSibling;
|
||||
choice.slot += 1;
|
||||
}
|
||||
taCandidate.value = candidateFromFilterChoice(choice);
|
||||
onCandidateChanged();
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onKeyPressed = function(ev) {
|
||||
// Esc
|
||||
if ( ev.key === 'Escape' || ev.which === 27 ) {
|
||||
onQuitClicked();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onStartMoving = (( ) => {
|
||||
let mx0 = 0, my0 = 0;
|
||||
let mx1 = 0, my1 = 0;
|
||||
let r0 = 0, b0 = 0;
|
||||
let rMax = 0, bMax = 0;
|
||||
let timer;
|
||||
|
||||
const move = ( ) => {
|
||||
timer = undefined;
|
||||
let r1 = Math.min(Math.max(r0 - mx1 + mx0, 4), rMax);
|
||||
let b1 = Math.min(Math.max(b0 - my1 + my0, 4), bMax);
|
||||
dialog.style.setProperty('right', `${r1}px`, 'important');
|
||||
dialog.style.setProperty('bottom', `${b1}px`, 'important');
|
||||
};
|
||||
|
||||
const moveAsync = ev => {
|
||||
if ( ev.isTrusted === false ) { return; }
|
||||
eatEvent(ev);
|
||||
if ( timer !== undefined ) { return; }
|
||||
mx1 = ev.pageX;
|
||||
my1 = ev.pageY;
|
||||
timer = self.requestAnimationFrame(move);
|
||||
};
|
||||
|
||||
const stop = ev => {
|
||||
if ( ev.isTrusted === false ) { return; }
|
||||
if ( dialog.classList.contains('moving') === false ) { return; }
|
||||
dialog.classList.remove('moving');
|
||||
self.removeEventListener('mousemove', moveAsync, { capture: true });
|
||||
self.removeEventListener('mouseup', stop, { capture: true, once: true });
|
||||
eatEvent(ev);
|
||||
};
|
||||
|
||||
return function(ev) {
|
||||
if ( ev.isTrusted === false ) { return; }
|
||||
const target = dialog.querySelector('#toolbar');
|
||||
if ( ev.target !== target ) { return; }
|
||||
if ( dialog.classList.contains('moving') ) { return; }
|
||||
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 = pickerBody.clientWidth - 4 - rect.width ;
|
||||
bMax = pickerBody.clientHeight - 4 - rect.height;
|
||||
dialog.classList.add('moving');
|
||||
self.addEventListener('mousemove', moveAsync, { capture: true });
|
||||
self.addEventListener('mouseup', stop, { capture: true, once: true });
|
||||
eatEvent(ev);
|
||||
};
|
||||
})();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const eatEvent = function(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const showDialog = function(details) {
|
||||
pickerBody.classList.add('paused');
|
||||
|
||||
const { netFilters, cosmeticFilters, filter, options } = details;
|
||||
|
||||
// https://github.com/gorhill/uBlock/issues/738
|
||||
// Trim dots.
|
||||
filterHostname = details.hostname;
|
||||
if ( filterHostname.slice(-1) === '.' ) {
|
||||
filterHostname = filterHostname.slice(0, -1);
|
||||
}
|
||||
filterOrigin = details.origin;
|
||||
|
||||
// Create lists of candidate filters
|
||||
const populate = function(src, des) {
|
||||
const root = dialog.querySelector(des);
|
||||
const ul = root.querySelector('ul');
|
||||
while ( ul.firstChild !== null ) {
|
||||
ul.firstChild.remove();
|
||||
}
|
||||
for ( let i = 0; i < src.length; i++ ) {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = src[i];
|
||||
ul.appendChild(li);
|
||||
}
|
||||
if ( src.length !== 0 ) {
|
||||
root.style.removeProperty('display');
|
||||
} else {
|
||||
root.style.setProperty('display', 'none', 'important');
|
||||
}
|
||||
};
|
||||
|
||||
populate(netFilters, '#netFilters');
|
||||
populate(cosmeticFilters, '#cosmeticFilters');
|
||||
|
||||
dialog.querySelector('ul').style.display =
|
||||
netFilters.length || cosmeticFilters.length ? '' : 'none';
|
||||
dialog.querySelector('#create').disabled = true;
|
||||
|
||||
// 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 ) {
|
||||
taCandidate.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const filterChoice = {
|
||||
filters: filter.filters,
|
||||
slot: filter.slot,
|
||||
modifier: options.modifier || false
|
||||
};
|
||||
|
||||
taCandidate.value = candidateFromFilterChoice(filterChoice);
|
||||
onCandidateChanged();
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// Let's have the element picker code flushed from memory when no longer
|
||||
// in use: to ensure this, release all local references.
|
||||
|
||||
const stopPicker = function() {
|
||||
vAPI.shutdown.remove(stopPicker);
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const pickerBody = document.body;
|
||||
const dialog = $stor('aside');
|
||||
const taCandidate = $stor('textarea');
|
||||
let staticFilteringParser;
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const startDialog = function() {
|
||||
dialog.addEventListener('click', eatEvent);
|
||||
taCandidate.addEventListener('input', onCandidateChanged);
|
||||
$stor('body').addEventListener('mousedown', onPickClicked);
|
||||
$id('preview').addEventListener('click', onPreviewClicked);
|
||||
$id('create').addEventListener('click', onCreateClicked);
|
||||
$id('pick').addEventListener('click', onPickClicked);
|
||||
$id('quit').addEventListener('click', onQuitClicked);
|
||||
$id('candidateFilters').addEventListener('click', onCandidateClicked);
|
||||
$id('toolbar').addEventListener('mousedown', onStartMoving);
|
||||
self.addEventListener('keydown', onKeyPressed, true);
|
||||
staticFilteringParser = new vAPI.StaticFilteringParser({ interactive: true });
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onPickerMessage = function(msg) {
|
||||
switch ( msg.what ) {
|
||||
case 'showDialog':
|
||||
showDialog(msg);
|
||||
break;
|
||||
case 'filterResultset':
|
||||
filterResultset = msg.resultset;
|
||||
$id('resultsetCount').textContent = filterResultset.length;
|
||||
if ( filterResultset.length !== 0 ) {
|
||||
$id('create').removeAttribute('disabled');
|
||||
} else {
|
||||
$id('create').setAttribute('disabled', '');
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onConnectionMessage = function(msg) {
|
||||
switch ( msg.what ) {
|
||||
case 'connectionBroken':
|
||||
stopPicker();
|
||||
break;
|
||||
case 'connectionMessage':
|
||||
onPickerMessage(msg.payload);
|
||||
break;
|
||||
case 'connectionAccepted':
|
||||
epickerConnectionId = msg.id;
|
||||
startDialog();
|
||||
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
|
||||
what: 'dialogInit',
|
||||
});
|
||||
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;
|
|
@ -713,34 +713,19 @@ const onMessage = function(request, sender, callback) {
|
|||
switch ( request.what ) {
|
||||
case 'elementPickerArguments':
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', 'epicker.html', true);
|
||||
xhr.open('GET', 'css/epicker.css', true);
|
||||
xhr.overrideMimeType('text/html;charset=utf-8');
|
||||
xhr.responseType = 'text';
|
||||
xhr.onload = function() {
|
||||
this.onload = null;
|
||||
const i18n = {
|
||||
bidi_dir: document.body.getAttribute('dir'),
|
||||
create: vAPI.i18n('pickerCreate'),
|
||||
pick: vAPI.i18n('pickerPick'),
|
||||
quit: vAPI.i18n('pickerQuit'),
|
||||
preview: vAPI.i18n('pickerPreview'),
|
||||
netFilters: vAPI.i18n('pickerNetFilters'),
|
||||
cosmeticFilters: vAPI.i18n('pickerCosmeticFilters'),
|
||||
cosmeticFiltersHint: vAPI.i18n('pickerCosmeticFiltersHint')
|
||||
};
|
||||
const reStrings = /\{\{(\w+)\}\}/g;
|
||||
const replacer = function(a0, string) {
|
||||
return i18n[string];
|
||||
};
|
||||
|
||||
callback({
|
||||
frameContent: this.responseText.replace(reStrings, replacer),
|
||||
frameCSS: this.responseText,
|
||||
target: µb.epickerArgs.target,
|
||||
mouse: µb.epickerArgs.mouse,
|
||||
zap: µb.epickerArgs.zap,
|
||||
eprom: µb.epickerArgs.eprom,
|
||||
dialogURL: vAPI.getURL(`/web_accessible_resources/epicker-dialog.html${vAPI.warSecret()}`),
|
||||
});
|
||||
|
||||
µb.epickerArgs.target = '';
|
||||
};
|
||||
xhr.send();
|
||||
|
@ -754,21 +739,6 @@ const onMessage = function(request, sender, callback) {
|
|||
let response;
|
||||
|
||||
switch ( request.what ) {
|
||||
case 'compileCosmeticFilterSelector': {
|
||||
const parser = new vAPI.StaticFilteringParser();
|
||||
parser.analyze(request.selector);
|
||||
if ( (parser.flavorBits & parser.BITFlavorExtCosmetic) !== 0 ) {
|
||||
response = parser.result.compiled;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// https://github.com/gorhill/uBlock/issues/3497
|
||||
// This needs to be removed once issue is fixed.
|
||||
case 'createUserFilter':
|
||||
µb.createUserFilters(request);
|
||||
break;
|
||||
|
||||
case 'elementPickerEprom':
|
||||
µb.epickerArgs.eprom = request;
|
||||
break;
|
||||
|
|
|
@ -26,27 +26,28 @@
|
|||
/******************************************************************************/
|
||||
/******************************************************************************/
|
||||
|
||||
(( ) => {
|
||||
(async ( ) => {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
if (
|
||||
window.top !== window ||
|
||||
typeof vAPI !== 'object' ||
|
||||
vAPI.domFilterer instanceof Object === false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if ( window.top !== window || typeof vAPI !== 'object' ) { return; }
|
||||
|
||||
let pickerRoot = document.getElementById(vAPI.sessionId);
|
||||
if ( pickerRoot ) { return; }
|
||||
/******************************************************************************/
|
||||
|
||||
const epickerId = vAPI.randomToken();
|
||||
let epickerConnectionId;
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
let pickerRoot = document.querySelector(`[${vAPI.sessionId}]`);
|
||||
if ( pickerRoot !== null ) { return; }
|
||||
|
||||
let pickerBootArgs;
|
||||
let pickerBody = null;
|
||||
let svgOcean = null;
|
||||
let svgIslands = null;
|
||||
let svgRoot = null;
|
||||
let dialog = null;
|
||||
let taCandidate = null;
|
||||
|
||||
const netFilterCandidates = [];
|
||||
const cosmeticFilterCandidates = [];
|
||||
|
@ -61,18 +62,6 @@ let lastNetFilterUnion = '';
|
|||
|
||||
/******************************************************************************/
|
||||
|
||||
// For browsers not supporting `:scope`, it's not the end of the world: the
|
||||
// suggested CSS selectors may just end up being more verbose.
|
||||
|
||||
let cssScope = ':scope > ';
|
||||
try {
|
||||
document.querySelector(':scope *');
|
||||
} catch (e) {
|
||||
cssScope = '';
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const safeQuerySelectorAll = function(node, selector) {
|
||||
if ( node !== null ) {
|
||||
try {
|
||||
|
@ -85,14 +74,6 @@ const safeQuerySelectorAll = function(node, selector) {
|
|||
|
||||
/******************************************************************************/
|
||||
|
||||
const rawFilterFromTextarea = function() {
|
||||
const s = taCandidate.value;
|
||||
const pos = s.indexOf('\n');
|
||||
return pos === -1 ? s.trim() : s.slice(0, pos).trim();
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const getElementBoundingClientRect = function(elem) {
|
||||
let rect = typeof elem.getBoundingClientRect === 'function'
|
||||
? elem.getBoundingClientRect()
|
||||
|
@ -152,9 +133,7 @@ const highlightElements = function(elems, force) {
|
|||
|
||||
for ( let i = 0; i < elems.length; i++ ) {
|
||||
const elem = elems[i];
|
||||
if ( elem === pickerRoot ) {
|
||||
continue;
|
||||
}
|
||||
if ( elem === pickerRoot ) { continue; }
|
||||
const rect = getElementBoundingClientRect(elem);
|
||||
|
||||
// Ignore if it's not on the screen
|
||||
|
@ -480,11 +459,11 @@ const cosmeticFilterFromElement = function(elem) {
|
|||
if ( attr.v.length === 0 ) { continue; }
|
||||
v = elem.getAttribute(attr.k);
|
||||
if ( attr.v === v ) {
|
||||
selector += '[' + attr.k + '="' + attr.v + '"]';
|
||||
selector += `[${attr.k}="${attr.v}"]`;
|
||||
} else if ( v.startsWith(attr.v) ) {
|
||||
selector += '[' + attr.k + '^="' + attr.v + '"]';
|
||||
selector += `[${attr.k}^="${attr.v}"]`;
|
||||
} else {
|
||||
selector += '[' + attr.k + '*="' + attr.v + '"]';
|
||||
selector += `[${attr.k}*="${attr.v}"]`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -495,7 +474,7 @@ const cosmeticFilterFromElement = function(elem) {
|
|||
const parentNode = elem.parentNode;
|
||||
if (
|
||||
selector === '' ||
|
||||
safeQuerySelectorAll(parentNode, cssScope + selector).length > 1
|
||||
safeQuerySelectorAll(parentNode, `:scope > ${selector}`).length > 1
|
||||
) {
|
||||
selector = tagName + selector;
|
||||
}
|
||||
|
@ -504,7 +483,7 @@ const cosmeticFilterFromElement = function(elem) {
|
|||
// If the selector is still ambiguous at this point, further narrow using
|
||||
// `nth-of-type`. It is preferable to use `nth-of-type` as opposed to
|
||||
// `nth-child`, as `nth-of-type` is less volatile.
|
||||
if ( safeQuerySelectorAll(parentNode, cssScope + selector).length > 1 ) {
|
||||
if ( safeQuerySelectorAll(parentNode, `:scope > ${selector}`).length > 1 ) {
|
||||
let i = 1;
|
||||
while ( elem.previousSibling !== null ) {
|
||||
elem = elem.previousSibling;
|
||||
|
@ -515,7 +494,7 @@ const cosmeticFilterFromElement = function(elem) {
|
|||
i++;
|
||||
}
|
||||
}
|
||||
selector += ':nth-of-type(' + i + ')';
|
||||
selector += `:nth-of-type(${i})`;
|
||||
}
|
||||
|
||||
if ( bestCandidateFilter === null ) {
|
||||
|
@ -526,7 +505,7 @@ const cosmeticFilterFromElement = function(elem) {
|
|||
};
|
||||
}
|
||||
|
||||
cosmeticFilterCandidates.push('##' + selector);
|
||||
cosmeticFilterCandidates.push(`##${selector}`);
|
||||
|
||||
return 1;
|
||||
};
|
||||
|
@ -575,7 +554,7 @@ const filtersFrom = function(x, y) {
|
|||
// https://github.com/gorhill/uBlock/issues/1545
|
||||
// Network filter candidates from all other elements found at point (x, y).
|
||||
if ( typeof x === 'number' ) {
|
||||
let attrName = pickerRoot.id + '-clickblind';
|
||||
let attrName = vAPI.sessionId + '-clickblind';
|
||||
let previous;
|
||||
elem = first;
|
||||
while ( elem !== null ) {
|
||||
|
@ -587,7 +566,7 @@ const filtersFrom = function(x, y) {
|
|||
}
|
||||
netFilterFromElement(elem);
|
||||
}
|
||||
let elems = document.querySelectorAll('[' + attrName + ']');
|
||||
let elems = document.querySelectorAll(`[${attrName}]`);
|
||||
i = elems.length;
|
||||
while ( i-- ) {
|
||||
elems[i].removeAttribute(attrName);
|
||||
|
@ -601,7 +580,7 @@ const filtersFrom = function(x, y) {
|
|||
|
||||
/*******************************************************************************
|
||||
|
||||
filterToDOMInterface.set
|
||||
filterToDOMInterface.queryAll
|
||||
@desc Look-up all the HTML elements matching the filter passed in
|
||||
argument.
|
||||
@param string, a cosmetic or network filter.
|
||||
|
@ -767,24 +746,21 @@ const filterToDOMInterface = (( ) => {
|
|||
return out;
|
||||
};
|
||||
|
||||
let lastFilter,
|
||||
lastResultset,
|
||||
lastAction,
|
||||
appliedStyleTag,
|
||||
applied = false,
|
||||
previewing = false;
|
||||
let lastFilter;
|
||||
let lastResultset;
|
||||
let lastAction;
|
||||
let appliedStyleTag;
|
||||
let applied = false;
|
||||
let previewing = false;
|
||||
|
||||
const queryAll = async function(filter, callback) {
|
||||
const queryAll = function(details) {
|
||||
let { filter, compiled } = details;
|
||||
filter = filter.trim();
|
||||
if ( filter === lastFilter ) {
|
||||
callback(lastResultset);
|
||||
return;
|
||||
}
|
||||
if ( filter === lastFilter ) { return lastResultset; }
|
||||
unapply();
|
||||
if ( filter === '' ) {
|
||||
if ( filter === '' || filter === '!' ) {
|
||||
lastFilter = '';
|
||||
lastResultset = [];
|
||||
callback(lastResultset);
|
||||
return;
|
||||
}
|
||||
lastFilter = filter;
|
||||
|
@ -792,23 +768,17 @@ const filterToDOMInterface = (( ) => {
|
|||
if ( filter.startsWith('##') === false ) {
|
||||
lastResultset = fromNetworkFilter(filter);
|
||||
if ( previewing ) { apply(); }
|
||||
callback(lastResultset);
|
||||
return;
|
||||
return lastResultset;
|
||||
}
|
||||
lastResultset = fromPlainCosmeticFilter(filter.slice(2));
|
||||
lastResultset = fromPlainCosmeticFilter(compiled);
|
||||
if ( lastResultset ) {
|
||||
if ( previewing ) { apply(); }
|
||||
callback(lastResultset);
|
||||
return;
|
||||
return lastResultset;
|
||||
}
|
||||
// Procedural cosmetic filter
|
||||
const response = await vAPI.messaging.send('elementPicker', {
|
||||
what: 'compileCosmeticFilterSelector',
|
||||
selector: filter,
|
||||
});
|
||||
lastResultset = fromCompiledCosmeticFilter(response);
|
||||
lastResultset = fromCompiledCosmeticFilter(compiled);
|
||||
if ( previewing ) { apply(); }
|
||||
callback(lastResultset);
|
||||
return lastResultset;
|
||||
};
|
||||
|
||||
// https://github.com/gorhill/uBlock/issues/1629
|
||||
|
@ -896,20 +866,19 @@ const filterToDOMInterface = (( ) => {
|
|||
// https://www.reddit.com/r/uBlockOrigin/comments/c62irc/
|
||||
// Support injecting the cosmetic filters into the DOM filterer
|
||||
// immediately rather than wait for the next page load.
|
||||
const preview = function(rawFilter, permanent = false) {
|
||||
previewing = rawFilter !== false;
|
||||
const preview = function(state, permanent = false) {
|
||||
previewing = state !== false;
|
||||
pickerBody.classList.toggle('preview', previewing);
|
||||
if ( previewing === false ) {
|
||||
return unapply();
|
||||
}
|
||||
queryAll(rawFilter, items => {
|
||||
if ( items === undefined ) { return; }
|
||||
if ( lastResultset === undefined ) { return; }
|
||||
apply();
|
||||
if ( permanent === false ) { return; }
|
||||
if ( vAPI.domFilterer instanceof Object === false ) { return; }
|
||||
const cssSelectors = new Set();
|
||||
const proceduralSelectors = new Set();
|
||||
for ( const item of items ) {
|
||||
for ( const item of lastResultset ) {
|
||||
if ( item.type !== 'cosmetic' ) { continue; }
|
||||
if ( item.raw.startsWith('{') ) {
|
||||
proceduralSelectors.add(item.raw);
|
||||
|
@ -928,246 +897,17 @@ const filterToDOMInterface = (( ) => {
|
|||
Array.from(proceduralSelectors)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
previewing: function() { return previewing; },
|
||||
preview: preview,
|
||||
set: queryAll
|
||||
get previewing() { return previewing; },
|
||||
preview,
|
||||
queryAll,
|
||||
};
|
||||
})();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const userFilterFromCandidate = function(callback) {
|
||||
let v = rawFilterFromTextarea();
|
||||
filterToDOMInterface.set(v, items => {
|
||||
if ( !items || items.length === 0 ) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
// https://github.com/gorhill/uBlock/issues/738
|
||||
// Trim dots.
|
||||
let hostname = window.location.hostname;
|
||||
if ( hostname.slice(-1) === '.' ) {
|
||||
hostname = hostname.slice(0, -1);
|
||||
}
|
||||
|
||||
// Cosmetic filter?
|
||||
if ( v.startsWith('##') ) {
|
||||
callback(hostname + v, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Assume net filter
|
||||
const opts = [];
|
||||
|
||||
// If no domain included in filter, we need domain option
|
||||
if ( v.startsWith('||') === false ) {
|
||||
opts.push(`domain=${hostname}`);
|
||||
}
|
||||
|
||||
const item = items[0];
|
||||
if ( item.opts ) {
|
||||
opts.push(item.opts);
|
||||
}
|
||||
|
||||
if ( opts.length ) {
|
||||
v += '$' + opts.join(',');
|
||||
}
|
||||
|
||||
callback(v);
|
||||
});
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onCandidateChanged = (function() {
|
||||
const process = function(items) {
|
||||
const elems = [];
|
||||
const valid = items !== undefined;
|
||||
if ( valid ) {
|
||||
for ( const item of items ) {
|
||||
elems.push(item.elem);
|
||||
}
|
||||
}
|
||||
pickerBody.querySelector('#resultsetCount').textContent = valid ?
|
||||
items.length.toLocaleString() :
|
||||
'E';
|
||||
dialog.querySelector('section').classList.toggle('invalidFilter', !valid);
|
||||
dialog.querySelector('#create').disabled = elems.length === 0;
|
||||
highlightElements(elems, true);
|
||||
};
|
||||
|
||||
return function() {
|
||||
filterToDOMInterface.set(rawFilterFromTextarea(), process);
|
||||
};
|
||||
})();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const candidateFromFilterChoice = function(filterChoice) {
|
||||
let { slot, filters } = filterChoice;
|
||||
let filter = filters[slot];
|
||||
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/47
|
||||
for ( const elem of dialog.querySelectorAll('#candidateFilters li') ) {
|
||||
elem.classList.remove('active');
|
||||
}
|
||||
|
||||
if ( filter === undefined ) { return ''; }
|
||||
|
||||
// For net filters there no such thing as a path
|
||||
if ( filter.startsWith('##') === false ) {
|
||||
dialog.querySelector(`#netFilters li:nth-of-type(${slot+1})`)
|
||||
.classList.add('active');
|
||||
return filter;
|
||||
}
|
||||
|
||||
// At this point, we have a cosmetic filter
|
||||
|
||||
dialog.querySelector(`#cosmeticFilters li:nth-of-type(${slot+1})`)
|
||||
.classList.add('active');
|
||||
|
||||
// Modifier means "target broadly". Hence:
|
||||
// - Do not compute exact path.
|
||||
// - Discard narrowing directives.
|
||||
// - Remove the id if one or more classes exist
|
||||
// TODO: should remove tag name too? ¯\_(ツ)_/¯
|
||||
if ( filterChoice.modifier ) {
|
||||
filter = filter.replace(/:nth-of-type\(\d+\)/, '');
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/162
|
||||
// Mind escaped periods: they do not denote a class identifier.
|
||||
if ( filter.charAt(2) === '#' ) {
|
||||
let pos = filter.search(/[^\\]\./);
|
||||
if ( pos !== -1 ) {
|
||||
filter = '##' + filter.slice(pos + 1);
|
||||
}
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
// Return path: the target element, then all siblings prepended
|
||||
let selector = '', joiner = '';
|
||||
for ( ; slot < filters.length; slot++ ) {
|
||||
filter = filters[slot];
|
||||
// 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(2) === '#' ) {
|
||||
filter = filter.replace(/([^\\])\..+$/, '$1');
|
||||
}
|
||||
selector = filter.slice(2) + joiner + selector;
|
||||
// Stop at any element with an id: these are unique in a web page
|
||||
if ( filter.startsWith('###') ) { break; }
|
||||
// Stop if current selector matches only one element on the page
|
||||
if ( document.querySelectorAll(selector).length === 1 ) { break; }
|
||||
joiner = ' > ';
|
||||
}
|
||||
|
||||
// https://github.com/gorhill/uBlock/issues/2519
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/17
|
||||
if (
|
||||
slot === filters.length &&
|
||||
selector.startsWith('body > ') === false &&
|
||||
document.querySelectorAll(selector).length > 1
|
||||
) {
|
||||
selector = 'body > ' + selector;
|
||||
}
|
||||
|
||||
return '##' + selector;
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const filterChoiceFromEvent = function(ev) {
|
||||
let li = ev.target;
|
||||
const isNetFilter = li.textContent.startsWith('##') === false;
|
||||
const r = {
|
||||
filters: isNetFilter ? netFilterCandidates : cosmeticFilterCandidates,
|
||||
slot: 0,
|
||||
modifier: ev.ctrlKey || ev.metaKey
|
||||
};
|
||||
while ( li.previousElementSibling !== null ) {
|
||||
li = li.previousElementSibling;
|
||||
r.slot += 1;
|
||||
}
|
||||
return r;
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onDialogClicked = function(ev) {
|
||||
if ( ev.isTrusted === false ) { return; }
|
||||
|
||||
// If the dialog is hidden, clicking on it force it to become visible.
|
||||
if ( dialog.classList.contains('hide') ) {
|
||||
dialog.classList.add('show');
|
||||
dialog.classList.remove('hide');
|
||||
}
|
||||
|
||||
else if ( ev.target === null ) {
|
||||
/* do nothing */
|
||||
}
|
||||
|
||||
else if ( ev.target.id === 'create' ) {
|
||||
// We have to exit from preview mode: this guarantees matching elements
|
||||
// will be found for the candidate filter.
|
||||
filterToDOMInterface.preview(false);
|
||||
userFilterFromCandidate((filter = undefined, isCosmetic = false) => {
|
||||
if ( filter === undefined ) { return; }
|
||||
vAPI.messaging.send('elementPicker', {
|
||||
what: 'createUserFilter',
|
||||
autoComment: true,
|
||||
filters: filter,
|
||||
origin: window.location.origin,
|
||||
pageDomain: window.location.hostname,
|
||||
killCache: isCosmetic === false,
|
||||
});
|
||||
filterToDOMInterface.preview(rawFilterFromTextarea(), true);
|
||||
stopPicker();
|
||||
});
|
||||
}
|
||||
|
||||
else if ( ev.target.id === 'pick' ) {
|
||||
unpausePicker();
|
||||
}
|
||||
|
||||
else if ( ev.target.id === 'quit' ) {
|
||||
filterToDOMInterface.preview(false);
|
||||
stopPicker();
|
||||
}
|
||||
|
||||
else if ( ev.target.id === 'preview' ) {
|
||||
if ( filterToDOMInterface.previewing() ) {
|
||||
filterToDOMInterface.preview(false);
|
||||
} else {
|
||||
filterToDOMInterface.preview(rawFilterFromTextarea());
|
||||
}
|
||||
highlightElements(targetElements, true);
|
||||
}
|
||||
|
||||
else if ( ev.target.closest('.changeFilter') !== null ) {
|
||||
taCandidate.value = candidateFromFilterChoice(filterChoiceFromEvent(ev));
|
||||
onCandidateChanged();
|
||||
}
|
||||
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const removeAllChildren = function(parent) {
|
||||
while ( parent.firstChild ) {
|
||||
parent.removeChild(parent.firstChild);
|
||||
}
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const showDialog = function(options) {
|
||||
pausePicker();
|
||||
|
||||
|
@ -1178,47 +918,15 @@ const showDialog = function(options) {
|
|||
dialog.classList.toggle('show', options.show === true);
|
||||
dialog.classList.remove('hide');
|
||||
|
||||
// Create lists of candidate filters
|
||||
const populate = function(src, des) {
|
||||
const root = dialog.querySelector(des);
|
||||
const ul = root.querySelector('ul');
|
||||
removeAllChildren(ul);
|
||||
for ( let i = 0; i < src.length; i++ ) {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = src[i];
|
||||
ul.appendChild(li);
|
||||
}
|
||||
if ( src.length !== 0 ) {
|
||||
root.style.removeProperty('display');
|
||||
} else {
|
||||
root.style.setProperty('display', 'none', 'important');
|
||||
}
|
||||
};
|
||||
|
||||
populate(netFilterCandidates, '#netFilters');
|
||||
populate(cosmeticFilterCandidates, '#cosmeticFilters');
|
||||
|
||||
dialog.querySelector('ul').style.display =
|
||||
netFilterCandidates.length || cosmeticFilterCandidates.length
|
||||
? ''
|
||||
: 'none';
|
||||
dialog.querySelector('#create').disabled = true;
|
||||
|
||||
// Auto-select a candidate filter
|
||||
|
||||
if ( bestCandidateFilter === null ) {
|
||||
taCandidate.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const filterChoice = {
|
||||
filters: bestCandidateFilter.filters,
|
||||
slot: bestCandidateFilter.slot,
|
||||
modifier: options.modifier || false
|
||||
};
|
||||
|
||||
taCandidate.value = candidateFromFilterChoice(filterChoice);
|
||||
onCandidateChanged();
|
||||
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
|
||||
what: 'showDialog',
|
||||
hostname: self.location.hostname,
|
||||
origin: self.location.origin,
|
||||
netFilters: netFilterCandidates,
|
||||
cosmeticFilters: cosmeticFilterCandidates,
|
||||
filter: bestCandidateFilter,
|
||||
options,
|
||||
});
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
@ -1284,7 +992,7 @@ const elementFromPoint = (( ) => {
|
|||
|
||||
/******************************************************************************/
|
||||
|
||||
const onSvgHovered = (function() {
|
||||
const onSvgHovered = (( ) => {
|
||||
let timer;
|
||||
let mx = 0, my = 0;
|
||||
|
||||
|
@ -1315,7 +1023,7 @@ const onSvgHovered = (function() {
|
|||
|
||||
*/
|
||||
|
||||
const onSvgTouchStartStop = (function() {
|
||||
const onSvgTouchStartStop = (( ) => {
|
||||
var startX,
|
||||
startY;
|
||||
return function onTouch(ev) {
|
||||
|
@ -1380,8 +1088,8 @@ const onSvgClicked = function(ev) {
|
|||
|
||||
// If zap mode, highlight element under mouse, this makes the zapper usable
|
||||
// on touch screens.
|
||||
if ( pickerBody.classList.contains('zap') ) {
|
||||
var elem = targetElements.lenght !== 0 && targetElements[0];
|
||||
if ( pickerBootArgs.zap ) {
|
||||
let elem = targetElements.lenght !== 0 && targetElements[0];
|
||||
if ( !elem || ev.target !== svgIslands ) {
|
||||
elem = elementFromPoint(ev.clientX, ev.clientY);
|
||||
if ( elem !== null ) {
|
||||
|
@ -1400,7 +1108,7 @@ const onSvgClicked = function(ev) {
|
|||
// - click outside dialog AND
|
||||
// - not in preview mode
|
||||
if ( pickerBody.classList.contains('paused') ) {
|
||||
if ( filterToDOMInterface.previewing() === false ) {
|
||||
if ( filterToDOMInterface.previewing === false ) {
|
||||
unpausePicker();
|
||||
}
|
||||
return;
|
||||
|
@ -1417,7 +1125,7 @@ const onSvgClicked = function(ev) {
|
|||
/******************************************************************************/
|
||||
|
||||
const svgListening = function(on) {
|
||||
var action = (on ? 'add' : 'remove') + 'EventListener';
|
||||
const action = (on ? 'add' : 'remove') + 'EventListener';
|
||||
svgRoot[action]('mousemove', onSvgHovered, { passive: true });
|
||||
};
|
||||
|
||||
|
@ -1427,7 +1135,7 @@ const onKeyPressed = function(ev) {
|
|||
// Delete
|
||||
if (
|
||||
(ev.key === 'Delete' || ev.key === 'Backspace') &&
|
||||
pickerBody.classList.contains('zap')
|
||||
pickerBootArgs.zap
|
||||
) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
@ -1456,65 +1164,6 @@ const onScrolled = function() {
|
|||
|
||||
/******************************************************************************/
|
||||
|
||||
const onStartMoving = (( ) => {
|
||||
let mx0 = 0, my0 = 0;
|
||||
let mx1 = 0, my1 = 0;
|
||||
let r0 = 0, b0 = 0;
|
||||
let rMax = 0, bMax = 0;
|
||||
let timer;
|
||||
|
||||
const move = ( ) => {
|
||||
timer = undefined;
|
||||
let r1 = Math.min(Math.max(r0 - mx1 + mx0, 4), rMax);
|
||||
let b1 = Math.min(Math.max(b0 - my1 + my0, 4), bMax);
|
||||
dialog.style.setProperty('right', `${r1}px`, 'important');
|
||||
dialog.style.setProperty('bottom', `${b1}px`, 'important');
|
||||
};
|
||||
|
||||
const moveAsync = ev => {
|
||||
if ( ev.isTrusted === false ) { return; }
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
if ( timer !== undefined ) { return; }
|
||||
mx1 = ev.pageX;
|
||||
my1 = ev.pageY;
|
||||
timer = self.requestAnimationFrame(move);
|
||||
};
|
||||
|
||||
const stop = ev => {
|
||||
if ( ev.isTrusted === false ) { return; }
|
||||
if ( dialog.classList.contains('moving') === false ) { return; }
|
||||
dialog.classList.remove('moving');
|
||||
const pickerWin = pickerRoot.contentWindow;
|
||||
pickerWin.removeEventListener('mousemove', moveAsync, { capture: true });
|
||||
pickerWin.removeEventListener('mouseup', stop, { capture: true, once: true });
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
return function(ev) {
|
||||
if ( ev.isTrusted === false ) { return; }
|
||||
const target = dialog.querySelector('#toolbar');
|
||||
if ( ev.target !== target ) { return; }
|
||||
if ( dialog.classList.contains('moving') ) { return; }
|
||||
mx0 = ev.pageX; my0 = ev.pageY;
|
||||
const pickerWin = pickerRoot.contentWindow;
|
||||
const style = pickerWin.getComputedStyle(dialog);
|
||||
r0 = parseInt(style.right, 10);
|
||||
b0 = parseInt(style.bottom, 10);
|
||||
const rect = dialog.getBoundingClientRect();
|
||||
rMax = pickerBody.clientWidth - 4 - rect.width ;
|
||||
bMax = pickerBody.clientHeight - 4 - rect.height;
|
||||
dialog.classList.add('moving');
|
||||
pickerWin.addEventListener('mousemove', moveAsync, { capture: true });
|
||||
pickerWin.addEventListener('mouseup', stop, { capture: true, once: true });
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
};
|
||||
})();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const pausePicker = function() {
|
||||
pickerBody.classList.add('paused');
|
||||
svgListening(false);
|
||||
|
@ -1544,91 +1193,36 @@ const stopPicker = function() {
|
|||
|
||||
// https://github.com/gorhill/uBlock/issues/2060
|
||||
if ( vAPI.domFilterer instanceof Object ) {
|
||||
vAPI.userStylesheet.remove(pickerCSS1);
|
||||
vAPI.userStylesheet.remove(pickerCSS2);
|
||||
vAPI.userStylesheet.remove(pickerCSS);
|
||||
vAPI.userStylesheet.apply();
|
||||
}
|
||||
vAPI.domFilterer.unexcludeNode(pickerRoot);
|
||||
}
|
||||
|
||||
window.removeEventListener('scroll', onScrolled, true);
|
||||
svgListening(false);
|
||||
pickerRoot.parentNode.removeChild(pickerRoot);
|
||||
pickerRoot = pickerBody =
|
||||
svgRoot = svgOcean = svgIslands =
|
||||
dialog = taCandidate = null;
|
||||
pickerRoot.remove();
|
||||
pickerRoot = pickerBody = svgRoot = svgOcean = svgIslands = dialog = null;
|
||||
|
||||
window.focus();
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const startPicker = function(details) {
|
||||
pickerRoot.addEventListener('load', stopPicker);
|
||||
// Auto-select a specific target, if any, and if possible
|
||||
|
||||
const frameDoc = pickerRoot.contentDocument;
|
||||
const parsedDom = (new DOMParser()).parseFromString(
|
||||
details.frameContent,
|
||||
'text/html'
|
||||
);
|
||||
|
||||
// Provide an id users can use as anchor to personalize uBO's element
|
||||
// picker style properties.
|
||||
parsedDom.documentElement.id = 'ublock0-epicker';
|
||||
|
||||
// https://github.com/gorhill/uBlock/issues/2240
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/170
|
||||
// Remove the already declared inline style tag: we will create a new
|
||||
// one based on the removed one, and replace the old one.
|
||||
let style = parsedDom.querySelector('style');
|
||||
const styleText = style.textContent;
|
||||
style.parentNode.removeChild(style);
|
||||
style = frameDoc.createElement('style');
|
||||
style.textContent = styleText;
|
||||
parsedDom.head.appendChild(style);
|
||||
|
||||
frameDoc.replaceChild(
|
||||
frameDoc.adoptNode(parsedDom.documentElement),
|
||||
frameDoc.documentElement
|
||||
);
|
||||
|
||||
pickerBody = frameDoc.body;
|
||||
pickerBody.setAttribute('lang', navigator.language);
|
||||
pickerBody.classList.toggle('zap', details.zap === true);
|
||||
|
||||
dialog = pickerBody.querySelector('aside');
|
||||
dialog.addEventListener('click', onDialogClicked);
|
||||
|
||||
taCandidate = dialog.querySelector('textarea');
|
||||
taCandidate.addEventListener('input', onCandidateChanged);
|
||||
|
||||
dialog.querySelector('#toolbar').addEventListener('mousedown', onStartMoving);
|
||||
|
||||
svgRoot = pickerBody.querySelector('svg');
|
||||
svgOcean = svgRoot.firstChild;
|
||||
svgIslands = svgRoot.lastChild;
|
||||
const startPicker = function() {
|
||||
svgRoot.addEventListener('click', onSvgClicked);
|
||||
svgRoot.addEventListener('touchstart', onSvgTouchStartStop);
|
||||
svgRoot.addEventListener('touchend', onSvgTouchStartStop);
|
||||
svgListening(true);
|
||||
|
||||
window.addEventListener('scroll', onScrolled, true);
|
||||
self.addEventListener('scroll', onScrolled, true);
|
||||
pickerRoot.contentWindow.addEventListener('keydown', onKeyPressed, true);
|
||||
pickerRoot.contentWindow.focus();
|
||||
|
||||
// Restore net filter union data if it originate from the same URL.
|
||||
const eprom = details.eprom || null;
|
||||
if ( eprom !== null && eprom.lastNetFilterSession === lastNetFilterSession ) {
|
||||
lastNetFilterHostname = eprom.lastNetFilterHostname || '';
|
||||
lastNetFilterUnion = eprom.lastNetFilterUnion || '';
|
||||
}
|
||||
|
||||
// Auto-select a specific target, if any, and if possible
|
||||
|
||||
highlightElements([], true);
|
||||
|
||||
// Try using mouse position
|
||||
if (
|
||||
details.mouse &&
|
||||
pickerBootArgs.mouse &&
|
||||
typeof vAPI.mouseClick.x === 'number' &&
|
||||
vAPI.mouseClick.x > 0
|
||||
) {
|
||||
|
@ -1639,7 +1233,7 @@ const startPicker = function(details) {
|
|||
}
|
||||
|
||||
// No mouse position available, use suggested target
|
||||
const target = details.target || '';
|
||||
const target = pickerBootArgs.target || '';
|
||||
const pos = target.indexOf('\t');
|
||||
if ( pos === -1 ) { return; }
|
||||
|
||||
|
@ -1660,16 +1254,10 @@ const startPicker = function(details) {
|
|||
if ( elem === pickerRoot ) { continue; }
|
||||
const src = elem[attr];
|
||||
if ( typeof src !== 'string' ) { continue; }
|
||||
if (
|
||||
(src !== url) &&
|
||||
(src !== '' || url !== 'about:blank')
|
||||
) {
|
||||
if ( (src !== url) && (src !== '' || url !== 'about:blank') ) {
|
||||
continue;
|
||||
}
|
||||
elem.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
elem.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
filtersFrom(elem);
|
||||
showDialog({ modifier: true });
|
||||
return;
|
||||
|
@ -1681,18 +1269,71 @@ const startPicker = function(details) {
|
|||
|
||||
/******************************************************************************/
|
||||
|
||||
const bootstrapPicker = async function() {
|
||||
vAPI.shutdown.add(stopPicker);
|
||||
const details = await vAPI.messaging.send('elementPicker', {
|
||||
what: 'elementPickerArguments',
|
||||
const onDialogMessage = function(msg) {
|
||||
switch ( msg.what ) {
|
||||
case 'dialogInit':
|
||||
startPicker();
|
||||
break;
|
||||
case 'dialogPreview':
|
||||
filterToDOMInterface.preview(msg.state);
|
||||
break;
|
||||
case 'dialogCreate':
|
||||
filterToDOMInterface.queryAll(msg);
|
||||
filterToDOMInterface.preview(true, true);
|
||||
stopPicker();
|
||||
break;
|
||||
case 'dialogPick':
|
||||
unpausePicker();
|
||||
break;
|
||||
case 'dialogQuit':
|
||||
filterToDOMInterface.preview(false);
|
||||
stopPicker();
|
||||
break;
|
||||
case 'dialogSetFilter': {
|
||||
const resultset = filterToDOMInterface.queryAll(msg);
|
||||
highlightElements(resultset.map(a => a.elem), true);
|
||||
if ( msg.filter === '!' ) { break; }
|
||||
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
|
||||
what: 'filterResultset',
|
||||
resultset: resultset.map(a => {
|
||||
const o = Object.assign({}, a);
|
||||
o.elem = undefined;
|
||||
return o;
|
||||
}),
|
||||
});
|
||||
startPicker(details);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onConnectionMessage = function(msg) {
|
||||
if (
|
||||
msg.from !== `epickerDialog-${epickerId}` ||
|
||||
msg.to !== `epicker-${epickerId}`
|
||||
) {
|
||||
return;
|
||||
}
|
||||
switch ( msg.what ) {
|
||||
case 'connectionRequested':
|
||||
epickerConnectionId = msg.id;
|
||||
return true;
|
||||
case 'connectionBroken':
|
||||
stopPicker();
|
||||
break;
|
||||
case 'connectionMessage':
|
||||
onDialogMessage(msg.payload);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
pickerRoot = document.createElement('iframe');
|
||||
pickerRoot.id = vAPI.sessionId;
|
||||
pickerRoot.setAttribute(vAPI.sessionId, '');
|
||||
|
||||
const pickerCSSStyle = [
|
||||
'background: transparent',
|
||||
|
@ -1720,38 +1361,104 @@ const pickerCSSStyle = [
|
|||
pickerRoot.style.cssText = pickerCSSStyle;
|
||||
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/393
|
||||
// This needs to be injected as an inline style, *never* as a user styles,
|
||||
// This needs to be injected as an inline style, *never* as a user style,
|
||||
// hence why it's not added above as part of the pickerCSSStyle
|
||||
// properties.
|
||||
pickerRoot.style.setProperty('pointer-events', 'auto', 'important');
|
||||
|
||||
const pickerCSS1 = [
|
||||
`#${pickerRoot.id} {`,
|
||||
pickerCSSStyle,
|
||||
'}'
|
||||
].join('\n');
|
||||
const pickerCSS2 = [
|
||||
`[${pickerRoot.id}-clickblind] {`,
|
||||
'pointer-events: none !important;',
|
||||
'}'
|
||||
].join('\n');
|
||||
const pickerCSS = `
|
||||
[${vAPI.sessionId}] {
|
||||
${pickerCSSStyle}
|
||||
}
|
||||
[${vAPI.sessionId}-clickblind] {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
`;
|
||||
|
||||
{
|
||||
const pickerRootLoaded = new Promise(resolve => {
|
||||
pickerRoot.addEventListener('load', ( ) => { resolve(); }, { once: true });
|
||||
});
|
||||
document.documentElement.append(pickerRoot);
|
||||
|
||||
const results = await Promise.all([
|
||||
vAPI.messaging.send('elementPicker', { what: 'elementPickerArguments' }),
|
||||
pickerRootLoaded,
|
||||
]);
|
||||
|
||||
pickerBootArgs = results[0];
|
||||
|
||||
// The DOM filterer will not be present when cosmetic filtering is
|
||||
// disabled.
|
||||
if (
|
||||
pickerBootArgs.zap !== true &&
|
||||
vAPI.domFilterer instanceof Object === false
|
||||
) {
|
||||
pickerRoot.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore net filter union data if origin is the same.
|
||||
const eprom = pickerBootArgs.eprom || null;
|
||||
if ( eprom !== null && eprom.lastNetFilterSession === lastNetFilterSession ) {
|
||||
lastNetFilterHostname = eprom.lastNetFilterHostname || '';
|
||||
lastNetFilterUnion = eprom.lastNetFilterUnion || '';
|
||||
}
|
||||
|
||||
const frameDoc = pickerRoot.contentDocument;
|
||||
|
||||
// Provide an id users can use as anchor to personalize uBO's element
|
||||
// picker style properties.
|
||||
frameDoc.documentElement.id = 'ublock0-epicker';
|
||||
|
||||
// https://github.com/gorhill/uBlock/issues/2240
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/170
|
||||
// Remove the already declared inline style tag: we will create a new
|
||||
// one based on the removed one, and replace the old one.
|
||||
const style = frameDoc.createElement('style');
|
||||
style.textContent = pickerBootArgs.frameCSS;
|
||||
frameDoc.head.appendChild(style);
|
||||
|
||||
pickerBody = frameDoc.body;
|
||||
pickerBody.setAttribute('lang', navigator.language);
|
||||
pickerBody.classList.toggle('zap', pickerBootArgs.zap === true);
|
||||
|
||||
svgRoot = frameDoc.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svgOcean = frameDoc.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
svgRoot.append(svgOcean);
|
||||
svgIslands = frameDoc.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
svgRoot.append(svgIslands);
|
||||
pickerBody.append(svgRoot);
|
||||
|
||||
dialog = frameDoc.createElement('iframe');
|
||||
pickerBody.append(dialog);
|
||||
}
|
||||
|
||||
highlightElements([], true);
|
||||
|
||||
// https://github.com/gorhill/uBlock/issues/1529
|
||||
// In addition to inline styles, harden the element picker styles by using
|
||||
// dedicated CSS rules.
|
||||
vAPI.userStylesheet.add(pickerCSS1);
|
||||
vAPI.userStylesheet.add(pickerCSS2);
|
||||
vAPI.userStylesheet.add(pickerCSS);
|
||||
vAPI.userStylesheet.apply();
|
||||
|
||||
vAPI.shutdown.add(stopPicker);
|
||||
|
||||
// https://github.com/gorhill/uBlock/issues/3497
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/1215
|
||||
// Instantiate isolated element picker dialog.
|
||||
if ( pickerBootArgs.zap === true ) {
|
||||
startPicker();
|
||||
return;
|
||||
}
|
||||
|
||||
// https://github.com/gorhill/uBlock/issues/2060
|
||||
vAPI.domFilterer.excludeNode(pickerRoot);
|
||||
|
||||
pickerRoot.addEventListener(
|
||||
'load',
|
||||
( ) => { bootstrapPicker(); },
|
||||
{ once: true }
|
||||
);
|
||||
document.documentElement.appendChild(pickerRoot);
|
||||
if ( await vAPI.messaging.extend() !== true ) { return; }
|
||||
vAPI.MessagingConnection.addListener(onConnectionMessage);
|
||||
|
||||
dialog.contentWindow.location = `${pickerBootArgs.dialogURL}&epid=${epickerId}`;
|
||||
|
||||
/******************************************************************************/
|
||||
|
|
@ -428,7 +428,7 @@ const matchBucket = function(url, hostname, bucket, start) {
|
|||
});
|
||||
|
||||
await vAPI.tabs.executeScript(tabId, {
|
||||
file: '/js/scriptlets/element-picker.js',
|
||||
file: '/js/scriptlets/epicker.js',
|
||||
runAt: 'document_end',
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
<!DOCTYPE html>
|
||||
<html id="ublock0-epicker">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>uBlock Origin Element Picker</title>
|
||||
<link rel="stylesheet" href="../css/epicker-dialog.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<aside>
|
||||
<section>
|
||||
<div>
|
||||
<textarea lang="en" dir="ltr" spellcheck="false"></textarea>
|
||||
<div id="resultsetCount"></div>
|
||||
</div>
|
||||
<div id="toolbar">
|
||||
<div>
|
||||
<button id="preview" type="button" data-i18n="pickerPreview"></button>
|
||||
</div>
|
||||
<div>
|
||||
<button id="create" type="button" disabled data-i18n="pickerCreate"></button>
|
||||
<button id="pick" type="button" data-i18n="pickerPick"></button>
|
||||
<button id="quit" type="button" data-i18n="pickerQuit"></button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<ul id="candidateFilters">
|
||||
<li id="netFilters">
|
||||
<span data-i18n="pickerNetFilters"></span><ul lang="en" class="changeFilter"></ul>
|
||||
</li>
|
||||
<li id="cosmeticFilters">
|
||||
<span data-i18n="pickerCosmeticFilters"></span> <span data-i18n="pickerCosmeticFiltersHint"></span>
|
||||
<ul lang="en" class="changeFilter"></ul>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<script src="../js/vapi.js"></script>
|
||||
<script src="../js/vapi-common.js"></script>
|
||||
<script src="../js/vapi-client.js"></script>
|
||||
<script src="../js/vapi-client-extra.js"></script>
|
||||
<script src="../js/i18n.js"></script>
|
||||
<script src="../js/epicker-dialog.js"></script>
|
||||
<script src="../js/static-filtering-parser.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue