Skip to content

Commit 2554bc4

Browse files
committed
input-num spin fix + improvements
package.json working, though clumsy, and no minification
1 parent 2648368 commit 2554bc4

7 files changed

Lines changed: 445 additions & 113 deletions

File tree

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ displays 100 as: **$100/kg**
318318
#### Miscellaneous JavaScript Properties and Methods:
319319
- `text` (read-only) is the formatted text value, including currency, units, etc.
320320
- `useLocale` (read-only) returns `true` if `locale` is set.
321-
- `validate` is a `Function` for custom validation or transformation.
321+
- `validate` is a `Function` for custom validation and/or transformation.
322322
- `resize(forceIt)` resizes the element. When `autoResize` is `true`, it runs automatically after setting any attribute that affects the element's width or alignment. When `autoResize` is `false`, you must set `forceIt` to `true` or the function won't run. Call it after you change CSS font properties, for example.
323323
324324
### Events
@@ -334,10 +334,11 @@ When the user inputs via the spinner, the event object has two additional proper
334334
- `isSpinning` is set to `true`.
335335
- `isUp` is `true` when it's spinning up (incrementing) and `false` or `undefined` when spinning down (decrementing).
336336
337-
The `validate` property allows you to insert your own validation (or transformation) function before the value is committed and the `change` event is fired. Because it runs before committing the value, it runs before the internal `!isNaN()` validation. The function takes two arguments: `value` and `isSpinning`. `value` is a string (keyboard input) or a number (spinning). The return value falls into three categories:
338-
- `false` for invalid values
339-
- `undefined` or `value` to accept the current value
340-
- a different number: for when you want to round to the nearest prime number, or perform whatever transformation or restriction that can't be defined solely by `max` and `min`.
337+
The `validate` property allows you to insert your own validation and/or transformation function before the value is committed and the `change` event is fired. Because it runs before committing the value, it runs before the internal `!isNaN()` validation. The function takes two arguments: `value` and `isSpinning`:
338+
- `value` is a string (keyboard input) or a number (spinning)
339+
- `isSpinning` is a boolean indicating whether the user is inputting via keyboard (`false`) or the spinner (`true`).
340+
341+
To indicate an invalid value, return `false`. Otherwise return the value itself, transformed or not. Transforms are for those rare occasions when you want to round to the nearest prime number, or whatever transformation or restriction that can't be defined solely by `max` and `min`.
341342
342343
### Styling
343344
You can obviously style the element itself, but you can also style some of its parts via the `::part` pseudo-element. Remember that `::part` overrides the shadow DOM elements' style. You must use `!important` if you want to override `::part`.

