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.
This commit is contained in:
Eva
2025-04-04 09:39:27 +02:00
parent 417675cc4c
commit 12a7f4f78a
9 changed files with 46 additions and 12 deletions

View File

@ -4,18 +4,20 @@ 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 = pool.names.filter((name) => misc.wildcardMatch(text + "*", name, false));
pool.matchingNames = pool.matchingNames.length ? pool.matchingNames : pool.names;
let cssName = misc.makeCssName(pool.category, "pool");
const caption =
'<span class="' +
cssName +
'">' +
misc.escapeHtml(pool.names[0] + " (" + pool.postCount + ")") +
misc.escapeHtml(pool.matchingNames[0] + " (" + pool.postCount + ")") +
"</span>";
return {
caption: caption,
@ -46,7 +48,7 @@ class PoolAutoCompleteControl extends AutoCompleteControl {
{ noProgress: true }).then(
(response) =>
resolve(
_poolListToMatches(response.results, this._options)
_poolListToMatches(text, response.results, this._options)
),
reject
);
@ -55,6 +57,17 @@ class PoolAutoCompleteControl extends AutoCompleteControl {
super(input, options);
}
_getActiveSuggestion() {
if (this._activeResult === -1) {
return null;
}
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;
return result;
}
}
module.exports = PoolAutoCompleteControl;

View File

@ -5,24 +5,27 @@ 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 = tag.names.filter((name) => misc.wildcardMatch(text + "*", name, false));
tag.matchingNames = tag.matchingNames.length ? tag.matchingNames : tag.names;
let cssName = misc.makeCssName(tag.category, "tag");
if (options.isTaggedWith(tag.names[0])) {
cssName += " disabled";
}
if (negated) {
tag.names = tag.names.map((tagName) => "-"+tagName);
tag.matchingNames = tag.matchingNames.map((tagName) => "-"+tagName);
}
const caption =
'<span class="' +
cssName +
'">' +
misc.escapeHtml(tag.names[0] + " (" + tag.postCount + ")") +
misc.escapeHtml(tag.matchingNames[0] + " (" + tag.postCount + ")") +
"</span>";
return {
caption: caption,
@ -68,7 +71,7 @@ class TagAutoCompleteControl extends AutoCompleteControl {
{ noProgress: true }).then(
(response) =>
resolve(
_tagListToMatches(response.results, this._options, negated)
_tagListToMatches(text, response.results, this._options, negated)
),
reject
);
@ -77,6 +80,17 @@ class TagAutoCompleteControl extends AutoCompleteControl {
super(input, options);
}
_getActiveSuggestion() {
if (this._activeResult === -1) {
return null;
}
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;
return result;
}
}
module.exports = TagAutoCompleteControl;

View File

@ -211,6 +211,12 @@ function getPrettyName(tag) {
return tag;
}
function wildcardMatch(pattern, str, sensitive = false) {
let w = pattern.replace(/[.+^${}()|[\]\\?]/g, "\\$&");
const re = new RegExp(`^${w.replace(/\(--wildcard--\)|\*/g, ".*")}$`, sensitive ? "" : "i");
return re.test(str);
}
module.exports = {
range: range,
formatRelativeTime: formatRelativeTime,
@ -229,4 +235,5 @@ module.exports = {
escapeSearchTerm: escapeSearchTerm,
dataURItoBlob: dataURItoBlob,
getPrettyName: getPrettyName,
wildcardMatch: wildcardMatch,
};

View File

@ -27,7 +27,7 @@ class HomeView {
{
confirm: (tag) =>
this._autoCompleteControl.replaceSelectedText(
misc.escapeSearchTerm(tag.names[0]),
misc.escapeSearchTerm(tag.matchingNames[0]),
true
),
isNegationAllowed: true,

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

@ -21,7 +21,7 @@ class PoolsHeaderView extends events.EventTarget {
{
confirm: (pool) =>
this._autoCompleteControl.replaceSelectedText(
misc.escapeSearchTerm(pool.names[0]),
misc.escapeSearchTerm(pool.matchingNames[0]),
true
),
}

View File

@ -183,7 +183,7 @@ class PostsHeaderView extends events.EventTarget {
{
confirm: (tag) =>
this._autoCompleteControl.replaceSelectedText(
misc.escapeSearchTerm(tag.names[0]),
misc.escapeSearchTerm(tag.matchingNames[0]),
true
),
isNegationAllowed: true,

View File

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

View File

@ -21,7 +21,7 @@ class TagsHeaderView extends events.EventTarget {
{
confirm: (tag) =>
this._autoCompleteControl.replaceSelectedText(
misc.escapeSearchTerm(tag.names[0]),
misc.escapeSearchTerm(tag.matchingNames[0]),
true
),
}