This commit is contained in:
Eva
2025-04-04 09:31:49 +00:00
committed by GitHub
16 changed files with 108 additions and 28 deletions

View File

@ -369,6 +369,7 @@ input[type=file]:focus+.file-dropper,
a a
display: block display: block
padding: 0.1em 0.5em padding: 0.1em 0.5em
transition: none
&.active a, a:hover &.active a, a:hover
background: $button-enabled-background-color background: $button-enabled-background-color
color: $button-enabled-text-color color: $button-enabled-text-color

View File

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

View File

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

View File

@ -70,11 +70,18 @@ class AutoCompleteControl {
prefix = this._sourceInputNode.value.substring(0, index + 1); prefix = this._sourceInputNode.value.substring(0, index + 1);
middle = this._sourceInputNode.value.substring(index + 1); middle = this._sourceInputNode.value.substring(index + 1);
} }
suffix = spaceIndex < commaIndex ? suffix.replace(/^[^,]+/, "") : suffix.replace(/^\S+/, "");
suffix = suffix.trimLeft();
this._sourceInputNode.value = this._sourceInputNode.value =
prefix + result.toString() + delimiter + suffix.trimLeft(); prefix + result.toString() + delimiter + suffix;
if (!addSpace) { if (!addSpace) {
this._sourceInputNode.value = this._sourceInputNode.value.trim(); this._sourceInputNode.value = this._sourceInputNode.value.trimLeft();
} }
const selection = this._sourceInputNode.value.length - suffix.length;
if (!addSpace) {
this._sourceInputNode.value = this._sourceInputNode.value.trimRight();
}
this._sourceInputNode.setSelectionRange(selection, selection);
this._sourceInputNode.focus(); this._sourceInputNode.focus();
} }
@ -228,6 +235,13 @@ class AutoCompleteControl {
} }
_updateResults(textToFind) { _updateResults(textToFind) {
if (this._options.isNegationAllowed && textToFind === "-") {
this._results = [];
this._activeResult = -1;
this._refreshList();
return;
}
this._options.getMatches(textToFind).then((matches) => { this._options.getMatches(textToFind).then((matches) => {
const oldResults = this._results.slice(); const oldResults = this._results.slice();
this._results = matches.slice(0, this._options.maxResults); this._results = matches.slice(0, this._options.maxResults);

View File

@ -4,18 +4,19 @@ const misc = require("../util/misc.js");
const PoolList = require("../models/pool_list.js"); const PoolList = require("../models/pool_list.js");
const AutoCompleteControl = require("./auto_complete_control.js"); const AutoCompleteControl = require("./auto_complete_control.js");
function _poolListToMatches(pools, options) { function _poolListToMatches(text, pools, options) {
return [...pools] return [...pools]
.sort((pool1, pool2) => { .sort((pool1, pool2) => {
return pool2.postCount - pool1.postCount; return pool2.postCount - pool1.postCount;
}) })
.map((pool) => { .map((pool) => {
pool.matchingNames = misc.matchingNames(text, pool.names);
let cssName = misc.makeCssName(pool.category, "pool"); let cssName = misc.makeCssName(pool.category, "pool");
const caption = const caption =
'<span class="' + '<span class="' +
cssName + cssName +
'">' + '">' +
misc.escapeHtml(pool.names[0] + " (" + pool.postCount + ")") + misc.escapeHtml(pool.matchingNames[0] + " (" + pool.postCount + ")") +
"</span>"; "</span>";
return { return {
caption: caption, caption: caption,
@ -31,9 +32,9 @@ class PoolAutoCompleteControl extends AutoCompleteControl {
options.getMatches = (text) => { options.getMatches = (text) => {
const term = misc.escapeSearchTerm(text); const term = misc.escapeSearchTerm(text);
const query = const query =
(text.length < minLengthForPartialSearch (text.length >= minLengthForPartialSearch
? term + "*" ? "*" + term + "*"
: "*" + term + "*") + " sort:post-count"; : term + "*") + " sort:post-count";
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
PoolList.search(query, 0, this._options.maxResults, [ PoolList.search(query, 0, this._options.maxResults, [
@ -42,10 +43,11 @@ class PoolAutoCompleteControl extends AutoCompleteControl {
"category", "category",
"postCount", "postCount",
"version", "version",
]).then( ],
{ noProgress: true }).then(
(response) => (response) =>
resolve( resolve(
_poolListToMatches(response.results, this._options) _poolListToMatches(text, response.results, this._options)
), ),
reject reject
); );
@ -54,6 +56,16 @@ class PoolAutoCompleteControl extends AutoCompleteControl {
super(input, options); super(input, options);
} }
_getActiveSuggestion() {
if (this._activeResult === -1) {
return null;
}
const result = this._results[this._activeResult].value;
const textToFind = this._options.getTextToFind();
result.matchingNames = misc.matchingNames(textToFind, result.names);
return result;
}
} }
module.exports = PoolAutoCompleteControl; module.exports = PoolAutoCompleteControl;

View File

@ -5,21 +5,26 @@ const views = require("../util/views.js");
const TagList = require("../models/tag_list.js"); const TagList = require("../models/tag_list.js");
const AutoCompleteControl = require("./auto_complete_control.js"); const AutoCompleteControl = require("./auto_complete_control.js");
function _tagListToMatches(tags, options) { function _tagListToMatches(text, tags, options, negated) {
return [...tags] return [...tags]
.sort((tag1, tag2) => { .sort((tag1, tag2) => {
return tag2.usages - tag1.usages; return tag2.usages - tag1.usages;
}) })
.map((tag) => { .map((tag) => {
tag.matchingNames = misc.matchingNames(text, tag.names);
let cssName = misc.makeCssName(tag.category, "tag"); let cssName = misc.makeCssName(tag.category, "tag");
if (options.isTaggedWith(tag.names[0])) { if (options.isTaggedWith(tag.names[0])) {
cssName += " disabled"; cssName += " disabled";
} }
if (negated) {
tag.names = tag.names.map((tagName) => "-"+tagName);
tag.matchingNames = tag.matchingNames.map((tagName) => "-"+tagName);
}
const caption = const caption =
'<span class="' + '<span class="' +
cssName + cssName +
'">' + '">' +
misc.escapeHtml(tag.names[0] + " (" + tag.postCount + ")") + misc.escapeHtml(tag.matchingNames[0] + " (" + tag.postCount + ")") +
"</span>"; "</span>";
return { return {
caption: caption, caption: caption,
@ -35,26 +40,37 @@ class TagAutoCompleteControl extends AutoCompleteControl {
options = Object.assign( options = Object.assign(
{ {
isTaggedWith: (tag) => false, isTaggedWith: (tag) => false,
isNegationAllowed: false,
}, },
options options
); );
options.getMatches = (text) => { options.getMatches = (text) => {
const negated = options.isNegationAllowed && text[0] === "-";
if (negated) text = text.substring(1);
if (!text) {
return new Promise((resolve, reject) => {
(response) => resolve(null),
reject
});
}
const term = misc.escapeSearchTerm(text); const term = misc.escapeSearchTerm(text);
const query = const query =
(text.length < minLengthForPartialSearch (text.length >= minLengthForPartialSearch || (!options.isNegationAllowed && text[0] === "-")
? term + "*" ? "*" + term + "*"
: "*" + term + "*") + " sort:usages"; : term + "*") + " sort:usages";
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
TagList.search(query, 0, this._options.maxResults, [ TagList.search(query, 0, this._options.maxResults, [
"names", "names",
"category", "category",
"usages", "usages",
]).then( ],
{ noProgress: true }).then(
(response) => (response) =>
resolve( resolve(
_tagListToMatches(response.results, this._options) _tagListToMatches(text, response.results, this._options, negated)
), ),
reject reject
); );
@ -63,6 +79,16 @@ class TagAutoCompleteControl extends AutoCompleteControl {
super(input, options); super(input, options);
} }
_getActiveSuggestion() {
if (this._activeResult === -1) {
return null;
}
const result = this._results[this._activeResult].value;
const textToFind = this._options.getTextToFind();
result.matchingNames = misc.matchingNames(textToFind, result.names);
return result;
}
} }
module.exports = TagAutoCompleteControl; module.exports = TagAutoCompleteControl;

View File

@ -114,6 +114,7 @@ class TagInputControl extends events.EventTarget {
}, },
verticalShift: -2, verticalShift: -2,
isTaggedWith: (tagName) => this.tags.isTaggedWith(tagName), isTaggedWith: (tagName) => this.tags.isTaggedWith(tagName),
isNegationAllowed: false,
} }
); );

View File

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

View File

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

View File

@ -211,6 +211,24 @@ function getPrettyName(tag) {
return 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);
}
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 = { module.exports = {
range: range, range: range,
formatRelativeTime: formatRelativeTime, formatRelativeTime: formatRelativeTime,
@ -229,4 +247,6 @@ module.exports = {
escapeSearchTerm: escapeSearchTerm, escapeSearchTerm: escapeSearchTerm,
dataURItoBlob: dataURItoBlob, dataURItoBlob: dataURItoBlob,
getPrettyName: getPrettyName, getPrettyName: getPrettyName,
wildcardMatch: wildcardMatch,
matchingNames: matchingNames,
}; };

View File

@ -27,9 +27,10 @@ class HomeView {
{ {
confirm: (tag) => confirm: (tag) =>
this._autoCompleteControl.replaceSelectedText( this._autoCompleteControl.replaceSelectedText(
misc.escapeSearchTerm(tag.names[0]), misc.escapeSearchTerm(tag.matchingNames[0]),
true true
), ),
isNegationAllowed: true,
} }
); );
this._formNode.addEventListener("submit", (e) => this._formNode.addEventListener("submit", (e) =>

View File

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

View File

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

View File

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

View File

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

View File

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