Merge pull request #198 from toasted-nutbread/ignore-transparent-overlay-elements
Deep DOM scanning through transparent elements
This commit is contained in:
commit
e92af787d2
@ -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: {},
|
||||||
|
@ -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);
|
||||||
|
@ -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">
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user