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;
}
#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 {
border: 0;
@ -78,7 +86,7 @@ ul {
overflow: hidden;
}
aside > ul {
height: 16em;
max-height: 16em;
overflow-y: auto;
}
aside > ul > li:first-of-type {
@ -124,14 +132,19 @@ svg > path:first-child {
svg > path + path {
stroke: #F00;
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 {
background-color: #eee;
bottom: 4px;
box-sizing: border-box;
visibility: hidden;
height: calc(40% - 4px);
padding: 4px;
position: fixed;
right: 4px;
@ -154,7 +167,8 @@ body.paused > aside:hover {
<section>
<textarea lang="en" dir="ltr" spellcheck="false"></textarea>
<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="quit" type="button">{{quit}}</button><!--
--></div>

View File

@ -19,8 +19,6 @@
Home: https://github.com/gorhill/uBlock
*/
/* global µBlock, vAPI */
/******************************************************************************/
/******************************************************************************/
@ -603,6 +601,7 @@ var onMessage = function(request, sender, callback) {
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')

View File

@ -19,7 +19,7 @@
Home: https://github.com/gorhill/uBlock
*/
/* global CSS, CSSImportRule, CSSStyleRule */
/* global CSS */
/******************************************************************************/
/******************************************************************************/
@ -133,7 +133,7 @@ var pickerRoot = document.getElementById(vAPI.sessionId);
if ( pickerRoot ) {
return;
}
var pickerBody = null;
var pickerStyle = null;
var svgOcean = null;
var svgIslands = null;
@ -145,6 +145,9 @@ var netFilterCandidates = [];
var cosmeticFilterCandidates = [];
var targetElements = [];
var candidateElements = [];
var bestCandidateFilter = null;
var previewedElements = [];
var lastNetFilterSession = window.location.host + window.location.pathname;
var lastNetFilterHostname = '';
@ -267,26 +270,51 @@ var highlightElements = function(elems, force) {
var filterElements = function(filter) {
var items = elementsFromFilter(filter);
var i = items.length, item;
var i = items.length, item, elem, style;
while ( i-- ) {
item = items[i];
elem = item.elem;
style = elem.style;
if (
item.type === 'cosmetic' ||
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' ) {
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 matches = /^url\((["']?)([^"']+)\1\)$/.exec(value);
return matches !== null && matches.length === 3 ? matches[2] : '';
var preview = function(filter) {
filterElements(filter);
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 style = window.getComputedStyle(elem);
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.
var netFilterFromElement = function(elem, out) {
var netFilterFromElement = function(elem) {
if ( elem === null ) {
return;
return 0;
}
if ( elem.nodeType !== 1 ) {
return;
return 0;
}
var src = resourceURLFromElement(elem);
if ( src === '' ) {
return;
return 0;
}
if ( candidateElements.indexOf(elem) === -1 ) {
candidateElements.push(elem);
}
var candidates = netFilterCandidates;
var len = candidates.length;
// Remove fragment
var pos = src.indexOf('#');
if ( pos !== -1 ) {
@ -419,17 +455,25 @@ var netFilterFromElement = function(elem, out) {
var filter = src.replace(/^https?:\/\//, '||');
// Anchor absolute filter to hostname
out.push(filter);
if ( bestCandidateFilter === null ) {
bestCandidateFilter = {
filters: candidates,
slot: candidates.length
};
}
candidates.push(filter);
// Suggest a less narrow filter if possible
pos = filter.indexOf('?');
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.
netFilterFromUnion(src, out);
netFilterFromUnion(src, candidates);
return candidates.length - len;
};
var netFilter1stSources = {
@ -458,13 +502,18 @@ var filterTypes = {
// Extract the best possible cosmetic filter, i.e. as specific as possible.
var cosmeticFilterFromElement = function(elem, out) {
var cosmeticFilterFromElement = function(elem) {
if ( elem === null ) {
return;
return 0;
}
if ( elem.nodeType !== 1 ) {
return;
return 0;
}
if ( candidateElements.indexOf(elem) === -1 ) {
candidateElements.push(elem);
}
var tagName = elem.tagName.toLowerCase();
var prefix = '';
var suffix = [];
@ -549,22 +598,25 @@ var cosmeticFilterFromElement = function(elem, out) {
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) {
bestCandidateFilter = null;
netFilterCandidates.length = 0;
cosmeticFilterCandidates.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; }
};
candidateElements.length = 0;
// We need at least one element.
var first = null;
@ -574,16 +626,16 @@ var filtersFrom = function(x, y) {
first = x;
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;
while ( elem && elem !== document.body ) {
netFilterFromElement(elem, netFilterCandidates);
cosmeticFilterFromElement(elem, cosmeticFilterCandidates);
visited.add(elem);
cosmeticFilterFromElement(elem);
elem = elem.parentNode;
}
// 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
// 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' ) {
var attrName = vAPI.sessionId + '-clickblind';
var previous;
elem = first;
for (;;) {
while ( elem !== null ) {
previous = elem;
elem.setAttribute(attrName, '');
elem = elementFromPoint(x, y, true);
elem = elementFromPoint(x, y);
if ( elem === null || elem === previous ) {
break;
}
if ( visited.has(elem) === false ) {
netFilterFromElement(elem, netFilterCandidates);
visited.add(elem);
}
netFilterFromElement(elem);
}
var elems = document.querySelectorAll('[' + attrName + ']');
i = elems.length;
while ( i-- ) {
elems[i].removeAttribute(attrName);
}
netFilterFromElement(document.body);
}
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 out = [];
@ -754,8 +766,8 @@ var elementsFromFilter = function(filter) {
}
}
// Lookup by inline-styled background image.
elems = document.querySelectorAll('[style*="background-image"]');
// Find matching background image in current set of candidate elements.
elems = candidateElements;
iElem = elems.length;
while ( 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;
};
@ -825,13 +830,15 @@ var userFilterFromCandidate = function() {
/******************************************************************************/
var onCandidateChanged = function() {
unpreview();
var elems = [];
var items = elementsFromFilter(taCandidate.value);
for ( var i = 0; i < items.length; i++ ) {
elems.push(items[i].elem);
}
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
if ( filterChoice.type === 'net' || filterChoice.modifier ) {
if ( filter.lastIndexOf('##', 0) !== 0 || filterChoice.modifier ) {
return filter;
}
@ -869,7 +876,6 @@ var filterChoiceFromEvent = function(ev) {
var li = ev.target;
var isNetFilter = li.textContent.slice(0, 2) !== '##';
var r = {
type: isNetFilter ? 'net' : 'cosmetic',
filters: isNetFilter ? netFilterCandidates : cosmeticFilterCandidates,
slot: 0,
modifier: ev.ctrlKey || ev.metaKey
@ -889,6 +895,10 @@ var onDialogClicked = function(ev) {
}
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();
if ( filter ) {
var d = new Date();
@ -909,9 +919,19 @@ var onDialogClicked = function(ev) {
}
else if ( ev.target.id === 'quit' ) {
unpreview();
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') ) {
taCandidate.value = candidateFromFilterChoice(filterChoiceFromEvent(ev));
onCandidateChanged();
@ -960,39 +980,31 @@ var showDialog = function(options) {
dialog.querySelector('#create').disabled = true;
// Auto-select a candidate filter
var filterChoice = {
type: '',
filters: [],
slot: 0,
modifier: options.modifier || false
};
if ( netFilterCandidates.length ) {
filterChoice.type = 'net';
filterChoice.filters = netFilterCandidates;
} else if ( cosmeticFilterCandidates.length ) {
filterChoice.type = 'cosmetic';
filterChoice.filters = cosmeticFilterCandidates;
if ( bestCandidateFilter === null ) {
taCandidate.value = '';
return;
}
taCandidate.value = '';
if ( filterChoice.type !== '' ) {
taCandidate.value = candidateFromFilterChoice(filterChoice);
onCandidateChanged();
}
var filterChoice = {
filters: bestCandidateFilter.filters,
slot: bestCandidateFilter.slot,
modifier: options.modifier || false
};
taCandidate.value = candidateFromFilterChoice(filterChoice);
onCandidateChanged();
};
/******************************************************************************/
var elementFromPoint = function(x, y, includeBody) {
var elementFromPoint = function(x, y) {
if ( !pickerRoot ) {
return null;
}
pickerRoot.style.pointerEvents = 'none';
var elem = document.elementFromPoint(x, y);
if (
elem === document.body && !includeBody ||
elem === document.documentElement
) {
if ( elem === document.body || elem === document.documentElement ) {
elem = null;
}
pickerRoot.style.pointerEvents = '';
@ -1027,7 +1039,7 @@ var onSvgHovered = (function() {
var onSvgClicked = function(ev) {
// https://github.com/chrisaljoudi/uBlock/issues/810#issuecomment-74600694
// Unpause picker if user click outside dialog
if ( dialog.parentNode.classList.contains('paused') ) {
if ( pickerBody.classList.contains('paused') ) {
unpausePicker();
return;
}
@ -1067,14 +1079,15 @@ var onScrolled = function() {
/******************************************************************************/
var pausePicker = function() {
dialog.parentNode.classList.add('paused');
pickerBody.classList.add('paused');
svgListening(false);
};
/******************************************************************************/
var unpausePicker = function() {
dialog.parentNode.classList.remove('paused');
unpreview();
pickerBody.classList.remove('paused');
svgListening(true);
};
@ -1085,6 +1098,9 @@ var unpausePicker = function() {
var stopPicker = function() {
targetElements = [];
candidateElements = [];
bestCandidateFilter = null;
previewedElements = [];
if ( pickerRoot === null ) {
return;
@ -1100,6 +1116,7 @@ var stopPicker = function() {
pickerRoot.parentNode.removeChild(pickerRoot);
pickerRoot.onload = null;
pickerRoot =
pickerBody =
dialog =
svgRoot = svgOcean = svgIslands =
taCandidate = null;
@ -1127,15 +1144,16 @@ var startPicker = function(details) {
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);
taCandidate = dialog.querySelector('textarea');
taCandidate.addEventListener('input', onCandidateChanged);
svgRoot = frameDoc.body.querySelector('svg');
svgRoot = pickerBody.querySelector('svg');
svgOcean = svgRoot.firstChild;
svgIslands = svgRoot.lastChild;
svgRoot.addEventListener('click', onSvgClicked);