diff --git a/app/components/autocomplete_component.rb b/app/components/autocomplete_component.rb new file mode 100644 index 000000000..7ab304bb6 --- /dev/null +++ b/app/components/autocomplete_component.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class AutocompleteComponent < ApplicationComponent + attr_reader :autocomplete_service + + delegate :humanized_number, to: :helpers + delegate :autocomplete_results, to: :autocomplete_service + + def initialize(autocomplete_service:) + @autocomplete_service = autocomplete_service + end + + def link_to_result(result, &block) + case result.type + when "user" + link_to user_path(result.id), class: "user-#{result.level}", "@click.prevent": "", &block + when "pool" + link_to pool_path(result.id), class: "pool-category-#{result.category}", "@click.prevent": "", &block + else + link_to posts_path(tags: result.value), class: "tag-type-#{result.category}", "@click.prevent": "", &block + end + end +end diff --git a/app/components/autocomplete_component/autocomplete_component.html.erb b/app/components/autocomplete_component/autocomplete_component.html.erb new file mode 100644 index 000000000..992742dcc --- /dev/null +++ b/app/components/autocomplete_component/autocomplete_component.html.erb @@ -0,0 +1,20 @@ + diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 95df10f69..9ce5628de 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class AutocompleteController < ApplicationController - respond_to :xml, :json + respond_to :html, :xml, :json def index @query = params.dig(:search, :query) @@ -14,6 +14,6 @@ class AutocompleteController < ApplicationController @public = @autocomplete.cache_publicly? expires_in @expires_in, public: @public unless response.cache_control.present? - respond_with(@results) + respond_with(@results, layout: false) end end diff --git a/app/javascript/src/javascripts/autocomplete.js b/app/javascript/src/javascripts/autocomplete.js index 36b1ae62f..4ae4c3e3d 100644 --- a/app/javascript/src/javascripts/autocomplete.js +++ b/app/javascript/src/javascripts/autocomplete.js @@ -1,5 +1,6 @@ let Autocomplete = {}; +Autocomplete.VERSION = 1; // This should be bumped whenever the /autocomplete API changes in order to invalid client caches. Autocomplete.MAX_RESULTS = 10; Autocomplete.initialize_all = function() { @@ -138,64 +139,28 @@ Autocomplete.on_tab = function(event) { }; Autocomplete.render_item = function(list, item) { - var $link = $(""); - $link.text(item.label); - $link.attr("href", "/posts?tags=" + encodeURIComponent(item.value)); - $link.on("click.danbooru", function(e) { - e.preventDefault(); - }); - - if (item.antecedent) { - var antecedent = item.antecedent.replace(/_/g, " "); - var arrow = $("").html(" → ").addClass("autocomplete-arrow"); - var antecedent_element = $("").text(antecedent).addClass("autocomplete-antecedent"); - $link.prepend([ - antecedent_element, - arrow - ]); - } - - if (item.post_count !== undefined) { - var count = item.post_count; - - if (count >= 1000) { - count = Math.floor(count / 1000) + "k"; - } - - var $post_count = $("").addClass("post-count").css("float", "right").text(count); - $link.append($post_count); - } - - if (/^tag/.test(item.type)) { - $link.addClass("tag-type-" + item.category); - } else if (item.type === "user") { - var level_class = "user-" + item.level.toLowerCase(); - $link.addClass(level_class); - } else if (item.type === "pool") { - $link.addClass("pool-category-" + item.category); - } - - var $menu_item = $("
").append($link); - var $list_item = $("
  • ").data("item.autocomplete", item).append($menu_item); - - var data_attributes = ["type", "antecedent", "value", "category", "post_count"]; - data_attributes.forEach(attr => { - $list_item.attr(`data-autocomplete-${attr.replace(/_/g, "-")}`, item[attr]); - }); - - return $list_item.appendTo(list); + item.html.data("ui-autocomplete-item", item); + return list.append(item.html); }; -Autocomplete.autocomplete_source = function(query, type) { +Autocomplete.autocomplete_source = async function(query, type) { if (query === "") { return []; } - return $.getJSON("/autocomplete.json", { + let html = await $.get("/autocomplete", { "search[query]": query, "search[type]": type, + "version": Autocomplete.VERSION, "limit": Autocomplete.MAX_RESULTS }); + + let items = $(html).find("li").toArray().map(item => { + let $item = $(item); + return { value: $item.attr("data-autocomplete-value"), html: $item }; + }); + + return items; } Autocomplete.tag_prefixes = function() { diff --git a/app/logical/autocomplete_service.rb b/app/logical/autocomplete_service.rb index a713f6e98..424a00dd3 100644 --- a/app/logical/autocomplete_service.rb +++ b/app/logical/autocomplete_service.rb @@ -48,6 +48,7 @@ class AutocompleteService # @return [Array] the autocomplete results def autocomplete_results return [] if !enabled? + return autocomplete_opensearch(query) if type == :opensearch case type when :tag_query @@ -68,11 +69,9 @@ class AutocompleteService autocomplete_favorite_group(query) when :saved_search_label autocomplete_saved_search_label(query) - when :opensearch - autocomplete_opensearch(query) else [] - end + end.map { |result| OpenStruct.new(result) } end # Complete a tag search (a regular tag or a metatag) @@ -238,7 +237,7 @@ class AutocompleteService pools = Pool.undeleted.name_matches(string).search(order: "post_count").limit(limit) pools.map do |pool| - { type: "pool", label: pool.pretty_name, value: pool.name, post_count: pool.post_count, category: pool.category } + { type: "pool", label: pool.pretty_name, value: pool.name, id: pool.id, post_count: pool.post_count, category: pool.category } end end @@ -298,7 +297,7 @@ class AutocompleteService users = User.search(name_matches: string, current_user_first: true, order: "post_upload_count").limit(limit) users.map do |user| - { type: "user", label: user.pretty_name, value: user.name, level: user.level_string } + { type: "user", label: user.pretty_name, value: user.name, id: user.id, level: user.level_string.downcase } end end diff --git a/app/views/autocomplete/index.html.erb b/app/views/autocomplete/index.html.erb new file mode 100644 index 000000000..d7917f4d9 --- /dev/null +++ b/app/views/autocomplete/index.html.erb @@ -0,0 +1 @@ +<%= render AutocompleteComponent.new(autocomplete_service: @autocomplete) %> diff --git a/test/functional/autocomplete_controller_test.rb b/test/functional/autocomplete_controller_test.rb index b8d0257fb..3529c7c30 100644 --- a/test/functional/autocomplete_controller_test.rb +++ b/test/functional/autocomplete_controller_test.rb @@ -2,10 +2,10 @@ require "test_helper" class AutocompleteControllerTest < ActionDispatch::IntegrationTest def autocomplete(query, type) - get autocomplete_index_path(search: { query: query, type: type }), as: :json + get autocomplete_index_path(search: { query: query, type: type }) assert_response :success - response.parsed_body.map { |result| result["value"] } + response.parsed_body.css("li").map { |html| html["data-autocomplete-value"] } end def assert_autocomplete_equals(expected_value, query, type) @@ -35,6 +35,24 @@ class AutocompleteControllerTest < ActionDispatch::IntegrationTest assert_autocomplete_equals(["rating:sensitive"], "-rating:s", "tag_query") end + should "work for an aliased tag" do + create(:tag_alias, antecedent_name: "oc", consequent_name: "original") + + assert_autocomplete_equals(["original"], "oc", "tag_query") + end + + should "work for the user: metatag" do + create(:user, name: "foobar") + + assert_autocomplete_equals(["user:foobar"], "user:foo", "tag_query") + end + + should "work for the pool: metatag" do + as(create(:user)) { create(:pool, name: "foobar") } + + assert_autocomplete_equals(["pool:foobar"], "pool:foo", "tag_query") + end + should "work for a missing type" do get autocomplete_index_path(search: { query: "azur" }), as: :json @@ -48,7 +66,7 @@ class AutocompleteControllerTest < ActionDispatch::IntegrationTest end should "not set session cookies when the response is publicly cached" do - get autocomplete_index_path(search: { query: "azur", type: "tag_query" }), as: :json + get autocomplete_index_path(search: { query: "azur", type: "tag_query" }) assert_response :success assert_equal(true, response.cache_control[:public])