rangy icon indicating copy to clipboard operation
rangy copied to clipboard

The highlighter restore won't work perfectly.

Open poc7667 opened this issue 7 years ago • 12 comments

My highlighter css name is mydict_highlighted

THe three highlightened words are type:textContent|14682$14685$2$mydict_highlighted$|14705$14715$1$mydict_highlighted$|14821$14829$3$mydict_highlighted$

The demo page is http://www.imdb.com/title/tt2543312/

inline

After reloading the page, the result is so strange

restoreSelection() {
    var docURL = getDocumentURL();
    const storedWords = localStorage[docURL]
    if (storedWords)  {
        this.highlighter.deserialize(storedWords);
    }
}

inline

poc7667 avatar Apr 07 '17 06:04 poc7667

Hi, Chome or IE?

veremeenko avatar Apr 07 '17 16:04 veremeenko

Hi it's on chrome. i'm wirting a chrome extension

poc7667 avatar Apr 08 '17 00:04 poc7667

I had issues with highlighter in ie edge. Now i use classApllier

 rangy.getSelection().moveToBookmark(bookmark);
                   var selection = rangy.getSelection();
                   classApplier.applyToSelection();

Maybe you inserted some html code in this section? It can affects position start and end.

veremeenko avatar Apr 09 '17 12:04 veremeenko

@veremeenko I think the bug is due to the page content is dynamically generated by javascript. for example, if you try to highlight text on amazon product page and deseriliazed by reloading the page. It will give you the buggy result. I guess that's because the xpath for the selected text is inconsistent. any good sollution?

poc7667 avatar Apr 10 '17 16:04 poc7667

@poc7667 I have switched from the highlighter to bookmarks. It helped me.

veremeenko avatar Apr 11 '17 09:04 veremeenko

@veremeenko will try it later, thanks

poc7667 avatar Apr 12 '17 01:04 poc7667

Any updates on this issue, yet? I have the same issue, only that in my case the wrong are is highlighted when I refresh the page. The contents and the div exists when the page reloads, but for some reason, it is highlighting the wrong contents. It works when I put in a settimeout for 2 seconds (1 second is not enough).

And I am facing this issue in chrome.

senkaplan avatar Jan 06 '21 07:01 senkaplan

@veremeenko will try it later, thanks

Hi, I'm curious if the bookmarks solved the problem? Because I'm having the same problem now

ryzalyusoff avatar Feb 07 '21 13:02 ryzalyusoff

I found the issue and the solution to this. This issue is because even though you initialize rangy to a div, the highlight range you get is taken from the start of the html document. If there are some dynamic contents in the divs before the highlighter div, this will impact the highlight.

My solution is to find the offset of the highlighter div with respect to the html document. Then, I subtract the offset from the highlight offset before sending it to the database. Similiarly, when I get the highlight range from the database, I add the offset before deserializing the highlight.

I am using the textrange module.

Method to find Offset range:

getHighlighterRangeOffset() {
    if (this.highlighter) {
      const converter = this.highlighter.converter;
      const pageContainer = this.elementRef.nativeElement.querySelector(
        '#highlight-container'
      );
      if (pageContainer) {
        const containerRange = rangy.createRange(document);
        if (containerRange) {
          containerRange.setStart(pageContainer, 0);
          const containerCharRange = converter.rangeToCharacterRange(
            containerRange
          );
          const rangeOffset = containerCharRange.start;
          return parseInt(rangeOffset, 10);
        }
      }
    }
    return 0;
  }

Where

 this.highlighter = this.rangy.createHighlighter(
      this.elementRef.nativeElement.querySelector('#highlight-container'),
      'TextRange'
    );

Method to serialize with respective to the offset. Input is the result of 'this.highlighter.serialize()'

serializeHighlighterWithOffset(highlighterRange: string) {
    if (highlighterRange) {
      const highLighParts = highlighterRange.split('|');
      const module = highLighParts[0];

      if (module && module === 'type:TextRange') {
        const highlightContainerOffset = this.getHighlighterRangeOffset();
        let ranges = highLighParts.filter((part, index) => index > 0);
        if (ranges && ranges.filter((x) => x).length > 0) {
          ranges = ranges.map((range) => {
            const rangeParts = range.split('$');
            rangeParts[0] = (
              parseInt(rangeParts[0], 10) - highlightContainerOffset
            ).toString();
            rangeParts[1] = (
              parseInt(rangeParts[1], 10) - highlightContainerOffset
            ).toString();
            return rangeParts.join('$');
          });

          return 'type:TextRange|' + ranges.join('|');
        }
      }
    }

    return highlighterRange;
  }

Method to deserialize with respective to the offset. Input is the highlight range saved in the database'

  deserializeHighlighterWithOffset(highlighterRange: string) {
    if (highlighterRange) {
      const highLighParts = highlighterRange.split('|');
      const module = highLighParts[0];

      if (module && module === 'type:TextRange') {
        const highlightContainerOffset = this.getHighlighterRangeOffset();
        let ranges = highLighParts.filter((part, index) => index > 0);
        if (ranges && ranges.filter((x) => x).length > 0) {
          ranges = ranges.map((range) => {
            const rangeParts = range.split('$');
            rangeParts[0] = (
              parseInt(rangeParts[0], 10) + highlightContainerOffset
            ).toString();
            rangeParts[1] = (
              parseInt(rangeParts[1], 10) + highlightContainerOffset
            ).toString();
            return rangeParts.join('$');
          });

          return 'type:TextRange|' + ranges.join('|');
        }
      }
    }

    return highlighterRange;
  }
}

