this fixes #650; code reviewed changes re. #1202, #1545; fine tuning picker UI

This commit is contained in:
gorhill 2016-04-16 11:20:01 -04:00
parent 9099c09ea8
commit e475e1ece8
3 changed files with 159 additions and 128 deletions

View File

@ -42,7 +42,15 @@ button:not(:disabled):hover {
opacity: 1; opacity: 1;
} }
#create:not(:disabled) { #create:not(:disabled) {
background-color: #ffdca8; background-color: hsl(36, 100%, 83%);
border-color: hsl(36, 50%, 60%);
}
#preview {
float: left;
}
body.preview #preview {
background-color: hsl(204, 100%, 83%);
border-color: hsl(204, 50%, 60%);
} }
section { section {
border: 0; border: 0;
@ -78,7 +86,7 @@ ul {
overflow: hidden; overflow: hidden;
} }
aside > ul { aside > ul {
height: 16em; max-height: 16em;
overflow-y: auto; overflow-y: auto;
} }
aside > ul > li:first-of-type { aside > ul > li:first-of-type {
@ -124,14 +132,19 @@ svg > path:first-child {
svg > path + path { svg > path + path {
stroke: #F00; stroke: #F00;
stroke-width: 0.5px; stroke-width: 0.5px;
fill: rgba(255,31,31,0.25); fill: rgba(255,63,63,0.20);
}
body.preview svg > path:first-child {
fill: rgba(0,0,0,0.10);
}
body.preview svg > path + path {
fill: rgba(0,0,0,0.10);
} }
aside { aside {
background-color: #eee; background-color: #eee;
bottom: 4px; bottom: 4px;
box-sizing: border-box; box-sizing: border-box;
visibility: hidden; visibility: hidden;
height: calc(40% - 4px);
padding: 4px; padding: 4px;
position: fixed; position: fixed;
right: 4px; right: 4px;
@ -154,7 +167,8 @@ body.paused > aside:hover {
<section> <section>
<textarea lang="en" dir="ltr" spellcheck="false"></textarea> <textarea lang="en" dir="ltr" spellcheck="false"></textarea>
<div><!-- <div><!--
--><button id="create" type="button" disabled="disabled">{{create}}</button><!-- --><button id="preview" type="button">{{preview}}</button><!--
--><button id="create" type="button" disabled>{{create}}</button><!--
--><button id="pick" type="button">{{pick}}</button><!-- --><button id="pick" type="button">{{pick}}</button><!--
--><button id="quit" type="button">{{quit}}</button><!-- --><button id="quit" type="button">{{quit}}</button><!--
--></div> --></div>

View File

@ -19,8 +19,6 @@
Home: https://github.com/gorhill/uBlock Home: https://github.com/gorhill/uBlock
*/ */
/* global µBlock, vAPI */
/******************************************************************************/ /******************************************************************************/
/******************************************************************************/ /******************************************************************************/
@ -603,6 +601,7 @@ var onMessage = function(request, sender, callback) {
create: vAPI.i18n('pickerCreate'), create: vAPI.i18n('pickerCreate'),
pick: vAPI.i18n('pickerPick'), pick: vAPI.i18n('pickerPick'),
quit: vAPI.i18n('pickerQuit'), quit: vAPI.i18n('pickerQuit'),
preview: vAPI.i18n('pickerPreview'),
netFilters: vAPI.i18n('pickerNetFilters'), netFilters: vAPI.i18n('pickerNetFilters'),
cosmeticFilters: vAPI.i18n('pickerCosmeticFilters'), cosmeticFilters: vAPI.i18n('pickerCosmeticFilters'),
cosmeticFiltersHint: vAPI.i18n('pickerCosmeticFiltersHint') cosmeticFiltersHint: vAPI.i18n('pickerCosmeticFiltersHint')

View File

@ -19,7 +19,7 @@
Home: https://github.com/gorhill/uBlock Home: https://github.com/gorhill/uBlock
*/ */
/* global CSS, CSSImportRule, CSSStyleRule */ /* global CSS */
/******************************************************************************/ /******************************************************************************/
/******************************************************************************/ /******************************************************************************/
@ -133,7 +133,7 @@ var pickerRoot = document.getElementById(vAPI.sessionId);
if ( pickerRoot ) { if ( pickerRoot ) {
return; return;
} }
var pickerBody = null;
var pickerStyle = null; var pickerStyle = null;
var svgOcean = null; var svgOcean = null;
var svgIslands = null; var svgIslands = null;
@ -145,6 +145,9 @@ var netFilterCandidates = [];
var cosmeticFilterCandidates = []; var cosmeticFilterCandidates = [];
var targetElements = []; var targetElements = [];
var candidateElements = [];
var bestCandidateFilter = null;
var previewedElements = [];
var lastNetFilterSession = window.location.host + window.location.pathname; var lastNetFilterSession = window.location.host + window.location.pathname;
var lastNetFilterHostname = ''; var lastNetFilterHostname = '';
@ -267,26 +270,51 @@ var highlightElements = function(elems, force) {
var filterElements = function(filter) { var filterElements = function(filter) {
var items = elementsFromFilter(filter); var items = elementsFromFilter(filter);
var i = items.length, item; var i = items.length, item, elem, style;
while ( i-- ) { while ( i-- ) {
item = items[i]; item = items[i];
elem = item.elem;
style = elem.style;
if ( if (
item.type === 'cosmetic' || item.type === 'cosmetic' ||
item.type === 'network' && item.src !== undefined item.type === 'network' && item.src !== undefined
) { ) {
item.elem.style.setProperty('display', 'none', 'important'); previewedElements.push({
elem: elem,
prop: 'display',
value: style.display
});
style.display = 'none';
} }
if ( item.type === 'network' && item.style === 'background-image' ) { if ( item.type === 'network' && item.style === 'background-image' ) {
item.elem.style.setProperty('background-image', 'none', 'important'); previewedElements.push({
elem: elem,
prop: 'background-image',
value: style.backgroundImage
});
style.backgroundImage = 'none';
} }
} }
}; };
/******************************************************************************/ /******************************************************************************/
var urlFromCSSPropertyValue = function(value) { var preview = function(filter) {
var matches = /^url\((["']?)([^"']+)\1\)$/.exec(value); filterElements(filter);
return matches !== null && matches.length === 3 ? matches[2] : ''; pickerBody.classList.add('preview');
};
/******************************************************************************/
var unpreview = function() {
var items = previewedElements;
var i = items.length, item;
while ( i-- ) {
item = items[i];
item.elem.style[item.prop] = item.value;
}
previewedElements.length = 0;
pickerBody.classList.remove('preview');
}; };
/******************************************************************************/ /******************************************************************************/
@ -294,7 +322,8 @@ var urlFromCSSPropertyValue = function(value) {
var backgroundImageURLFromElement = function(elem) { var backgroundImageURLFromElement = function(elem) {
var style = window.getComputedStyle(elem); var style = window.getComputedStyle(elem);
var bgImg = style.backgroundImage || ''; var bgImg = style.backgroundImage || '';
return bgImg !== '' ? urlFromCSSPropertyValue(bgImg) : ''; var matches = /^url\((["']?)([^"']+)\1\)$/.exec(bgImg);
return matches !== null && matches.length === 3 ? matches[2] : '';
}; };
/******************************************************************************/ /******************************************************************************/
@ -399,18 +428,25 @@ var netFilterFromUnion = (function() {
// Extract the best possible net filter, i.e. as specific as possible. // Extract the best possible net filter, i.e. as specific as possible.
var netFilterFromElement = function(elem, out) { var netFilterFromElement = function(elem) {
if ( elem === null ) { if ( elem === null ) {
return; return 0;
} }
if ( elem.nodeType !== 1 ) { if ( elem.nodeType !== 1 ) {
return; return 0;
} }
var src = resourceURLFromElement(elem); var src = resourceURLFromElement(elem);
if ( src === '' ) { if ( src === '' ) {
return; return 0;
} }
if ( candidateElements.indexOf(elem) === -1 ) {
candidateElements.push(elem);
}
var candidates = netFilterCandidates;
var len = candidates.length;
// Remove fragment // Remove fragment
var pos = src.indexOf('#'); var pos = src.indexOf('#');
if ( pos !== -1 ) { if ( pos !== -1 ) {
@ -419,17 +455,25 @@ var netFilterFromElement = function(elem, out) {
var filter = src.replace(/^https?:\/\//, '||'); var filter = src.replace(/^https?:\/\//, '||');
// Anchor absolute filter to hostname if ( bestCandidateFilter === null ) {
out.push(filter); bestCandidateFilter = {
filters: candidates,
slot: candidates.length
};
}
candidates.push(filter);
// Suggest a less narrow filter if possible // Suggest a less narrow filter if possible
pos = filter.indexOf('?'); pos = filter.indexOf('?');
if ( pos !== -1 ) { if ( pos !== -1 ) {
out.push(filter.slice(0, pos)); candidates.push(filter.slice(0, pos));
} }
// Suggest a filter which is a result of combining more than one URL. // Suggest a filter which is a result of combining more than one URL.
netFilterFromUnion(src, out); netFilterFromUnion(src, candidates);
return candidates.length - len;
}; };
var netFilter1stSources = { var netFilter1stSources = {
@ -458,13 +502,18 @@ var filterTypes = {
// Extract the best possible cosmetic filter, i.e. as specific as possible. // Extract the best possible cosmetic filter, i.e. as specific as possible.
var cosmeticFilterFromElement = function(elem, out) { var cosmeticFilterFromElement = function(elem) {
if ( elem === null ) { if ( elem === null ) {
return; return 0;
} }
if ( elem.nodeType !== 1 ) { if ( elem.nodeType !== 1 ) {
return; return 0;
} }
if ( candidateElements.indexOf(elem) === -1 ) {
candidateElements.push(elem);
}
var tagName = elem.tagName.toLowerCase(); var tagName = elem.tagName.toLowerCase();
var prefix = ''; var prefix = '';
var suffix = []; var suffix = [];
@ -549,22 +598,25 @@ var cosmeticFilterFromElement = function(elem, out) {
selector += ':nth-of-type(' + i + ')'; selector += ':nth-of-type(' + i + ')';
} }
out.push('##' + selector); if ( bestCandidateFilter === null ) {
bestCandidateFilter = {
filters: cosmeticFilterCandidates,
slot: cosmeticFilterCandidates.length
};
}
cosmeticFilterCandidates.push('##' + selector);
return 1;
}; };
/******************************************************************************/ /******************************************************************************/
var filtersFrom = function(x, y) { var filtersFrom = function(x, y) {
bestCandidateFilter = null;
netFilterCandidates.length = 0; netFilterCandidates.length = 0;
cosmeticFilterCandidates.length = 0; cosmeticFilterCandidates.length = 0;
candidateElements.length = 0;
// This is to prevent revisiting the same element more than once.
var visited = typeof Set === 'function' ?
new Set() :
{
add: function() {},
has: function() { return true; }
};
// We need at least one element. // We need at least one element.
var first = null; var first = null;
@ -574,16 +626,16 @@ var filtersFrom = function(x, y) {
first = x; first = x;
x = undefined; x = undefined;
} }
if ( first === null ) {
return 0; // Network filter from element which was clicked.
if ( first !== null ) {
netFilterFromElement(first);
} }
// Extract filter candidates from self and all ancestors. // Cosmetic filter candidates from ancestors.
var elem = first; var elem = first;
while ( elem && elem !== document.body ) { while ( elem && elem !== document.body ) {
netFilterFromElement(elem, netFilterCandidates); cosmeticFilterFromElement(elem);
cosmeticFilterFromElement(elem, cosmeticFilterCandidates);
visited.add(elem);
elem = elem.parentNode; elem = elem.parentNode;
} }
// The body tag is needed as anchor only when the immediate child // The body tag is needed as anchor only when the immediate child
@ -594,28 +646,27 @@ var filtersFrom = function(x, y) {
} }
// https://github.com/gorhill/uBlock/issues/1545 // https://github.com/gorhill/uBlock/issues/1545
// Extract filter candidates from all elements found at point (x, y). // Network filter candidates from all other elements found at point (x, y).
if ( typeof x === 'number' ) { if ( typeof x === 'number' ) {
var attrName = vAPI.sessionId + '-clickblind'; var attrName = vAPI.sessionId + '-clickblind';
var previous; var previous;
elem = first; elem = first;
for (;;) { while ( elem !== null ) {
previous = elem; previous = elem;
elem.setAttribute(attrName, ''); elem.setAttribute(attrName, '');
elem = elementFromPoint(x, y, true); elem = elementFromPoint(x, y);
if ( elem === null || elem === previous ) { if ( elem === null || elem === previous ) {
break; break;
} }
if ( visited.has(elem) === false ) { netFilterFromElement(elem);
netFilterFromElement(elem, netFilterCandidates);
visited.add(elem);
}
} }
var elems = document.querySelectorAll('[' + attrName + ']'); var elems = document.querySelectorAll('[' + attrName + ']');
i = elems.length; i = elems.length;
while ( i-- ) { while ( i-- ) {
elems[i].removeAttribute(attrName); elems[i].removeAttribute(attrName);
} }
netFilterFromElement(document.body);
} }
return netFilterCandidates.length + cosmeticFilterCandidates.length; return netFilterCandidates.length + cosmeticFilterCandidates.length;
@ -623,45 +674,6 @@ var filtersFrom = function(x, y) {
/******************************************************************************/ /******************************************************************************/
var elementsFromStylesheet = function(sheet, reURL, out) {
var rules = sheet.rules;
if ( !rules ) {
return;
}
var iRule = rules.length;
var rule, value, src, elems, iElem;
while ( iRule-- ) {
rule = rules[iRule];
if ( rule instanceof CSSImportRule ) {
elementsFromStylesheet(rule.styleSheet, reURL, out);
continue;
}
if ( rule instanceof CSSStyleRule === false ) {
continue;
}
value = rule.style.backgroundImage;
if ( value.lastIndexOf('url(', 0) !== 0 ) {
continue;
}
src = urlFromCSSPropertyValue(value);
if ( reURL.test(src) === false ) {
continue;
}
elems = document.querySelectorAll(rule.selectorText);
iElem = elems.length;
while ( iElem-- ) {
out.push({
type: 'network',
elem: elems[iElem],
style: 'background-image',
opts: 'image'
});
}
}
};
/******************************************************************************/
var elementsFromFilter = function(filter) { var elementsFromFilter = function(filter) {
var out = []; var out = [];
@ -754,8 +766,8 @@ var elementsFromFilter = function(filter) {
} }
} }
// Lookup by inline-styled background image. // Find matching background image in current set of candidate elements.
elems = document.querySelectorAll('[style*="background-image"]'); elems = candidateElements;
iElem = elems.length; iElem = elems.length;
while ( iElem-- ) { while ( iElem-- ) {
elem = elems[iElem]; elem = elems[iElem];
@ -769,13 +781,6 @@ var elementsFromFilter = function(filter) {
} }
} }
// Lookup by stylesheet-styled background image.
var sheets = document.styleSheets;
var iSheet = sheets.length;
while ( iSheet-- ) {
elementsFromStylesheet(sheets[iSheet], reFilter, out);
}
return out; return out;
}; };
@ -825,13 +830,15 @@ var userFilterFromCandidate = function() {
/******************************************************************************/ /******************************************************************************/
var onCandidateChanged = function() { var onCandidateChanged = function() {
unpreview();
var elems = []; var elems = [];
var items = elementsFromFilter(taCandidate.value); var items = elementsFromFilter(taCandidate.value);
for ( var i = 0; i < items.length; i++ ) { for ( var i = 0; i < items.length; i++ ) {
elems.push(items[i].elem); elems.push(items[i].elem);
} }
dialog.querySelector('#create').disabled = elems.length === 0; dialog.querySelector('#create').disabled = elems.length === 0;
highlightElements(elems); highlightElements(elems, true);
}; };
/******************************************************************************/ /******************************************************************************/
@ -846,7 +853,7 @@ var candidateFromFilterChoice = function(filterChoice) {
} }
// For net filters there no such thing as a path // For net filters there no such thing as a path
if ( filterChoice.type === 'net' || filterChoice.modifier ) { if ( filter.lastIndexOf('##', 0) !== 0 || filterChoice.modifier ) {
return filter; return filter;
} }
@ -869,7 +876,6 @@ var filterChoiceFromEvent = function(ev) {
var li = ev.target; var li = ev.target;
var isNetFilter = li.textContent.slice(0, 2) !== '##'; var isNetFilter = li.textContent.slice(0, 2) !== '##';
var r = { var r = {
type: isNetFilter ? 'net' : 'cosmetic',
filters: isNetFilter ? netFilterCandidates : cosmeticFilterCandidates, filters: isNetFilter ? netFilterCandidates : cosmeticFilterCandidates,
slot: 0, slot: 0,
modifier: ev.ctrlKey || ev.metaKey modifier: ev.ctrlKey || ev.metaKey
@ -889,6 +895,10 @@ var onDialogClicked = function(ev) {
} }
else if ( ev.target.id === 'create' ) { else if ( ev.target.id === 'create' ) {
// We have to exit from preview mode: this guarantees matching elements
// will be found for the candidate filter.
unpreview();
var filter = userFilterFromCandidate(); var filter = userFilterFromCandidate();
if ( filter ) { if ( filter ) {
var d = new Date(); var d = new Date();
@ -909,9 +919,19 @@ var onDialogClicked = function(ev) {
} }
else if ( ev.target.id === 'quit' ) { else if ( ev.target.id === 'quit' ) {
unpreview();
stopPicker(); stopPicker();
} }
else if ( ev.target.id === 'preview' ) {
if ( pickerBody.classList.contains('preview') ) {
unpreview();
} else {
preview(taCandidate.value);
}
highlightElements(targetElements, true);
}
else if ( ev.target.parentNode.classList.contains('changeFilter') ) { else if ( ev.target.parentNode.classList.contains('changeFilter') ) {
taCandidate.value = candidateFromFilterChoice(filterChoiceFromEvent(ev)); taCandidate.value = candidateFromFilterChoice(filterChoiceFromEvent(ev));
onCandidateChanged(); onCandidateChanged();
@ -960,39 +980,31 @@ var showDialog = function(options) {
dialog.querySelector('#create').disabled = true; dialog.querySelector('#create').disabled = true;
// Auto-select a candidate filter // Auto-select a candidate filter
var filterChoice = {
type: '', if ( bestCandidateFilter === null ) {
filters: [], taCandidate.value = '';
slot: 0, return;
modifier: options.modifier || false
};
if ( netFilterCandidates.length ) {
filterChoice.type = 'net';
filterChoice.filters = netFilterCandidates;
} else if ( cosmeticFilterCandidates.length ) {
filterChoice.type = 'cosmetic';
filterChoice.filters = cosmeticFilterCandidates;
} }
taCandidate.value = ''; var filterChoice = {
if ( filterChoice.type !== '' ) { filters: bestCandidateFilter.filters,
taCandidate.value = candidateFromFilterChoice(filterChoice); slot: bestCandidateFilter.slot,
onCandidateChanged(); modifier: options.modifier || false
} };
taCandidate.value = candidateFromFilterChoice(filterChoice);
onCandidateChanged();
}; };
/******************************************************************************/ /******************************************************************************/
var elementFromPoint = function(x, y, includeBody) { var elementFromPoint = function(x, y) {
if ( !pickerRoot ) { if ( !pickerRoot ) {
return null; return null;
} }
pickerRoot.style.pointerEvents = 'none'; pickerRoot.style.pointerEvents = 'none';
var elem = document.elementFromPoint(x, y); var elem = document.elementFromPoint(x, y);
if ( if ( elem === document.body || elem === document.documentElement ) {
elem === document.body && !includeBody ||
elem === document.documentElement
) {
elem = null; elem = null;
} }
pickerRoot.style.pointerEvents = ''; pickerRoot.style.pointerEvents = '';
@ -1027,7 +1039,7 @@ var onSvgHovered = (function() {
var onSvgClicked = function(ev) { var onSvgClicked = function(ev) {
// https://github.com/chrisaljoudi/uBlock/issues/810#issuecomment-74600694 // https://github.com/chrisaljoudi/uBlock/issues/810#issuecomment-74600694
// Unpause picker if user click outside dialog // Unpause picker if user click outside dialog
if ( dialog.parentNode.classList.contains('paused') ) { if ( pickerBody.classList.contains('paused') ) {
unpausePicker(); unpausePicker();
return; return;
} }
@ -1067,14 +1079,15 @@ var onScrolled = function() {
/******************************************************************************/ /******************************************************************************/
var pausePicker = function() { var pausePicker = function() {
dialog.parentNode.classList.add('paused'); pickerBody.classList.add('paused');
svgListening(false); svgListening(false);
}; };
/******************************************************************************/ /******************************************************************************/
var unpausePicker = function() { var unpausePicker = function() {
dialog.parentNode.classList.remove('paused'); unpreview();
pickerBody.classList.remove('paused');
svgListening(true); svgListening(true);
}; };
@ -1085,6 +1098,9 @@ var unpausePicker = function() {
var stopPicker = function() { var stopPicker = function() {
targetElements = []; targetElements = [];
candidateElements = [];
bestCandidateFilter = null;
previewedElements = [];
if ( pickerRoot === null ) { if ( pickerRoot === null ) {
return; return;
@ -1100,6 +1116,7 @@ var stopPicker = function() {
pickerRoot.parentNode.removeChild(pickerRoot); pickerRoot.parentNode.removeChild(pickerRoot);
pickerRoot.onload = null; pickerRoot.onload = null;
pickerRoot = pickerRoot =
pickerBody =
dialog = dialog =
svgRoot = svgOcean = svgIslands = svgRoot = svgOcean = svgIslands =
taCandidate = null; taCandidate = null;
@ -1127,15 +1144,16 @@ var startPicker = function(details) {
frameDoc.documentElement frameDoc.documentElement
); );
frameDoc.body.setAttribute('lang', navigator.language); pickerBody = frameDoc.body;
pickerBody.setAttribute('lang', navigator.language);
dialog = frameDoc.body.querySelector('aside'); dialog = pickerBody.querySelector('aside');
dialog.addEventListener('click', onDialogClicked); dialog.addEventListener('click', onDialogClicked);
taCandidate = dialog.querySelector('textarea'); taCandidate = dialog.querySelector('textarea');
taCandidate.addEventListener('input', onCandidateChanged); taCandidate.addEventListener('input', onCandidateChanged);
svgRoot = frameDoc.body.querySelector('svg'); svgRoot = pickerBody.querySelector('svg');
svgOcean = svgRoot.firstChild; svgOcean = svgRoot.firstChild;
svgIslands = svgRoot.lastChild; svgIslands = svgRoot.lastChild;
svgRoot.addEventListener('click', onSvgClicked); svgRoot.addEventListener('click', onSvgClicked);