light-odometer 0.1.2 → 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 +47 -13
- package/dist/light-odometer.d.ts +173 -169
- package/dist/light-odometer.js +1 -1
- package/package.json +12 -11
package/README.md
CHANGED
|
@@ -14,20 +14,15 @@ No theme is shipped here, no docs either.
|
|
|
14
14
|
## What's changed ?
|
|
15
15
|
For a quick overview in a real-world usage, see https://github.com/EDM115/website/blob/master/app/components/ui/Odometer.vue
|
|
16
16
|
|
|
17
|
+
### Breaking changes
|
|
18
|
+
- Renamed from `TmOdometer` to `LightOdometer`
|
|
17
19
|
- Removal of all built artifacts, themes, demo, screenshots and CoffeeScript code
|
|
18
20
|
- Switch from Rollup to tsdown (Rolldown)
|
|
19
|
-
- Added linting and formatting (OxLint + ESLint Stylistic)
|
|
20
|
-
- Keep only the ESM build and switch to a default export
|
|
21
|
-
- Better TS config and stricter types (no more `any` nor `as`)
|
|
22
|
-
- Target ES2023 instead of ES2015
|
|
23
21
|
- Remove compatibility layers (Internet Explorer, jQuery)
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
- SSR friendly (you can import it top-level without blowing up) and use safe fallbacks fror browser-land functions
|
|
29
|
-
- Multiple performance improvements
|
|
30
|
-
- `value` is now stripped from spaces
|
|
22
|
+
- Keep only the ESM build and switch to a default export
|
|
23
|
+
|
|
24
|
+
### New features
|
|
25
|
+
- SSR friendly (you can import it top-level without blowing up) and use safe fallbacks for browser-land functions
|
|
31
26
|
- Each instance can now have an `id`
|
|
32
27
|
- Ability to customize the framerate
|
|
33
28
|
```ts
|
|
@@ -54,7 +49,7 @@ For a quick overview in a real-world usage, see https://github.com/EDM115/websit
|
|
|
54
49
|
console.log(LightOdometer.getGlobalOptions.selector)
|
|
55
50
|
LightOdometer.setGlobalOptions({ selector: ".my-odometer" })
|
|
56
51
|
```
|
|
57
|
-
- Add per-instance subscriptable animation start/end events
|
|
52
|
+
- Add per-instance subscriptable animation start/end events, they give back the instance id, el, instance, value, oldValue, options
|
|
58
53
|
```ts
|
|
59
54
|
const odo = new LightOdometer({ ... })
|
|
60
55
|
console.log(odo.isAnimating)
|
|
@@ -77,7 +72,7 @@ For a quick overview in a real-world usage, see https://github.com/EDM115/websit
|
|
|
77
72
|
odo.off("odometerstart", onStart)
|
|
78
73
|
odo.off("odometerdone", onDone)
|
|
79
74
|
```
|
|
80
|
-
- Print the instance
|
|
75
|
+
- Print the instance as a JSON-friendly string
|
|
81
76
|
```ts
|
|
82
77
|
const odo = new LightOdometer({ ... })
|
|
83
78
|
console.log(odo.toString())
|
|
@@ -105,3 +100,42 @@ For a quick overview in a real-world usage, see https://github.com/EDM115/websit
|
|
|
105
100
|
"isAnimating": false
|
|
106
101
|
}
|
|
107
102
|
```
|
|
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
|
+
|
|
130
|
+
### Improvements
|
|
131
|
+
- `value` is now stripped from spaces
|
|
132
|
+
- Multiple performance improvements
|
|
133
|
+
- Use overall more optimized methods
|
|
134
|
+
- Stabilize and simplify the interfaces
|
|
135
|
+
|
|
136
|
+
### Other changes
|
|
137
|
+
- No more const functions
|
|
138
|
+
- Better TS config and stricter types (no more `any` nor `as`)
|
|
139
|
+
- Use classes private elements
|
|
140
|
+
- Added linting and formatting (OxLint + ESLint Stylistic)
|
|
141
|
+
- Target ES2023 instead of ES2015
|
package/dist/light-odometer.d.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
//#region src/shared/interfaces.d.ts
|
|
2
2
|
/**
|
|
3
|
-
* LightOdometer global options interface
|
|
4
|
-
* @property {string} [selector] - The selector for the odometer elements.
|
|
5
|
-
* @property {boolean} [auto] - Whether to automatically initialize odometers.
|
|
6
|
-
*/
|
|
3
|
+
* LightOdometer global options interface
|
|
4
|
+
* @property {string} [selector] - The selector for the odometer elements.
|
|
5
|
+
* @property {boolean} [auto] - Whether to automatically initialize odometers.
|
|
6
|
+
*/
|
|
7
7
|
interface LightOdometerGlobalOptions {
|
|
8
8
|
selector?: string;
|
|
9
9
|
auto?: boolean;
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* LightOdometer config interface
|
|
13
|
-
* @property {HTMLElement} el - The HTML element to attach the odometer to.
|
|
14
|
-
* @property {string | number | null} [value] - The initial value of the odometer.
|
|
15
|
-
* @property {string} [format] - The format string for the odometer.
|
|
16
|
-
* @property {number} [duration] - The duration of the animation in milliseconds.
|
|
17
|
-
* @property {number} [framerate] - Target framerate for slide animation.
|
|
18
|
-
* @property {number} [countFramerate] - Target framerate for count animation.
|
|
19
|
-
* @property {'count' | 'slide'} [animation] - The animation type ('count' or 'slide').
|
|
20
|
-
* @property {(value: number) => string} [formatFunction] - A custom format function.
|
|
21
|
-
*/
|
|
12
|
+
* LightOdometer config interface
|
|
13
|
+
* @property {HTMLElement} el - The HTML element to attach the odometer to.
|
|
14
|
+
* @property {string | number | null} [value] - The initial value of the odometer.
|
|
15
|
+
* @property {string} [format] - The format string for the odometer.
|
|
16
|
+
* @property {number} [duration] - The duration of the animation in milliseconds.
|
|
17
|
+
* @property {number} [framerate] - Target framerate for slide animation.
|
|
18
|
+
* @property {number} [countFramerate] - Target framerate for count animation.
|
|
19
|
+
* @property {'count' | 'slide'} [animation] - The animation type ('count' or 'slide').
|
|
20
|
+
* @property {(value: number) => string} [formatFunction] - A custom format function.
|
|
21
|
+
*/
|
|
22
22
|
interface LightOdometerOptions {
|
|
23
23
|
el: HTMLElement;
|
|
24
24
|
/** Optional identifier for this instance; propagated in event details */
|
|
@@ -28,16 +28,22 @@ 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
|
}
|
|
34
40
|
type LightOdometerEventName = "odometerstart" | "odometerdone";
|
|
35
41
|
/**
|
|
36
|
-
* FormatObject interface
|
|
37
|
-
* @property {string} repeating - The repeating part of the format. (i.e. '(,ddd)')
|
|
38
|
-
* @property {string} [radix] - The radix separator. (i.e. '.')
|
|
39
|
-
* @property {number} precision - The number of decimal places. (i.e. 'dd')
|
|
40
|
-
*/
|
|
42
|
+
* FormatObject interface
|
|
43
|
+
* @property {string} repeating - The repeating part of the format. (i.e. '(,ddd)')
|
|
44
|
+
* @property {string} [radix] - The radix separator. (i.e. '.')
|
|
45
|
+
* @property {number} precision - The number of decimal places. (i.e. 'dd')
|
|
46
|
+
*/
|
|
41
47
|
interface FormatObject {
|
|
42
48
|
repeating: string;
|
|
43
49
|
radix?: string;
|
|
@@ -53,9 +59,9 @@ declare global {
|
|
|
53
59
|
}
|
|
54
60
|
//#endregion
|
|
55
61
|
//#region src/core/odometer.d.ts
|
|
56
|
-
declare class LightOdometer {
|
|
62
|
+
export declare class LightOdometer {
|
|
63
|
+
#private;
|
|
57
64
|
static options: LightOdometerGlobalOptions;
|
|
58
|
-
private _isAnimating;
|
|
59
65
|
options: LightOdometerOptions;
|
|
60
66
|
el: HTMLElement;
|
|
61
67
|
value: number;
|
|
@@ -70,216 +76,214 @@ declare class LightOdometer {
|
|
|
70
76
|
MAX_VALUES: number;
|
|
71
77
|
digits: HTMLElement[];
|
|
72
78
|
ribbons: Record<number, HTMLElement>;
|
|
73
|
-
|
|
74
|
-
private _countRafId?;
|
|
75
|
-
private _msPerFrame;
|
|
76
|
-
private _countMsPerFrame;
|
|
77
|
-
private _onTransitionEnd?;
|
|
79
|
+
get isSpinning(): Readonly<boolean>;
|
|
78
80
|
/**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
* Initializes a new instance of the LightOdometer class.
|
|
82
|
+
* Sets up the odometer's options, formats, and DOM structure.
|
|
83
|
+
* If an odometer instance already exists on the element, it returns the existing instance.
|
|
84
|
+
* @param {LightOdometerOptions} options - Configuration options for the odometer.
|
|
85
|
+
*/
|
|
84
86
|
constructor(options: LightOdometerOptions);
|
|
85
87
|
/**
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
* Renders the inner container of the odometer.
|
|
89
|
+
* Clears the root element (`this.el`) and appends a new child element with the class `odometer-inside`.
|
|
90
|
+
* @returns {void}
|
|
91
|
+
*/
|
|
90
92
|
renderInside(): void;
|
|
91
93
|
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
* Observes changes to the root element's content and updates the odometer accordingly.
|
|
95
|
+
* This is a fallback for environments like Safari where `.innerHTML` cannot be wrapped.
|
|
96
|
+
* @returns {void}
|
|
97
|
+
*/
|
|
96
98
|
watchForMutations(): void;
|
|
97
99
|
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
* Starts observing mutations on the root element (`this.el`).
|
|
101
|
+
* Listens for changes to the element's child nodes (e.g., additions or removals).
|
|
102
|
+
* Requires `this.watchMutations` to be `true` and a `MutationObserver` to be initialized.
|
|
103
|
+
* @returns {void}
|
|
104
|
+
*/
|
|
103
105
|
startWatchingMutations(): void;
|
|
104
106
|
/**
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
* Stops observing mutations on the root element (`this.el`).
|
|
108
|
+
* Disconnects the `MutationObserver` if it is initialized.
|
|
109
|
+
* @returns {void}
|
|
110
|
+
*/
|
|
109
111
|
stopWatchingMutations(): void;
|
|
110
112
|
/**
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
* Cleans and normalizes a value to ensure it can be processed as a number.
|
|
114
|
+
* Converts formatted strings into numeric values by handling radix symbols and removing unnecessary characters.
|
|
115
|
+
* @param {string | number} val - The value to clean and normalize.
|
|
116
|
+
* @returns {number} The cleaned and rounded numeric value.
|
|
117
|
+
*/
|
|
116
118
|
cleanValue(val: string | number): number;
|
|
117
119
|
/**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
* Binds transition end events to the root element (`this.el`).
|
|
121
|
+
* Ensures that the odometer re-renders only once per transition, even if multiple transition end events are triggered. After rendering, it dispatches the `odometerdone` custom event.
|
|
122
|
+
* @returns {void}
|
|
123
|
+
*/
|
|
122
124
|
bindTransitionEnd(): void;
|
|
123
125
|
/**
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
* Resets and parses the odometer's format configuration.
|
|
127
|
+
* Extracts the repeating pattern, radix symbol, and precision from the format string.
|
|
128
|
+
* Throws an error if the format string is invalid or unparsable.
|
|
129
|
+
* @returns {void}
|
|
130
|
+
*/
|
|
129
131
|
resetFormat(): void;
|
|
130
132
|
/**
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
* Renders the odometer with the specified value.
|
|
134
|
+
* Updates the DOM structure, applies the appropriate classes, and formats the digits for display.
|
|
135
|
+
* @param {number} [value] - The value to render. Defaults to the current value (`this.value`).
|
|
136
|
+
* @returns {void}
|
|
137
|
+
*/
|
|
136
138
|
render(value?: number): void;
|
|
137
139
|
/**
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
140
|
+
* Formats the given value into individual digits and renders them.
|
|
141
|
+
* If a custom format function is provided, it uses that to format the value.
|
|
142
|
+
* Otherwise, it preserves the precision and formats the value based on the odometer's configuration.
|
|
143
|
+
* @param {number} value - The value to format and render as digits.
|
|
144
|
+
* @returns {void}
|
|
145
|
+
*/
|
|
144
146
|
formatDigits(value: number): void;
|
|
145
147
|
/**
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
* Ensures the value maintains the specified precision by adding trailing zeros if necessary.
|
|
149
|
+
* This is used to keep the decimal places consistent at the end of the animation.
|
|
150
|
+
* @param {number} value - The numeric value to format with preserved precision.
|
|
151
|
+
* @returns {string} The value as a string with the required precision.
|
|
152
|
+
*/
|
|
151
153
|
preservePrecision(value: number): string;
|
|
152
154
|
/**
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
* Updates the odometer to display a new value.
|
|
156
|
+
* Cleans and normalizes the input value, determines the difference from the current value, and triggers the appropriate animations and DOM updates.
|
|
157
|
+
* @param {string | number} newValue - The new value to update the odometer to.
|
|
158
|
+
* @returns {number} The updated value of the odometer.
|
|
159
|
+
*/
|
|
158
160
|
update(newValue: string | number): number;
|
|
159
161
|
/**
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
* Creates and returns a new digit element for the odometer.
|
|
163
|
+
* The digit element is generated from the predefined `DIGIT_HTML` template.
|
|
164
|
+
* @returns {HTMLElement} The newly created digit element.
|
|
165
|
+
*/
|
|
164
166
|
renderDigit(): HTMLElement;
|
|
165
167
|
/**
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
168
|
+
* Inserts a digit element into the odometer's inner container.
|
|
169
|
+
* If a reference element (`before`) is provided, the digit is inserted before it.
|
|
170
|
+
* Otherwise, the digit is appended to the container or inserted at the beginning if other children exist.
|
|
171
|
+
* @param {HTMLElement} digit - The digit element to insert.
|
|
172
|
+
* @param {HTMLElement | null} [before] - The reference element to insert the digit before. Defaults to `null`.
|
|
173
|
+
* @returns {HTMLElement} The inserted digit element.
|
|
174
|
+
*/
|
|
173
175
|
insertDigit(digit: HTMLElement, before?: HTMLElement | null): HTMLElement;
|
|
174
176
|
/**
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
177
|
+
* Creates and inserts a spacer element into the odometer's inner container.
|
|
178
|
+
* A spacer is a non-digit element (e.g., a comma or decimal point) used for formatting.
|
|
179
|
+
* @param {string} chr - The character to display in the spacer.
|
|
180
|
+
* @param {HTMLElement | null} [before] - The reference element to insert the spacer before. Defaults to `null`.
|
|
181
|
+
* @param {string} [extraClasses] - Additional CSS classes to apply to the spacer element.
|
|
182
|
+
* @returns {HTMLElement} The inserted spacer element.
|
|
183
|
+
*/
|
|
182
184
|
addSpacer(chr: string, before?: HTMLElement | null, extraClasses?: string): HTMLElement;
|
|
183
185
|
/**
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
186
|
+
* Adds a digit or spacer element to the odometer's inner container.
|
|
187
|
+
* Handles special cases for negation (`-`) and radix (`.`) characters, and ensures the format's repeating pattern is respected.
|
|
188
|
+
* @param {string} value - The digit or character to add.
|
|
189
|
+
* @param {boolean} [repeating=true] - Whether to use the repeating format pattern. Defaults to `true`.
|
|
190
|
+
* @returns {HTMLElement} The inserted digit or spacer element.
|
|
191
|
+
* @throws {Error} If the format string is invalid or lacks digits.
|
|
192
|
+
*/
|
|
191
193
|
addDigit(value: string, repeating?: boolean): HTMLElement;
|
|
192
194
|
/**
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
animate(newValue: number): void;
|
|
195
|
+
* Animates the odometer to transition to a new value.
|
|
196
|
+
* Chooses the appropriate animation method (`count` or `slide`) based on the configuration and browser support.
|
|
197
|
+
* @param {number} newValue - The new value to animate the odometer to.
|
|
198
|
+
* @returns {void}
|
|
199
|
+
*/
|
|
200
|
+
animate(newValue: number, oldValue?: number): void;
|
|
199
201
|
/**
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
animateCount(newValue: number): void;
|
|
202
|
+
* Animates the odometer by incrementing or decrementing the value over time.
|
|
203
|
+
* Uses a "counting" animation to transition smoothly to the new value.
|
|
204
|
+
* @param {number} newValue - The new value to animate the odometer to.
|
|
205
|
+
* @returns {void}
|
|
206
|
+
*/
|
|
207
|
+
animateCount(newValue: number, oldValue?: number): void;
|
|
206
208
|
/**
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
* Calculates the number of digits in the largest absolute value from the provided numbers.
|
|
210
|
+
* @param {...number} values - A list of numbers to evaluate.
|
|
211
|
+
* @returns {number} The number of digits in the largest absolute value.
|
|
212
|
+
*/
|
|
211
213
|
getDigitCount(...values: number[]): number;
|
|
212
214
|
/**
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
215
|
+
* Calculates the maximum number of fractional digits (decimal places) among the provided numbers.
|
|
216
|
+
* Assumes the values have already been rounded to the specified precision.
|
|
217
|
+
* @param {...number} values - A list of numbers to evaluate.
|
|
218
|
+
* @returns {number} The maximum number of fractional digits.
|
|
219
|
+
*/
|
|
218
220
|
getFractionalDigitCount(...values: number[]): number;
|
|
219
221
|
/**
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
* Resets the odometer's digits and ribbons.
|
|
223
|
+
* Clears the inner container, resets the format configuration, and prepares the odometer for re-rendering.
|
|
224
|
+
* @returns {void}
|
|
225
|
+
*/
|
|
224
226
|
resetDigits(): void;
|
|
225
227
|
/**
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
228
|
+
* Creates an array of numbers between two values
|
|
229
|
+
* @param start The starting value of the range
|
|
230
|
+
* @param end The ending value of the range
|
|
231
|
+
* @param inclusive Whether to include the end value in the range
|
|
232
|
+
* @returns An array containing the range of numbers
|
|
233
|
+
*/
|
|
232
234
|
createRange(start: number, end: number, inclusive: boolean): number[];
|
|
233
235
|
/**
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
animateSlide(newValue: number): void;
|
|
236
|
+
* Animates the odometer to transition to a new value using a sliding animation.
|
|
237
|
+
* Breaks the value into individual digits, calculates the frames for each digit's animation, and updates the DOM to reflect the sliding effect.
|
|
238
|
+
* @param {number} newValue - The new value to animate the odometer to.
|
|
239
|
+
* @returns {void}
|
|
240
|
+
*/
|
|
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;
|
|
240
244
|
/**
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
+
* Initializes all odometer elements on the page.
|
|
246
|
+
* Selects elements matching the configured selector or the default `.odometer` class, and creates a `LightOdometer` instance for each element.
|
|
247
|
+
* @returns {LightOdometer[]} An array of initialized `LightOdometer` instances.
|
|
248
|
+
*/
|
|
245
249
|
static init(): LightOdometer[];
|
|
246
250
|
/**
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
+
* Mutate this instance's options on-the-fly.
|
|
252
|
+
* Recomputes timing fields and applies changes immediately.
|
|
253
|
+
* If a new value is provided, it triggers update() with the new configuration.
|
|
254
|
+
*/
|
|
251
255
|
setOptions(newOptions: Partial<LightOdometerOptions>): void;
|
|
252
256
|
/**
|
|
253
|
-
|
|
254
|
-
|
|
257
|
+
* Update global default options at runtime. Does not retroactively affect existing instances.
|
|
258
|
+
*/
|
|
255
259
|
static setGlobalOptions(newOptions: Partial<LightOdometerGlobalOptions>): void;
|
|
256
260
|
/** Subscribe to odometer events ("odometerstart" | "odometerdone") for this instance */
|
|
257
261
|
on(event: LightOdometerEventName, handler: EventListener): void;
|
|
258
262
|
/** Unsubscribe from odometer events for this instance */
|
|
259
263
|
off(event: LightOdometerEventName, handler: EventListener): void;
|
|
260
264
|
/**
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
265
|
+
* Get a shallow copy snapshot of this instance's current options.
|
|
266
|
+
* Mutating the returned object will not affect the instance.
|
|
267
|
+
*/
|
|
264
268
|
getOptions(): Readonly<LightOdometerOptions>;
|
|
265
269
|
/**
|
|
266
|
-
|
|
267
|
-
|
|
270
|
+
* Get a shallow copy snapshot of global default options.
|
|
271
|
+
*/
|
|
268
272
|
static getGlobalOptions(): Readonly<LightOdometerGlobalOptions>;
|
|
269
273
|
/**
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
274
|
+
* Animate to a value exactly once and then disconnect listeners/observers.
|
|
275
|
+
* Useful for static numbers that only animate on first reveal.
|
|
276
|
+
*/
|
|
273
277
|
animateOnceAndDisconnect(toValue?: number | string): void;
|
|
274
278
|
/**
|
|
275
|
-
|
|
276
|
-
|
|
279
|
+
* Disconnect all observers and cancel animation frames. Remove transition listener flag.
|
|
280
|
+
*/
|
|
277
281
|
disconnect(): void;
|
|
278
282
|
/**
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
283
|
+
* Returns the current odometer object along with the global options.
|
|
284
|
+
* @returns {string} A JSON-parseable string representation of the current odometer state.
|
|
285
|
+
*/
|
|
282
286
|
toString(): string;
|
|
283
287
|
}
|
|
284
288
|
//#endregion
|
|
285
|
-
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??{}:{};_isAnimating=!1;options;el;value=0;inside;observer;watchMutations=!1;transitionEndBound=!1;destroyed=!1;format={repeating:``,precision:0};get isAnimating(){return this._isAnimating}MAX_VALUES;digits=[];ribbons={};_rafId;_countRafId;_msPerFrame;_countMsPerFrame;_onTransitionEnd;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._msPerFrame=1e3/n,e._countMsPerFrame=1e3/r,e.MAX_VALUES=e.options.duration/e._msPerFrame/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._msPerFrame=1e3/n,this._countMsPerFrame=1e3/r,this.MAX_VALUES=this.options.duration/this._msPerFrame/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._onTransitionEnd=()=>e?!0:(e=!0,setTimeout(()=>{this.render(),e=!1,this._isAnimating=!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._onTransitionEnd,!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,i,a,o]=n;this.format={repeating:i,radix:a,precision:o?.length||0}}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]=``);for(let n=0;n<this.format.precision;n++)e[1][n]||(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._isAnimating=!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.children[0]):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(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._isAnimating=!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._countMsPerFrame){r=o();let e=t*(c/(this.options.duration||0));i+=e,this.render(Math.round(i))}this._countRafId=u(s)};this._countRafId=u(s)}getDigitCount(...e){for(let t=0;t<e.length;t++)e[t]=Math.abs(e[t]);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].toString(),i=t.exec(r);e[n]=i?i[1].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?1:0);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++)l[e]=Math.abs(l[e]%10);a.push(l)}this.resetDigits();let d=a.toReversed();for(let e=0;e<d.length;e++){let t=d[e];if(this.digits[e]||this.addDigit(` `,e>=n),this.ribbons[e]===void 0){let t=this.digits[e].querySelector(`.odometer-ribbon-inner`);t&&(this.ribbons[e]=t)}this.ribbons[e].textContent=``,r<0&&(t=t.toReversed());for(let n=0;n<t.length;n++){let r=t[n],a=document.createElement(`div`);a.className=`odometer-value`,a.textContent=r.toString(),this.ribbons[e].appendChild(a),n===t.length-1&&i(a,`odometer-last-value`),n===0&&i(a,`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._msPerFrame=1e3/a,this._countMsPerFrame=1e3/o,this.MAX_VALUES=this.options.duration/this._msPerFrame/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._rafId),d(this._countRafId),this._onTransitionEnd&&=(this.el.removeEventListener(`transitionend`,this._onTransitionEnd),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,15 +12,16 @@
|
|
|
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.
|
|
16
|
-
"@typescript/native
|
|
17
|
-
"edm115-lint": "~0.2.
|
|
18
|
-
"eslint": "~10.
|
|
19
|
-
"jiti": "~2.
|
|
20
|
-
"oxlint": "~1.
|
|
21
|
-
"oxlint-tsgolint": "~0.
|
|
22
|
-
"tsdown": "~0.
|
|
23
|
-
"
|
|
15
|
+
"@typescript-eslint/parser": "~8.69.0",
|
|
16
|
+
"@typescript/native": "npm:typescript@^7.0.2",
|
|
17
|
+
"edm115-lint": "~0.2.1",
|
|
18
|
+
"eslint": "~10.10.0",
|
|
19
|
+
"jiti": "~2.7.0",
|
|
20
|
+
"oxlint": "~1.81.0",
|
|
21
|
+
"oxlint-tsgolint": "~7.0.2001",
|
|
22
|
+
"tsdown": "~0.23.0",
|
|
23
|
+
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
24
|
+
"unplugin-unused": "~0.6.0"
|
|
24
25
|
},
|
|
25
26
|
"exports": {
|
|
26
27
|
".": {
|
|
@@ -61,13 +62,13 @@
|
|
|
61
62
|
"url": "git+https://github.com/EDM115/light-odometer.git"
|
|
62
63
|
},
|
|
63
64
|
"type": "module",
|
|
64
|
-
"version": "0.
|
|
65
|
+
"version": "0.2.0",
|
|
65
66
|
"scripts": {
|
|
66
67
|
"build": "tsdown",
|
|
67
68
|
"format": "eslint -c eslint.stylistic.config.ts --concurrency=auto --fix .",
|
|
68
69
|
"lint": "oxlint",
|
|
69
70
|
"lint:fix": "oxlint --fix",
|
|
70
71
|
"release": "pnpm publish",
|
|
71
|
-
"typecheck": "
|
|
72
|
+
"typecheck": "tsc --noEmit"
|
|
72
73
|
}
|
|
73
74
|
}
|