Merge pull request #3284 from SillyTavern/css-var-slash-command Add `/css-var` slash command
Signed| @@ -4,7 +4,7 @@ import { eventSource, event_types, saveSettings, saveSettingsDebounced, getReque | |||
| 4 | import { showLoader } from './loader.js'; | 4 | import { showLoader } from './loader.js'; |
| 5 | import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; | 5 | import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; |
| 6 | import { renderTemplate, renderTemplateAsync } from './templates.js'; | 6 | import { renderTemplate, renderTemplateAsync } from './templates.js'; |
| 7 | import { delay, isSubsetOf, setValueByPath } from './utils.js'; | 7 | import { delay, isSubsetOf, sanitizeSelector, setValueByPath } from './utils.js'; |
| 8 | import { getContext } from './st-context.js'; | 8 | import { getContext } from './st-context.js'; |
| 9 | import { isAdmin } from './user.js'; | 9 | import { isAdmin } from './user.js'; |
| 10 | import { t } from './i18n.js'; | 10 | import { t } from './i18n.js'; |
| @@ -509,10 +509,11 @@ function addExtensionStyle(name, manifest) { | |||
| 509 | 509 | ||
| 510 | return new Promise((resolve, reject) => { | 510 | return new Promise((resolve, reject) => { |
| 511 | const url = `/scripts/extensions/${name}/${manifest.css}`; | 511 | const url = `/scripts/extensions/${name}/${manifest.css}`; |
| 512 | const id = sanitizeSelector(`${name}-css`); | ||
| 512 | 513 | ||
| 513 | if ($(`link[id="${name}"]`).length === 0) { | 514 | if ($(`link[id="${id}"]`).length === 0) { |
| 514 | const link = document.createElement('link'); | 515 | const link = document.createElement('link'); |
| 515 | link.id = name; | 516 | link.id = id; |
| 516 | link.rel = 'stylesheet'; | 517 | link.rel = 'stylesheet'; |
| 517 | link.type = 'text/css'; | 518 | link.type = 'text/css'; |
| 518 | link.href = url; | 519 | link.href = url; |
| @@ -540,11 +541,12 @@ function addExtensionScript(name, manifest) { | |||
| 540 | 541 | ||
| 541 | return new Promise((resolve, reject) => { | 542 | return new Promise((resolve, reject) => { |
| 542 | const url = `/scripts/extensions/${name}/${manifest.js}`; | 543 | const url = `/scripts/extensions/${name}/${manifest.js}`; |
| 544 | const id = sanitizeSelector(`${name}-js`); | ||
| 543 | let ready = false; | 545 | let ready = false; |
| 544 | 546 | ||
| 545 | if ($(`script[id="${name}"]`).length === 0) { | 547 | if ($(`script[id="${id}"]`).length === 0) { |
| 546 | const script = document.createElement('script'); | 548 | const script = document.createElement('script'); |
| 547 | script.id = name; | 549 | script.id = id; |
| 548 | script.type = 'module'; | 550 | script.type = 'module'; |
| 549 | script.src = url; | 551 | script.src = url; |
| 550 | script.async = true; | 552 | script.async = true; |
| @@ -277,7 +277,7 @@ function makeMovable(id = 'gallery') { | |||
| 277 | const newElement = $(template); | 277 | const newElement = $(template); |
| 278 | newElement.css('background-color', 'var(--SmartThemeBlurTintColor)'); | 278 | newElement.css('background-color', 'var(--SmartThemeBlurTintColor)'); |
| 279 | newElement.attr('forChar', id); | 279 | newElement.attr('forChar', id); |
| 280 | newElement.attr('id', `${id}`); | 280 | newElement.attr('id', id); |
| 281 | newElement.find('.drag-grabber').attr('id', `${id}header`); | 281 | newElement.find('.drag-grabber').attr('id', `${id}header`); |
| 282 | newElement.find('.dragTitle').text('Image Gallery'); | 282 | newElement.find('.dragTitle').text('Image Gallery'); |
| 283 | //add a div for the gallery | 283 | //add a div for the gallery |
| @@ -3963,6 +3963,95 @@ $(document).ready(() => { | |||
| 3963 | `, | 3963 | `, |
| 3964 | })); | 3964 | })); |
| 3965 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 3965 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 3966 | name: 'css-var', | ||
| 3967 | /** @param {{to: string, varname: string }} args @param {string} value @returns {string} */ | ||
| 3968 | callback: (args, value) => { | ||
| 3969 | // Map enum to target selector | ||
| 3970 | const targetSelector = { | ||
| 3971 | chat: '#chat', | ||
| 3972 | background: '#bg1', | ||
| 3973 | gallery: '#gallery', | ||
| 3974 | zoomedAvatar: 'div.zoomed_avatar', | ||
| 3975 | }[args.to || 'chat']; | ||
| 3976 | |||
| 3977 | if (!targetSelector) { | ||
| 3978 | toastr.error(`Invalid target: ${args.to}`); | ||
| 3979 | return; | ||
| 3980 | } | ||
| 3981 | |||
| 3982 | if (!args.varname) { | ||
| 3983 | toastr.error('CSS variable name is required'); | ||
| 3984 | return; | ||
| 3985 | } | ||
| 3986 | if (!args.varname.startsWith('--')) { | ||
| 3987 | toastr.error('CSS variable names must start with "--"'); | ||
| 3988 | return; | ||
| 3989 | } | ||
| 3990 | |||
| 3991 | const elements = document.querySelectorAll(targetSelector); | ||
| 3992 | if (elements.length === 0) { | ||
| 3993 | toastr.error(`No elements found for ${args.to ?? 'chat'} with selector "${targetSelector}"`); | ||
| 3994 | return; | ||
| 3995 | } | ||
| 3996 | |||
| 3997 | elements.forEach(element => { | ||
| 3998 | element.style.setProperty(args.varname, value); | ||
| 3999 | }); | ||
| 4000 | |||
| 4001 | console.info(`Set CSS variable "${args.varname}" to "${value}" on "${targetSelector}"`); | ||
| 4002 | }, | ||
| 4003 | namedArgumentList: [ | ||
| 4004 | SlashCommandNamedArgument.fromProps({ | ||
| 4005 | name: 'varname', | ||
| 4006 | description: 'CSS variable name (starting with double dashes)', | ||
| 4007 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 4008 | isRequired: true, | ||
| 4009 | }), | ||
| 4010 | SlashCommandNamedArgument.fromProps({ | ||
| 4011 | name: 'to', | ||
| 4012 | description: 'The target element to which the CSS variable will be applied', | ||
| 4013 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 4014 | enumList: [ | ||
| 4015 | new SlashCommandEnumValue('chat', null, enumTypes.enum, enumIcons.message), | ||
| 4016 | new SlashCommandEnumValue('background', null, enumTypes.enum, enumIcons.image), | ||
| 4017 | new SlashCommandEnumValue('zoomedAvatar', null, enumTypes.enum, enumIcons.character), | ||
| 4018 | new SlashCommandEnumValue('gallery', null, enumTypes.enum, enumIcons.image), | ||
| 4019 | ], | ||
| 4020 | defaultValue: 'chat', | ||
| 4021 | }), | ||
| 4022 | ], | ||
| 4023 | unnamedArgumentList: [ | ||
| 4024 | SlashCommandArgument.fromProps({ | ||
| 4025 | description: 'CSS variable value', | ||
| 4026 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 4027 | isRequired: true, | ||
| 4028 | }), | ||
| 4029 | ], | ||
| 4030 | helpString: ` | ||
| 4031 | <div> | ||
| 4032 | Sets a CSS variable to a specified value on a target element. | ||
| 4033 | <br /> | ||
| 4034 | Only setting of variable names is supported. They have to be prefixed with double dashes ("--exampleVar"). | ||
| 4035 | Setting actual CSS properties is not supported. Custom CSS in the theme settings can be used for that. | ||
| 4036 | <br /><br /> | ||
| 4037 | <b>This value will be gone after a page reload!</b> | ||
| 4038 | </div> | ||
| 4039 | <div> | ||
| 4040 | <strong>Example:</strong> | ||
| 4041 | <ul> | ||
| 4042 | <li> | ||
| 4043 | <pre><code>/css-var varname="--SmartThemeBodyColor" #ff0000</code></pre> | ||
| 4044 | Sets the text color of the chat to red | ||
| 4045 | </li> | ||
| 4046 | <li> | ||
| 4047 | <pre><code>/css-var to=zoomedAvatar varname="--SmartThemeBlurStrength" 0</code></pre> | ||
| 4048 | Remove the blur from the zoomed avatar | ||
| 4049 | </li> | ||
| 4050 | </ul> | ||
| 4051 | </div> | ||
| 4052 | `, | ||
| 4053 | })); | ||
| 4054 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | ||
| 3966 | name: 'movingui', | 4055 | name: 'movingui', |
| 3967 | callback: setmovingUIPreset, | 4056 | callback: setmovingUIPreset, |
| 3968 | unnamedArgumentList: [ | 4057 | unnamedArgumentList: [ |
| @@ -37,6 +37,7 @@ export const enumIcons = { | |||
| 37 | voice: '🎤', | 37 | voice: '🎤', |
| 38 | server: '🖥️', | 38 | server: '🖥️', |
| 39 | popup: '🗔', | 39 | popup: '🗔', |
| 40 | image: '🖼️', | ||
| 40 | 41 | ||
| 41 | true: '✔️', | 42 | true: '✔️', |
| 42 | false: '❌', | 43 | false: '❌', |
| @@ -67,6 +67,16 @@ export function escapeHtml(str) { | |||
| 67 | return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); | 67 | return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); |
| 68 | } | 68 | } |
| 69 | 69 | ||
| 70 | /** | ||
| 71 | * Make string safe for use as a CSS selector. | ||
| 72 | * @param {string} str String to sanitize | ||
| 73 | * @param {string} replacement Replacement for invalid characters | ||
| 74 | * @returns {string} Sanitized string | ||
| 75 | */ | ||
| 76 | export function sanitizeSelector(str, replacement = '_') { | ||
| 77 | return String(str).replace(/[^a-z0-9_-]/ig, replacement); | ||
| 78 | } | ||
| 79 | |||
| 70 | export function isValidUrl(value) { | 80 | export function isValidUrl(value) { |
| 71 | try { | 81 | try { |
| 72 | new URL(value); | 82 | new URL(value); |