summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Yatskov <FooSoft@users.noreply.github.com>2019-09-02 10:41:49 -0700
committerGitHub <noreply@github.com>2019-09-02 10:41:49 -0700
commite92af787d2bfba0262ffe09451f5cb15af3a5a90 (patch)
tree0a2d9fdf77de36f68b24d8fe43ad5c03b35e1467
parent3c9f7ba15267f52dd1bf37cd8835e2f7b76819e7 (diff)
parent33076e9db9a4a4d6c33541dcfa6d76252ade95dc (diff)
Merge pull request #198 from toasted-nutbread/ignore-transparent-overlay-elements
Deep DOM scanning through transparent elements
-rw-r--r--ext/bg/js/options.js3
-rw-r--r--ext/bg/js/settings.js2
-rw-r--r--ext/bg/settings.html4
-rw-r--r--ext/fg/js/document.js150
-rw-r--r--ext/fg/js/frontend.js2
-rw-r--r--ext/mixed/js/display.js2
6 files changed, 136 insertions, 27 deletions
diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js
index c76525b9..7d993987 100644
--- a/ext/bg/js/options.js
+++ b/ext/bg/js/options.js
@@ -218,7 +218,8 @@ function optionsSetDefaults(options) {
autoHideResults: false,
delay: 20,
length: 10,
- modifier: 'shift'
+ modifier: 'shift',
+ deepDomScan: false
},
dictionaries: {},
diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js
index c4eb4842..f5d669b2 100644
--- a/ext/bg/js/settings.js
+++ b/ext/bg/js/settings.js
@@ -47,6 +47,7 @@ async function formRead() {
optionsNew.scanning.selectText = $('#select-matched-text').prop('checked');
optionsNew.scanning.alphanumeric = $('#search-alphanumeric').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.length = parseInt($('#scan-length').val(), 10);
optionsNew.scanning.modifier = $('#scan-modifier-key').val();
@@ -187,6 +188,7 @@ async function onReady() {
$('#select-matched-text').prop('checked', options.scanning.selectText);
$('#search-alphanumeric').prop('checked', options.scanning.alphanumeric);
$('#auto-hide-results').prop('checked', options.scanning.autoHideResults);
+ $('#deep-dom-scan').prop('checked', options.scanning.deepDomScan);
$('#scan-delay').val(options.scanning.delay);
$('#scan-length').val(options.scanning.length);
$('#scan-modifier-key').val(options.scanning.modifier);
diff --git a/ext/bg/settings.html b/ext/bg/settings.html
index 778dcee0..cc140023 100644
--- a/ext/bg/settings.html
+++ b/ext/bg/settings.html
@@ -192,6 +192,10 @@
<label><input type="checkbox" id="auto-hide-results"> Automatically hide results</label>
</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">
<label for="scan-delay">Scan delay (in milliseconds)</label>
<input type="number" min="1" id="scan-delay" class="form-control">
diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js
index dc2a9b87..bd876e5d 100644
--- a/ext/fg/js/document.js
+++ b/ext/fg/js/document.js
@@ -17,6 +17,8 @@
*/
+const REGEX_TRANSPARENT_COLOR = /rgba\s*\([^\)]*,\s*0(?:\.0+)?\s*\)/;
+
function docSetImposterStyle(style, propertyName, value) {
style.setProperty(propertyName, value, 'important');
}
@@ -87,11 +89,12 @@ function docImposterCreate(element, isTextarea) {
return [imposter, container];
}
-function docRangeFromPoint(point) {
- const element = document.elementFromPoint(point.x, point.y);
+function docRangeFromPoint({x, y}, options) {
+ const elements = document.elementsFromPoint(x, y);
let imposter = null;
let imposterContainer = null;
- if (element) {
+ if (elements.length > 0) {
+ const element = elements[0];
switch (element.nodeName) {
case 'IMG':
case 'BUTTON':
@@ -105,8 +108,8 @@ function docRangeFromPoint(point) {
}
}
- const range = document.caretRangeFromPoint(point.x, point.y);
- if (range !== null && isPointInRange(point, range)) {
+ const range = caretRangeFromPointExt(x, y, options.scanning.deepDomScan ? elements : []);
+ if (range !== null) {
if (imposter !== null) {
docSetImposterStyle(imposterContainer.style, 'z-index', '-2147483646');
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
const nodePre = range.endContainer;
const offsetPre = range.endOffset;
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);
- if (isPointInAnyRect(point, range.getClientRects())) {
+ if (!isWhitespace(content) && isPointInAnyRect(x, y, range.getClientRects())) {
return true;
}
} finally {
@@ -207,11 +215,11 @@ function isPointInRange(point, range) {
}
// 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);
- if (isPointInAnyRect(point, range.getClientRects())) {
- // This purposefully leaves the starting offset as modified and sets teh range length to 0.
+ if (!isWhitespace(content) && isPointInAnyRect(x, y, range.getClientRects())) {
+ // This purposefully leaves the starting offset as modified and sets the range length to 0.
range.setEnd(node, offset);
return true;
}
@@ -220,30 +228,124 @@ function isPointInRange(point, range) {
return false;
}
-function isPointInAnyRect(point, rects) {
+function isWhitespace(string) {
+ return string.trim().length === 0;
+}
+
+function isPointInAnyRect(x, y, rects) {
for (const rect of rects) {
- if (isPointInRect(point, rect)) {
+ if (isPointInRect(x, y, rect)) {
return true;
}
}
return false;
}
-function isPointInRect(point, rect) {
+function isPointInRect(x, y, rect) {
return (
- point.x >= rect.left && point.x < rect.right &&
- point.y >= rect.top && point.y < rect.bottom);
+ x >= rect.left && x < rect.right &&
+ y >= rect.top && y < rect.bottom);
}
-if (typeof document.caretRangeFromPoint !== 'function') {
- document.caretRangeFromPoint = (x, y) => {
- const position = document.caretPositionFromPoint(x, y);
- if (position && position.offsetNode && position.offsetNode.nodeType === Node.TEXT_NODE) {
+const caretRangeFromPoint = (() => {
+ if (typeof document.caretRangeFromPoint === 'function') {
+ // Chrome, Edge
+ return (x, y) => document.caretRangeFromPoint(x, y);
+ }
+
+ if (typeof document.caretPositionFromPoint === 'function') {
+ // Firefox
+ return (x, y) => {
+ const position = document.caretPositionFromPoint(x, y);
+ const node = position.offsetNode;
+ if (node === null) {
+ return null;
+ }
+
const range = document.createRange();
- range.setStart(position.offsetNode, position.offset);
- range.setEnd(position.offsetNode, position.offset);
+ 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;
+ }
}
- 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);
}
diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js
index 5a8d18c1..8a5c48d0 100644
--- a/ext/fg/js/frontend.js
+++ b/ext/fg/js/frontend.js
@@ -285,7 +285,7 @@ class Frontend {
return;
}
- const textSource = docRangeFromPoint(point);
+ const textSource = docRangeFromPoint(point, this.options);
let hideResults = !textSource || !textSource.containsPoint(point);
let searched = false;
let success = false;
diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js
index 4620e198..ebf56897 100644
--- a/ext/mixed/js/display.js
+++ b/ext/mixed/js/display.js
@@ -80,7 +80,7 @@ class Display {
const {docRangeFromPoint, docSentenceExtract} = this.dependencies;
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) {
return false;
}