From 8491bef6e6b2e67600ecb297aad1d77858369697 Mon Sep 17 00:00:00 2001 From: evazion Date: Fri, 2 Sep 2022 00:15:26 -0500 Subject: [PATCH] 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. --- app/components/autocomplete_component.rb | 60 ++++++++++++++++++- .../autocomplete_component.html.erb | 7 ++- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/app/components/autocomplete_component.rb b/app/components/autocomplete_component.rb index 7ab304bb6..1a257bc31 100644 --- a/app/components/autocomplete_component.rb +++ b/app/components/autocomplete_component.rb @@ -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*") => "very_long_hair" + 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*") => "very_long_hair" + 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 diff --git a/app/components/autocomplete_component/autocomplete_component.html.erb b/app/components/autocomplete_component/autocomplete_component.html.erb index 992742dcc..335bad548 100644 --- a/app/components/autocomplete_component/autocomplete_component.html.erb +++ b/app/components/autocomplete_component/autocomplete_component.html.erb @@ -4,11 +4,12 @@
<%= link_to_result result do %> <% if result.antecedent.present? %> - <%= result.antecedent.tr("_", " ") %> + <%= highlight_antecedent(result) %> + <%= result.label %> + <% else %> + <%= highlight_result(result) %> <% end %> - - <%= result.label %> <% end %> <% if result.post_count %>