Files
danbooru/app/javascript/src/javascripts/autocomplete.js.erb
evazion 883856d4af simple form: refactor DText form fields to use SimpleForm.
* Refactors DText form fields to use a custom SimpleForm input instead
  of manually generated html. This fixes it so that DText fields use the
  same markup as normal SimpleForm fields, which lets us apply browser
  maxlength validations to DText input fields.

* Fixes autocomplete for @-mentions only working in comments and forum posts.
  Now @-mention autocomplete works in all DText fields, including dmails.
  Known bug: it applies in artist commentary fields when it shouldn't.
2020-06-25 16:28:09 -05:00

438 lines
13 KiB
Plaintext

import CurrentUser from './current_user'
let Autocomplete = {};
/* eslint-disable */
Autocomplete.METATAGS = <%= PostQueryBuilder::METATAGS.to_json.html_safe %>;
Autocomplete.TAG_CATEGORIES = <%= TagCategory.mapping.to_json.html_safe %>;
Autocomplete.ORDER_METATAGS = <%= PostQueryBuilder::ORDER_METATAGS.to_json.html_safe %>;
Autocomplete.DISAPPROVAL_REASONS = <%= PostDisapproval::REASONS.to_json.html_safe %>;
/* eslint-enable */
Autocomplete.MISC_STATUSES = ["deleted", "active", "pending", "flagged", "banned", "modqueue", "unmoderated"];
Autocomplete.TAG_PREFIXES = "-|~|" + Object.keys(Autocomplete.TAG_CATEGORIES).map(category => category + ":").join("|");
Autocomplete.METATAGS_REGEX = Autocomplete.METATAGS.concat(Object.keys(Autocomplete.TAG_CATEGORIES)).join("|");
Autocomplete.TERM_REGEX = new RegExp(`([-~]*)(?:(${Autocomplete.METATAGS_REGEX}):)?(\\S*)$`, "i");
Autocomplete.MAX_RESULTS = 10;
Autocomplete.initialize_all = function() {
if (CurrentUser.data("enable-auto-complete")) {
$.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"]'), Autocomplete.tag_source);
this.initialize_fields($('[data-autocomplete="artist"]'), Autocomplete.artist_source);
this.initialize_fields($('[data-autocomplete="pool"]'), Autocomplete.pool_source);
this.initialize_fields($('[data-autocomplete="user"]'), Autocomplete.user_source);
this.initialize_fields($('[data-autocomplete="wiki-page"]'), Autocomplete.wiki_source);
this.initialize_fields($('[data-autocomplete="favorite-group"]'), Autocomplete.favorite_group_source);
this.initialize_fields($('[data-autocomplete="saved-search-label"]'), Autocomplete.saved_search_source);
}
}
Autocomplete.initialize_fields = function($fields, autocomplete) {
$fields.autocomplete({
source: async function(request, respond) {
let results = await autocomplete(request.term);
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.user_source(name, "@");
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) {
// Prevent Upload.initialize_enter_on_tags from running if the
// Enter key is used to select a tag from the autocomplete menu.
if (event.key === "Enter") {
event.stopImmediatePropagation();
}
Autocomplete.insert_completion(this, ui.item.value);
return false;
},
source: async function(req, resp) {
var query = Autocomplete.parse_query(req.term, this.element.get(0).selectionStart);
var metatag = query.metatag;
var term = query.term;
var results = [];
switch (metatag) {
case "order":
case "status":
case "rating":
case "locked":
case "child":
case "parent":
case "filetype":
case "disapproved":
case "embedded":
results = Autocomplete.static_metatag_source(term, metatag);
break;
case "user":
case "approver":
case "commenter":
case "comm":
case "noter":
case "noteupdater":
case "commentaryupdater":
case "artcomm":
case "fav":
case "ordfav":
case "appealer":
case "flagger":
case "upvote":
case "downvote":
results = await Autocomplete.user_source(term, metatag + ":");
break;
case "pool":
case "ordpool":
results = await Autocomplete.pool_source(term, metatag + ":");
break;
case "favgroup":
case "ordfavgroup":
results = await Autocomplete.favorite_group_source(term, metatag + ":", CurrentUser.data("id"));
break;
case "search":
results = await Autocomplete.saved_search_source(term, metatag + ":");
break;
case "tag":
results = await Autocomplete.tag_source(term);
break;
default:
results = [];
break;
}
resp(results);
}
});
}
Autocomplete.parse_query = function(text, caret) {
let before_caret_text = text.substring(0, caret);
let match = before_caret_text.match(Autocomplete.TERM_REGEX);
let operator = match[1];
let metatag = match[2] ? match[2].toLowerCase() : "tag";
let term = match[3];
if (metatag in Autocomplete.TAG_CATEGORIES) {
metatag = "tag";
}
return { operator: operator, metatag: metatag, term: term };
};
// 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 + ")?\\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) {
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(" &rarr; ").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 (item.type === "tag") {
$link.addClass("tag-type-" + item.category);
} else if (item.type === "user") {
var level_class = "user-" + item.level.toLowerCase();
$link.addClass(level_class);
if (CurrentUser.data("style-usernames")) {
$link.addClass("with-style");
}
} 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", "source", "antecedent", "value", "category", "post_count", "weight"];
data_attributes.forEach(attr => {
$list_item.attr(`data-autocomplete-${attr.replace(/_/g, "-")}`, item[attr]);
});
return $list_item.appendTo(list);
};
Autocomplete.static_metatags = {
order: Autocomplete.ORDER_METATAGS,
status: ["any"].concat(Autocomplete.MISC_STATUSES),
rating: [
"safe", "questionable", "explicit"
],
locked: [
"rating", "note", "status"
],
embedded: [
"true", "false"
],
child: ["any", "none"].concat(Autocomplete.MISC_STATUSES),
parent: ["any", "none"].concat(Autocomplete.MISC_STATUSES),
filetype: [
"jpg", "png", "gif", "swf", "zip", "webm", "mp4"
],
commentary: [
"true", "false", "translated", "untranslated"
],
disapproved: Autocomplete.DISAPPROVAL_REASONS
}
Autocomplete.static_metatag_source = function(term, metatag) {
var sub_metatags = this.static_metatags[metatag];
var matches = sub_metatags.filter(sub_metatag => sub_metatag.startsWith(term.toLowerCase()));
matches = matches.map(sub_metatag => `${metatag}:${sub_metatag}`).sort().slice(0, Autocomplete.MAX_RESULTS);
return matches;
}
Autocomplete.tag_source = async function(term) {
if (term === "") {
return [];
}
let tags = await $.getJSON("/tags/autocomplete", {
"search[name_matches]": term,
"limit": Autocomplete.MAX_RESULTS,
"expiry": 7
});
return tags.map(function(tag) {
return {
type: "tag",
label: tag.name.replace(/_/g, " "),
antecedent: tag.antecedent_name,
value: tag.name,
category: tag.category,
source: tag.source,
weight: tag.weight,
post_count: tag.post_count
};
});
}
Autocomplete.artist_source = async function(term) {
let artists = await $.getJSON("/artists", {
"search[name_like]": term.trim().replace(/\s+/g, "_") + "*",
"search[is_deleted]": false,
"search[order]": "post_count",
"limit": Autocomplete.MAX_RESULTS,
"expiry": 7
});
return artists.map(function(artist) {
return {
type: "tag",
label: artist.name.replace(/_/g, " "),
value: artist.name,
category: Autocomplete.TAG_CATEGORIES.artist,
};
});
};
Autocomplete.wiki_source = async function(term) {
let wiki_pages = await $.getJSON("/wiki_pages", {
"search[title_normalize]": term + "*",
"search[hide_deleted]": "Yes",
"search[order]": "post_count",
"limit": Autocomplete.MAX_RESULTS,
"expiry": 7
});
return wiki_pages.map(function(wiki_page) {
return {
type: "tag",
label: wiki_page.title.replace(/_/g, " "),
value: wiki_page.title,
category: wiki_page.category_name
};
});
};
Autocomplete.user_source = async function(term, prefix = "") {
let users = await $.getJSON("/users", {
"search[order]": "post_upload_count",
"search[current_user_first]": "true",
"search[name_matches]": term + "*",
"limit": Autocomplete.MAX_RESULTS
});
return users.map(function(user) {
return {
type: "user",
label: user.name.replace(/_/g, " "),
value: prefix + user.name,
level: user.level_string
};
});
};
Autocomplete.pool_source = async function(term, prefix = "") {
let pools = await $.getJSON("/pools", {
"search[name_matches]": term,
"search[is_deleted]": false,
"search[order]": "post_count",
"limit": Autocomplete.MAX_RESULTS
});
return pools.map(function(pool) {
return {
type: "pool",
label: pool.name.replace(/_/g, " "),
value: prefix + pool.name,
post_count: pool.post_count,
category: pool.category
};
});
};
Autocomplete.favorite_group_source = async function(term, prefix = "", creator_id = null) {
let favgroups = await $.getJSON("/favorite_groups", {
"search[creator_id]": creator_id,
"search[name_matches]": term,
"limit": Autocomplete.MAX_RESULTS
});
return favgroups.map(function(favgroup) {
return {
label: favgroup.name.replace(/_/g, " "),
value: prefix + favgroup.name,
post_count: favgroup.post_count
};
});
};
Autocomplete.saved_search_source = async function(term, prefix = "") {
let labels = await $.getJSON("/saved_searches/labels", {
"search[label]": term + "*",
"limit": Autocomplete.MAX_RESULTS
});
return labels.map(function(label) {
return {
label: label.replace(/_/g, " "),
value: prefix + label,
};
});
}
$(document).ready(function() {
Autocomplete.initialize_all();
});
export default Autocomplete;