7 Commits

Author SHA1 Message Date
Eva
0d2df6db95 client/misc: add matchingNames function and fall back to partial matches 2025-04-04 11:31:43 +02:00
Eva
ffd62a936f client/css: disable color transitions in tag autocomplete 2025-04-04 11:31:43 +02:00
Eva
12a7f4f78a client/search: autocomplete tag alias that the user searched for
Instead of always using the first alias.
"Tag input" fields (edit page, bulk tagging, etc.) will display the
matched name in suggestions, and use the first alias upon selection.
2025-04-04 11:31:43 +02:00
Eva
417675cc4c client/search: make autocomplete replace current word and set cursor pos
Better behavior for autocomplete in the middle of an already typed tag.
2025-04-04 11:15:58 +02:00
Eva
f5c5b0bfb1 client/pools: don't flash progress for pool name autocomplete 2025-04-04 11:15:58 +02:00
Eva
01f7f6eabb client/tags: don't flash progress for tag autocomplete 2025-04-04 11:13:17 +02:00
Eva
7eff539df5 client/search: autocomplete negated tags in search fields
This doesn't apply to tag inputs that aren't for searching, like the tag
input box when editing a post.
2025-04-04 11:13:17 +02:00
8 changed files with 38 additions and 22 deletions

View File

@ -102,7 +102,8 @@ class PoolListController {
this._ctx.parameters.query,
offset,
limit,
fields
fields,
{}
);
},
pageRenderer: (pageCtx) => {

View File

@ -235,7 +235,7 @@ class AutoCompleteControl {
}
_updateResults(textToFind) {
if (this._options.isNegationAllowed && textToFind == "-") {
if (this._options.isNegationAllowed && textToFind === "-") {
this._results = [];
this._activeResult = -1;
this._refreshList();

View File

@ -4,12 +4,13 @@ const misc = require("../util/misc.js");
const PoolList = require("../models/pool_list.js");
const AutoCompleteControl = require("./auto_complete_control.js");
function _poolListToMatches(pools, options) {
function _poolListToMatches(text, pools, options) {
return [...pools]
.sort((pool1, pool2) => {
return pool2.postCount - pool1.postCount;
})
.map((pool) => {
pool.matchingNames = misc.matchingNames(text, pool.names);
let cssName = misc.makeCssName(pool.category, "pool");
const caption =
'<span class="' +
@ -31,9 +32,9 @@ class PoolAutoCompleteControl extends AutoCompleteControl {
options.getMatches = (text) => {
const term = misc.escapeSearchTerm(text);
const query =
(text.length < minLengthForPartialSearch
? term + "*"
: "*" + term + "*") + " sort:post-count";
(text.length >= minLengthForPartialSearch
? "*" + term + "*"
: term + "*") + " sort:post-count";
return new Promise((resolve, reject) => {
PoolList.search(query, 0, this._options.maxResults, [
@ -42,10 +43,11 @@ class PoolAutoCompleteControl extends AutoCompleteControl {
"category",
"postCount",
"version",
]).then(
],
{ noProgress: true }).then(
(response) =>
resolve(
_poolListToMatches(response.results, this._options)
_poolListToMatches(text, response.results, this._options)
),
reject
);
@ -61,8 +63,7 @@ class PoolAutoCompleteControl extends AutoCompleteControl {
}
const result = this._results[this._activeResult].value;
const textToFind = this._options.getTextToFind();
result.matchingNames = result.names.filter((name) => misc.wildcardMatch(textToFind + "*", name, false));
result.matchingNames = result.matchingNames.length ? result.matchingNames : result.names;
result.matchingNames = misc.matchingNames(textToFind, result.names);
return result;
}
}

View File

@ -5,12 +5,13 @@ const views = require("../util/views.js");
const TagList = require("../models/tag_list.js");
const AutoCompleteControl = require("./auto_complete_control.js");
function _tagListToMatches(tags, options, negated) {
function _tagListToMatches(text, tags, options, negated) {
return [...tags]
.sort((tag1, tag2) => {
return tag2.usages - tag1.usages;
})
.map((tag) => {
tag.matchingNames = misc.matchingNames(text, tag.names);
let cssName = misc.makeCssName(tag.category, "tag");
if (options.isTaggedWith(tag.names[0])) {
cssName += " disabled";
@ -45,7 +46,7 @@ class TagAutoCompleteControl extends AutoCompleteControl {
);
options.getMatches = (text) => {
const negated = options.isNegationAllowed && text[0] == "-";
const negated = options.isNegationAllowed && text[0] === "-";
if (negated) text = text.substring(1);
if (!text) {
return new Promise((resolve, reject) => {
@ -56,9 +57,9 @@ class TagAutoCompleteControl extends AutoCompleteControl {
const term = misc.escapeSearchTerm(text);
const query =
(text.length < minLengthForPartialSearch && text[0] != "-"
? term + "*"
: "*" + term + "*") + " sort:usages";
(text.length >= minLengthForPartialSearch || (!options.isNegationAllowed && text[0] === "-")
? "*" + term + "*"
: term + "*") + " sort:usages";
return new Promise((resolve, reject) => {
TagList.search(query, 0, this._options.maxResults, [
@ -69,7 +70,7 @@ class TagAutoCompleteControl extends AutoCompleteControl {
{ noProgress: true }).then(
(response) =>
resolve(
_tagListToMatches(response.results, this._options, negated)
_tagListToMatches(text, response.results, this._options, negated)
),
reject
);
@ -85,8 +86,7 @@ class TagAutoCompleteControl extends AutoCompleteControl {
}
const result = this._results[this._activeResult].value;
const textToFind = this._options.getTextToFind();
result.matchingNames = result.names.filter((name) => misc.wildcardMatch(textToFind + "*", name, false));
result.matchingNames = result.matchingNames.length ? result.matchingNames : result.names;
result.matchingNames = misc.matchingNames(textToFind, result.names);
return result;
}
}

View File

@ -6,7 +6,7 @@ const AbstractList = require("./abstract_list.js");
const Pool = require("./pool.js");
class PoolList extends AbstractList {
static search(text, offset, limit, fields) {
static search(text, offset, limit, fields, options) {
return api
.get(
uri.formatApiLink("pools", {
@ -14,7 +14,8 @@ class PoolList extends AbstractList {
offset: offset,
limit: limit,
fields: fields.join(","),
})
}),
options
)
.then((response) => {
return Promise.resolve(

View File

@ -217,6 +217,18 @@ function wildcardMatch(pattern, str, sensitive = false) {
return re.test(str);
}
function matchingNames(text, names) {
const minLengthForPartialSearch = 3;
let matches = names.filter((name) => wildcardMatch(text + "*", name, false));
if (!matches.length && text.length >= minLengthForPartialSearch) {
matches = names.filter((name) => wildcardMatch("*" + text + "*", name, false));
}
matches = matches.length ? matches : names;
return matches;
}
module.exports = {
range: range,
formatRelativeTime: formatRelativeTime,
@ -236,4 +248,5 @@ module.exports = {
dataURItoBlob: dataURItoBlob,
getPrettyName: getPrettyName,
wildcardMatch: wildcardMatch,
matchingNames: matchingNames,
};

View File

@ -25,7 +25,7 @@ class PoolMergeView extends events.EventTarget {
confirm: (pool) => {
this._targetPoolId = pool.id;
this._autoCompleteControl.replaceSelectedText(
pool.names[0],
pool.matchingNames[0],
false
);
},

View File

@ -23,7 +23,7 @@ class TagMergeView extends events.EventTarget {
{
confirm: (tag) =>
this._autoCompleteControl.replaceSelectedText(
tag.names[0],
tag.matchingNames[0],
false
),
}