autocomplete: render html server-side.
Render the HTML for autocomplete results server-side instead of in Javascript. This is cleaner than building HTML in Javascript, but it may hurt caching because the HTTP responses are larger. Fixes #4698: user autocomplete contains links to /posts Also fixes a bug where tag counts in the autocomplete menu were different from tag counts displayed elsewhere because of differences in rounding.
This commit is contained in:
23
app/components/autocomplete_component.rb
Normal file
23
app/components/autocomplete_component.rb
Normal file
@@ -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
|
||||
@@ -0,0 +1,20 @@
|
||||
<ul>
|
||||
<% autocomplete_results.each do |result| %>
|
||||
<%= tag.li class: "ui-menu-item", "data-autocomplete-type": result.type, "data-autocomplete-antecedent": result.antecedent, "data-autocomplete-value": result.value, "data-autocomplete-category": result.category, "data-autocomplete-post-count": result.post_count do %>
|
||||
<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-arrow">→</span>
|
||||
<% end %>
|
||||
|
||||
<%= result.label %>
|
||||
<% end %>
|
||||
|
||||
<% if result.post_count %>
|
||||
<%= tag.span humanized_number(result.post_count), class: "post-count", style: "float: right;" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ul>
|
||||
@@ -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
|
||||
|
||||
@@ -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 = $("<a/>");
|
||||
$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 = $("<span/>").html(" → ").addClass("autocomplete-arrow");
|
||||
var antecedent_element = $("<span/>").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 = $("<span/>").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 = $("<div/>").append($link);
|
||||
var $list_item = $("<li/>").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() {
|
||||
|
||||
@@ -48,6 +48,7 @@ class AutocompleteService
|
||||
# @return [Array<Hash>] 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
|
||||
|
||||
|
||||
1
app/views/autocomplete/index.html.erb
Normal file
1
app/views/autocomplete/index.html.erb
Normal file
@@ -0,0 +1 @@
|
||||
<%= render AutocompleteComponent.new(autocomplete_service: @autocomplete) %>
|
||||
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user