feat: prevent scrolling of containers when using focused number inputs (#4629) * feat: prevent scrolling of containers when using focused number inputs #4607 * feat: enhance number input wheel behavior with slider sync and Firefox support * refactor: optimize number input wheel handler with native DOM methods and type checks * Refactor: Move into dom-handlers folder * refactor: use optional chaining for slider element selection * Simplify file paths * Unlock scroll to edit in misc controls * refactor: throttle wheel event handler updates * refactor: Extract value update into a function * fix: NaN-aware value clamping * fix: Add sanity checks for input value calculations --------- Co-authored-by: Wolfsblvt <wolfsblvt@gmail.com>

df8f2a477a3200a8d31409bd482fec97aa79549b

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

Signed
3 files changed, +72 -4Ignore whitespace
public/index.html+4 -4
@@ -253,7 +253,7 @@
253 </div>253 </div>
254 <div id="common-gen-settings-block" class="width100p">254 <div id="common-gen-settings-block" class="width100p">
255 <div id="pro-settings-block" class="flex-container gap10h5v justifyCenter">255 <div id="pro-settings-block" class="flex-container gap10h5v justifyCenter">
256 <div id="amount_gen_block" class="alignitemscenter flex-container marginBot5 flexFlowColumn flexBasis48p flexGrow flexShrink gap0">256 <div id="amount_gen_block" class="range-block-range-and-counter alignitemscenter flex-container marginBot5 flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
257 <small data-i18n="response legth(tokens)">Response (tokens)</small>257 <small data-i18n="response legth(tokens)">Response (tokens)</small>
258 <input class="neo-range-slider" type="range" id="amount_gen" name="volume" min="16" max="2048" step="1">258 <input class="neo-range-slider" type="range" id="amount_gen" name="volume" min="16" max="2048" step="1">
259 <div data-randomization-disabled="true" class="wide100p">259 <div data-randomization-disabled="true" class="wide100p">
@@ -284,7 +284,7 @@
284 </label>284 </label>
285 </div>285 </div>
286 </div>286 </div>
287 <div id="max_context_block" class="alignitemscenter flex-container marginBot5 flexFlowColumn flexBasis48p flexGrow flexShrink gap0">287 <div id="max_context_block" class="range-block-range-and-counter alignitemscenter flex-container marginBot5 flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
288 <small data-i18n="context size(tokens)">Context (tokens)</small>288 <small data-i18n="context size(tokens)">Context (tokens)</small>
289 <input class="neo-range-slider" type="range" id="max_context" name="volume" min="512" max="8192" step="64">289 <input class="neo-range-slider" type="range" id="max_context" name="volume" min="512" max="8192" step="64">
290 <div data-randomization-disabled="true" class="wide100p">290 <div data-randomization-disabled="true" class="wide100p">
@@ -648,7 +648,7 @@
648 Max Response Length (tokens)648 Max Response Length (tokens)
649 </div>649 </div>
650 <div class="wide100p">650 <div class="wide100p">
651 <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="1" max="65536">651 <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="1" max="65536" step="1">
652 </div>652 </div>
653 </div>653 </div>
654 <div class="range-block" data-source="openai,custom,xai,aimlapi,moonshot,azure_openai">654 <div class="range-block" data-source="openai,custom,xai,aimlapi,moonshot,azure_openai">
@@ -4214,7 +4214,7 @@
4214 Token Padding4214 Token Padding
4215 </small>4215 </small>
4216 </div>4216 </div>
4217 <input id="token_padding" class="text_pole textarea_compact" type="number" min="-2048" max="2048" />4217 <input id="token_padding" class="text_pole textarea_compact" type="number" min="-2048" max="2048" step="1" />
4218 </div>4218 </div>
4219 </div>4219 </div>
4220 <div>4220 <div>
public/script.js+2 -0
@@ -270,6 +270,7 @@ import { getSystemMessageByType, initSystemMessages, SAFETY_CHAT, sendSystemMess
270import { event_types, eventSource } from './scripts/events.js';270import { event_types, eventSource } from './scripts/events.js';
271import { initAccessibility } from './scripts/a11y.js';271import { initAccessibility } from './scripts/a11y.js';
272import { applyStreamFadeIn } from './scripts/util/stream-fadein.js';272import { applyStreamFadeIn } from './scripts/util/stream-fadein.js';
273import { initDomHandlers } from './scripts/dom-handlers.js';
273274
274// API OBJECT FOR EXTERNAL WIRING275// API OBJECT FOR EXTERNAL WIRING
275globalThis.SillyTavern = {276globalThis.SillyTavern = {
@@ -640,6 +641,7 @@ async function firstLoadInit() {
640641
641 showLoader();642 showLoader();
642 registerPromptManagerMigration();643 registerPromptManagerMigration();
644 initDomHandlers();
643 initStandaloneMode();645 initStandaloneMode();
644 initLibraryShims();646 initLibraryShims();
645 addShowdownPatch(showdown);647 addShowdownPatch(showdown);
public/scripts/dom-handlers.js+66 -0
@@ -0,0 +1,66 @@
1import { throttle } from './utils.js';
2
3export function initDomHandlers() {
4 handleInputWheel();
5}
6
7/**
8 * Trap mouse wheel inside of focused number inputs to prevent scrolling their containers.
9 * Instead of firing wheel events, manually update both slider and input values.
10 * This also makes wheel work inside Firefox.
11 */
12function handleInputWheel() {
13 const minInterval = 25; // ms
14
15 /**
16 * Update input and slider values based on wheel delta
17 * @param {HTMLInputElement} input The number input element
18 * @param {HTMLInputElement|null} slider The associated range input element, if any
19 * @param {number} deltaY The wheel deltaY value
20 */
21 function updateValue(input, slider, deltaY) {
22 const currentValue = parseFloat(input.value);
23 const step = parseFloat(input.step);
24 const min = parseFloat(input.min);
25 const max = parseFloat(input.max);
26
27 // Sanity checks before trying to calculate new value
28 if (isNaN(currentValue) || isNaN(step) || step <= 0 || deltaY === 0) return;
29
30 // Calculate new value based on wheel movement delta (negative = up, positive = down)
31 let newValue = currentValue + (deltaY > 0 ? -step : step);
32 // Ensure it's a multiple of step
33 newValue = Math.round(newValue / step) * step;
34 // Ensure it's within the min and max range (NaN-aware)
35 newValue = !isNaN(min) ? Math.max(newValue, min) : newValue;
36 newValue = !isNaN(max) ? Math.min(newValue, max) : newValue;
37 // Simple fix for floating point precision issues
38 newValue = Math.round(newValue * 1e10) / 1e10;
39
40 // Update both input and slider values
41 input.value = newValue.toString();
42 if (slider) slider.value = newValue.toString();
43 // Trigger input event (just ONE) to update any listeners
44 const inputEvent = new Event('input', { bubbles: true });
45 input.dispatchEvent(inputEvent);
46 }
47
48 const updateValueThrottled = throttle(updateValue, minInterval);
49
50 document.addEventListener('wheel', (e) => {
51 // Try to carefully narrow down if we even need to fire this handler
52 const input = document.activeElement instanceof HTMLInputElement ? document.activeElement : null;
53 if (input && input.type === 'number' && input.hasAttribute('step')) {
54 const parent = input.closest('.range-block-range-and-counter') ?? input.closest('div') ?? input.parentElement;
55 const slider = /** @type {HTMLInputElement} */ (parent?.querySelector('input[type="range"]'));
56
57 // Stop propagation for either target
58 if (e.target === input || (slider && e.target === slider)) {
59 e.stopPropagation();
60 e.preventDefault();
61
62 updateValueThrottled(input, slider, e.deltaY);
63 }
64 }
65 }, { passive: false });
66}