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>

fa10833e520b2e39c6597308126a0d0276ca904d

RossAscends <124905043+RossAscends@users.noreply.github.com>

Signed
4 files changed, +57 -33Ignore whitespace
public/index.html+8 -0
@@ -5041,6 +5041,14 @@
50415041 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
50425042 </div>
50435043 <div class="inline-drawer-content">
5044+ <label for="stscript_autocomplete_state">
5045+ <small data-i18n="Visibility">Visibility</small>
5046+ </label>
5047+ <select id="stscript_autocomplete_state">
5048+ <option value="0" data-i18n="Don't show">Don't show</option>
5049+ <option value="1" data-i18n="Input length > 1">Input length > 1</option>
5050+ <option value="2" data-i18n="Always show">Always show</option>
5051+ </select>
50445052 <label class="checkbox_label" for="stscript_autocomplete_autoHide">
50455053 <input id="stscript_autocomplete_autoHide" type="checkbox" />
50465054 <small data-i18n="Automatically hide details">
public/scripts/autocomplete/AutoComplete.js+36 -30
@@ -21,6 +21,14 @@ export const AUTOCOMPLETE_SELECT_KEY = {
2121 'ENTER': 2, // 2^1
2222};
2323
24+/** @readonly */
25+/** @enum {Number} */
26+export const AUTOCOMPLETE_STATE = {
27+ DISABLED: 0,
28+ MIN_LENGTH: 1,
29+ ALWAYS: 2,
30+};
31+
2432export class AutoComplete {
2533 /**@type {HTMLTextAreaElement|HTMLInputElement}*/ textarea;
2634 /**@type {boolean}*/ isFloating = false;
@@ -109,20 +117,20 @@ export class AutoComplete {
109117 this.updateDetailsPositionDebounced = debounce(this.updateDetailsPosition.bind(this), 10);
110118 this.updateFloatingPositionDebounced = debounce(this.updateFloatingPosition.bind(this), 10);
111119
112120 textarea.addEventListener('input', () => {
113121 this.selectionStart = this.textarea.selectionStart;
114122 if (this.text != this.textarea.value) this.show(true, this.wasForced);
115123 });
116124 textarea.addEventListener('keydown', (evt) => this.handleKeyDown(evt));
117125 textarea.addEventListener('click', () => {
118126 this.selectionStart = this.textarea.selectionStart;
119127 if (this.isActive) this.show();
120128 });
121129 textarea.addEventListener('blur', () => this.hide());
122130 if (isFloating) {
123131 textarea.addEventListener('scroll', () => this.updateFloatingPositionDebounced());
124132 }
125133 window.addEventListener('resize', () => this.updatePositionDebounced());
126134 }
127135
128136 /**
@@ -132,9 +140,9 @@ export class AutoComplete {
132140 makeItem(option) {
133141 const li = option.renderItem();
134142 // gotta listen to pointerdown (happens before textarea-blur)
135143 li.addEventListener('pointerdown', (evt) => {
136144 evt.preventDefault();
137145 this.selectedItem = this.result.find(it => it.name == li.getAttribute('data-name'));
138146 this.select();
139147 });
140148 return li;
@@ -149,7 +157,7 @@ export class AutoComplete {
149157 const chars = Array.from(item.dom.querySelector('.name').children);
150158 switch (this.matchType) {
151159 case 'strict': {
152160 chars.forEach((it, idx) => {
153161 if (idx + item.nameOffset < item.name.length) {
154162 it.classList.add('matched');
155163 } else {
@@ -160,7 +168,7 @@ export class AutoComplete {
160168 }
161169 case 'includes': {
162170 const start = item.name.toLowerCase().search(this.name);
163171 chars.forEach((it, idx) => {
164172 if (idx + item.nameOffset < start) {
165173 it.classList.remove('matched');
166174 } else if (idx + item.nameOffset < start + item.name.length) {
@@ -172,18 +180,18 @@ export class AutoComplete {
172180 break;
173181 }
174182 case 'fuzzy': {
175183 item.name.replace(this.fuzzyRegex, (_, ...parts) => {
176184 parts.splice(-2, 2);
177185 if (parts.length == 2) {
178186 chars.forEach(c => c.classList.remove('matched'));
179187 } else {
180188 let cIdx = item.nameOffset;
181189 parts.forEach((it, idx) => {
182190 if (it === null || it.length == 0) return '';
183191 if (idx % 2 == 1) {
184192 chars.slice(cIdx, cIdx + it.length).forEach(c => c.classList.add('matched'));
185193 } else {
186194 chars.slice(cIdx, cIdx + it.length).forEach(c => c.classList.remove('matched'));
187195 }
188196 cIdx += it.length;
189197 });
@@ -230,7 +238,7 @@ export class AutoComplete {
230238 if (current.length > 0) {
231239 consecutive.push(current);
232240 }
233241 consecutive.sort((a, b) => b.length - a.length);
234242 option.score = new AutoCompleteFuzzyScore(start, consecutive[0]?.length ?? 0);
235243 return option;
236244 }
@@ -254,8 +262,7 @@ export class AutoComplete {
254262 + this.parserResult.name.length
255263 + (this.startQuote ? 1 : 0)
256264 + (this.endQuote ? 1 : 0)
257265 + 1;
258- ;
259266 }
260267
261268 /**
@@ -344,7 +351,7 @@ export class AutoComplete {
344351
345352 if (this.matchType == 'fuzzy') {
346353 // only build the fuzzy regex if match type is set to fuzzy
347354 this.fuzzyRegex = new RegExp(`^(.*?)${this.name.split('').map(char => `(${escapeRegex(char)})`).join('(.*?)')}(.*?)$`, 'i');
348355 }
349356
350357 //TODO maybe move the matchers somewhere else; a single match function? matchType is available as property
@@ -358,12 +365,12 @@ export class AutoComplete {
358365 // filter the list of options by the partial name according to the matching type
359366 .filter(it => this.isReplaceable || it.name == '' ? (it.matchProvider ? it.matchProvider(this.name) : matchers[this.matchType](it.name)) : it.name.toLowerCase() == this.name)
360367 // remove aliases
361368 .filter((it, idx, list) => list.findIndex(opt => opt.value == it.value) == idx);
362369
363370 if (this.result.length == 0 && this.effectiveParserResult != this.parserResult && isForced) {
364371 // no matching secondary results and forced trigger -> show current command details
365372 this.secondaryParserResult = null;
366373 this.result = [this.effectiveParserResult.optionList.find(it => it.name == this.effectiveParserResult.name)];
367374 this.name = this.effectiveParserResult.name;
368375 this.fuzzyRegex = /(.*)(.*)(.*)/;
369376 }
@@ -387,8 +394,7 @@ export class AutoComplete {
387394 return option;
388395 })
389396 // sort by fuzzy score or alphabetical
390397 .toSorted(this.matchType == 'fuzzy' ? this.fuzzyScoreCompare : (a, b) => a.name.localeCompare(b.name));
391- ;
392398
393399
394400
@@ -628,12 +634,12 @@ export class AutoComplete {
628634 this.clone.style.position = 'fixed';
629635 this.clone.style.visibility = 'hidden';
630636 document.body.append(this.clone);
631637 const mo = new MutationObserver(muts => {
632638 if (muts.find(it => Array.from(it.removedNodes).includes(this.textarea))) {
633639 this.clone.remove();
634640 }
635641 });
636642 mo.observe(this.textarea.parentElement, { childList: true });
637643 }
638644 this.clone.style.height = `${inputRect.height}px`;
639645 this.clone.style.left = `${inputRect.left}px`;
@@ -685,7 +691,7 @@ export class AutoComplete {
685691 this.textarea.selectionDirection = selectionEnd;
686692 }
687693 this.wasForced = false;
688694 this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
689695 this.onSelect?.(this.selectedItem);
690696 }
691697
@@ -700,7 +706,7 @@ export class AutoComplete {
700706 this.selectedItem.dom.classList.add('selected');
701707 const rect = this.selectedItem.dom.children[0].getBoundingClientRect();
702708 const rectParent = this.dom.getBoundingClientRect();
703709 if (rect.top < rectParent.top || rect.bottom > rectParent.bottom ) {
704710 this.dom.scrollTop += rect.top < rectParent.top ? rect.top - rectParent.top : rect.bottom - rectParent.bottom;
705711 }
706712 this.renderDetailsDebounced();
@@ -809,8 +815,8 @@ export class AutoComplete {
809815 }
810816 // await keyup to see if cursor position or text has changed
811817 const oldText = this.textarea.value;
812818 await new Promise(resolve => {
813819 window.addEventListener('keyup', resolve, { once: true });
814820 });
815821 if (this.selectionStart != this.textarea.selectionStart) {
816822 this.selectionStart = this.textarea.selectionStart;
public/scripts/power-user.js+11 -1
@@ -49,7 +49,7 @@ import { FILTER_TYPES } from './filters.js';
4949import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
5050import { SlashCommand } from './slash-commands/SlashCommand.js';
5151import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
5252import { AUTOCOMPLETE_SELECT_KEY, AUTOCOMPLETE_STATE, AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js';
5353import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
5454import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
5555import { POPUP_TYPE, callGenericPopup, fixToastrForDialogs } from './popup.js';
@@ -306,6 +306,7 @@ let power_user = {
306306 stscript: {
307307 matching: 'fuzzy',
308308 autocomplete: {
309+ state: AUTOCOMPLETE_STATE.ALWAYS,
309310 autoHide: false,
310311 style: 'theme',
311312 font: {
@@ -1505,6 +1506,9 @@ async function loadPowerUserSettings(settings, data) {
15051506 if (power_user.stscript.autocomplete === undefined) {
15061507 power_user.stscript.autocomplete = defaultStscript.autocomplete;
15071508 } else {
1509+ if (power_user.stscript.autocomplete.state === undefined) {
1510+ power_user.stscript.autocomplete.state = defaultStscript.autocomplete.state;
1511+ }
15081512 if (power_user.stscript.autocomplete.width === undefined) {
15091513 power_user.stscript.autocomplete.width = defaultStscript.autocomplete.width;
15101514 }
@@ -1642,6 +1646,7 @@ async function loadPowerUserSettings(settings, data) {
16421646 $('#aux_field').val(power_user.aux_field);
16431647 $('#tag_import_setting').val(power_user.tag_import_setting);
16441648
1649+ $('#stscript_autocomplete_state').val(power_user.stscript.autocomplete.state).trigger('input');
16451650 $('#stscript_autocomplete_autoHide').prop('checked', power_user.stscript.autocomplete.autoHide ?? false).trigger('input');
16461651 $('#stscript_matching').val(power_user.stscript.matching ?? 'fuzzy');
16471652 $('#stscript_autocomplete_style').val(power_user.stscript.autocomplete.style ?? 'theme');
@@ -3871,6 +3876,11 @@ $(document).ready(() => {
38713876 saveSettingsDebounced();
38723877 });
38733878
3879+ $('#stscript_autocomplete_state').on('input', function () {
3880+ power_user.stscript.autocomplete.state = Number($(this).val());
3881+ saveSettingsDebounced();
3882+ });
3883+
38743884 $('#stscript_autocomplete_autoHide').on('input', function () {
38753885 power_user.stscript.autocomplete.autoHide = !!$(this).prop('checked');
38763886 saveSettingsDebounced();
public/scripts/slash-commands.js+2 -2
@@ -67,7 +67,7 @@ import { background_settings } from './backgrounds.js';
6767import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
6868import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js';
6969import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
7070import { AutoComplete, AUTOCOMPLETE_STATE } from './autocomplete/AutoComplete.js';
7171import { SlashCommand } from './slash-commands/SlashCommand.js';
7272import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';
7373import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashCommandNamedArgumentAssignment.js';
@@ -4927,7 +4927,7 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
49274927 const parser = new SlashCommandParser();
49284928 const ac = new AutoComplete(
49294929 textarea,
4930- () => ac.text[0] == '/',
4930+ () => ac.text[0] == '/' && (power_user.stscript.autocomplete.state === AUTOCOMPLETE_STATE.ALWAYS || power_user.stscript.autocomplete.state === AUTOCOMPLETE_STATE.MIN_LENGTH && ac.text.length > 2),
49314931 async (text, index) => await parser.getNameAt(text, index),
49324932 isFloating,
49334933 );