mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
cleanup and comment
This commit is contained in:
@ -1808,16 +1808,20 @@ export function setSlashCommandAutoComplete(textarea, isFloating = false) {
|
|||||||
};
|
};
|
||||||
const show = (isInput = false, isForced = false) => {
|
const show = (isInput = false, isForced = false) => {
|
||||||
//TODO check if isInput and isForced are both required
|
//TODO check if isInput and isForced are both required
|
||||||
// isForced = isForced || isInput;
|
|
||||||
text = textarea.value;
|
text = textarea.value;
|
||||||
// only show with textarea in focus
|
// only show with textarea in focus
|
||||||
if (document.activeElement != textarea) return hide();
|
if (document.activeElement != textarea) return hide();
|
||||||
// only show for slash commands
|
// only show for slash commands
|
||||||
if (text[0] != '/') return hide();
|
if (text[0] != '/') return hide();
|
||||||
|
|
||||||
|
// request parser to get command executor (potentially "incomplete", i.e. not an actual existing command) for
|
||||||
|
// cursor position
|
||||||
executor = parser.getCommandAt(text, textarea.selectionStart);
|
executor = parser.getCommandAt(text, textarea.selectionStart);
|
||||||
let slashCommand = executor?.name?.toLowerCase() ?? '';
|
let slashCommand = executor?.name?.toLowerCase() ?? '';
|
||||||
|
// do autocomplete if triggered by a user input and we either don't have an executor or the cursor is at the end
|
||||||
|
// of the name part of the command
|
||||||
isReplacable = isInput && (!executor ? true : textarea.selectionStart == executor.start - 2 + executor.name.length + 1);
|
isReplacable = isInput && (!executor ? true : textarea.selectionStart == executor.start - 2 + executor.name.length + 1);
|
||||||
|
// if forced (ctrl+space) or user input and cursor is in the middle of the name part (not at the end)
|
||||||
if ((isForced || isInput) && executor && textarea.selectionStart > executor.start - 2 && textarea.selectionStart <= executor.start - 2 + executor.name.length + 1) {
|
if ((isForced || isInput) && executor && textarea.selectionStart > executor.start - 2 && textarea.selectionStart <= executor.start - 2 + executor.name.length + 1) {
|
||||||
slashCommand = slashCommand.slice(0, textarea.selectionStart - (executor.start - 2) - 1);
|
slashCommand = slashCommand.slice(0, textarea.selectionStart - (executor.start - 2) - 1);
|
||||||
executor.name = slashCommand;
|
executor.name = slashCommand;
|
||||||
@ -1902,7 +1906,6 @@ export function setSlashCommandAutoComplete(textarea, isFloating = false) {
|
|||||||
const helpStrings = Object
|
const helpStrings = Object
|
||||||
.keys(parser.commands) // Get all slash commands
|
.keys(parser.commands) // Get all slash commands
|
||||||
.filter(it => executor.name == '' || isReplacable ? matchers[matchType](it) : it.toLowerCase() == slashCommand) // Filter by the input
|
.filter(it => executor.name == '' || isReplacable ? matchers[matchType](it) : it.toLowerCase() == slashCommand) // Filter by the input
|
||||||
// .sort((a, b) => a.localeCompare(b)) // Sort alphabetically
|
|
||||||
;
|
;
|
||||||
result = helpStrings
|
result = helpStrings
|
||||||
.filter((it,idx)=>[idx, -1].includes(helpStrings.indexOf(parser.commands[it].name.toLowerCase()))) // remove duplicates
|
.filter((it,idx)=>[idx, -1].includes(helpStrings.indexOf(parser.commands[it].name.toLowerCase()))) // remove duplicates
|
||||||
@ -1912,16 +1915,17 @@ export function setSlashCommandAutoComplete(textarea, isFloating = false) {
|
|||||||
value: `/${it}`,
|
value: `/${it}`,
|
||||||
score: matchType == 'fuzzy' ? fuzzyScore(it) : null,
|
score: matchType == 'fuzzy' ? fuzzyScore(it) : null,
|
||||||
li: null,
|
li: null,
|
||||||
})) // Map to the help string
|
})) // Map to the help string and score
|
||||||
.toSorted(matchType == 'fuzzy' ? fuzzyScoreCompare : (a, b) => a.name.localeCompare(b.name))
|
.toSorted(matchType == 'fuzzy' ? fuzzyScoreCompare : (a, b) => a.name.localeCompare(b.name)) // sort by score (if fuzzy) or name
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add notice if no match found
|
|
||||||
if (result.length == 0) {
|
if (result.length == 0) {
|
||||||
|
// no result and no input? hide autocomplete
|
||||||
if (!isInput) {
|
if (!isInput) {
|
||||||
return hide();
|
return hide();
|
||||||
}
|
}
|
||||||
|
// otherwise add "no match" notice
|
||||||
result.push({
|
result.push({
|
||||||
name: '',
|
name: '',
|
||||||
label: `No matching commands for "/${slashCommand}"`,
|
label: `No matching commands for "/${slashCommand}"`,
|
||||||
@ -1930,9 +1934,11 @@ export function setSlashCommandAutoComplete(textarea, isFloating = false) {
|
|||||||
li: null,
|
li: null,
|
||||||
});
|
});
|
||||||
} else if (result.length == 1 && result[0].value == `/${executor.name}`) {
|
} else if (result.length == 1 && result[0].value == `/${executor.name}`) {
|
||||||
|
// only one result that is exactly the current value? just show hint, no autocomplete
|
||||||
isReplacable = false;
|
isReplacable = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// render autocomplete list
|
||||||
dom.innerHTML = '';
|
dom.innerHTML = '';
|
||||||
for (const item of result) {
|
for (const item of result) {
|
||||||
const li = document.createElement('li'); {
|
const li = document.createElement('li'); {
|
||||||
@ -1941,13 +1947,15 @@ export function setSlashCommandAutoComplete(textarea, isFloating = false) {
|
|||||||
li.classList.add('selected');
|
li.classList.add('selected');
|
||||||
}
|
}
|
||||||
li.innerHTML = item.label;
|
li.innerHTML = item.label;
|
||||||
|
// gotta listen to pointerdown (happens before textarea-blur)
|
||||||
li.addEventListener('pointerdown', ()=>{
|
li.addEventListener('pointerdown', ()=>{
|
||||||
mouseup = new Promise(resolve=>{
|
// gotta catch pointerup to restore focus to textarea (blurs after pointerdown)
|
||||||
|
pointerup = new Promise(resolve=>{
|
||||||
const resolver = ()=>{
|
const resolver = ()=>{
|
||||||
window.removeEventListener('mouseup', resolver);
|
window.removeEventListener('pointerup', resolver);
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
window.addEventListener('mouseup', resolver);
|
window.addEventListener('pointerup', resolver);
|
||||||
});
|
});
|
||||||
selectedItem = item;
|
selectedItem = item;
|
||||||
select();
|
select();
|
||||||
@ -1978,6 +1986,7 @@ export function setSlashCommandAutoComplete(textarea, isFloating = false) {
|
|||||||
if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) return hide();
|
if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) return hide();
|
||||||
const left = Math.max(rect.left, location.left);
|
const left = Math.max(rect.left, location.left);
|
||||||
if (location.top <= window.innerHeight / 2) {
|
if (location.top <= window.innerHeight / 2) {
|
||||||
|
// if cursor is in lower half of window, show list above line
|
||||||
dom.style.top = `${location.bottom}px`;
|
dom.style.top = `${location.bottom}px`;
|
||||||
dom.style.bottom = 'auto';
|
dom.style.bottom = 'auto';
|
||||||
dom.style.left = `${left}px`;
|
dom.style.left = `${left}px`;
|
||||||
@ -1985,6 +1994,7 @@ export function setSlashCommandAutoComplete(textarea, isFloating = false) {
|
|||||||
dom.style.maxWidth = `calc(99vw - ${left}px)`;
|
dom.style.maxWidth = `calc(99vw - ${left}px)`;
|
||||||
dom.style.maxHeight = `calc(${location.bottom}px - 1vh)`;
|
dom.style.maxHeight = `calc(${location.bottom}px - 1vh)`;
|
||||||
} else {
|
} else {
|
||||||
|
// if cursor is in upper half of window, show list below line
|
||||||
dom.style.top = 'auto';
|
dom.style.top = 'auto';
|
||||||
dom.style.bottom = `calc(100vh - ${location.top}px)`;
|
dom.style.bottom = `calc(100vh - ${location.top}px)`;
|
||||||
dom.style.left = `${left}px`;
|
dom.style.left = `${left}px`;
|
||||||
@ -1993,6 +2003,10 @@ export function setSlashCommandAutoComplete(textarea, isFloating = false) {
|
|||||||
dom.style.maxHeight = `calc(${location.top}px - 1vh)`;
|
dom.style.maxHeight = `calc(${location.top}px - 1vh)`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Creates a temporary invisible clone of the textarea to determine cursor coordinates.
|
||||||
|
* @returns {{left:Number, top:Number, bottom:Number}} cursor coordinates
|
||||||
|
*/
|
||||||
const getCursorPosition = () => {
|
const getCursorPosition = () => {
|
||||||
const inputRect = textarea.getBoundingClientRect();
|
const inputRect = textarea.getBoundingClientRect();
|
||||||
clone?.remove();
|
clone?.remove();
|
||||||
@ -2005,15 +2019,11 @@ export function setSlashCommandAutoComplete(textarea, isFloating = false) {
|
|||||||
clone.style.left = `${inputRect.left}px`;
|
clone.style.left = `${inputRect.left}px`;
|
||||||
clone.style.top = `${inputRect.top}px`;
|
clone.style.top = `${inputRect.top}px`;
|
||||||
clone.style.position = 'fixed';
|
clone.style.position = 'fixed';
|
||||||
// clone.style.whiteSpace = 'pre-wrap';
|
|
||||||
clone.style.zIndex = '10000';
|
|
||||||
clone.style.visibility = 'hidden';
|
clone.style.visibility = 'hidden';
|
||||||
// clone.style.opacity = 0.5;
|
|
||||||
const text = textarea.value;
|
const text = textarea.value;
|
||||||
const before = text.slice(0, textarea.selectionStart);
|
const before = text.slice(0, textarea.selectionStart);
|
||||||
clone.textContent = before;
|
clone.textContent = before;
|
||||||
const locator = document.createElement('span');
|
const locator = document.createElement('span');
|
||||||
// locator.textContent = '.';
|
|
||||||
locator.textContent = text[textarea.selectionStart];
|
locator.textContent = text[textarea.selectionStart];
|
||||||
clone.append(locator);
|
clone.append(locator);
|
||||||
clone.append(text.slice(textarea.selectionStart + 1));
|
clone.append(text.slice(textarea.selectionStart + 1));
|
||||||
@ -2023,17 +2033,17 @@ export function setSlashCommandAutoComplete(textarea, isFloating = false) {
|
|||||||
const locatorRect = locator.getBoundingClientRect();
|
const locatorRect = locator.getBoundingClientRect();
|
||||||
const location = {
|
const location = {
|
||||||
left: locatorRect.left,
|
left: locatorRect.left,
|
||||||
top: locatorRect.top,// - textarea.scrollTop,
|
top: locatorRect.top,
|
||||||
bottom: locatorRect.bottom,// - textarea.scrollTop,
|
bottom: locatorRect.bottom,
|
||||||
};
|
};
|
||||||
clone.remove();
|
clone.remove();
|
||||||
return location;
|
return location;
|
||||||
};
|
};
|
||||||
let mouseup = Promise.resolve();
|
let pointerup = Promise.resolve();
|
||||||
const select = async() => {
|
const select = async() => {
|
||||||
if (isReplacable) {
|
if (isReplacable) {
|
||||||
textarea.value = `${text.slice(0, executor.start - 2)}${selectedItem.value}${text.slice(executor.start - 2 + executor.name.length + 1)}`;
|
textarea.value = `${text.slice(0, executor.start - 2)}${selectedItem.value}${text.slice(executor.start - 2 + executor.name.length + 1)}`;
|
||||||
await mouseup;
|
await pointerup;
|
||||||
textarea.focus();
|
textarea.focus();
|
||||||
textarea.selectionStart = executor.start - 2 + selectedItem.value.length;
|
textarea.selectionStart = executor.start - 2 + selectedItem.value.length;
|
||||||
textarea.selectionEnd = textarea.selectionStart;
|
textarea.selectionEnd = textarea.selectionStart;
|
||||||
@ -2044,10 +2054,11 @@ export function setSlashCommandAutoComplete(textarea, isFloating = false) {
|
|||||||
textarea.addEventListener('input', ()=>showAutoCompleteDebounced(true));
|
textarea.addEventListener('input', ()=>showAutoCompleteDebounced(true));
|
||||||
textarea.addEventListener('click', ()=>showAutoCompleteDebounced());
|
textarea.addEventListener('click', ()=>showAutoCompleteDebounced());
|
||||||
textarea.addEventListener('keydown', (evt)=>{
|
textarea.addEventListener('keydown', (evt)=>{
|
||||||
// autocomplete is shown and cursor at end of current command name
|
// autocomplete is shown and cursor at end of current command name (or inside name and typed or forced)
|
||||||
if (isActive && isReplacable) {
|
if (isActive && isReplacable) {
|
||||||
switch (evt.key) {
|
switch (evt.key) {
|
||||||
case 'ArrowUp': {
|
case 'ArrowUp': {
|
||||||
|
// select previous item
|
||||||
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
@ -2066,6 +2077,7 @@ export function setSlashCommandAutoComplete(textarea, isFloating = false) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 'ArrowDown': {
|
case 'ArrowDown': {
|
||||||
|
// select next item
|
||||||
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
@ -2083,6 +2095,7 @@ export function setSlashCommandAutoComplete(textarea, isFloating = false) {
|
|||||||
}
|
}
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
case 'Tab': {
|
case 'Tab': {
|
||||||
|
// pick the selected item to autocomplete
|
||||||
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
evt.stopImmediatePropagation();
|
evt.stopImmediatePropagation();
|
||||||
@ -2095,6 +2108,7 @@ export function setSlashCommandAutoComplete(textarea, isFloating = false) {
|
|||||||
if (isActive) {
|
if (isActive) {
|
||||||
switch (evt.key) {
|
switch (evt.key) {
|
||||||
case 'Escape': {
|
case 'Escape': {
|
||||||
|
// close autocomplete
|
||||||
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
@ -2102,6 +2116,7 @@ export function setSlashCommandAutoComplete(textarea, isFloating = false) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 'Enter': {
|
case 'Enter': {
|
||||||
|
// hide autocomplete on enter (send, execute, ...)
|
||||||
if (!evt.shiftKey) {
|
if (!evt.shiftKey) {
|
||||||
hide();
|
hide();
|
||||||
return;
|
return;
|
||||||
@ -2114,6 +2129,7 @@ export function setSlashCommandAutoComplete(textarea, isFloating = false) {
|
|||||||
switch (evt.key) {
|
switch (evt.key) {
|
||||||
case ' ': {
|
case ' ': {
|
||||||
if (evt.ctrlKey) {
|
if (evt.ctrlKey) {
|
||||||
|
// ctrl-space to force show autocomplete
|
||||||
showAutoCompleteDebounced(true, true);
|
showAutoCompleteDebounced(true, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -2121,6 +2137,7 @@ export function setSlashCommandAutoComplete(textarea, isFloating = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (['Control', 'Shift', 'Alt'].includes(evt.key)) {
|
if (['Control', 'Shift', 'Alt'].includes(evt.key)) {
|
||||||
|
// ignore keydown on modifier keys
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showAutoCompleteDebounced();
|
showAutoCompleteDebounced();
|
||||||
@ -2131,9 +2148,4 @@ export function setSlashCommandAutoComplete(textarea, isFloating = false) {
|
|||||||
}
|
}
|
||||||
window.addEventListener('resize', debounce(updatePosition, 100));
|
window.addEventListener('resize', debounce(updatePosition, 100));
|
||||||
}
|
}
|
||||||
|
setSlashCommandAutoComplete(document.querySelector('#send_textarea'));
|
||||||
jQuery(function () {
|
|
||||||
const textarea = $('#send_textarea');
|
|
||||||
// setSlashCommandAutocomplete(textarea);
|
|
||||||
setSlashCommandAutoComplete(document.querySelector('#send_textarea'));
|
|
||||||
});
|
|
||||||
|
Reference in New Issue
Block a user