mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
add minimum requirement of 2 [A-za-z] for slashcommand autocomplete to show up (#4080)
* add minimum requirement of 2 [A-za-z] for slashcommand autocomplete to show up * Migrate to dedicated AC toggle * Replace state checkbox with select --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
This commit is contained in:
@@ -21,6 +21,14 @@ export const AUTOCOMPLETE_SELECT_KEY = {
|
||||
'ENTER': 2, // 2^1
|
||||
};
|
||||
|
||||
/** @readonly */
|
||||
/** @enum {Number} */
|
||||
export const AUTOCOMPLETE_STATE = {
|
||||
DISABLED: 0,
|
||||
MIN_LENGTH: 1,
|
||||
ALWAYS: 2,
|
||||
};
|
||||
|
||||
export class AutoComplete {
|
||||
/**@type {HTMLTextAreaElement|HTMLInputElement}*/ textarea;
|
||||
/**@type {boolean}*/ isFloating = false;
|
||||
@@ -109,20 +117,20 @@ export class AutoComplete {
|
||||
this.updateDetailsPositionDebounced = debounce(this.updateDetailsPosition.bind(this), 10);
|
||||
this.updateFloatingPositionDebounced = debounce(this.updateFloatingPosition.bind(this), 10);
|
||||
|
||||
textarea.addEventListener('input', ()=>{
|
||||
textarea.addEventListener('input', () => {
|
||||
this.selectionStart = this.textarea.selectionStart;
|
||||
if (this.text != this.textarea.value) this.show(true, this.wasForced);
|
||||
});
|
||||
textarea.addEventListener('keydown', (evt)=>this.handleKeyDown(evt));
|
||||
textarea.addEventListener('click', ()=>{
|
||||
textarea.addEventListener('keydown', (evt) => this.handleKeyDown(evt));
|
||||
textarea.addEventListener('click', () => {
|
||||
this.selectionStart = this.textarea.selectionStart;
|
||||
if (this.isActive) this.show();
|
||||
});
|
||||
textarea.addEventListener('blur', ()=>this.hide());
|
||||
textarea.addEventListener('blur', () => this.hide());
|
||||
if (isFloating) {
|
||||
textarea.addEventListener('scroll', ()=>this.updateFloatingPositionDebounced());
|
||||
textarea.addEventListener('scroll', () => this.updateFloatingPositionDebounced());
|
||||
}
|
||||
window.addEventListener('resize', ()=>this.updatePositionDebounced());
|
||||
window.addEventListener('resize', () => this.updatePositionDebounced());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,9 +140,9 @@ export class AutoComplete {
|
||||
makeItem(option) {
|
||||
const li = option.renderItem();
|
||||
// gotta listen to pointerdown (happens before textarea-blur)
|
||||
li.addEventListener('pointerdown', (evt)=>{
|
||||
li.addEventListener('pointerdown', (evt) => {
|
||||
evt.preventDefault();
|
||||
this.selectedItem = this.result.find(it=>it.name == li.getAttribute('data-name'));
|
||||
this.selectedItem = this.result.find(it => it.name == li.getAttribute('data-name'));
|
||||
this.select();
|
||||
});
|
||||
return li;
|
||||
@@ -149,7 +157,7 @@ export class AutoComplete {
|
||||
const chars = Array.from(item.dom.querySelector('.name').children);
|
||||
switch (this.matchType) {
|
||||
case 'strict': {
|
||||
chars.forEach((it, idx)=>{
|
||||
chars.forEach((it, idx) => {
|
||||
if (idx + item.nameOffset < item.name.length) {
|
||||
it.classList.add('matched');
|
||||
} else {
|
||||
@@ -160,7 +168,7 @@ export class AutoComplete {
|
||||
}
|
||||
case 'includes': {
|
||||
const start = item.name.toLowerCase().search(this.name);
|
||||
chars.forEach((it, idx)=>{
|
||||
chars.forEach((it, idx) => {
|
||||
if (idx + item.nameOffset < start) {
|
||||
it.classList.remove('matched');
|
||||
} else if (idx + item.nameOffset < start + item.name.length) {
|
||||
@@ -172,18 +180,18 @@ export class AutoComplete {
|
||||
break;
|
||||
}
|
||||
case 'fuzzy': {
|
||||
item.name.replace(this.fuzzyRegex, (_, ...parts)=>{
|
||||
item.name.replace(this.fuzzyRegex, (_, ...parts) => {
|
||||
parts.splice(-2, 2);
|
||||
if (parts.length == 2) {
|
||||
chars.forEach(c=>c.classList.remove('matched'));
|
||||
chars.forEach(c => c.classList.remove('matched'));
|
||||
} else {
|
||||
let cIdx = item.nameOffset;
|
||||
parts.forEach((it, idx)=>{
|
||||
parts.forEach((it, idx) => {
|
||||
if (it === null || it.length == 0) return '';
|
||||
if (idx % 2 == 1) {
|
||||
chars.slice(cIdx, cIdx + it.length).forEach(c=>c.classList.add('matched'));
|
||||
chars.slice(cIdx, cIdx + it.length).forEach(c => c.classList.add('matched'));
|
||||
} else {
|
||||
chars.slice(cIdx, cIdx + it.length).forEach(c=>c.classList.remove('matched'));
|
||||
chars.slice(cIdx, cIdx + it.length).forEach(c => c.classList.remove('matched'));
|
||||
}
|
||||
cIdx += it.length;
|
||||
});
|
||||
@@ -230,7 +238,7 @@ export class AutoComplete {
|
||||
if (current.length > 0) {
|
||||
consecutive.push(current);
|
||||
}
|
||||
consecutive.sort((a,b)=>b.length - a.length);
|
||||
consecutive.sort((a, b) => b.length - a.length);
|
||||
option.score = new AutoCompleteFuzzyScore(start, consecutive[0]?.length ?? 0);
|
||||
return option;
|
||||
}
|
||||
@@ -254,8 +262,7 @@ export class AutoComplete {
|
||||
+ this.parserResult.name.length
|
||||
+ (this.startQuote ? 1 : 0)
|
||||
+ (this.endQuote ? 1 : 0)
|
||||
+ 1
|
||||
;
|
||||
+ 1;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -344,7 +351,7 @@ export class AutoComplete {
|
||||
|
||||
if (this.matchType == 'fuzzy') {
|
||||
// only build the fuzzy regex if match type is set to fuzzy
|
||||
this.fuzzyRegex = new RegExp(`^(.*?)${this.name.split('').map(char=>`(${escapeRegex(char)})`).join('(.*?)')}(.*?)$`, 'i');
|
||||
this.fuzzyRegex = new RegExp(`^(.*?)${this.name.split('').map(char => `(${escapeRegex(char)})`).join('(.*?)')}(.*?)$`, 'i');
|
||||
}
|
||||
|
||||
//TODO maybe move the matchers somewhere else; a single match function? matchType is available as property
|
||||
@@ -358,12 +365,12 @@ export class AutoComplete {
|
||||
// filter the list of options by the partial name according to the matching type
|
||||
.filter(it => this.isReplaceable || it.name == '' ? (it.matchProvider ? it.matchProvider(this.name) : matchers[this.matchType](it.name)) : it.name.toLowerCase() == this.name)
|
||||
// remove aliases
|
||||
.filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx);
|
||||
.filter((it, idx, list) => list.findIndex(opt => opt.value == it.value) == idx);
|
||||
|
||||
if (this.result.length == 0 && this.effectiveParserResult != this.parserResult && isForced) {
|
||||
// no matching secondary results and forced trigger -> show current command details
|
||||
this.secondaryParserResult = null;
|
||||
this.result = [this.effectiveParserResult.optionList.find(it=>it.name == this.effectiveParserResult.name)];
|
||||
this.result = [this.effectiveParserResult.optionList.find(it => it.name == this.effectiveParserResult.name)];
|
||||
this.name = this.effectiveParserResult.name;
|
||||
this.fuzzyRegex = /(.*)(.*)(.*)/;
|
||||
}
|
||||
@@ -387,8 +394,7 @@ export class AutoComplete {
|
||||
return option;
|
||||
})
|
||||
// sort by fuzzy score or alphabetical
|
||||
.toSorted(this.matchType == 'fuzzy' ? this.fuzzyScoreCompare : (a, b) => a.name.localeCompare(b.name))
|
||||
;
|
||||
.toSorted(this.matchType == 'fuzzy' ? this.fuzzyScoreCompare : (a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
|
||||
|
||||
@@ -628,12 +634,12 @@ export class AutoComplete {
|
||||
this.clone.style.position = 'fixed';
|
||||
this.clone.style.visibility = 'hidden';
|
||||
document.body.append(this.clone);
|
||||
const mo = new MutationObserver(muts=>{
|
||||
if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) {
|
||||
const mo = new MutationObserver(muts => {
|
||||
if (muts.find(it => Array.from(it.removedNodes).includes(this.textarea))) {
|
||||
this.clone.remove();
|
||||
}
|
||||
});
|
||||
mo.observe(this.textarea.parentElement, { childList:true });
|
||||
mo.observe(this.textarea.parentElement, { childList: true });
|
||||
}
|
||||
this.clone.style.height = `${inputRect.height}px`;
|
||||
this.clone.style.left = `${inputRect.left}px`;
|
||||
@@ -685,7 +691,7 @@ export class AutoComplete {
|
||||
this.textarea.selectionDirection = selectionEnd;
|
||||
}
|
||||
this.wasForced = false;
|
||||
this.textarea.dispatchEvent(new Event('input', { bubbles:true }));
|
||||
this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
this.onSelect?.(this.selectedItem);
|
||||
}
|
||||
|
||||
@@ -700,7 +706,7 @@ export class AutoComplete {
|
||||
this.selectedItem.dom.classList.add('selected');
|
||||
const rect = this.selectedItem.dom.children[0].getBoundingClientRect();
|
||||
const rectParent = this.dom.getBoundingClientRect();
|
||||
if (rect.top < rectParent.top || rect.bottom > rectParent.bottom ) {
|
||||
if (rect.top < rectParent.top || rect.bottom > rectParent.bottom) {
|
||||
this.dom.scrollTop += rect.top < rectParent.top ? rect.top - rectParent.top : rect.bottom - rectParent.bottom;
|
||||
}
|
||||
this.renderDetailsDebounced();
|
||||
@@ -809,8 +815,8 @@ export class AutoComplete {
|
||||
}
|
||||
// await keyup to see if cursor position or text has changed
|
||||
const oldText = this.textarea.value;
|
||||
await new Promise(resolve=>{
|
||||
window.addEventListener('keyup', resolve, { once:true });
|
||||
await new Promise(resolve => {
|
||||
window.addEventListener('keyup', resolve, { once: true });
|
||||
});
|
||||
if (this.selectionStart != this.textarea.selectionStart) {
|
||||
this.selectionStart = this.textarea.selectionStart;
|
||||
|
@@ -49,7 +49,7 @@ import { FILTER_TYPES } from './filters.js';
|
||||
import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
|
||||
import { AUTOCOMPLETE_SELECT_KEY, AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js';
|
||||
import { AUTOCOMPLETE_SELECT_KEY, AUTOCOMPLETE_STATE, AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js';
|
||||
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
|
||||
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
import { POPUP_TYPE, callGenericPopup, fixToastrForDialogs } from './popup.js';
|
||||
@@ -306,6 +306,7 @@ let power_user = {
|
||||
stscript: {
|
||||
matching: 'fuzzy',
|
||||
autocomplete: {
|
||||
state: AUTOCOMPLETE_STATE.ALWAYS,
|
||||
autoHide: false,
|
||||
style: 'theme',
|
||||
font: {
|
||||
@@ -1505,6 +1506,9 @@ async function loadPowerUserSettings(settings, data) {
|
||||
if (power_user.stscript.autocomplete === undefined) {
|
||||
power_user.stscript.autocomplete = defaultStscript.autocomplete;
|
||||
} else {
|
||||
if (power_user.stscript.autocomplete.state === undefined) {
|
||||
power_user.stscript.autocomplete.state = defaultStscript.autocomplete.state;
|
||||
}
|
||||
if (power_user.stscript.autocomplete.width === undefined) {
|
||||
power_user.stscript.autocomplete.width = defaultStscript.autocomplete.width;
|
||||
}
|
||||
@@ -1642,6 +1646,7 @@ async function loadPowerUserSettings(settings, data) {
|
||||
$('#aux_field').val(power_user.aux_field);
|
||||
$('#tag_import_setting').val(power_user.tag_import_setting);
|
||||
|
||||
$('#stscript_autocomplete_state').val(power_user.stscript.autocomplete.state).trigger('input');
|
||||
$('#stscript_autocomplete_autoHide').prop('checked', power_user.stscript.autocomplete.autoHide ?? false).trigger('input');
|
||||
$('#stscript_matching').val(power_user.stscript.matching ?? 'fuzzy');
|
||||
$('#stscript_autocomplete_style').val(power_user.stscript.autocomplete.style ?? 'theme');
|
||||
@@ -3871,6 +3876,11 @@ $(document).ready(() => {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#stscript_autocomplete_state').on('input', function () {
|
||||
power_user.stscript.autocomplete.state = Number($(this).val());
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#stscript_autocomplete_autoHide').on('input', function () {
|
||||
power_user.stscript.autocomplete.autoHide = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
|
@@ -67,7 +67,7 @@ import { background_settings } from './backgrounds.js';
|
||||
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
|
||||
import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
|
||||
import { AutoComplete } from './autocomplete/AutoComplete.js';
|
||||
import { AutoComplete, AUTOCOMPLETE_STATE } from './autocomplete/AutoComplete.js';
|
||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';
|
||||
import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashCommandNamedArgumentAssignment.js';
|
||||
@@ -4927,7 +4927,7 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
|
||||
const parser = new SlashCommandParser();
|
||||
const ac = new AutoComplete(
|
||||
textarea,
|
||||
() => ac.text[0] == '/',
|
||||
() => ac.text[0] == '/' && (power_user.stscript.autocomplete.state === AUTOCOMPLETE_STATE.ALWAYS || power_user.stscript.autocomplete.state === AUTOCOMPLETE_STATE.MIN_LENGTH && ac.text.length > 2),
|
||||
async (text, index) => await parser.getNameAt(text, index),
|
||||
isFloating,
|
||||
);
|
||||
|
Reference in New Issue
Block a user