Skip to content

Commit

Permalink
Simple search highlighting
Browse files Browse the repository at this point in the history
  • Loading branch information
the-teacher committed Jan 14, 2023
1 parent c316aa7 commit 9f5329f
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 2 deletions.
4 changes: 4 additions & 0 deletions app/assets/stylesheets/articles.css
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,7 @@ section {
border: 1px solid rgb(187, 210, 165);
padding: 10px;
}

em.tsh {
background-color: yellow;
}
2 changes: 1 addition & 1 deletion app/controllers/articles_controller.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class ArticlesController < ApplicationController
def index
# Example of ElasticSearch/Chewy search
@search_query = 'stench'
@search_query = params[:search] || 'Article'
@found_articles = ArticlesIndex
.query(
query_string: {
Expand Down
2 changes: 2 additions & 0 deletions app/javascript/application.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import ArticlesIndex from './articlesIndex'
import TheSearchHighlight from './theSearchHighlight'

console.log("Hello World! This is `importmap` entry point")

ArticlesIndex.init()
TheSearchHighlight.init('.the-search-highlight')
82 changes: 82 additions & 0 deletions app/javascript/theSearchHighlight.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Simple implementation of search results highlighting on FE side
const TheSearchHighlight = (() => {
const SEARCH_WORDS_NAME = 'searchWords'

const TMP_START_TAG = '!_A_!'
const TMP_END_TAG = '!_Z_!'

const START_TAG = '<em class="tsh">'
const END_TAG = '</em>'

let wordsToHighlight = []

const prepareSearchKeyword = (searchKeyword) => {
return searchKeyword.trim().replace(/\s\s+/g, ' ')
}

const getFragmentsToHighlight = (highlightSelector) =>
document.querySelectorAll(highlightSelector)

const getWordsToSearch = (fragment) => {
const searchKeywords = JSON.parse(fragment.dataset[SEARCH_WORDS_NAME])
return searchKeywords.map((searchKeyword) => {
return prepareSearchKeyword(searchKeyword).split(' ')
}).flat()
}

const highlighterBuilder = (wordsToSearch) => {
return (str) => {
wordsToSearch.forEach(wordToSearch => {
const regExp = new RegExp(wordToSearch, 'gi')
const matches = [...str.matchAll(regExp)]
const uniqMatchedWords = matches.map((aryItem) => aryItem[0])
const matchedWords = [...new Set(uniqMatchedWords)]

if (matchedWords.length) {
matchedWords.forEach((foundWord) => {
const _regExp = new RegExp(foundWord, 'g')
str = str.replaceAll(_regExp, `${TMP_START_TAG}${foundWord}${TMP_END_TAG}`)
})
}
})
return str
}
}

const recursiveChildrenRunner = (element, highlighter) => {
for (const child of element.children) {
recursiveChildrenRunner(child, highlighter)

for (var element of child.childNodes) {
if (element.nodeType === Node.TEXT_NODE) {
const oldText = element.nodeValue
const newText = highlighter(element.nodeValue)
if (oldText !== newText) {
element.nodeValue = newText
}
}
}
}
}

const highlightFragment = (fragment) => {
const wordsToSearch = getWordsToSearch(fragment)
if (!wordsToSearch.length) { return }
const highlighter = highlighterBuilder(wordsToSearch)
recursiveChildrenRunner(fragment, highlighter)
}

return {
init: (highlightSelector) => {
const fragments = getFragmentsToHighlight(highlightSelector)
fragments.forEach((fragment) => {
highlightFragment(fragment)
fragment.innerHTML = fragment.innerHTML
.replaceAll(TMP_START_TAG, START_TAG)
.replaceAll(TMP_END_TAG, END_TAG);
})
}
}
})()

export default TheSearchHighlight
11 changes: 10 additions & 1 deletion app/views/articles/_found_articles.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@
<p>Term/Displayed/Found: "<%= term %>"/<%= articles.size %>/<%= articles.count %></p>
</div>

<% if articles.size.zero? %>
<div class="section-description">
<p>Sorry. No Search Results</p>
</div>
<% end %>

<% articles.each do |article| %>
<%= render partial: 'article', locals: { article: article } %>
<article class="article the-search-highlight" data-search-words="<%= [term].to_json %>">
<h3 class="article-header"><%= article.title %></h3>
<p class="article-content"><%= raw article.content %></p>
</article>
<% end %>
</section>

0 comments on commit 9f5329f

Please sign in to comment.