Files
danbooru/app/javascript/src/javascripts/autocomplete.js
evazion 1968c9c69d autocomplete: lowercase searches clientside.
Lowercase searches clientside before sending them to the server. This is
to improve cache hit rates when users accidentally type in uppercase,
especially on mobile where the first letter often gets capitalized.
2022-09-03 23:54:28 -05:00

176 lines
5.6 KiB
JavaScript

let Autocomplete = {};
Autocomplete.VERSION = 1; // This should be bumped whenever the /autocomplete API changes in order to invalid client caches.
Autocomplete.MAX_RESULTS = 20;
Autocomplete.initialize_all = function() {
$.widget("ui.autocomplete", $.ui.autocomplete, {
options: {
delay: 0,
minLength: 1,
autoFocus: false,
focus: function() { return false; },
},
_create: function() {
this.element.on("keydown.Autocomplete.tab", null, "tab", Autocomplete.on_tab);
this._super();
},
_renderItem: Autocomplete.render_item,
search: function(value, event) {
if ($(this).data("ui-autocomplete")) {
$(this).data("ui-autocomplete").menu.bindings = $();
}
this._super(value, event);
},
});
this.initialize_tag_autocomplete();
this.initialize_mention_autocomplete($("form div.input.dtext textarea"));
this.initialize_fields($('[data-autocomplete="tag"]'), "tag");
this.initialize_fields($('[data-autocomplete="artist"]'), "artist");
this.initialize_fields($('[data-autocomplete="pool"]'), "pool");
this.initialize_fields($('[data-autocomplete="user"]'), "user");
this.initialize_fields($('[data-autocomplete="wiki-page"]'), "wiki_page");
this.initialize_fields($('[data-autocomplete="favorite-group"]'), "favorite_group");
this.initialize_fields($('[data-autocomplete="saved-search-label"]'), "saved_search_label");
}
Autocomplete.initialize_fields = function($fields, type) {
$fields.autocomplete({
source: async function(request, respond) {
let results = await Autocomplete.autocomplete_source(request.term, type);
respond(results);
},
});
};
Autocomplete.initialize_mention_autocomplete = function($fields) {
$fields.autocomplete({
select: function(event, ui) {
Autocomplete.insert_completion(this, ui.item.value);
return false;
},
source: async function(req, resp) {
var cursor = this.element.get(0).selectionStart;
var name = null;
for (var i = cursor; i >= 1; --i) {
if (req.term[i - 1] === " ") {
return;
}
if (req.term[i - 1] === "@") {
if (i === 1 || /[ \r\n]/.test(req.term[i - 2])) {
name = req.term.substring(i, cursor);
break;
} else {
return;
}
}
}
if (name) {
let results = await Autocomplete.autocomplete_source(name, "mention");
resp(results);
}
}
});
}
Autocomplete.initialize_tag_autocomplete = function() {
var $fields_multiple = $('[data-autocomplete="tag-query"], [data-autocomplete="tag-edit"]');
$fields_multiple.autocomplete({
select: function(event, ui) {
Autocomplete.insert_completion(this, ui.item.value);
return false;
},
source: async function(req, resp) {
let term = Autocomplete.current_term(this.element);
let results = await Autocomplete.autocomplete_source(term, "tag_query");
resp(results);
}
});
}
Autocomplete.current_term = function($input) {
let query = $input.get(0).value;
let caret = $input.get(0).selectionStart;
let regexp = new RegExp(`^[-~(]*(${Autocomplete.tag_prefixes().join("|")})?`);
let match = query.substring(0, caret).match(/\S*$/)[0].replace(regexp, "").toLowerCase();
return match;
};
// Update the input field with the item currently focused in the
// autocomplete menu, then position the caret just after the inserted completion.
Autocomplete.insert_completion = function(input, completion) {
// Trim all whitespace (tabs, spaces) except for line returns
var before_caret_text = input.value.substring(0, input.selectionStart).replace(/^[ \t]+|[ \t]+$/gm, "");
var after_caret_text = input.value.substring(input.selectionStart).replace(/^[ \t]+|[ \t]+$/gm, "");
var regexp = new RegExp(`([-~(]*(?:${Autocomplete.tag_prefixes().join("|")})?)\\S+$`, "g");
before_caret_text = before_caret_text.replace(regexp, "$1") + completion + " ";
input.value = before_caret_text + after_caret_text;
input.selectionStart = input.selectionEnd = before_caret_text.length;
};
// If we press tab while the autocomplete menu is open but nothing is
// focused, complete the first item and close the menu.
Autocomplete.on_tab = function(event) {
var input = this;
var autocomplete = $(input).autocomplete("instance");
var $autocomplete_menu = autocomplete.menu.element;
if (!$autocomplete_menu.is(":visible")) {
return;
}
if ($autocomplete_menu.has(".ui-state-active").length === 0) {
var $first_item = $autocomplete_menu.find(".ui-menu-item").first();
var completion = $first_item.data().uiAutocompleteItem.value;
Autocomplete.insert_completion(input, completion);
autocomplete.close();
}
// Prevent the tab key from moving focus to the next element.
event.preventDefault();
};
Autocomplete.render_item = function(list, item) {
item.html.data("ui-autocomplete-item", item);
return list.append(item.html);
};
Autocomplete.autocomplete_source = async function(query, type) {
if (query === "") {
return [];
}
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() {
return JSON.parse($("meta[name=autocomplete-tag-prefixes]").attr("content"));
};
$(document).ready(function() {
Autocomplete.initialize_all();
});
export default Autocomplete;