mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge pull request #3730 from SillyTavern/feat/command-buttons-multiple
Add support for toggleable buttons/multiselect in `/buttons` command
This commit is contained in:
@ -69,7 +69,7 @@ import { SlashCommand } from './slash-commands/SlashCommand.js';
|
|||||||
import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';
|
import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';
|
||||||
import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashCommandNamedArgumentAssignment.js';
|
import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashCommandNamedArgumentAssignment.js';
|
||||||
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
|
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
|
||||||
import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
|
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
|
||||||
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
|
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||||
import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
|
import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
|
||||||
import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
|
import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
|
||||||
@ -77,6 +77,7 @@ import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHel
|
|||||||
import { accountStorage } from './util/AccountStorage.js';
|
import { accountStorage } from './util/AccountStorage.js';
|
||||||
import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js';
|
import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js';
|
||||||
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
|
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
|
||||||
|
import { t } from './i18n.js';
|
||||||
export {
|
export {
|
||||||
executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
|
executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
|
||||||
};
|
};
|
||||||
@ -1556,16 +1557,28 @@ export function initDefaultSlashCommands() {
|
|||||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||||
name: 'buttons',
|
name: 'buttons',
|
||||||
callback: buttonsCallback,
|
callback: buttonsCallback,
|
||||||
returns: 'clicked button label',
|
returns: 'clicked button label (or array of labels if multiple is enabled)',
|
||||||
namedArgumentList: [
|
namedArgumentList: [
|
||||||
new SlashCommandNamedArgument(
|
SlashCommandNamedArgument.fromProps({
|
||||||
'labels', 'button labels', [ARGUMENT_TYPE.LIST], true,
|
name: 'labels',
|
||||||
),
|
description: 'button labels',
|
||||||
|
typeList: [ARGUMENT_TYPE.LIST],
|
||||||
|
isRequired: true,
|
||||||
|
}),
|
||||||
|
SlashCommandNamedArgument.fromProps({
|
||||||
|
name: 'multiple',
|
||||||
|
description: 'if enabled multiple buttons can be clicked/toggled, and all clicked buttons are returned as an array',
|
||||||
|
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||||||
|
enumList: commonEnumProviders.boolean('trueFalse')(),
|
||||||
|
defaultValue: 'false',
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
unnamedArgumentList: [
|
unnamedArgumentList: [
|
||||||
new SlashCommandArgument(
|
SlashCommandArgument.fromProps({
|
||||||
'text', [ARGUMENT_TYPE.STRING], true,
|
description: 'text',
|
||||||
),
|
typeList: [ARGUMENT_TYPE.STRING],
|
||||||
|
isRequired: true,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
helpString: `
|
helpString: `
|
||||||
<div>
|
<div>
|
||||||
@ -2377,6 +2390,18 @@ async function trimTokensCallback(arg, value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a popup with buttons based on provided labels and handles button interactions.
|
||||||
|
*
|
||||||
|
* @param {object} args - Named arguments for the command
|
||||||
|
* @param {string} args.labels - JSON string of an array of button labels
|
||||||
|
* @param {string} [args.multiple=false] - Flag indicating if multiple buttons can be toggled
|
||||||
|
* @param {string} text - The text content to be displayed within the popup
|
||||||
|
*
|
||||||
|
* @returns {Promise<string>} - A promise that resolves to a string of the button labels selected
|
||||||
|
* If 'multiple' is true, returns a JSON string array of labels.
|
||||||
|
* If 'multiple' is false, returns a single label string.
|
||||||
|
*/
|
||||||
async function buttonsCallback(args, text) {
|
async function buttonsCallback(args, text) {
|
||||||
try {
|
try {
|
||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
@ -2387,6 +2412,10 @@ async function buttonsCallback(args, text) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {Set<number>} */
|
||||||
|
const multipleToggledState = new Set();
|
||||||
|
const multiple = isTrueBoolean(args?.multiple);
|
||||||
|
|
||||||
// Map custom buttons to results. Start at 2 because 1 and 0 are reserved for ok and cancel
|
// Map custom buttons to results. Start at 2 because 1 and 0 are reserved for ok and cancel
|
||||||
const resultToButtonMap = new Map(buttons.map((button, index) => [index + 2, button]));
|
const resultToButtonMap = new Map(buttons.map((button, index) => [index + 2, button]));
|
||||||
|
|
||||||
@ -2404,11 +2433,24 @@ async function buttonsCallback(args, text) {
|
|||||||
|
|
||||||
for (const [result, button] of resultToButtonMap) {
|
for (const [result, button] of resultToButtonMap) {
|
||||||
const buttonElement = document.createElement('div');
|
const buttonElement = document.createElement('div');
|
||||||
buttonElement.classList.add('menu_button', 'result-control', 'wide100p');
|
buttonElement.classList.add('menu_button', 'wide100p');
|
||||||
buttonElement.dataset.result = String(result);
|
|
||||||
buttonElement.addEventListener('click', async () => {
|
if (multiple) {
|
||||||
await popup.complete(result);
|
buttonElement.classList.add('toggleable');
|
||||||
});
|
buttonElement.dataset.toggleValue = String(result);
|
||||||
|
buttonElement.addEventListener('click', async () => {
|
||||||
|
buttonElement.classList.toggle('toggled');
|
||||||
|
if (buttonElement.classList.contains('toggled')) {
|
||||||
|
multipleToggledState.add(result);
|
||||||
|
} else {
|
||||||
|
multipleToggledState.delete(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
buttonElement.classList.add('result-control');
|
||||||
|
buttonElement.dataset.result = String(result);
|
||||||
|
}
|
||||||
|
|
||||||
buttonElement.innerText = button;
|
buttonElement.innerText = button;
|
||||||
buttonContainer.appendChild(buttonElement);
|
buttonContainer.appendChild(buttonElement);
|
||||||
}
|
}
|
||||||
@ -2424,10 +2466,19 @@ async function buttonsCallback(args, text) {
|
|||||||
popupContainer.style.flexDirection = 'column';
|
popupContainer.style.flexDirection = 'column';
|
||||||
popupContainer.style.maxHeight = '80vh'; // Limit the overall height of the popup
|
popupContainer.style.maxHeight = '80vh'; // Limit the overall height of the popup
|
||||||
|
|
||||||
popup = new Popup(popupContainer, POPUP_TYPE.TEXT, '', { okButton: 'Cancel', allowVerticalScrolling: true });
|
popup = new Popup(popupContainer, POPUP_TYPE.TEXT, '', { okButton: multiple ? t`Ok` : t`Cancel`, allowVerticalScrolling: true });
|
||||||
popup.show()
|
popup.show()
|
||||||
.then((result => resolve(typeof result === 'number' ? resultToButtonMap.get(result) ?? '' : '')))
|
.then((result => resolve(getResult(result))))
|
||||||
.catch(() => resolve(''));
|
.catch(() => resolve(''));
|
||||||
|
|
||||||
|
/** @returns {string} @param {string|number|boolean} result */
|
||||||
|
function getResult(result) {
|
||||||
|
if (multiple) {
|
||||||
|
const array = result === POPUP_RESULT.AFFIRMATIVE ? Array.from(multipleToggledState).map(r => resultToButtonMap.get(r) ?? '') : [];
|
||||||
|
return JSON.stringify(array);
|
||||||
|
}
|
||||||
|
return typeof result === 'number' ? resultToButtonMap.get(result) ?? '' : '';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
|
@ -2911,6 +2911,35 @@ select option:not(:checked) {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu_button.toggleable {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu_button.toggleable.toggled {
|
||||||
|
border-color: var(--active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu_button.toggleable:not(.toggled) {
|
||||||
|
filter: brightness(80%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu_button.toggleable::before {
|
||||||
|
font-family: "Font Awesome 6 Free";
|
||||||
|
margin-left: 10px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu_button.toggleable.toggled::before {
|
||||||
|
content: "\f00c";
|
||||||
|
color: var(--active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu_button.toggleable:not(.toggled)::before {
|
||||||
|
content: "\f00d";
|
||||||
|
color: var(--fullred);
|
||||||
|
}
|
||||||
|
|
||||||
.fav_on {
|
.fav_on {
|
||||||
color: var(--golden) !important;
|
color: var(--golden) !important;
|
||||||
}
|
}
|
||||||
@ -2921,7 +2950,7 @@ select option:not(:checked) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.menu_button.togglable:not(.toggleEnabled) {
|
.menu_button.togglable:not(.toggleEnabled) {
|
||||||
color: red;
|
color: var(--fullred);
|
||||||
}
|
}
|
||||||
|
|
||||||
.displayBlock {
|
.displayBlock {
|
||||||
|
Reference in New Issue
Block a user