light-odometer 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/light-odometer.d.ts +14 -5
- package/dist/light-odometer.js +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -101,6 +101,32 @@ For a quick overview in a real-world usage, see https://github.com/EDM115/websit
|
|
|
101
101
|
}
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
+
### Animation controls
|
|
105
|
+
`maxValues` caps each slide ribbon at 32 value elements by default (configurable from 2 to 256). `framerate` can lower the sampling density further, it does not throttle CSS animation frames. Digit containers are reused between renders.
|
|
106
|
+
Timing changes made with `setOptions()` apply to the next animation. Format changes apply immediately, including when the numeric value stays the same. A new `update()` supersedes the previous animation, only the current animation emits `odometerdone`.
|
|
107
|
+
`respectReducedMotion` defaults to `true`. Reduced motion and a zero duration render the target immediately while preserving the start/done event order. `disconnect()` cancels work, restores wrapped element properties, removes subscriptions registered with `on()` and leaves a static target. Disconnected instances are terminal, create a new instance to reuse the element.
|
|
108
|
+
Use `onRender` to decorate the complete digit structure synchronously without a mutation observer. The callback runs on the initial render as well as later renders, keep it limited to decorating the DOM.
|
|
109
|
+
```ts
|
|
110
|
+
const odo = new LightOdometer({
|
|
111
|
+
el,
|
|
112
|
+
value: 0,
|
|
113
|
+
maxValues: 32,
|
|
114
|
+
respectReducedMotion: true,
|
|
115
|
+
onRender(instance) {
|
|
116
|
+
// instance.inside contains the complete structure
|
|
117
|
+
// instance.digits is ordered from the least significant digit
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
```
|
|
121
|
+
`spin(digitCount)` rolls 1–32 digits continuously using 11 value elements per digit. `duration` controls a full revolution with a small speed difference between columns. It keeps the last numeric value unchanged and emits a start event, it has no done event until a later numeric update completes. `isSpinning` distinguishes this mode from a finite animation. Under reduced motion or zero duration, the requested digits remain stationary. A change in the motion preference updates a running spinner automatically.
|
|
122
|
+
```ts
|
|
123
|
+
odo.spin(6)
|
|
124
|
+
odo.update(123456) // Stops spinning and settles on this value
|
|
125
|
+
odo.disconnect() // Also stops an active spinner
|
|
126
|
+
```
|
|
127
|
+
Numeric inputs must be finite and safely representable after scaling to the format precision, formats support up to 15 decimal places. Invalid values, negative durations, and nonpositive framerates throw `RangeError`. Use `spin()` for indefinite motion instead of passing numeric `Infinity`.
|
|
128
|
+
For an accessible integration, expose a separate formatted text value and mark the decorative wheel `aria-hidden="true"`.
|
|
129
|
+
|
|
104
130
|
### Improvements
|
|
105
131
|
- `value` is now stripped from spaces
|
|
106
132
|
- Multiple performance improvements
|
package/dist/light-odometer.d.ts
CHANGED
|
@@ -28,6 +28,12 @@ interface LightOdometerOptions {
|
|
|
28
28
|
duration?: number;
|
|
29
29
|
framerate?: number;
|
|
30
30
|
countFramerate?: number;
|
|
31
|
+
/** Maximum value elements per sliding digit (2–256, defaults to 32). */
|
|
32
|
+
maxValues?: number;
|
|
33
|
+
/** Honor the user's reduced-motion preference by default. */
|
|
34
|
+
respectReducedMotion?: boolean;
|
|
35
|
+
/** Called synchronously after a complete digit structure is attached. */
|
|
36
|
+
onRender?: (instance: LightOdometer) => void;
|
|
31
37
|
animation?: "count" | "slide";
|
|
32
38
|
formatFunction?: (value: number) => string;
|
|
33
39
|
}
|
|
@@ -53,7 +59,7 @@ declare global {
|
|
|
53
59
|
}
|
|
54
60
|
//#endregion
|
|
55
61
|
//#region src/core/odometer.d.ts
|
|
56
|
-
declare class LightOdometer {
|
|
62
|
+
export declare class LightOdometer {
|
|
57
63
|
#private;
|
|
58
64
|
static options: LightOdometerGlobalOptions;
|
|
59
65
|
options: LightOdometerOptions;
|
|
@@ -70,6 +76,7 @@ declare class LightOdometer {
|
|
|
70
76
|
MAX_VALUES: number;
|
|
71
77
|
digits: HTMLElement[];
|
|
72
78
|
ribbons: Record<number, HTMLElement>;
|
|
79
|
+
get isSpinning(): Readonly<boolean>;
|
|
73
80
|
/**
|
|
74
81
|
* Initializes a new instance of the LightOdometer class.
|
|
75
82
|
* Sets up the odometer's options, formats, and DOM structure.
|
|
@@ -190,14 +197,14 @@ declare class LightOdometer {
|
|
|
190
197
|
* @param {number} newValue - The new value to animate the odometer to.
|
|
191
198
|
* @returns {void}
|
|
192
199
|
*/
|
|
193
|
-
animate(newValue: number): void;
|
|
200
|
+
animate(newValue: number, oldValue?: number): void;
|
|
194
201
|
/**
|
|
195
202
|
* Animates the odometer by incrementing or decrementing the value over time.
|
|
196
203
|
* Uses a "counting" animation to transition smoothly to the new value.
|
|
197
204
|
* @param {number} newValue - The new value to animate the odometer to.
|
|
198
205
|
* @returns {void}
|
|
199
206
|
*/
|
|
200
|
-
animateCount(newValue: number): void;
|
|
207
|
+
animateCount(newValue: number, oldValue?: number): void;
|
|
201
208
|
/**
|
|
202
209
|
* Calculates the number of digits in the largest absolute value from the provided numbers.
|
|
203
210
|
* @param {...number} values - A list of numbers to evaluate.
|
|
@@ -231,7 +238,9 @@ declare class LightOdometer {
|
|
|
231
238
|
* @param {number} newValue - The new value to animate the odometer to.
|
|
232
239
|
* @returns {void}
|
|
233
240
|
*/
|
|
234
|
-
animateSlide(newValue: number): void;
|
|
241
|
+
animateSlide(newValue: number, oldValue?: number): void;
|
|
242
|
+
/** Roll a bounded number of digits until update() or disconnect() is called. */
|
|
243
|
+
spin(digitCount: number): void;
|
|
235
244
|
/**
|
|
236
245
|
* Initializes all odometer elements on the page.
|
|
237
246
|
* Selects elements matching the configured selector or the default `.odometer` class, and creates a `LightOdometer` instance for each element.
|
|
@@ -277,4 +286,4 @@ declare class LightOdometer {
|
|
|
277
286
|
toString(): string;
|
|
278
287
|
}
|
|
279
288
|
//#endregion
|
|
280
|
-
export { LightOdometer
|
|
289
|
+
export { LightOdometer as default };
|
package/dist/light-odometer.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const e=/^\(?([^)]*)\)?(?:(.)(d+))?$/,t=2e3;function n(e){let t=document.createElement(`div`);if(t.innerHTML=e,!(t.firstElementChild instanceof HTMLElement))throw Error(`Invalid HTML: No valid root element found.`);return t.firstElementChild}function r(e,t){let n=t.split(` `);for(let t of n)e.classList.remove(t);return e.className}function i(e,t){let n=t.split(` `);for(let t of n)t&&e.classList.add(t);return e.className}function a(e,t,n){let r=new CustomEvent(t,{bubbles:!0,cancelable:!0,detail:n});e.dispatchEvent(r)}function o(){return typeof performance<`u`&&typeof performance.now==`function`?performance.now():Date.now()}function s(e,t){return t??=0,t?(e*=10**t,e+=.5,e=Math.floor(e),e/=10**t):Math.round(e)}function c(e){return e<0?Math.ceil(e):Math.floor(e)}function l(){return typeof window<`u`&&typeof document<`u`}function u(e){return l()&&typeof requestAnimationFrame==`function`?requestAnimationFrame(e):setTimeout(()=>e(o()),16)}function d(e){e!=null&&(l()&&typeof cancelAnimationFrame==`function`&&typeof e==`number`?cancelAnimationFrame(e):clearTimeout(e))}function f(e){l()&&(document.readyState===`complete`||document.readyState===`interactive`?setTimeout(e,0):document.addEventListener(`DOMContentLoaded`,e,{once:!0}))}function p(e){l()&&setTimeout(()=>{window.odometerOptions&&(e.options={...e.options,...window.odometerOptions})},0)}function m(e){f(()=>{e.options.auto!==!1&&e.init()})}var h=class f{static options=typeof window<`u`?window.odometerOptions??{}:{};#e=!1;options;el;value=0;inside;observer;watchMutations=!1;transitionEndBound=!1;destroyed=!1;format={repeating:``,precision:0};get isAnimating(){return this.#e}MAX_VALUES;digits=[];ribbons={};#t;#n;#r;#i;#a;constructor(e){if(this.options=e,this.el=this.options.el,this.el.odometer){let e=this.el.odometer;e.options={...e.options,...this.options},e.options.duration??=t;let n=e.options.framerate??30,r=e.options.countFramerate??20;return e.#r=1e3/n,e.#i=1e3/r,e.MAX_VALUES=e.options.duration/e.#r/2|0,e.resetFormat(),this.options.value==null?l()&&e.render():e.update(this.options.value),e}this.el.odometer=this,this.options={...f.options,...this.options},this.options.duration??=t;let n=this.options.framerate??30,r=this.options.countFramerate??20;this.#r=1e3/n,this.#i=1e3/r,this.MAX_VALUES=this.options.duration/this.#r/2|0,this.resetFormat(),this.value=this.cleanValue(this.options.value??``),l()&&(this.renderInside(),this.render());try{for(let e of[`innerHTML`,`innerText`,`textContent`])Object.defineProperty(this.el,e,{get:()=>e===`innerHTML`?this.inside?.outerHTML??``:this.inside?.innerText??this.inside?.textContent??``,set:e=>this.update(e)})}catch{l()&&this.watchForMutations()}}renderInside(){l()&&(this.inside=document.createElement(`div`),this.inside.className=`odometer-inside`,this.el.textContent=``,this.el.appendChild(this.inside))}watchForMutations(){try{this.observer??=new MutationObserver(e=>{let t=this.el.innerText||``;this.renderInside(),this.render(this.value),this.update(t)}),this.watchMutations=!0,this.startWatchingMutations()}catch{}}startWatchingMutations(){this.watchMutations&&l()&&this.observer?.observe(this.el,{childList:!0})}stopWatchingMutations(){this.observer?.disconnect()}cleanValue(e){return typeof e==`string`&&(e=e.replace(this.format.radix??`.`,`<radix>`),e=e.replace(/[.,\s\u00A0\u202F]/g,``),e=e.replace(`<radix>`,`.`),e=parseFloat(e)||0),s(e,this.format.precision)}bindTransitionEnd(){if(this.transitionEndBound)return;this.transitionEndBound=!0;let e=!1;this.#a=()=>e?!0:(e=!0,setTimeout(()=>{this.render(),e=!1,this.#e=!1,a(this.el,`odometerdone`,{id:this.options.id,el:this.el,instance:this,value:this.value,options:this.getOptions()})},0),!0),this.el.addEventListener(`transitionend`,this.#a,!1)}resetFormat(){let t=this.options.format??`(,ddd).dd`;t||=`d`;let n=e.exec(t);if(!n)throw Error(`LightOdometer: Unparsable digit format`);let r=n[1]??``,i=n[2],a=(n[3]??``).length;this.format={repeating:r,radix:i,precision:a}}render(e){if(l()){e??=this.value,this.stopWatchingMutations(),this.resetFormat(),this.inside.textContent=``;for(let e of Array.from(this.el.classList))/^odometer(-|$)/.test(e)&&this.el.classList.remove(e);this.el.classList.add(`odometer`,`odometer-auto-theme`),this.el.style.setProperty(`--odometer-duration`,`${this.options.duration??2e3}ms`),this.ribbons={},this.formatDigits(e),this.startWatchingMutations()}}formatDigits(e){if(this.digits=[],this.options.formatFunction){let t=this.options.formatFunction(e);for(let e of t.split(``).toReversed())if(/\d/.test(e)){let t=this.renderDigit(),n=t.querySelector(`.odometer-value`);n.textContent=e,i(n,`odometer-first-value odometer-last-value`),this.digits.push(t),this.insertDigit(t)}else this.addSpacer(e)}else{let t=this.preservePrecision(e),n=!this.format.precision;for(let e of t.split(``).toReversed())e===`.`&&(n=!0),this.addDigit(e,n)}}preservePrecision(e){let t=e.toString();if(this.format.precision){let e=t.split(`.`);e.length===1&&(t+=`.`,e[1]=``);let n=e[1]??``;for(let e=0;e<this.format.precision;e++)n[e]||(t+=`0`)}return t}update(e){if(!l())return this.value=this.cleanValue(e),this.value;e=this.cleanValue(e);let t=e-this.value;return t?(r(this.el,`odometer-animating-up odometer-animating-down odometer-animating`),t>0?i(this.el,`odometer-animating-up`):i(this.el,`odometer-animating-down`),this.#e=!0,a(this.el,`odometerstart`,{id:this.options.id,el:this.el,instance:this,value:e,oldValue:this.value,options:this.getOptions()}),this.stopWatchingMutations(),this.animate(e),this.startWatchingMutations(),setTimeout(()=>{this.el.offsetHeight,i(this.el,`odometer-animating`)},0),this.value=e,this.value):this.value}renderDigit(){return n(`<span class="odometer-digit"><span class="odometer-digit-spacer">8</span><span class="odometer-digit-inner"><span class="odometer-ribbon"><span class="odometer-ribbon-inner"><span class="odometer-value"></span></span></span></span></span>`)}insertDigit(e,t){return t?this.inside.insertBefore(e,t):this.inside.children.length?this.inside.insertBefore(e,this.inside.firstElementChild):this.inside.appendChild(e)}addSpacer(e,t,r){let a=n(`<span class="odometer-formatting-mark"></span>`);return a.textContent=e,r&&i(a,r),this.insertDigit(a,t)}addDigit(e,t){if(t??=!0,e===`-`)return this.addSpacer(e,null,`odometer-negation-mark`);if(e===`.`)return this.addSpacer(this.format.radix??`.`,null,`odometer-radix-mark`);if(t){let e=!1;for(;;){if(!this.format.repeating.length){if(e)throw Error(`Bad odometer format without digits`);this.resetFormat(),e=!0}let t=this.format.repeating[this.format.repeating.length-1];if(t!=null){if(this.format.repeating=this.format.repeating.substring(0,this.format.repeating.length-1),t===`d`)break;this.addSpacer(t)}}}let n=this.renderDigit(),r=n.querySelector(`.odometer-value`);return r.textContent=e,i(r,`odometer-first-value odometer-last-value`),this.digits.push(n),this.insertDigit(n)}animate(e){this.options.animation===`count`?this.animateCount(e):this.animateSlide(e)}animateCount(e){if(!l())return;let t=e-this.value;if(!t)return;let n=o(),r=n,i=this.value,s=()=>{if(o()-n>(this.options.duration||0)){this.value=e,this.render(),this.#e=!1,a(this.el,`odometerdone`,{id:this.options.id,el:this.el,instance:this,value:this.value,options:this.getOptions()});return}let c=o()-r;if(c>this.#i){r=o();let e=c/(this.options.duration||0),n=t*e;i+=n,this.render(Math.round(i))}this.#n=u(s)};this.#n=u(s)}getDigitCount(...e){for(let t=0;t<e.length;t++){let n=e[t]??0;e[t]=Math.abs(n)}let t=Math.max(...e);return Math.ceil(Math.log(t+1)/Math.log(10))}getFractionalDigitCount(...e){let t=/^-?\d*\.(\d*?)0*$/;for(let n=0;n<e.length;n++){let r=(e[n]??0).toString(),i=t.exec(r),a=i?.[1]??``;e[n]=i?a.length:0}return Math.max(...e)}resetDigits(){this.digits=[],this.ribbons={},this.inside.textContent=``,this.resetFormat()}createRange(e,t,n){let r=e<t,i=Math.abs(t-e)+ +!!n;return Array.from({length:i},(t,n)=>r?e+n:e-n)}animateSlide(e){if(!l())return;let t=this.value,n=this.format.precision;n&&(e*=10**n,t*=10**n);let r=e-t;if(!r)return;this.bindTransitionEnd();let a=[],o=this.getDigitCount(t,e),s=0,u=t;for(let n=0;n<o;n++){u=c(t/10**(o-n-1));let r=c(e/10**(o-n-1)),i=r-u,l;if(Math.abs(i)>this.MAX_VALUES){l=[];let e=i/(this.MAX_VALUES*(1+s*.5)),t=u;for(;i>0&&t<r||i<0&&t>r;)l.push(Math.round(t)),t+=e;l[l.length-1]!==r&&l.push(r),s++}else l=this.createRange(u,r,!0);for(let e=0;e<l.length;e++){let t=l[e]??0;l[e]=Math.abs(t%10)}a.push(l)}this.resetDigits();let d=a.toReversed();for(let e=0;e<d.length;e++){let t=d[e]??[];this.digits[e]||this.addDigit(` `,e>=n);let a=this.digits[e];if(!a)continue;if(this.ribbons[e]===void 0){let t=a.querySelector(`.odometer-ribbon-inner`);t&&(this.ribbons[e]=t)}let o=this.ribbons[e];if(o){o.textContent=``,r<0&&(t=t.toReversed());for(let e=0;e<t.length;e++){let n=t[e]??0,r=document.createElement(`div`);r.className=`odometer-value`,r.textContent=n.toString(),o.appendChild(r),e===t.length-1&&i(r,`odometer-last-value`),e===0&&i(r,`odometer-first-value`)}}}u<0&&this.addDigit(`-`);let f=this.inside.querySelector(`.odometer-radix-mark`);f&&f.parentNode.removeChild(f),n&&this.addSpacer(this.format.radix??`.`,this.digits[n-1],`odometer-radix-mark`)}static init(){if(!l())return[];let e=document.querySelectorAll(f.options.selector||`.odometer`);return Array.from(e,e=>e.odometer=new f({el:e,value:e.innerText??e.textContent}))}setOptions(e){if(!e||typeof e!=`object`)return;`el`in e&&delete e.el;let n=Object.prototype.hasOwnProperty.call(e,`value`),r=Object.prototype.hasOwnProperty.call(e,`format`)||Object.prototype.hasOwnProperty.call(e,`formatFunction`),i=Object.prototype.hasOwnProperty.call(e,`duration`)||Object.prototype.hasOwnProperty.call(e,`framerate`)||Object.prototype.hasOwnProperty.call(e,`countFramerate`);this.options={...this.options,...e},this.options.duration??=t;let a=this.options.framerate??30,o=this.options.countFramerate??20;if(this.#r=1e3/a,this.#i=1e3/o,this.MAX_VALUES=this.options.duration/this.#r/2|0,l()&&this.el.style.setProperty(`--odometer-duration`,`${this.options.duration}ms`),r&&this.resetFormat(),n){this.update(this.options.value??0);return}(r||i)&&l()&&(this.stopWatchingMutations(),this.render(),this.startWatchingMutations())}static setGlobalOptions(e){f.options={...f.options,...e}}on(e,t){this.el.addEventListener(e,t)}off(e,t){this.el.removeEventListener(e,t)}getOptions(){return{...this.options}}static getGlobalOptions(){return{...f.options}}animateOnceAndDisconnect(e){e!=null&&this.update(e);let t=()=>this.disconnect(),n=e=>{this.el.removeEventListener(`odometerdone`,n),t()};this.el.addEventListener(`odometerdone`,n,{once:!0}),setTimeout(t,(this.options.duration??2e3)+100)}disconnect(){this.destroyed||=(this.stopWatchingMutations(),d(this.#t),d(this.#n),this.#a&&=(this.el.removeEventListener(`transitionend`,this.#a),void 0),this.transitionEndBound=!1,!0)}toString(){let{el:e,...t}={...this.getOptions()},n={id:this.options.id,value:this.value,options:t,globalOptions:f.getGlobalOptions(),watchMutations:this.watchMutations,transitionEndBound:this.transitionEndBound,destroyed:this.destroyed,format:this.format,isAnimating:this.isAnimating};return JSON.stringify(n)}};p(h),m(h);export{h as LightOdometer,h as default};
|
|
1
|
+
const e=/^\(?([^)]*)\)?(?:(.)(d+))?$/;function t(e){let t=document.createElement(`div`);if(t.innerHTML=e,!(t.firstElementChild instanceof HTMLElement))throw Error(`Invalid HTML: No valid root element found.`);return t.firstElementChild}function n(e,t){let n=t.split(` `);for(let t of n)e.classList.remove(t);return e.className}function r(e,t){let n=t.split(` `);for(let t of n)t&&e.classList.add(t);return e.className}function i(e,t,n){let r=new CustomEvent(t,{bubbles:!0,cancelable:!0,detail:n});e.dispatchEvent(r)}function a(){return typeof performance<`u`&&typeof performance.now==`function`?performance.now():Date.now()}function o(e,t){if(t??=0,!t)return Math.round(e);let n=10**t;return Math.round(e*n)/n}function s(e){return e<0?Math.ceil(e):Math.floor(e)}function c(){return typeof window<`u`&&typeof document<`u`}function l(e){return c()&&typeof requestAnimationFrame==`function`?requestAnimationFrame(e):setTimeout(()=>e(a()),16)}function u(e){e!=null&&(c()&&typeof cancelAnimationFrame==`function`&&typeof e==`number`?cancelAnimationFrame(e):clearTimeout(e))}function d(e){c()&&(document.readyState===`complete`||document.readyState===`interactive`?setTimeout(e,0):document.addEventListener(`DOMContentLoaded`,e,{once:!0}))}function f(e){c()&&setTimeout(()=>{window.odometerOptions&&(e.options={...e.options,...window.odometerOptions})},0)}function p(e){d(()=>{e.options.auto!==!1&&e.init()})}var m=class d{static options=typeof window<`u`?window.odometerOptions??{}:{};#e=!1;options;el;value=0;inside;observer;watchMutations=!1;transitionEndBound=!1;destroyed=!1;format={repeating:``,precision:0};get isAnimating(){return this.#e}MAX_VALUES;digits=[];ribbons={};#t;#n;#r;#i;#a;#o=0;#s=0;#c=0;#l=0;#u=!1;#d=[];#f;#p=new Map;#m=new Map;#h;#g=[];#_=0;get isSpinning(){return this.#_>0}#v=()=>{this.isSpinning?this.spin(this.#_):this.#y()&&this.#e&&this.#C(this.#o)};#y(){return this.options.respectReducedMotion!==!1&&(this.#h?.matches??!1)}#b(e){let t=e.duration??2e3;if(!Number.isFinite(t)||t<0||t>2147483547)throw RangeError(`LightOdometer: Duration must be between 0 and 2147483547 milliseconds`);for(let t of[e.framerate??30,e.countFramerate??20])if(!Number.isFinite(t)||t<=0)throw RangeError(`LightOdometer: Framerates must be finite and positive`);let n=e.maxValues??32;if(!Number.isInteger(n)||n<2||n>256)throw RangeError(`LightOdometer: maxValues must be an integer between 2 and 256`)}#x(){this.options.duration??=2e3,this.#r=1e3/(this.options.framerate??30),this.#i=1e3/(this.options.countFramerate??20),this.MAX_VALUES=Math.max(2,Math.min(this.options.maxValues??32,Math.floor(this.options.duration/this.#r/2)+1)),c()&&!this.#e&&this.el.style.setProperty(`--odometer-duration`,`${this.options.duration}ms`)}#S(){this.#o++,u(this.#t),u(this.#n),this.#t=void 0,this.#n=void 0,this.#e=!1;for(let e of this.#g)e.cancel();this.#g=[],this.#_=0}#C(e){if(this.destroyed||e!==this.#o||!this.#e)return;let t=this.value,n=this.#c,r=this.#u,a=this.getOptions();this.#u=!1,this.#S(),this.render();let o=this.#o;i(this.el,`odometerdone`,{id:a.id,el:this.el,instance:this,value:t,oldValue:n,options:a}),r&&o===this.#o&&this.disconnect()}#w(){this.#f&&=(this.inside.replaceChildren(this.#f),void 0),this.#d=[],this.options.onRender?.(this)}constructor(e){if(this.options={...d.options,...e},this.el=e.el,this.#b(this.options),this.el.odometer&&!this.el.odometer.destroyed){let t=this.el.odometer;return t.setOptions(e),t}this.resetFormat(),this.value=this.cleanValue(this.options.value??``),this.#l=this.value,this.#x(),this.el.odometer=this,c()&&(this.#h=window.matchMedia?.(`(prefers-reduced-motion: reduce)`),this.#h?.addEventListener(`change`,this.#v),this.renderInside(),this.render());try{for(let e of[`innerHTML`,`innerText`,`textContent`]){let t=Object.getOwnPropertyDescriptor(this.el,e);Object.defineProperty(this.el,e,{configurable:!0,get:()=>e===`innerHTML`?this.inside?.outerHTML??``:this.inside?.innerText??this.inside?.textContent??``,set:e=>this.update(e)}),this.#p.set(e,t)}}catch{c()&&this.watchForMutations()}return this}renderInside(){c()&&(this.inside=document.createElement(`div`),this.inside.className=`odometer-inside`,this.el.replaceChildren(this.inside))}watchForMutations(){try{this.observer??=new MutationObserver(e=>{let t=this.el.innerText||``;this.renderInside(),this.render(this.value),this.update(t)}),this.watchMutations=!0,this.startWatchingMutations()}catch{}}startWatchingMutations(){!this.destroyed&&this.watchMutations&&c()&&this.observer?.observe(this.el,{childList:!0})}stopWatchingMutations(){this.observer?.disconnect()}cleanValue(e){typeof e==`string`&&(e=e.replace(this.format.radix??`.`,`<radix>`),e=e.replace(/[.,\s\u00A0\u202F]/g,``),e=e.replace(`<radix>`,`.`),e=parseFloat(e)||0);let t=Math.round(e*10**this.format.precision);if(!Number.isFinite(e)||!Number.isSafeInteger(t))throw RangeError(`LightOdometer: Value must be finite and safely representable at the configured precision`);return o(e,this.format.precision)}bindTransitionEnd(){this.transitionEndBound||this.destroyed||(this.transitionEndBound=!0,this.#a=e=>{this.#t==null&&!this.isSpinning&&e.propertyName===`transform`&&e.target instanceof HTMLElement&&Object.values(this.ribbons).includes(e.target)&&(Object.values(this.ribbons).some(e=>e.getAnimations().some(e=>e.playState===`running`||e.pending))||this.#C(this.#o))},this.el.addEventListener(`transitionend`,this.#a),this.el.addEventListener(`transitioncancel`,this.#a))}resetFormat(){let t=this.options.format??`(,ddd).dd`;t||=`d`;let n=e.exec(t);if(!n)throw Error(`LightOdometer: Unparsable digit format`);let r=n[1]??``,i=n[2],a=(n[3]??``).length;if(!r.includes(`d`)||a>15)throw RangeError(`LightOdometer: Format must contain digits and at most 15 decimal places`);this.format={repeating:r,radix:i,precision:a}}render(e){c()&&!this.destroyed&&(e=this.cleanValue(e??this.value),this.#l=e,this.stopWatchingMutations(),this.resetDigits(),n(this.el,`odometer-animating-up odometer-animating-down odometer-animating`),this.el.classList.add(`odometer`,`odometer-auto-theme`),this.#e||this.#x(),this.formatDigits(e),this.#w(),this.startWatchingMutations())}formatDigits(e){if(this.digits=[],this.options.formatFunction){let t=this.options.formatFunction(e);for(let e of t.split(``).toReversed())if(/\d/.test(e)){let t=this.renderDigit(),n=t.querySelector(`.odometer-value`);n.textContent=e,r(n,`odometer-first-value odometer-last-value`),this.digits.push(t),this.insertDigit(t)}else this.addSpacer(e)}else{let t=this.preservePrecision(e),n=!this.format.precision;for(let e of t.split(``).toReversed())e===`.`&&(n=!0),this.addDigit(e,n)}}preservePrecision(e){return e.toFixed(this.format.precision)}update(e){if(this.destroyed||(e=this.cleanValue(e),e===this.value&&!this.isSpinning))return this.value;let t=this.#e&&this.options.animation===`count`?this.#l:this.value;if(this.#S(),this.value=e,!c())return this.value;this.#c=t,this.#s=this.options.duration??2e3,this.#e=!0;let a=this.#o;return i(this.el,`odometerstart`,{id:this.options.id,el:this.el,instance:this,value:e,oldValue:t,options:this.getOptions()}),a!==this.#o||this.destroyed?this.value:this.#s===0||this.#y()?(this.#C(a),this.value):(n(this.el,`odometer-animating-up odometer-animating-down odometer-animating`),this.el.style.setProperty(`--odometer-duration`,`${this.#s}ms`),r(this.el,e>t?`odometer-animating-up`:`odometer-animating-down`),this.stopWatchingMutations(),this.animate(e,t),this.startWatchingMutations(),this.options.animation!==`count`&&(this.#t=l(()=>{if(this.#t=void 0,a!==this.#o||this.destroyed)return;this.el.offsetHeight,r(this.el,`odometer-animating`);let e=Object.values(this.ribbons).flatMap(e=>e.getAnimations?.()??[]).filter(e=>e instanceof CSSTransition&&e.transitionProperty===`transform`);e.length?Promise.allSettled(e.map(e=>e.finished)).then(()=>this.#C(a)):this.#C(a)})),this.value)}renderDigit(){let e=this.#d[this.digits.length]??t(`<span class="odometer-digit"><span class="odometer-digit-spacer">8</span><span class="odometer-digit-inner"><span class="odometer-ribbon"><span class="odometer-ribbon-inner"><span class="odometer-value"></span></span></span></span></span>`),n=e.querySelector(`.odometer-ribbon-inner`),r=n.firstElementChild??document.createElement(`div`);return r.className=`odometer-value`,r.textContent=``,n.replaceChildren(r),e}insertDigit(e,t){let n=this.#f??this.inside;return n.insertBefore(e,t??n.firstChild)}addSpacer(e,n,i){let a=t(`<span class="odometer-formatting-mark"></span>`);return a.textContent=e,i&&r(a,i),this.insertDigit(a,n)}addDigit(e,t){if(t??=!0,e===`-`)return this.addSpacer(e,null,`odometer-negation-mark`);if(e===`.`)return this.addSpacer(this.format.radix??`.`,null,`odometer-radix-mark`);if(t){let e=!1;for(;;){if(!this.format.repeating.length){if(e)throw Error(`Bad odometer format without digits`);this.resetFormat(),e=!0}let t=this.format.repeating[this.format.repeating.length-1];if(t!=null){if(this.format.repeating=this.format.repeating.substring(0,this.format.repeating.length-1),t===`d`)break;this.addSpacer(t)}}}let n=this.renderDigit(),i=n.querySelector(`.odometer-value`);return i.textContent=e,r(i,`odometer-first-value odometer-last-value`),this.digits.push(n),this.insertDigit(n)}animate(e,t=this.value){this.options.animation===`count`?this.animateCount(e,t):this.animateSlide(e,t)}animateCount(e,t=this.value){if(!c()||this.destroyed)return;let n=this.#o,r=this.#s,i=this.#i,s=a(),u=s,d=this.#l,f=a=>{if(n!==this.#o||this.destroyed)return;let c=a-s;if(c>=r){this.#C(n);return}if(a-u>=i){u=a;let n=o(t+(e-t)*c/r,this.format.precision);n!==d&&(d=n,this.render(n))}this.#n=l(f)};this.#n=l(f)}getDigitCount(...e){for(let t=0;t<e.length;t++){let n=e[t]??0;e[t]=Math.abs(n)}let t=Math.max(...e);return Math.max(1,Math.trunc(t).toString().length)}getFractionalDigitCount(...e){let t=/^-?\d*\.(\d*?)0*$/;for(let n=0;n<e.length;n++){let r=(e[n]??0).toString(),i=t.exec(r),a=i?.[1]??``;e[n]=i?a.length:0}return Math.max(...e)}resetDigits(){this.#d=this.digits.slice(),this.digits=[],this.ribbons={},this.#f=document.createDocumentFragment(),this.resetFormat()}createRange(e,t,n){let r=e<t,i=Math.abs(t-e)+ +!!n;return Array.from({length:i},(t,n)=>r?e+n:e-n)}animateSlide(e,t=this.value){if(!c()||this.destroyed)return;let n=this.format.precision,r=10**n,i=Math.round(e*r),a=Math.round(t*r),o=Math.max(n+1,this.getDigitCount(a,i)),l=[];this.bindTransitionEnd();for(let e=0;e<o;e++){let t=10**(o-e-1),n=s(a/t),r=s(i/t),c=r-n,u=Math.max(2,Math.floor(this.MAX_VALUES*(.5+(e+1)/o/2))),d=Math.min(Math.abs(c)+1,u),f=Array.from({length:d},(e,t)=>{let i=t===d-1?r:Math.round(n+c*t/(d-1));return Math.abs(i%10)});l.push(f)}this.resetDigits();for(let r=l.length-1;r>=0;r--){let i=l[r],a=this.addDigit(` `,l.length-r-1>=n).querySelector(`.odometer-ribbon-inner`),o=document.createDocumentFragment(),s=e<t?i.toReversed():i;for(let e=0;e<s.length;e++){let t=document.createElement(`div`);t.className=`odometer-value`,t.textContent=String(s[e]),e===0&&t.classList.add(`odometer-first-value`),e===s.length-1&&t.classList.add(`odometer-last-value`),o.appendChild(t)}a.replaceChildren(o),this.ribbons[l.length-r-1]=a}e<0&&this.addDigit(`-`),n&&this.addSpacer(this.format.radix??`.`,this.digits[n-1],`odometer-radix-mark`),this.#w()}spin(e){if(this.destroyed)return;if(!Number.isInteger(e)||e<1||e>32)throw RangeError(`LightOdometer: spin() requires between 1 and 32 digits`);if(this.#S(),this.#u=!1,this.#_=e,!c())return;this.stopWatchingMutations(),this.resetDigits(),n(this.el,`odometer-animating-up odometer-animating-down odometer-animating`);for(let t=0;t<e;t++){let e=this.addDigit(`0`).querySelector(`.odometer-ribbon-inner`);if(this.ribbons[t]=e,!this.#y()&&(this.options.duration??2e3)>0){let t=document.createDocumentFragment();for(let e=0;e<=10;e++){let n=document.createElement(`div`);n.className=`odometer-value`,n.textContent=String(e%10),e===0?n.classList.add(`odometer-first-value`):e===10&&n.classList.add(`odometer-last-value`),t.appendChild(n)}e.replaceChildren(t)}}this.#w(),this.startWatchingMutations(),this.#e=!this.#y()&&this.options.duration!==0;let t=this.#o;if(i(this.el,`odometerstart`,{id:this.options.id,el:this.el,instance:this,value:this.value,options:this.getOptions()}),t===this.#o&&!this.destroyed&&this.#e)for(let[e,t]of Object.entries(this.ribbons)){let n=(this.options.duration??2e3)/(1+Number(e)*.1);this.#g.push(t.animate([{transform:`translateY(0)`},{transform:`translateY(-100%)`}],{duration:n,iterations:1/0,easing:`linear`}))}}static init(){if(!c())return[];let e=document.querySelectorAll(d.options.selector||`.odometer`);return Array.from(e,e=>e.odometer=new d({el:e,value:e.innerText??e.textContent}))}setOptions(e){if(this.destroyed||!e||typeof e!=`object`)return;let{el:t,...n}=e,r=this.options,i=this.format,a={...this.options,...n},o=Object.prototype.hasOwnProperty.call(n,`value`),s=Object.prototype.hasOwnProperty.call(n,`format`)||Object.prototype.hasOwnProperty.call(n,`formatFunction`),c,l;this.#b(a),this.options=a;try{this.resetFormat(),c=this.cleanValue(this.value),l=o?this.cleanValue(a.value??0):c}catch(e){throw this.options=r,this.format=i,e}this.value=c,this.#x(),o&&(l!==c||this.isSpinning)?(s&&(this.#e||this.isSpinning)&&(this.#S(),this.render()),this.update(l)):this.isSpinning?this.spin(this.#_):this.#e&&(s||this.#y())?this.#C(this.#o):(s||Object.prototype.hasOwnProperty.call(n,`onRender`))&&this.render()}static setGlobalOptions(e){d.options={...d.options,...e}}on(e,t){if(this.destroyed)return;let n=this.#m.get(e)??new Set;n.add(t),this.#m.set(e,n),this.el.addEventListener(e,t)}off(e,t){this.#m.get(e)?.delete(t),this.el.removeEventListener(e,t)}getOptions(){return{...this.options}}static getGlobalOptions(){return{...d.options}}animateOnceAndDisconnect(e){this.destroyed||(e!=null&&this.cleanValue(e),this.#u=!0,e!=null&&this.update(e),!this.#e&&this.#u&&(this.#u=!1,this.disconnect()))}disconnect(){if(this.destroyed)return;let e=this.#e||this.isSpinning;this.#S(),this.#u=!1,this.stopWatchingMutations(),this.watchMutations=!1,this.#h?.removeEventListener(`change`,this.#v),this.#a&&=(this.el.removeEventListener(`transitionend`,this.#a),this.el.removeEventListener(`transitioncancel`,this.#a),void 0),this.transitionEndBound=!1,c()&&e&&this.render(),this.destroyed=!0;for(let[e,t]of this.#p)t?Object.defineProperty(this.el,e,t):Reflect.deleteProperty(this.el,e);this.#p.clear();for(let[e,t]of this.#m)for(let n of t)this.el.removeEventListener(e,n);this.#m.clear(),this.el.odometer===this&&delete this.el.odometer}toString(){let{el:e,...t}={...this.getOptions()},n={id:this.options.id,value:this.value,options:t,globalOptions:d.getGlobalOptions(),watchMutations:this.watchMutations,transitionEndBound:this.transitionEndBound,destroyed:this.destroyed,format:this.format,isAnimating:this.isAnimating,isSpinning:this.isSpinning};return JSON.stringify(n)}};f(m),p(m);export{m as LightOdometer,m as default};
|
package/package.json
CHANGED
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
"description": "for personal use only, see EDM115/website",
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"@stylistic/eslint-plugin": "~5.10.0",
|
|
15
|
-
"@typescript-eslint/parser": "~8.
|
|
15
|
+
"@typescript-eslint/parser": "~8.69.0",
|
|
16
16
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
17
17
|
"edm115-lint": "~0.2.1",
|
|
18
|
-
"eslint": "~10.
|
|
18
|
+
"eslint": "~10.10.0",
|
|
19
19
|
"jiti": "~2.7.0",
|
|
20
|
-
"oxlint": "~1.
|
|
20
|
+
"oxlint": "~1.81.0",
|
|
21
21
|
"oxlint-tsgolint": "~7.0.2001",
|
|
22
|
-
"tsdown": "~0.
|
|
22
|
+
"tsdown": "~0.23.0",
|
|
23
23
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
24
24
|
"unplugin-unused": "~0.6.0"
|
|
25
25
|
},
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"url": "git+https://github.com/EDM115/light-odometer.git"
|
|
63
63
|
},
|
|
64
64
|
"type": "module",
|
|
65
|
-
"version": "0.
|
|
65
|
+
"version": "0.2.0",
|
|
66
66
|
"scripts": {
|
|
67
67
|
"build": "tsdown",
|
|
68
68
|
"format": "eslint -c eslint.stylistic.config.ts --concurrency=auto --fix .",
|