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 attr_reader :autocomplete_service
delegate :humanized_number, to: :helpers delegate :humanized_number, to: :helpers
delegate :autocomplete_results, to: :autocomplete_service delegate :autocomplete_results, :query, :metatag, to: :autocomplete_service
def initialize(autocomplete_service:) def initialize(autocomplete_service:)
@autocomplete_service = 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 link_to posts_path(tags: result.value), class: "tag-type-#{result.category}", "@click.prevent": "", &block
end end
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 end

View File

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