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>
Signed| @@ -253,7 +253,7 @@ | ||
| 253 | 253 | </div> |
| 254 | 254 | <div id="common-gen-settings-block" class="width100p"> |
| 255 | 255 | <div id="pro-settings-block" class="flex-container gap10h5v justifyCenter"> |
| 256 | 256 | <div id="amount_gen_block" class="range-block-range-and-counter alignitemscenter flex-container marginBot5 flexFlowColumn flexBasis48p flexGrow flexShrink gap0"> |
| 257 | 257 | <small data-i18n="response legth(tokens)">Response (tokens)</small> |
| 258 | 258 | <input class="neo-range-slider" type="range" id="amount_gen" name="volume" min="16" max="2048" step="1"> |
| 259 | 259 | <div data-randomization-disabled="true" class="wide100p"> |
| @@ -284,7 +284,7 @@ | ||
| 284 | 284 | </label> |
| 285 | 285 | </div> |
| 286 | 286 | </div> |
| 287 | 287 | <div id="max_context_block" class="range-block-range-and-counter alignitemscenter flex-container marginBot5 flexFlowColumn flexBasis48p flexGrow flexShrink gap0"> |
| 288 | 288 | <small data-i18n="context size(tokens)">Context (tokens)</small> |
| 289 | 289 | <input class="neo-range-slider" type="range" id="max_context" name="volume" min="512" max="8192" step="64"> |
| 290 | 290 | <div data-randomization-disabled="true" class="wide100p"> |
| @@ -648,7 +648,7 @@ | ||
| 648 | 648 | Max Response Length (tokens) |
| 649 | 649 | </div> |
| 650 | 650 | <div class="wide100p"> |
| 651 | 651 | <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="1" max="65536" step="1"> |
| 652 | 652 | </div> |
| 653 | 653 | </div> |
| 654 | 654 | <div class="range-block" data-source="openai,custom,xai,aimlapi,moonshot,azure_openai"> |
| @@ -4214,7 +4214,7 @@ | ||
| 4214 | 4214 | Token Padding |
| 4215 | 4215 | </small> |
| 4216 | 4216 | </div> |
| 4217 | 4217 | <input id="token_padding" class="text_pole textarea_compact" type="number" min="-2048" max="2048" step="1" /> |
| 4218 | 4218 | </div> |
| 4219 | 4219 | </div> |
| 4220 | 4220 | <div> |
| @@ -270,6 +270,7 @@ import { getSystemMessageByType, initSystemMessages, SAFETY_CHAT, sendSystemMess | ||
| 270 | 270 | import { event_types, eventSource } from './scripts/events.js'; |
| 271 | 271 | import { initAccessibility } from './scripts/a11y.js'; |
| 272 | 272 | import { applyStreamFadeIn } from './scripts/util/stream-fadein.js'; |
| 273 | +import { initDomHandlers } from './scripts/dom-handlers.js'; | |
| 273 | 274 | |
| 274 | 275 | // API OBJECT FOR EXTERNAL WIRING |
| 275 | 276 | globalThis.SillyTavern = { |
| @@ -640,6 +641,7 @@ async function firstLoadInit() { | ||
| 640 | 641 | |
| 641 | 642 | showLoader(); |
| 642 | 643 | registerPromptManagerMigration(); |
| 644 | + initDomHandlers(); | |
| 643 | 645 | initStandaloneMode(); |
| 644 | 646 | initLibraryShims(); |
| 645 | 647 | addShowdownPatch(showdown); |
| @@ -0,0 +1,66 @@ | ||
| 1 | +import { throttle } from './utils.js'; | |
| 2 | + | |
| 3 | +export 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 | + */ | |
| 12 | +function 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 | +} | |