apps/multi-state/index.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,6 @@ function textLabel(elm) {
130130
return ` label="${elm.label}"`;
131131
}
132132
//==============================================================================
133-
// HTML displays using non-breaking hyphen U+2011, which is not a valid HTML
134-
// character, so it must be replaced with a regular hyphen.
135133
function copyToClipboard(evt) {
136134
const tar = evt.target;
137135
writeText(tar, fromHTML(elms.html[splitDash(tar.id)[0]].textContent));

custom-elements.json

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,9 +1086,6 @@
10861086
{
10871087
"name": "hoverIn"
10881088
},
1089-
{
1090-
"name": "setHref"
1091-
},
10921089
{
10931090
"name": "state"
10941091
},
@@ -1287,6 +1284,24 @@
12871284
}
12881285
]
12891286
},
1287+
{
1288+
"kind": "method",
1289+
"name": "#clearSpin",
1290+
"privacy": "private"
1291+
},
1292+
{
1293+
"kind": "method",
1294+
"name": "#isOoB",
1295+
"privacy": "private",
1296+
"parameters": [
1297+
{
1298+
"name": "val"
1299+
},
1300+
{
1301+
"name": "isUp"
1302+
}
1303+
]
1304+
},
12901305
{
12911306
"kind": "method",
12921307
"name": "resize",

input-num.js

Lines changed: 62 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,8 @@ static observedAttributes = [
369369
let args;
370370
const id = evt.target.id;
371371
if (this.#isSpinning) { // if spinning, spin the other way
372-
this.#spin(undefined, null); // cancel w/o href or #spinId = null
372+
if (this.#spinId >= 0)
373+
this.#clearSpin(); // clearInterval()
373374
args = [false, id == TOP]; // restart at full speed
374375
}
375376
this.#overOut(id, `${this.#getState(id)}`, args);
@@ -472,7 +473,7 @@ static observedAttributes = [
472473
this.classList.add(BEEP);
473474
}
474475
else {
475-
this.#spin(true);
476+
this.#spin(true); // start spinning
476477
if (this.#outFocus) // Chrome: prevent next element's
477478
evt.preventDefault(); // text selection on double-click,
478479
} // but allow focus to happen first
@@ -544,22 +545,28 @@ static observedAttributes = [
544545
return;
545546
case code.down:
546547
case code.up:
547-
// Keyboard repeat rate is an OS setting that has it's own delay
548-
// then interval, thus #spin(null). Separate href for delayed image
549-
// because quick keydown/up sequences flicker, and a delay can look
550-
// just as flickery, depending on the timing. If it's only a slight
551-
// difference from #spinner-idle, it looks good.
552-
const topBot = key[evt.code];
553-
if (this.#isSpinning)
554-
this._setHref(`${SPINNER}${SPIN}${topBot}`);
548+
// Keyboard repeat rate is an OS setting that has its own delay then
549+
// interval, thus #spin(null), which doesn't set href or #spinId.
550+
// Separate href for delayed image because fast keydown/up sequences
551+
// flicker, and a delay can look just as flickery, depending on the
552+
// timing. It looks better if the initial, pre-delay image is only
553+
// slightly different from #spinner-idle.
554+
const
555+
topBot = key[evt.code],
556+
isSpin = this.#isSpinning, // next line might change it
557+
inBounds = this.#spin(null, topBot == TOP);
558+
if (isSpin) {
559+
if (inBounds)
560+
this._setHref(`${SPINNER}${SPIN}${topBot}`);
561+
}
555562
else {
556563
this._setHref(`${SPINNER}key-${topBot}`);
557-
this.#spinId = -1;
558-
} // #spin(null) doesn't set href or #spinId
559-
this.#spin(null, topBot == TOP);
564+
this.#spinId = -1; // so that #isSpinning = true
565+
}
560566
default:
561567
}
562568
}
569+
//--------------------------------------------------------------------------
563570
#keyUp(evt) { // Esc key never makes it this far
564571
if (this.#inFocus) { // any character accepted...
565572
if (evt.code == code.enter)
@@ -647,51 +654,67 @@ static observedAttributes = [
647654

648655
// #spin() controls the spinning process
649656
#spin(state, isUp = (this.#hoverIn == TOP)) {
650-
if (state === undefined) { // cancel:
657+
if (state === undefined) { // cancel:
651658
if (this.#isSpinning) {
652-
clearInterval(this.#spinId); // interchangeable w/clearTimeout()
653-
clearTimeout (this.#erId);
654-
if (isUp !== null) { // for mouseover while spinning
659+
this.#clearSpin();
660+
if (isUp !== null) { // for mouseover while spinning
655661
const state = this.#hoverIn ? `${HOVER}${this.#hoverIn}`
656662
: IDLE;
657663
this._setHref(`${SPINNER}${state}`);
658664
this.#spinId = null;
659665
}
660-
this.#erId = null;
661666
}
662667
}
663-
else { // spin:
664-
let val = this.#attrs[VALUE]; // if already clamped, skip it
665-
if ((isUp && val != this.max) || (!isUp && val != this.min)) {
666-
const n = val + (this.#attrs[STEP] * (isUp ? 1 : -1));
667-
val = this.#validate?.(n, true) ?? n; // true for isSpinning
668-
if (val !== false) { // clamp it between min and max:
669-
this.setAttribute(VALUE, Math.max(this.min, Math.min(this.max, val)));
670-
this.#input.value = this.#getText(false); // false because #input w/focus does not spin
668+
else { // spin:
669+
let
670+
val = this.#attrs[VALUE],
671+
oob = this.#isOoB(val, isUp);
672+
if (!oob) {
673+
val += this.#attrs[STEP] * (isUp ? 1 : -1);
674+
val = this.#validate?.(val, true) ?? val; // true for isSpinning
675+
if (val !== false) {
676+
oob = this.#isOoB(val, isUp);
677+
if (oob) {
678+
this.#clearSpin(); // stop spinning and clamp the value
679+
val = Math.max(this.min, Math.min(this.max, val));
680+
}
681+
this.setAttribute(VALUE, val); // false: #input w/focus doesn't spin
682+
this.#input.value = this.#getText(false);
671683
const evt = new Event("change");
672684
evt.isUp = isUp;
673685
evt.isSpinning = true;
674686
this.dispatchEvent(evt);
675687
}
676-
if (state) { // start spin with initial step
677-
const
678-
topBot = isUp ? TOP : BOT,
679-
now = `${SPINNER}${ACTIVE}${topBot}`,
680-
later = `${SPINNER}${SPIN}${topBot}`;
688+
}
689+
if (oob)
690+
this.#spinId = -1; // not null, same as keyboard spin
681691

682-
this._setHref(now);
683-
this.#erId = setTimeout(this._setHref.bind(this), this.delay, later);
692+
if (state) { // start spin with initial step
693+
const
694+
topBot = isUp ? TOP : BOT,
695+
now = `${SPINNER}${ACTIVE}${topBot}`,
696+
later = `${SPINNER}${SPIN}${topBot}`;
684697

685-
this.#spinId = setTimeout (this.#spin.bind(this), this.delay, false, isUp);
698+
this._setHref(now);
699+
if (!oob) {
700+
this.#erId = setTimeout(this._setHref.bind(this), this.delay, later);
701+
this.#spinId = setTimeout(this.#spin.bind(this), this.delay, false, isUp);
686702
}
687-
else if (state === false) // start spinning full speed
688-
this.#spinId = setInterval(this.#spin.bind(this), this.interval, null, isUp);
689-
// else state === null >>>> continue spinning full speed
690703
}
691-
else // I prefer this to an expired id for a non-null value
692-
this.#spinId = -1; // for keyboard spin, clamped values
704+
else if (state === false) // start spinning full speed
705+
this.#spinId = setInterval(this.#spin.bind(this), this.interval, null, isUp);
706+
// else state === null >>>> continue spinning full speed
707+
return !oob;
693708
}
694709
}
710+
#clearSpin() {
711+
clearInterval(this.#spinId); // interchangeable w/clearTimeout()
712+
clearTimeout (this.#erId);
713+
this.#erId = null;
714+
}
715+
#isOoB(val, isUp) { // really out of and including bounds...
716+
return isUp ? val >= this.max : val <= this.min;
717+
}
695718
// =============================================================================
696719
// resize() calculates the correct width and applies it to this.#input
697720
resize(forceIt) {

0 commit comments

Comments
 (0)