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 @@
5041 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>5041 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
5042 </div>5042 </div>
5043 <div class="inline-drawer-content">5043 <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>
5044 <label class="checkbox_label" for="stscript_autocomplete_autoHide">5052 <label class="checkbox_label" for="stscript_autocomplete_autoHide">
5045 <input id="stscript_autocomplete_autoHide" type="checkbox" />5053 <input id="stscript_autocomplete_autoHide" type="checkbox" />
5046 <small data-i18n="Automatically hide details">5054 <small data-i18n="Automatically hide details">
public/scripts/autocomplete/AutoComplete.js+36 -30
@@ -21,6 +21,14 @@ export const AUTOCOMPLETE_SELECT_KEY = {
21 'ENTER': 2, // 2^121 'ENTER': 2, // 2^1
22};22};
2323
24/** @readonly */
25/** @enum {Number} */
26export const AUTOCOMPLETE_STATE = {
27 DISABLED: 0,
28 MIN_LENGTH: 1,
29 ALWAYS: 2,
30};
31
24export class AutoComplete {32export class AutoComplete {
25 /**@type {HTMLTextAreaElement|HTMLInputElement}*/ textarea;33 /**@type {HTMLTextAreaElement|HTMLInputElement}*/ textarea;
26 /**@type {boolean}*/ isFloating = false;34 /**@type {boolean}*/ isFloating = false;
@@ -109,20 +117,20 @@ export class AutoComplete {
109 this.updateDetailsPositionDebounced = debounce(this.updateDetailsPosition.bind(this), 10);117 this.updateDetailsPositionDebounced = debounce(this.updateDetailsPosition.bind(this), 10);
110 this.updateFloatingPositionDebounced = debounce(this.updateFloatingPosition.bind(this), 10);118 this.updateFloatingPositionDebounced = debounce(this.updateFloatingPosition.bind(this), 10);
111119
112 textarea.addEventListener('input', ()=>{120 textarea.addEventListener('input', () => {
113 this.selectionStart = this.textarea.selectionStart;121 this.selectionStart = this.textarea.selectionStart;
114 if (this.text != this.textarea.value) this.show(true, this.wasForced);122 if (this.text != this.textarea.value) this.show(true, this.wasForced);
115 });123 });
116 textarea.addEventListener('keydown', (evt)=>this.handleKeyDown(evt));124 textarea.addEventListener('keydown', (evt) => this.handleKeyDown(evt));
117 textarea.addEventListener('click', ()=>{125 textarea.addEventListener('click', () => {
118 this.selectionStart = this.textarea.selectionStart;126 this.selectionStart = this.textarea.selectionStart;
119 if (this.isActive) this.show();127 if (this.isActive) this.show();
120 });128 });
121 textarea.addEventListener('blur', ()=>this.hide());129 textarea.addEventListener('blur', () => this.hide());
122 if (isFloating) {130 if (isFloating) {
123 textarea.addEventListener('scroll', ()=>this.updateFloatingPositionDebounced());131 textarea.addEventListener('scroll', () => this.updateFloatingPositionDebounced());
124 }132 }
125 window.addEventListener('resize', ()=>this.updatePositionDebounced());133 window.addEventListener('resize', () => this.updatePositionDebounced());
126 }134 }
127135
128 /**136 /**
@@ -132,9 +140,9 @@ export class AutoComplete {
132 makeItem(option) {140 makeItem(option) {
133 const li = option.renderItem();141 const li = option.renderItem();
134 // gotta listen to pointerdown (happens before textarea-blur)142 // gotta listen to pointerdown (happens before textarea-blur)
135 li.addEventListener('pointerdown', (evt)=>{143 li.addEventListener('pointerdown', (evt) => {
136 evt.preventDefault();144 evt.preventDefault();
137 this.selectedItem = this.result.find(it=>it.name == li.getAttribute('data-name'));145 this.selectedItem = this.result.find(it => it.name == li.getAttribute('data-name'));
138 this.select();146 this.select();
139 });147 });
140 return li;148 return li;
@@ -149,7 +157,7 @@ export class AutoComplete {
149 const chars = Array.from(item.dom.querySelector('.name').children);157 const chars = Array.from(item.dom.querySelector('.name').children);
150 switch (this.matchType) {158 switch (this.matchType) {
151 case 'strict': {159 case 'strict': {
152 chars.forEach((it, idx)=>{160 chars.forEach((it, idx) => {
153 if (idx + item.nameOffset < item.name.length) {161 if (idx + item.nameOffset < item.name.length) {
154 it.classList.add('matched');162 it.classList.add('matched');
155 } else {163 } else {
@@ -160,7 +168,7 @@ export class AutoComplete {
160 }168 }
161 case 'includes': {169 case 'includes': {
162 const start = item.name.toLowerCase().search(this.name);170 const start = item.name.toLowerCase().search(this.name);
163 chars.forEach((it, idx)=>{171 chars.forEach((it, idx) => {
164 if (idx + item.nameOffset < start) {172 if (idx + item.nameOffset < start) {
165 it.classList.remove('matched');173 it.classList.remove('matched');
166 } else if (idx + item.nameOffset < start + item.name.length) {174 } else if (idx + item.nameOffset < start + item.name.length) {
@@ -172,18 +180,18 @@ export class AutoComplete {
172 break;180 break;
173 }181 }
174 case 'fuzzy': {182 case 'fuzzy': {
175 item.name.replace(this.fuzzyRegex, (_, ...parts)=>{183 item.name.replace(this.fuzzyRegex, (_, ...parts) => {
176 parts.splice(-2, 2);184 parts.splice(-2, 2);
177 if (parts.length == 2) {185 if (parts.length == 2) {
178 chars.forEach(c=>c.classList.remove('matched'));186 chars.forEach(c => c.classList.remove('matched'));
179 } else {187 } else {
180 let cIdx = item.nameOffset;188 let cIdx = item.nameOffset;
181 parts.forEach((it, idx)=>{189 parts.forEach((it, idx) => {
182 if (it === null || it.length == 0) return '';190 if (it === null || it.length == 0) return '';
183 if (idx % 2 == 1) {191 if (idx % 2 == 1) {
184 chars.slice(cIdx, cIdx + it.length).forEach(c=>c.classList.add('matched'));192 chars.slice(cIdx, cIdx + it.length).forEach(c => c.classList.add('matched'));
185 } else {193 } else {
186 chars.slice(cIdx, cIdx + it.length).forEach(c=>c.classList.remove('matched'));194 chars.slice(cIdx, cIdx + it.length).forEach(c => c.classList.remove('matched'));
187 }195 }
188 cIdx += it.length;196 cIdx += it.length;
189 });197 });
@@ -230,7 +238,7 @@ export class AutoComplete {
230 if (current.length > 0) {238 if (current.length > 0) {
231 consecutive.push(current);239 consecutive.push(current);
232 }240 }
233 consecutive.sort((a,b)=>b.length - a.length);241 consecutive.sort((a, b) => b.length - a.length);
234 option.score = new AutoCompleteFuzzyScore(start, consecutive[0]?.length ?? 0);242 option.score = new AutoCompleteFuzzyScore(start, consecutive[0]?.length ?? 0);
235 return option;243 return option;
236 }244 }
@@ -254,8 +262,7 @@ export class AutoComplete {
254 + this.parserResult.name.length262 + this.parserResult.name.length
255 + (this.startQuote ? 1 : 0)263 + (this.startQuote ? 1 : 0)
256 + (this.endQuote ? 1 : 0)264 + (this.endQuote ? 1 : 0)
257 + 1265 + 1;
258 ;
259 }266 }
260267
261 /**268 /**
@@ -344,7 +351,7 @@ export class AutoComplete {
344351
345 if (this.matchType == 'fuzzy') {352 if (this.matchType == 'fuzzy') {
346 // only build the fuzzy regex if match type is set to fuzzy353 // only build the fuzzy regex if match type is set to fuzzy
347 this.fuzzyRegex = new RegExp(`^(.*?)${this.name.split('').map(char=>`(${escapeRegex(char)})`).join('(.*?)')}(.*?)$`, 'i');354 this.fuzzyRegex = new RegExp(`^(.*?)${this.name.split('').map(char => `(${escapeRegex(char)})`).join('(.*?)')}(.*?)$`, 'i');
348 }355 }
349356
350 //TODO maybe move the matchers somewhere else; a single match function? matchType is available as property357 //TODO maybe move the matchers somewhere else; a single match function? matchType is available as property
@@ -358,12 +365,12 @@ export class AutoComplete {
358 // filter the list of options by the partial name according to the matching type365 // filter the list of options by the partial name according to the matching type
359 .filter(it => this.isReplaceable || it.name == '' ? (it.matchProvider ? it.matchProvider(this.name) : matchers[this.matchType](it.name)) : it.name.toLowerCase() == this.name)366 .filter(it => this.isReplaceable || it.name == '' ? (it.matchProvider ? it.matchProvider(this.name) : matchers[this.matchType](it.name)) : it.name.toLowerCase() == this.name)
360 // remove aliases367 // remove aliases
361 .filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx);368 .filter((it, idx, list) => list.findIndex(opt => opt.value == it.value) == idx);
362369
363 if (this.result.length == 0 && this.effectiveParserResult != this.parserResult && isForced) {370 if (this.result.length == 0 && this.effectiveParserResult != this.parserResult && isForced) {
364 // no matching secondary results and forced trigger -> show current command details371 // no matching secondary results and forced trigger -> show current command details
365 this.secondaryParserResult = null;372 this.secondaryParserResult = null;
366 this.result = [this.effectiveParserResult.optionList.find(it=>it.name == this.effectiveParserResult.name)];373 this.result = [this.effectiveParserResult.optionList.find(it => it.name == this.effectiveParserResult.name)];
367 this.name = this.effectiveParserResult.name;374 this.name = this.effectiveParserResult.name;
368 this.fuzzyRegex = /(.*)(.*)(.*)/;375 this.fuzzyRegex = /(.*)(.*)(.*)/;
369 }376 }
@@ -387,8 +394,7 @@ export class AutoComplete {
387 return option;394 return option;
388 })395 })
389 // sort by fuzzy score or alphabetical396 // sort by fuzzy score or alphabetical
390 .toSorted(this.matchType == 'fuzzy' ? this.fuzzyScoreCompare : (a, b) => a.name.localeCompare(b.name))397 .toSorted(this.matchType == 'fuzzy' ? this.fuzzyScoreCompare : (a, b) => a.name.localeCompare(b.name));
391 ;
392398
393399
394400
@@ -628,12 +634,12 @@ export class AutoComplete {
628 this.clone.style.position = 'fixed';634 this.clone.style.position = 'fixed';
629 this.clone.style.visibility = 'hidden';635 this.clone.style.visibility = 'hidden';
630 document.body.append(this.clone);636 document.body.append(this.clone);
631 const mo = new MutationObserver(muts=>{637 const mo = new MutationObserver(muts => {
632 if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) {638 if (muts.find(it => Array.from(it.removedNodes).includes(this.textarea))) {
633 this.clone.remove();639 this.clone.remove();
634 }640 }
635 });641 });
636 mo.observe(this.textarea.parentElement, { childList:true });642 mo.observe(this.textarea.parentElement, { childList: true });
637 }643 }
638 this.clone.style.height = `${inputRect.height}px`;644 this.clone.style.height = `${inputRect.height}px`;
639 this.clone.style.left = `${inputRect.left}px`;645 this.clone.style.left = `${inputRect.left}px`;
@@ -685,7 +691,7 @@ export class AutoComplete {
685 this.textarea.selectionDirection = selectionEnd;691 this.textarea.selectionDirection = selectionEnd;
686 }692 }
687 this.wasForced = false;693 this.wasForced = false;
688 this.textarea.dispatchEvent(new Event('input', { bubbles:true }));694 this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
689 this.onSelect?.(this.selectedItem);695 this.onSelect?.(this.selectedItem);
690 }696 }
691697
@@ -700,7 +706,7 @@ export class AutoComplete {
700 this.selectedItem.dom.classList.add('selected');706 this.selectedItem.dom.classList.add('selected');
701 const rect = this.selectedItem.dom.children[0].getBoundingClientRect();707 const rect = this.selectedItem.dom.children[0].getBoundingClientRect();
702 const rectParent = this.dom.getBoundingClientRect();708 const rectParent = this.dom.getBoundingClientRect();
703 if (rect.top < rectParent.top || rect.bottom > rectParent.bottom ) {709 if (rect.top < rectParent.top || rect.bottom > rectParent.bottom) {
704 this.dom.scrollTop += rect.top < rectParent.top ? rect.top - rectParent.top : rect.bottom - rectParent.bottom;710 this.dom.scrollTop += rect.top < rectParent.top ? rect.top - rectParent.top : rect.bottom - rectParent.bottom;
705 }711 }
706 this.renderDetailsDebounced();712 this.renderDetailsDebounced();
@@ -809,8 +815,8 @@ export class AutoComplete {
809 }815 }
810 // await keyup to see if cursor position or text has changed816 // await keyup to see if cursor position or text has changed
811 const oldText = this.textarea.value;817 const oldText = this.textarea.value;
812 await new Promise(resolve=>{818 await new Promise(resolve => {
813 window.addEventListener('keyup', resolve, { once:true });819 window.addEventListener('keyup', resolve, { once: true });
814 });820 });
815 if (this.selectionStart != this.textarea.selectionStart) {821 if (this.selectionStart != this.textarea.selectionStart) {
816 this.selectionStart = this.textarea.selectionStart;822 this.selectionStart = this.textarea.selectionStart;
public/scripts/power-user.js+11 -1
@@ -49,7 +49,7 @@ import { FILTER_TYPES } from './filters.js';
49import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';49import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
50import { SlashCommand } from './slash-commands/SlashCommand.js';50import { SlashCommand } from './slash-commands/SlashCommand.js';
51import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';51import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
52import { AUTOCOMPLETE_SELECT_KEY, AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js';52import { AUTOCOMPLETE_SELECT_KEY, AUTOCOMPLETE_STATE, AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js';
53import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';53import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
54import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';54import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
55import { POPUP_TYPE, callGenericPopup, fixToastrForDialogs } from './popup.js';55import { POPUP_TYPE, callGenericPopup, fixToastrForDialogs } from './popup.js';
@@ -306,6 +306,7 @@ let power_user = {
306 stscript: {306 stscript: {
307 matching: 'fuzzy',307 matching: 'fuzzy',
308 autocomplete: {308 autocomplete: {
309 state: AUTOCOMPLETE_STATE.ALWAYS,
309 autoHide: false,310 autoHide: false,
310 style: 'theme',311 style: 'theme',
311 font: {312 font: {
@@ -1505,6 +1506,9 @@ async function loadPowerUserSettings(settings, data) {
1505 if (power_user.stscript.autocomplete === undefined) {1506 if (power_user.stscript.autocomplete === undefined) {
1506 power_user.stscript.autocomplete = defaultStscript.autocomplete;1507 power_user.stscript.autocomplete = defaultStscript.autocomplete;
1507 } else {1508 } else {
1509 if (power_user.stscript.autocomplete.state === undefined) {
1510 power_user.stscript.autocomplete.state = defaultStscript.autocomplete.state;
1511 }
1508 if (power_user.stscript.autocomplete.width === undefined) {1512 if (power_user.stscript.autocomplete.width === undefined) {
1509 power_user.stscript.autocomplete.width = defaultStscript.autocomplete.width;1513 power_user.stscript.autocomplete.width = defaultStscript.autocomplete.width;
1510 }1514 }
@@ -1642,6 +1646,7 @@ async function loadPowerUserSettings(settings, data) {
1642 $('#aux_field').val(power_user.aux_field);1646 $('#aux_field').val(power_user.aux_field);
1643 $('#tag_import_setting').val(power_user.tag_import_setting);1647 $('#tag_import_setting').val(power_user.tag_import_setting);
16441648
1649 $('#stscript_autocomplete_state').val(power_user.stscript.autocomplete.state).trigger('input');
1645 $('#stscript_autocomplete_autoHide').prop('checked', power_user.stscript.autocomplete.autoHide ?? false).trigger('input');1650 $('#stscript_autocomplete_autoHide').prop('checked', power_user.stscript.autocomplete.autoHide ?? false).trigger('input');
1646 $('#stscript_matching').val(power_user.stscript.matching ?? 'fuzzy');1651 $('#stscript_matching').val(power_user.stscript.matching ?? 'fuzzy');
1647 $('#stscript_autocomplete_style').val(power_user.stscript.autocomplete.style ?? 'theme');1652 $('#stscript_autocomplete_style').val(power_user.stscript.autocomplete.style ?? 'theme');
@@ -3871,6 +3876,11 @@ $(document).ready(() => {
3871 saveSettingsDebounced();3876 saveSettingsDebounced();
3872 });3877 });
38733878
3879 $('#stscript_autocomplete_state').on('input', function () {
3880 power_user.stscript.autocomplete.state = Number($(this).val());
3881 saveSettingsDebounced();
3882 });
3883
3874 $('#stscript_autocomplete_autoHide').on('input', function () {3884 $('#stscript_autocomplete_autoHide').on('input', function () {
3875 power_user.stscript.autocomplete.autoHide = !!$(this).prop('checked');3885 power_user.stscript.autocomplete.autoHide = !!$(this).prop('checked');
3876 saveSettingsDebounced();3886 saveSettingsDebounced();
public/scripts/slash-commands.js+2 -2
@@ -67,7 +67,7 @@ import { background_settings } from './backgrounds.js';
67import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';67import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
68import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js';68import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js';
69import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';69import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
70import { AutoComplete } from './autocomplete/AutoComplete.js';70import { AutoComplete, AUTOCOMPLETE_STATE } from './autocomplete/AutoComplete.js';
71import { SlashCommand } from './slash-commands/SlashCommand.js';71import { SlashCommand } from './slash-commands/SlashCommand.js';
72import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';72import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';
73import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashCommandNamedArgumentAssignment.js';73import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashCommandNamedArgumentAssignment.js';
@@ -4927,7 +4927,7 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
4927 const parser = new SlashCommandParser();4927 const parser = new SlashCommandParser();
4928 const ac = new AutoComplete(4928 const ac = new AutoComplete(
4929 textarea,4929 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),
4931 async (text, index) => await parser.getNameAt(text, index),4931 async (text, index) => await parser.getNameAt(text, index),
4932 isFloating,4932 isFloating,
4933 );4933 );