Skip to content

Commit ef56eda

Browse files
authored
Merge pull request SillyTavern#3730 from SillyTavern/feat/command-buttons-multiple
Add support for toggleable buttons/multiselect in `/buttons` command
2 parents e3c4652 + 40f2eae commit ef56eda

File tree

2 files changed

+96
-16
lines changed

2 files changed

+96
-16
lines changed

public/scripts/slash-commands.js

+66-15
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,15 @@ import { SlashCommand } from './slash-commands/SlashCommand.js';
6969
import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';
7070
import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashCommandNamedArgumentAssignment.js';
7171
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
72-
import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
72+
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
7373
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
7474
import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
7575
import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
7676
import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
7777
import { accountStorage } from './util/AccountStorage.js';
7878
import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js';
7979
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
80+
import { t } from './i18n.js';
8081
export {
8182
executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
8283
};
@@ -1556,16 +1557,28 @@ export function initDefaultSlashCommands() {
15561557
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
15571558
name: 'buttons',
15581559
callback: buttonsCallback,
1559-
returns: 'clicked button label',
1560+
returns: 'clicked button label (or array of labels if multiple is enabled)',
15601561
namedArgumentList: [
1561-
new SlashCommandNamedArgument(
1562-
'labels', 'button labels', [ARGUMENT_TYPE.LIST], true,
1563-
),
1562+
SlashCommandNamedArgument.fromProps({
1563+
name: 'labels',
1564+
description: 'button labels',
1565+
typeList: [ARGUMENT_TYPE.LIST],
1566+
isRequired: true,
1567+
}),
1568+
SlashCommandNamedArgument.fromProps({
1569+
name: 'multiple',
1570+
description: 'if enabled multiple buttons can be clicked/toggled, and all clicked buttons are returned as an array',
1571+
typeList: [ARGUMENT_TYPE.BOOLEAN],
1572+
enumList: commonEnumProviders.boolean('trueFalse')(),
1573+
defaultValue: 'false',
1574+
}),
15641575
],
15651576
unnamedArgumentList: [
1566-
new SlashCommandArgument(
1567-
'text', [ARGUMENT_TYPE.STRING], true,
1568-
),
1577+
SlashCommandArgument.fromProps({
1578+
description: 'text',
1579+
typeList: [ARGUMENT_TYPE.STRING],
1580+
isRequired: true,
1581+
}),
15691582
],
15701583
helpString: `
15711584
<div>
@@ -2377,6 +2390,18 @@ async function trimTokensCallback(arg, value) {
23772390
}
23782391
}
23792392

2393+
/**
2394+
* Displays a popup with buttons based on provided labels and handles button interactions.
2395+
*
2396+
* @param {object} args - Named arguments for the command
2397+
* @param {string} args.labels - JSON string of an array of button labels
2398+
* @param {string} [args.multiple=false] - Flag indicating if multiple buttons can be toggled
2399+
* @param {string} text - The text content to be displayed within the popup
2400+
*
2401+
* @returns {Promise<string>} - A promise that resolves to a string of the button labels selected
2402+
* If 'multiple' is true, returns a JSON string array of labels.
2403+
* If 'multiple' is false, returns a single label string.
2404+
*/
23802405
async function buttonsCallback(args, text) {
23812406
try {
23822407
/** @type {string[]} */
@@ -2387,6 +2412,10 @@ async function buttonsCallback(args, text) {
23872412
return '';
23882413
}
23892414

2415+
/** @type {Set<number>} */
2416+
const multipleToggledState = new Set();
2417+
const multiple = isTrueBoolean(args?.multiple);
2418+
23902419
// Map custom buttons to results. Start at 2 because 1 and 0 are reserved for ok and cancel
23912420
const resultToButtonMap = new Map(buttons.map((button, index) => [index + 2, button]));
23922421

@@ -2404,11 +2433,24 @@ async function buttonsCallback(args, text) {
24042433

24052434
for (const [result, button] of resultToButtonMap) {
24062435
const buttonElement = document.createElement('div');
2407-
buttonElement.classList.add('menu_button', 'result-control', 'wide100p');
2408-
buttonElement.dataset.result = String(result);
2409-
buttonElement.addEventListener('click', async () => {
2410-
await popup.complete(result);
2411-
});
2436+
buttonElement.classList.add('menu_button', 'wide100p');
2437+
2438+
if (multiple) {
2439+
buttonElement.classList.add('toggleable');
2440+
buttonElement.dataset.toggleValue = String(result);
2441+
buttonElement.addEventListener('click', async () => {
2442+
buttonElement.classList.toggle('toggled');
2443+
if (buttonElement.classList.contains('toggled')) {
2444+
multipleToggledState.add(result);
2445+
} else {
2446+
multipleToggledState.delete(result);
2447+
}
2448+
});
2449+
} else {
2450+
buttonElement.classList.add('result-control');
2451+
buttonElement.dataset.result = String(result);
2452+
}
2453+
24122454
buttonElement.innerText = button;
24132455
buttonContainer.appendChild(buttonElement);
24142456
}
@@ -2424,10 +2466,19 @@ async function buttonsCallback(args, text) {
24242466
popupContainer.style.flexDirection = 'column';
24252467
popupContainer.style.maxHeight = '80vh'; // Limit the overall height of the popup
24262468

2427-
popup = new Popup(popupContainer, POPUP_TYPE.TEXT, '', { okButton: 'Cancel', allowVerticalScrolling: true });
2469+
popup = new Popup(popupContainer, POPUP_TYPE.TEXT, '', { okButton: multiple ? t`Ok` : t`Cancel`, allowVerticalScrolling: true });
24282470
popup.show()
2429-
.then((result => resolve(typeof result === 'number' ? resultToButtonMap.get(result) ?? '' : '')))
2471+
.then((result => resolve(getResult(result))))
24302472
.catch(() => resolve(''));
2473+
2474+
/** @returns {string} @param {string|number|boolean} result */
2475+
function getResult(result) {
2476+
if (multiple) {
2477+
const array = result === POPUP_RESULT.AFFIRMATIVE ? Array.from(multipleToggledState).map(r => resultToButtonMap.get(r) ?? '') : [];
2478+
return JSON.stringify(array);
2479+
}
2480+
return typeof result === 'number' ? resultToButtonMap.get(result) ?? '' : '';
2481+
}
24312482
});
24322483
} catch {
24332484
return '';

public/style.css

+30-1
Original file line numberDiff line numberDiff line change
@@ -2911,6 +2911,35 @@ select option:not(:checked) {
29112911
pointer-events: none;
29122912
}
29132913

2914+
.menu_button.toggleable {
2915+
padding-left: 20px;
2916+
}
2917+
2918+
.menu_button.toggleable.toggled {
2919+
border-color: var(--active);
2920+
}
2921+
2922+
.menu_button.toggleable:not(.toggled) {
2923+
filter: brightness(80%);
2924+
}
2925+
2926+
.menu_button.toggleable::before {
2927+
font-family: "Font Awesome 6 Free";
2928+
margin-left: 10px;
2929+
position: absolute;
2930+
left: 0;
2931+
}
2932+
2933+
.menu_button.toggleable.toggled::before {
2934+
content: "\f00c";
2935+
color: var(--active);
2936+
}
2937+
2938+
.menu_button.toggleable:not(.toggled)::before {
2939+
content: "\f00d";
2940+
color: var(--fullred);
2941+
}
2942+
29142943
.fav_on {
29152944
color: var(--golden) !important;
29162945
}
@@ -2921,7 +2950,7 @@ select option:not(:checked) {
29212950
}
29222951

29232952
.menu_button.togglable:not(.toggleEnabled) {
2924-
color: red;
2953+
color: var(--fullred);
29252954
}
29262955

29272956
.displayBlock {

0 commit comments

Comments
 (0)