Merge pull request #2795 from SillyTavern/expand-echo-command Expand `/echo` command with `cssClass`, `color`, `escapeHtml` and `onClick`

b0b9558a6c26152ea21fc9616b25a981390a11be

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
1 files changed, +59 -8Ignore whitespace
public/scripts/slash-commands.js+59 -8
@@ -797,6 +797,27 @@ export function initDefaultSlashCommands() {
797 defaultValue: 'false',797 defaultValue: 'false',
798 enumList: commonEnumProviders.boolean('trueFalse')(),798 enumList: commonEnumProviders.boolean('trueFalse')(),
799 }),799 }),
800 SlashCommandNamedArgument.fromProps({
801 name: 'cssClass',
802 description: 'additional CSS class to add to the toast message (e.g. for custom styling)',
803 typeList: [ARGUMENT_TYPE.STRING],
804 }),
805 SlashCommandNamedArgument.fromProps({
806 name: 'color',
807 description: 'custom CSS color of the toast message. Accepts all valid CSS color values (e.g. \'red\', \'#FF0000\', \'rgb(255, 0, 0)\').<br />>Can be more customizable with the \'cssClass\' argument and custom classes.',
808 }),
809 SlashCommandNamedArgument.fromProps({
810 name: 'escapeHtml',
811 description: 'whether to escape HTML in the toast message.',
812 typeList: [ARGUMENT_TYPE.BOOLEAN],
813 defaultValue: 'true',
814 enumList: commonEnumProviders.boolean('trueFalse')(),
815 }),
816 SlashCommandNamedArgument.fromProps({
817 name: 'onClick',
818 description: 'a closure to call when the toast is clicked. This executed closure receives scope as provided in the script. Careful about possible side effects when manipulating variables and more.',
819 typeList: [ARGUMENT_TYPE.CLOSURE],
820 }),
800 ],821 ],
801 unnamedArgumentList: [822 unnamedArgumentList: [
802 new SlashCommandArgument(823 new SlashCommandArgument(
@@ -805,13 +826,19 @@ export function initDefaultSlashCommands() {
805 ],826 ],
806 helpString: `827 helpString: `
807 <div>828 <div>
808 Echoes the provided text to a toast message. Useful for pipes debugging.829 Echoes the provided text to a toast message. Can be used to display informational messages or for pipes debugging.
809 </div>830 </div>
810 <div>831 <div>
811 <strong>Example:</strong>832 <strong>Example:</strong>
812 <ul>833 <ul>
813 <li>834 <li>
814 <pre><code>/echo title="My Message" severity=info This is an info message</code></pre>835 <pre><code>/echo title="My Message" severity=warning This is a warning message</code></pre>
836 </li>
837 <li>
838 <pre><code>/echo color=purple This message is purple</code></pre>
839 </li>
840 <li>
841 <pre><code>/echo onClick={: /echo escapeHtml=false color=transparent cssClass=wider_dialogue_popup &lt;img src="/img/five.png" /&gt; :} timeout=5000 Clicking on this message within 5 seconds will open the image.</code></pre>
815 </li>842 </li>
816 </ul>843 </ul>
817 </div>844 </div>
@@ -2181,7 +2208,7 @@ async function generateCallback(args, value) {
21812208
2182/**2209/**
2183 *2210 *
2184 * @param {{title?: string, severity?: string, timeout?: string, extendedTimeout?: string, preventDuplicates?: string, awaitDismissal?: string}} args - named arguments from the slash command2211 * @param {{title?: string, severity?: string, timeout?: string, extendedTimeout?: string, preventDuplicates?: string, awaitDismissal?: string, cssClass?: string, color?: string, escapeHtml?: string, onClick?: SlashCommandClosure}} args - named arguments from the slash command
2185 * @param {string} value - The string to echo (unnamed argument from the slash command)2212 * @param {string} value - The string to echo (unnamed argument from the slash command)
2186 * @returns {Promise<string>} The text that was echoed2213 * @returns {Promise<string>} The text that was echoed
2187 */2214 */
@@ -2200,7 +2227,7 @@ async function echoCallback(args, value) {
2200 // Make sure that the value is a string2227 // Make sure that the value is a string
2201 value = String(value);2228 value = String(value);
22022229
2203 const title = args.title ? args.title : undefined;2230 let title = args.title ? args.title : undefined;
2204 const severity = args.severity ? args.severity : 'info';2231 const severity = args.severity ? args.severity : 'info';
22052232
2206 /** @type {ToastrOptions} */2233 /** @type {ToastrOptions} */
@@ -2208,6 +2235,8 @@ async function echoCallback(args, value) {
2208 if (args.timeout && !isNaN(parseInt(args.timeout))) options.timeOut = parseInt(args.timeout);2235 if (args.timeout && !isNaN(parseInt(args.timeout))) options.timeOut = parseInt(args.timeout);
2209 if (args.extendedTimeout && !isNaN(parseInt(args.extendedTimeout))) options.extendedTimeOut = parseInt(args.extendedTimeout);2236 if (args.extendedTimeout && !isNaN(parseInt(args.extendedTimeout))) options.extendedTimeOut = parseInt(args.extendedTimeout);
2210 if (isTrueBoolean(args.preventDuplicates)) options.preventDuplicates = true;2237 if (isTrueBoolean(args.preventDuplicates)) options.preventDuplicates = true;
2238 if (args.cssClass) options.toastClass = args.cssClass;
2239 if (args.escapeHtml !== undefined) options.escapeHtml = isTrueBoolean(args.escapeHtml);
22112240
2212 // Prepare possible await handling2241 // Prepare possible await handling
2213 let awaitDismissal = isTrueBoolean(args.awaitDismissal);2242 let awaitDismissal = isTrueBoolean(args.awaitDismissal);
@@ -2216,23 +2245,45 @@ async function echoCallback(args, value) {
2216 if (awaitDismissal) {2245 if (awaitDismissal) {
2217 options.onHidden = () => resolveToastDismissal(value);2246 options.onHidden = () => resolveToastDismissal(value);
2218 }2247 }
2248 if (args.onClick) {
2249 if (args.onClick instanceof SlashCommandClosure) {
2250 options.onclick = async () => {
2251 // Execute the slash command directly, with its internal scope and everything. Clear progress handler so it doesn't interfere with command execution progress.
2252 args.onClick.onProgress = null;
2253 await args.onClick.execute();
2254 };
2255 } else {
2256 toastr.warning('Invalid onClick provided for /echo command. This is not a closure');
2257 }
2258 }
2259
2260 // If we allow HTML, we need to sanitize it to prevent security risks
2261 if (!options.escapeHtml) {
2262 if (title) title = DOMPurify.sanitize(title, { FORBID_TAGS: ['style'] });
2263 value = DOMPurify.sanitize(value, { FORBID_TAGS: ['style'] });
2264 }
22192265
2266 let toast;
2220 switch (severity) {2267 switch (severity) {
2221 case 'error':2268 case 'error':
2222 toastr.error(value, title, options);2269 toast = toastr.error(value, title, options);
2223 break;2270 break;
2224 case 'warning':2271 case 'warning':
2225 toastr.warning(value, title, options);2272 toast = toastr.warning(value, title, options);
2226 break;2273 break;
2227 case 'success':2274 case 'success':
2228 toastr.success(value, title, options);2275 toast = toastr.success(value, title, options);
2229 break;2276 break;
2230 case 'info':2277 case 'info':
2231 default:2278 default:
2232 toastr.info(value, title, options);2279 toast = toastr.info(value, title, options);
2233 break;2280 break;
2234 }2281 }
22352282
2283 if (args.color) {
2284 toast.css('background-color', args.color);
2285 }
2286
2236 if (awaitDismissal) {2287 if (awaitDismissal) {
2237 return new Promise((resolve) => {2288 return new Promise((resolve) => {
2238 resolveToastDismissal = resolve;2289 resolveToastDismissal = resolve;