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 | </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 Padding | 4214 | 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> |
| @@ -270,6 +270,7 @@ import { getSystemMessageByType, initSystemMessages, SAFETY_CHAT, sendSystemMess | |||
| 270 | import { event_types, eventSource } from './scripts/events.js'; | 270 | import { event_types, eventSource } from './scripts/events.js'; |
| 271 | import { initAccessibility } from './scripts/a11y.js'; | 271 | import { initAccessibility } from './scripts/a11y.js'; |
| 272 | import { applyStreamFadeIn } from './scripts/util/stream-fadein.js'; | 272 | import { applyStreamFadeIn } from './scripts/util/stream-fadein.js'; |
| 273 | import { initDomHandlers } from './scripts/dom-handlers.js'; | ||
| 273 | 274 | ||
| 274 | // API OBJECT FOR EXTERNAL WIRING | 275 | // API OBJECT FOR EXTERNAL WIRING |
| 275 | globalThis.SillyTavern = { | 276 | globalThis.SillyTavern = { |
| @@ -640,6 +641,7 @@ async function firstLoadInit() { | |||
| 640 | 641 | ||
| 641 | showLoader(); | 642 | showLoader(); |
| 642 | registerPromptManagerMigration(); | 643 | registerPromptManagerMigration(); |
| 644 | initDomHandlers(); | ||
| 643 | initStandaloneMode(); | 645 | initStandaloneMode(); |
| 644 | initLibraryShims(); | 646 | initLibraryShims(); |
| 645 | addShowdownPatch(showdown); | 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 | } | ||