| /* | |
| * This content is licensed according to the W3C Software License at | |
| * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document | |
| * | |
| * File: quantity-spinbutton.js | |
| */ | |
| 'use strict'; | |
| class SpinButton { | |
| constructor(el) { | |
| this.el = el; | |
| this.id = el.id; | |
| this.controls = Array.from( | |
| document.querySelectorAll(`button[aria-controls="${this.id}"]`) | |
| ); | |
| this.output = document.querySelector(`output[for="${this.id}"]`); | |
| this.useAriaNotify = typeof el.ariaNotify === 'function'; | |
| if (this.useAriaNotify && this.output) { | |
| this.output.remove(); | |
| this.output = null; | |
| } | |
| this.timer = null; | |
| this.setBounds(); | |
| el.addEventListener('input', () => this.setValue(el.value, true)); | |
| el.addEventListener('blur', () => { | |
| if (el.value === '' && this.hasMin) { | |
| this.setValue(this.min, true); | |
| } | |
| }); | |
| el.addEventListener('keydown', (e) => this.handleKey(e)); | |
| this.controls.forEach((btn) => | |
| btn.addEventListener('click', () => this.handleClick(btn)) | |
| ); | |
| this.setValue(el.value); | |
| } | |
| clamp(n) { | |
| return Math.min(Math.max(n, this.min), this.max); | |
| } | |
| parseValue(raw) { | |
| const s = String(raw).trim(); | |
| if (!s) return null; | |
| const n = parseInt(s.replace(/[^\d-]/g, ''), 10); | |
| return isNaN(n) ? null : n; | |
| } | |
| setBounds() { | |
| const el = this.el; | |
| this.hasMin = el.hasAttribute('aria-valuemin'); | |
| this.hasMax = el.hasAttribute('aria-valuemax'); | |
| this.min = this.hasMin | |
| ? +el.getAttribute('aria-valuemin') | |
| : Number.MIN_SAFE_INTEGER; | |
| this.max = this.hasMax | |
| ? +el.getAttribute('aria-valuemax') | |
| : Number.MAX_SAFE_INTEGER; | |
| } | |
| isValid(val) { | |
| if (val === '' || val === null) return true; | |
| const numVal = +val; | |
| return !( | |
| (this.hasMin && numVal < this.min) || | |
| (this.hasMax && numVal > this.max) | |
| ); | |
| } | |
| snapValue(val, currentVal) { | |
| let result = val; | |
| if (this.hasMin) result = Math.max(result, this.min); | |
| if (this.hasMax) result = Math.min(result, this.max); | |
| if ( | |
| (result < currentVal && val > currentVal) || | |
| (result > currentVal && val < currentVal) | |
| ) { | |
| return currentVal; | |
| } | |
| return result; | |
| } | |
| setValue(raw, fromInput = false) { | |
| let val = typeof raw === 'number' ? raw : this.parseValue(raw); | |
| if (!fromInput) { | |
| const currentVal = +this.el.value || 0; | |
| val = this.snapValue(val, currentVal); | |
| } | |
| this.el.value = val; | |
| this.el.ariaValueNow = val; | |
| this.el.ariaInvalid = !this.isValid(val) ? 'true' : null; | |
| this.updateButtonStates(); | |
| } | |
| updateButtonStates() { | |
| const val = +this.el.value; | |
| this.controls.forEach((btn) => { | |
| const op = btn.getAttribute('data-spinbutton-operation'); | |
| btn.ariaDisabled = ( | |
| op === 'decrement' ? val <= this.min : val >= this.max | |
| ) | |
| ? 'true' | |
| : null; | |
| }); | |
| } | |
| announce() { | |
| if (this.useAriaNotify) { | |
| this.el.ariaNotify(String(this.el.value)); | |
| return; | |
| } | |
| if (!this.output) return; | |
| this.output.textContent = this.el.value; | |
| clearTimeout(this.timer); | |
| this.timer = setTimeout(() => { | |
| this.output.textContent = ''; | |
| this.timer = null; | |
| }, this.output.dataset.selfDestruct || 1000); | |
| } | |
| handleKey(e) { | |
| let v = +this.el.value || 0; | |
| if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { | |
| e.preventDefault(); | |
| this.setValue(v + (e.key === 'ArrowUp' ? 1 : -1)); | |
| } else if (e.key === 'Home') { | |
| e.preventDefault(); | |
| this.setValue(this.min); | |
| } else if (e.key === 'End') { | |
| e.preventDefault(); | |
| this.setValue(this.max); | |
| } | |
| } | |
| handleClick(btn) { | |
| const dir = | |
| btn.getAttribute('data-spinbutton-operation') === 'decrement' ? -1 : 1; | |
| this.setValue((+this.el.value || 0) + dir); | |
| this.announce(); | |
| } | |
| } | |
| window.addEventListener('load', () => | |
| document | |
| .querySelectorAll('[role="spinbutton"]') | |
| .forEach((el) => new SpinButton(el)) | |
| ); |