1

Following scenario.

I wrote a angular2 application with material2.

In my SideNav is a search input field. When a user types in it, he is redirected (via routing) to the search component, while the searched word is handed over as a routing parameter.

The search component shows all pages of the application, which contain the searched word (index in the background). Once the user clicks on the entry, he's redirected to this page, and the searched word is appended as a query parameter. I'm now trying to highlight all appearances of the searchword on the page, the user gets redirected to. At the moment i'm doing this:

subscription: ISubscription;
searchTerm: string;

constructor(private router: Router, private elementRef: ElementRef) {}

ngOnInit(): void {
  this.subscription = this.router.routerState.queryParams.subscribe(queryParams => {
    let searchTerm = queryParams['searchTerm'];
    if (searchTerm) {
      this.searchTerm = searchTerm;
    } else {
      this.searchTerm = null;
    }
  });
}

ngAfterContentInit(): void {
  if (this.searchTerm && isStaticDoc) {  
    let regExp = new RegExp(`(${this.searchTerm})`, 'i');
    this.highlightWords(this.elementRef.nativeElement, regExp);
  }
}

ngOnDestroy(): void {
  this.subscription.unsubscribe();
}

highlightWords(node, regExp: RegExp) {
  if (!node || ! regExp) {
    return;
  }
  if (node.nodeType === 3) {
  let regs = regExp.exec(node.nodeValue);
    if (regs) {
      let match = document.createElement('span');
      match.appendChild(document.createTextNode(regs[0]));
      match.classList.add('search-hl');

      let after = node.splitText(regs.index);
      after.nodeValue = after.nodeValue.substring(regs[0].length);
      node.parentNode.insertBefore(match, after);
    }
  } else if (node.hasChildNodes()) {
    for (let i = 0; i < node.childNodes.length; i++) {
      this.highlightWords(node.childNodes[i], regExp);
    }
  }
}

Now the issue is, that i get an error RangeError: Maximum call stack size exceeded, which might be a hint, that the recursion level is way to deep. I've already tried to use 3rd party libraries, bot non of them is really made to be used from angular2 and on top, the written code isn't that difficult... but its not working.

Any ideas how to stage beneath the maximum call stack size following the same or an similar approach?


tl;dr trying to highlight all appearances of searchTerm(which is passed over as a queryParam) on the page -> my approach (see code) is not working due to max call stack size.


Edit: Using rc4 atm, upgrading soon, but this shouldn't be an issue (i guess)

Mit0x2
  • 53
  • 8
  • Why don't you use a string replace function on your searchresults before you insert them into the dom? (replace 'searchvalue' with 'searchvalue') For replace all see: http://stackoverflow.com/questions/1144783/replacing-all-occurrences-of-a-string-in-javascript – user3791775 Sep 15 '16 at 12:20
  • first, thanks for your answer. But the issue is, by simply calling replace on `node#innerHtml` occurrences of the searchTerm in (for example) a `link#href` or in an id would be surrounded by the `span` as well, which results in a highlighted but very broken page (depending on the search term). I also tried to only match words not standing in a tag, but since there are endless possibilities in link (to other sites) it's nearly impossible to find the right regex, especially with the lack of negative look behind in js... – Mit0x2 Sep 15 '16 at 12:25
  • 1
    Then you could find occurrences where the first occurrence of '<' or '>' before the match is '>' and after the match is '<'. (so you know it is not inside tags) – user3791775 Sep 15 '16 at 12:38
  • @user3791775 i've made up a solution that works with your advice . Thank you very much! – Mit0x2 Sep 15 '16 at 14:04
  • Happy to hear that! – user3791775 Sep 15 '16 at 16:57

1 Answers1

2

Thanks to user3791775 I've come up with an solution.

highlightWords(html: string, searchTerm: string): string {

  let regExp = new RegExp(`(${searchTerm})`, 'i');
  let results = regExp.exec(html);
  
  if (results) {
    let before = html.substr(0, results.index);
    let after = html.substr(results.index + searchTerm.length);
    
    let indexOpenTag = before.lastIndexOf('<');
    let indexCloseTag = before.lastIndexOf('>');
    let indexOpenTagAfter = after.indexOf('<');
    let indexCloseTagAfter = after.indexOf('>');
    
    if (indexOpenTag <= indexCloseTag && indexOpenTagAfter <= indexCloseTagAfter) {
      return `${before}<span class="search-hl">${results[0]}</span>${this.highlightWords(after, searchTerm)}`;
    } else {
      return `${before}${results[0]}${this.highlightWords(after, searchTerm)}`;
    }
  } else {
    return html;
  }
}

This can be used the following way

let ref = document.getElementById('my-highlicht-content');
ref.innerHtml = this.highlightWords(ref.innerHtml, this.searchTerm)

Thanks for helping!


Edit: Had another edgecase, which made it necessary to inspect the part after the keyword as well. Updated my example.

Community
  • 1
  • 1
Mit0x2
  • 53
  • 8