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