Hope this helps

senkaplan avatar Feb 10 '21 05:02 senkaplan

I found the issue and the solution to this. This issue is because even though you initialize rangy to a div, the highlight range you get is taken from the start of the html document. If there are some dynamic contents in the divs before the highlighter div, this will impact the highlight.

My solution is to find the offset of the highlighter div with respect to the html document. Then, I subtract the offset from the highlight offset before sending it to the database. Similiarly, when I get the highlight range from the database, I add the offset before deserializing the highlight.

I am using the textrange module.

Method to find Offset range:

getHighlighterRangeOffset() {
    if (this.highlighter) {
      const converter = this.highlighter.converter;
      const pageContainer = this.elementRef.nativeElement.querySelector(
        '#highlight-container'
      );
      if (pageContainer) {
        const containerRange = rangy.createRange(document);
        if (containerRange) {
          containerRange.setStart(pageContainer, 0);
          const containerCharRange = converter.rangeToCharacterRange(
            containerRange
          );
          const rangeOffset = containerCharRange.start;
          return parseInt(rangeOffset, 10);
        }
      }
    }
    return 0;
  }

Where

 this.highlighter = this.rangy.createHighlighter(
      this.elementRef.nativeElement.querySelector('#highlight-container'),
      'TextRange'
    );

Method to serialize with respective to the offset. Input is the result of 'this.highlighter.serialize()'

serializeHighlighterWithOffset(highlighterRange: string) {
    if (highlighterRange) {
      const highLighParts = highlighterRange.split('|');
      const module = highLighParts[0];

      if (module && module === 'type:TextRange') {
        const highlightContainerOffset = this.getHighlighterRangeOffset();
        let ranges = highLighParts.filter((part, index) => index > 0);
        if (ranges && ranges.filter((x) => x).length > 0) {
          ranges = ranges.map((range) => {
            const rangeParts = range.split('$');
            rangeParts[0] = (
              parseInt(rangeParts[0], 10) - highlightContainerOffset
            ).toString();
            rangeParts[1] = (
              parseInt(rangeParts[1], 10) - highlightContainerOffset
            ).toString();
            return rangeParts.join('$');
          });

          return 'type:TextRange|' + ranges.join('|');
        }
      }
    }

    return highlighterRange;
  }

Method to deserialize with respective to the offset. Input is the highlight range saved in the database'

  deserializeHighlighterWithOffset(highlighterRange: string) {
    if (highlighterRange) {
      const highLighParts = highlighterRange.split('|');
      const module = highLighParts[0];

      if (module && module === 'type:TextRange') {
        const highlightContainerOffset = this.getHighlighterRangeOffset();
        let ranges = highLighParts.filter((part, index) => index > 0);
        if (ranges && ranges.filter((x) => x).length > 0) {
          ranges = ranges.map((range) => {
            const rangeParts = range.split('$');
            rangeParts[0] = (
              parseInt(rangeParts[0], 10) + highlightContainerOffset
            ).toString();
            rangeParts[1] = (
              parseInt(rangeParts[1], 10) + highlightContainerOffset
            ).toString();
            return rangeParts.join('$');
          });

          return 'type:TextRange|' + ranges.join('|');
        }
      }
    }

    return highlighterRange;
  }
}

Hope this helps

@senkaplan Thanks for the reply! But it's not working for me as the Offset i got is always 0. I don't have a particular container in my case (it's a Chrome Extension and highlighting can be done on any webpages; Eg. https://vuejs.org/v2/guide/ ) so I thought I changed the this.elementRef.nativeElement.querySelector('#highlight-container') to document.getElementsByTagName("BODY")[0] . Any advice?

ryzalyusoff avatar Feb 10 '21 11:02 ryzalyusoff

@ryzalyusoff

Despite adding this code, I still have a settimeout of 2000ms to deserialize the highlight range.

The 2000ms delay is after the page has loaded and the content is loaded and the angular change detection was run.

You can debug it by doing the deserialization yourself once the page is completely loaded and see if that has an impact. If it works, then you have to introduce a delay or check for the page load completion.

May be this will help too.

senkaplan avatar Feb 11 '21 07:02 senkaplan

Hey Hi, I am also facing same issue. I have tried so many ways to fix and spent lot of time to check this. How to check page load completion ? Because I have tried below, but its not working here as these onload will just verify DOM elements are loaded/not. When we have external plugin or dynamic loading I really cant figure either entire page is loaded or not.

document.addEventListener("DOMContentLoaded", function(event) { console.log("DOMContentLoaded with event"); restoreHighlights(ItemInfo); });

window.onload = function () {
restoreHighlights(ItemInfo); }

window.onload = function() { restoreHighlights(ItemInfo); console.log("onload"); };

window.addEventListener('load', function() { restoreHighlights(ItemInfo); console.log("addEventListener"); });

SowjanyaChandrala avatar Feb 23 '24 14:02 SowjanyaChandrala