autocomplete: highlight matches in autocomplete menu.

Highlight the matching part of the tag in the autocomplete menu. For
example, if you search "hair", then the word "hair" will be bolded in
every matching tag. This is so users can tell why a particular tag was
matched.
This commit is contained in:
evazion
2022-09-02 00:15:26 -05:00
parent f8e4e5724f
commit 8491bef6e6
2 changed files with 63 additions and 4 deletions

View File

@@ -4,7 +4,7 @@ class AutocompleteComponent < ApplicationComponent
attr_reader :autocomplete_service
delegate :humanized_number, to: :helpers
delegate :autocomplete_results, to: :autocomplete_service
delegate :autocomplete_results, :query, :metatag, to: :autocomplete_service
def initialize(autocomplete_service:)
@autocomplete_service = autocomplete_service
@@ -20,4 +20,62 @@ class AutocompleteComponent < ApplicationComponent
link_to posts_path(tags: result.value), class: "tag-type-#{result.category}", "@click.prevent": "", &block
end
end
def highlight_antecedent(result)
if result.type == "tag-word"
highlight_matching_words(result.antecedent, query)
else
highlight_wildcard_match(result.antecedent, query)
end
end
def highlight_result(result)
if result.type == "tag-word"
highlight_matching_words(result.value, query)
elsif metatag.present? && metatag.value.include?("*")
highlight_wildcard_match(result.label, metatag.value)
elsif metatag.present? && metatag.name.in?(%w[pool favgroup])
highlight_wildcard_match(result.label, "*" + metatag.value + "*")
elsif metatag.present?
highlight_wildcard_match(result.label, metatag.value + "*")
else
highlight_wildcard_match(result.value, query)
end
end
# Highlight the words in the `target` string matching the words in the search `pattern`.
#
# highlight_matching_words("very_long_hair", "long_ha*") => "<span>very_</span><b>long</b><span>_</span><b>hair</b>"
def highlight_matching_words(target, pattern)
pattern_words = Tag.parse_query(pattern)
pattern_words.sort_by! { |word| [word.include?("*") ? 0 : 1, -word.size] }
target_words = Tag.split_words(target)
target_words.map do |word|
pat = pattern_words.find { |pat| word.ilike?(pat) }
highlight_wildcard_match(word, pat)
end.join("").html_safe
end
# Highlight the parts of the `target` string that match the wildcard search `pattern`.
#
# highlight_wildcard_match("very_long_hair", "*long*") => "<span>very_</span><b>long</b><span>_hair</span>"
def highlight_wildcard_match(target, pattern)
return tag.span(target.tr("_", " ")) if !target.ilike?(pattern.to_s)
words = pattern.split(/(\*)/).compact_blank # "*black*" => ["*", "black", "*"]
regexp = words.map { |w| w == "*" ? "(.*)" : "(#{Regexp.escape(w)})" }.join # "*black*" => "(.*)(black)(.*)"
regexp = Regexp.new(regexp, "i")
captures = target.match(regexp).captures # "black_thighhighs" =~ /(.*)(black)(.*)/ => ["", "black", "_thighhighs"]
captures.zip(words).map do |substring, word|
if substring == ""
""
elsif word == "*"
tag.span(substring.tr("_", " "))
else
tag.b(substring.tr("_", " "))
end
end.join.html_safe
end
end

View File

@@ -4,11 +4,12 @@
<div class="ui-menu-item-wrapper" tabindex="-1">
<%= link_to_result result do %>
<% if result.antecedent.present? %>
<span class="autocomplete-antecedent"><%= result.antecedent.tr("_", " ") %></span>
<span class="autocomplete-antecedent"><%= highlight_antecedent(result) %></span>
<span class="autocomplete-arrow">→</span>
<%= result.label %>
<% else %>
<%= highlight_result(result) %>
<% end %>
<%= result.label %>
<% end %>
<% if result.post_count %>