Merge pull request #198 from toasted-nutbread/ignore-transparent-overlay-elements

Deep DOM scanning through transparent elements
This commit is contained in:
Alex Yatskov 2019-09-02 10:41:49 -07:00 committed by GitHub
commit e92af787d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 136 additions and 27 deletions

View File

@ -218,7 +218,8 @@ function optionsSetDefaults(options) {
autoHideResults: false, autoHideResults: false,
delay: 20, delay: 20,
length: 10, length: 10,
modifier: 'shift' modifier: 'shift',
deepDomScan: false
}, },
dictionaries: {}, dictionaries: {},

View File

@ -47,6 +47,7 @@ async function formRead() {
optionsNew.scanning.selectText = $('#select-matched-text').prop('checked'); optionsNew.scanning.selectText = $('#select-matched-text').prop('checked');
optionsNew.scanning.alphanumeric = $('#search-alphanumeric').prop('checked'); optionsNew.scanning.alphanumeric = $('#search-alphanumeric').prop('checked');
optionsNew.scanning.autoHideResults = $('#auto-hide-results').prop('checked'); optionsNew.scanning.autoHideResults = $('#auto-hide-results').prop('checked');
optionsNew.scanning.deepDomScan = $('#deep-dom-scan').prop('checked');
optionsNew.scanning.delay = parseInt($('#scan-delay').val(), 10); optionsNew.scanning.delay = parseInt($('#scan-delay').val(), 10);
optionsNew.scanning.length = parseInt($('#scan-length').val(), 10); optionsNew.scanning.length = parseInt($('#scan-length').val(), 10);
optionsNew.scanning.modifier = $('#scan-modifier-key').val(); optionsNew.scanning.modifier = $('#scan-modifier-key').val();
@ -187,6 +188,7 @@ async function onReady() {
$('#select-matched-text').prop('checked', options.scanning.selectText); $('#select-matched-text').prop('checked', options.scanning.selectText);
$('#search-alphanumeric').prop('checked', options.scanning.alphanumeric); $('#search-alphanumeric').prop('checked', options.scanning.alphanumeric);
$('#auto-hide-results').prop('checked', options.scanning.autoHideResults); $('#auto-hide-results').prop('checked', options.scanning.autoHideResults);
$('#deep-dom-scan').prop('checked', options.scanning.deepDomScan);
$('#scan-delay').val(options.scanning.delay); $('#scan-delay').val(options.scanning.delay);
$('#scan-length').val(options.scanning.length); $('#scan-length').val(options.scanning.length);
$('#scan-modifier-key').val(options.scanning.modifier); $('#scan-modifier-key').val(options.scanning.modifier);

View File

@ -192,6 +192,10 @@
<label><input type="checkbox" id="auto-hide-results"> Automatically hide results</label> <label><input type="checkbox" id="auto-hide-results"> Automatically hide results</label>
</div> </div>
<div class="checkbox options-advanced">
<label><input type="checkbox" id="deep-dom-scan"> Deep DOM scan</label>
</div>
<div class="form-group options-advanced"> <div class="form-group options-advanced">
<label for="scan-delay">Scan delay (in milliseconds)</label> <label for="scan-delay">Scan delay (in milliseconds)</label>
<input type="number" min="1" id="scan-delay" class="form-control"> <input type="number" min="1" id="scan-delay" class="form-control">

View File

@ -17,6 +17,8 @@
*/ */
const REGEX_TRANSPARENT_COLOR = /rgba\s*\([^\)]*,\s*0(?:\.0+)?\s*\)/;
function docSetImposterStyle(style, propertyName, value) { function docSetImposterStyle(style, propertyName, value) {
style.setProperty(propertyName, value, 'important'); style.setProperty(propertyName, value, 'important');
} }
@ -87,11 +89,12 @@ function docImposterCreate(element, isTextarea) {
return [imposter, container]; return [imposter, container];
} }
function docRangeFromPoint(point) { function docRangeFromPoint({x, y}, options) {
const element = document.elementFromPoint(point.x, point.y); const elements = document.elementsFromPoint(x, y);
let imposter = null; let imposter = null;
let imposterContainer = null; let imposterContainer = null;
if (element) { if (elements.length > 0) {
const element = elements[0];
switch (element.nodeName) { switch (element.nodeName) {
case 'IMG': case 'IMG':
case 'BUTTON': case 'BUTTON':
@ -105,8 +108,8 @@ function docRangeFromPoint(point) {
} }
} }
const range = document.caretRangeFromPoint(point.x, point.y); const range = caretRangeFromPointExt(x, y, options.scanning.deepDomScan ? elements : []);
if (range !== null && isPointInRange(point, range)) { if (range !== null) {
if (imposter !== null) { if (imposter !== null) {
docSetImposterStyle(imposterContainer.style, 'z-index', '-2147483646'); docSetImposterStyle(imposterContainer.style, 'z-index', '-2147483646');
docSetImposterStyle(imposter.style, 'pointer-events', 'none'); docSetImposterStyle(imposter.style, 'pointer-events', 'none');
@ -191,15 +194,20 @@ function docSentenceExtract(source, extent) {
}; };
} }
function isPointInRange(point, range) { function isPointInRange(x, y, range) {
// Require a text node to start
if (range.startContainer.nodeType !== Node.TEXT_NODE) {
return false;
}
// Scan forward // Scan forward
const nodePre = range.endContainer; const nodePre = range.endContainer;
const offsetPre = range.endOffset; const offsetPre = range.endOffset;
try { try {
const {node, offset} = TextSourceRange.seekForward(range.endContainer, range.endOffset, 1); const {node, offset, content} = TextSourceRange.seekForward(range.endContainer, range.endOffset, 1);
range.setEnd(node, offset); range.setEnd(node, offset);
if (isPointInAnyRect(point, range.getClientRects())) { if (!isWhitespace(content) && isPointInAnyRect(x, y, range.getClientRects())) {
return true; return true;
} }
} finally { } finally {
@ -207,11 +215,11 @@ function isPointInRange(point, range) {
} }
// Scan backward // Scan backward
const {node, offset} = TextSourceRange.seekBackward(range.startContainer, range.startOffset, 1); const {node, offset, content} = TextSourceRange.seekBackward(range.startContainer, range.startOffset, 1);
range.setStart(node, offset); range.setStart(node, offset);
if (isPointInAnyRect(point, range.getClientRects())) { if (!isWhitespace(content) && isPointInAnyRect(x, y, range.getClientRects())) {
// This purposefully leaves the starting offset as modified and sets teh range length to 0. // This purposefully leaves the starting offset as modified and sets the range length to 0.
range.setEnd(node, offset); range.setEnd(node, offset);
return true; return true;
} }
@ -220,30 +228,124 @@ function isPointInRange(point, range) {
return false; return false;
} }
function isPointInAnyRect(point, rects) { function isWhitespace(string) {
return string.trim().length === 0;
}
function isPointInAnyRect(x, y, rects) {
for (const rect of rects) { for (const rect of rects) {
if (isPointInRect(point, rect)) { if (isPointInRect(x, y, rect)) {
return true; return true;
} }
} }
return false; return false;
} }
function isPointInRect(point, rect) { function isPointInRect(x, y, rect) {
return ( return (
point.x >= rect.left && point.x < rect.right && x >= rect.left && x < rect.right &&
point.y >= rect.top && point.y < rect.bottom); y >= rect.top && y < rect.bottom);
} }
if (typeof document.caretRangeFromPoint !== 'function') { const caretRangeFromPoint = (() => {
document.caretRangeFromPoint = (x, y) => { if (typeof document.caretRangeFromPoint === 'function') {
const position = document.caretPositionFromPoint(x, y); // Chrome, Edge
if (position && position.offsetNode && position.offsetNode.nodeType === Node.TEXT_NODE) { return (x, y) => document.caretRangeFromPoint(x, y);
const range = document.createRange();
range.setStart(position.offsetNode, position.offset);
range.setEnd(position.offsetNode, position.offset);
return range;
} }
if (typeof document.caretPositionFromPoint === 'function') {
// Firefox
return (x, y) => {
const position = document.caretPositionFromPoint(x, y);
const node = position.offsetNode;
if (node === null) {
return null; return null;
}
const range = document.createRange();
const offset = (node.nodeType === Node.TEXT_NODE ? position.offset : 0);
range.setStart(node, offset);
range.setEnd(node, offset);
return range;
}; };
} }
// No support
return () => null;
})();
function caretRangeFromPointExt(x, y, elements) {
const modifications = [];
try {
let i = 0;
let startContinerPre = null;
while (true) {
const range = caretRangeFromPoint(x, y);
if (range === null) {
return null;
}
const startContainer = range.startContainer;
if (startContinerPre !== startContainer) {
if (isPointInRange(x, y, range)) {
return range;
}
startContinerPre = startContainer;
}
i = disableTransparentElement(elements, i, modifications);
if (i < 0) {
return null;
}
}
} finally {
if (modifications.length > 0) {
restoreElementStyleModifications(modifications);
}
}
}
function disableTransparentElement(elements, i, modifications) {
while (true) {
if (i >= elements.length) {
return -1;
}
const element = elements[i++];
if (isElementTransparent(element)) {
const style = element.hasAttribute('style') ? element.getAttribute('style') : null;
modifications.push({element, style});
element.style.pointerEvents = 'none';
return i;
}
}
}
function restoreElementStyleModifications(modifications) {
for (const {element, style} of modifications) {
if (style === null) {
element.removeAttribute('style');
} else {
element.setAttribute('style', style);
}
}
}
function isElementTransparent(element) {
if (
element === document.body ||
element === document.documentElement
) {
return false;
}
const style = window.getComputedStyle(element);
return (
parseFloat(style.opacity) < 0 ||
style.visibility === 'hidden' ||
(style.backgroundImage === 'none' && isColorTransparent(style.backgroundColor))
);
}
function isColorTransparent(cssColor) {
return REGEX_TRANSPARENT_COLOR.test(cssColor);
}

View File

@ -285,7 +285,7 @@ class Frontend {
return; return;
} }
const textSource = docRangeFromPoint(point); const textSource = docRangeFromPoint(point, this.options);
let hideResults = !textSource || !textSource.containsPoint(point); let hideResults = !textSource || !textSource.containsPoint(point);
let searched = false; let searched = false;
let success = false; let success = false;

View File

@ -80,7 +80,7 @@ class Display {
const {docRangeFromPoint, docSentenceExtract} = this.dependencies; const {docRangeFromPoint, docSentenceExtract} = this.dependencies;
const clickedElement = $(e.target); const clickedElement = $(e.target);
const textSource = docRangeFromPoint({x: e.clientX, y: e.clientY}); const textSource = docRangeFromPoint({x: e.clientX, y: e.clientY}, this.options);
if (textSource === null) { if (textSource === null) {
return false; return false;
} }