diff options
| author | Alex Yatskov <FooSoft@users.noreply.github.com> | 2019-09-02 10:41:49 -0700 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-09-02 10:41:49 -0700 | 
| commit | e92af787d2bfba0262ffe09451f5cb15af3a5a90 (patch) | |
| tree | 0a2d9fdf77de36f68b24d8fe43ad5c03b35e1467 | |
| parent | 3c9f7ba15267f52dd1bf37cd8835e2f7b76819e7 (diff) | |
| parent | 33076e9db9a4a4d6c33541dcfa6d76252ade95dc (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.js | 3 | ||||
| -rw-r--r-- | ext/bg/js/settings.js | 2 | ||||
| -rw-r--r-- | ext/bg/settings.html | 4 | ||||
| -rw-r--r-- | ext/fg/js/document.js | 150 | ||||
| -rw-r--r-- | ext/fg/js/frontend.js | 2 | ||||
| -rw-r--r-- | ext/mixed/js/display.js | 2 | 
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;              } |