fix: 修复关闭SSH终端标签页时会话状态未更新的问题

This commit is contained in:
2026-04-18 02:35:38 +08:00
commit 6e2e2f9387
43467 changed files with 5489040 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
# This file is for unifying the coding style for different editors and IDEs
# editorconfig.org
root = true
[*]
end_of_line = lf
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
+21
View File
@@ -0,0 +1,21 @@
module.exports = {
root: true,
ignorePatterns: ['*.js', '*.html', 'node_modules/*'],
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'no-prototype-builtins': 'off',
},
env: {
browser: true,
es6: true,
}
};
+13
View File
@@ -0,0 +1,13 @@
<!-- Please enter the info below -->
```
[ ] Bug
[ ] Feature request
CountUp.js version:
```
## Description
<!-- If this is a bug, provide steps to reproduce the issue. -->
<!-- If this is a feature request, describe the use case. -->
+29
View File
@@ -0,0 +1,29 @@
## I'm submitting a...
```
[ ] Bug Fix
[ ] Feature
[ ] Other (Refactoring, Added tests, Documentation, ...)
```
## Checklist
- [ ] Test your changes
- [ ] Followed the build steps
## Description
_please describe the changes that you are making_
_for features, please describe how to use the new feature_
_please include a reference to an existing issue, if applicable_
## Does this PR introduce a breaking change?
```
[ ] Yes
[ ] No
```
+18
View File
@@ -0,0 +1,18 @@
The MIT License (MIT)
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+281
View File
@@ -0,0 +1,281 @@
# CountUp.js
CountUp.js is a dependency-free, lightweight Javascript class that animates a numerical value by counting to it.
Despite its name, CountUp can count in either direction, depending on the start and end values that you provide.
CountUp.js supports all browsers. MIT license.
## [Try the demo](https://inorganik.github.io/countUp.js)
Or tinker with CountUp in [Stackblitz](https://stackblitz.com/edit/countup-typescript)
## Jump to:
- **[Usage](#usage)**
- **[Including CountUp](#including-countup)**
- **[Contributing](#contributing)**
- **[Creating Animation Plugins](#creating-animation-plugins)**
## Features
- **Auto-animate when element becomes visible.** Use option `autoAnimate = true`.
- **Highly customizable** with a large range of options, you can even substitute numerals.
- **Smart easing**: CountUp intelligently defers easing to make it visually noticeable. Configurable in the [options](#options).
- **Plugins** allow for alternate animations like the [Odometer plugin](https://www.npmjs.com/package/odometer_countup)
![Odometer plugin](./demo/images/odometer_plugin.gif)
## Usage:
**Use CountUp with:**
- [Angular 2+](https://github.com/inorganik/ngx-countUp)
- [React](https://gist.github.com/inorganik/2cf776865a4c65c12857027870e9898e)
- [Svelte](https://gist.github.com/inorganik/85a66941ab88cc10c5fa5b26aead5f2a)
- [Vue](https://github.com/xlsdg/vue-countup-v2)
- [WordPress](https://wordpress.org/plugins/countup-js/)
- [jQuery](https://gist.github.com/inorganik/b63dbe5b3810ff2c0175aee4670a4732)
- [custom element](https://github.com/lekoala/formidable-elements/blob/master/docs/count-up.md)
**Use CountUp directly:**
On npm as `countup.js`. You can import as a module, or include the UMD script and access CountUp as a global. See [detailed instructions](#including-countup) on including CountUp.
**Params**:
- `target: string | HTMLElement | HTMLInputElement` - id of html element, input, svg text element, or DOM element reference where counting occurs.
- `endVal: number | null` - the value you want to arrive at. Leave null to use the number in the target element.
- `options?: CountUpOptions` - optional configuration object for fine-grain control
**Options**:
| Option | Type | Default | Description |
| ---------------------- | --------------- | ------------- | ------------------------------------------------------- |
| `startVal` | `number` | `0` | Number to start at |
| `decimalPlaces` | `number` | `0` | Number of decimal places |
| `duration` | `number` | `2` | Animation duration in seconds |
| `useGrouping` | `boolean` | `true` | Example: 1,000 vs 1000 |
| `useIndianSeparators` | `boolean` | `false` | Example: 1,00,000 vs 100,000 |
| `useEasing` | `boolean` | `true` | Ease animation |
| `smartEasingThreshold` | `number` | `999` | Smooth easing for large numbers above this if useEasing |
| `smartEasingAmount` | `number` | `333` | Amount to be eased for numbers above threshold |
| `separator` | `string` | `','` | Grouping separator |
| `decimal` | `string` | `'.'` | Decimal character |
| `easingFn` | `function` | `easeOutExpo` | Easing function for animation |
| `formattingFn` | `function` | — | Custom function to format the result |
| `prefix` | `string` | `''` | Text prepended to result |
| `suffix` | `string` | `''` | Text appended to result |
| `numerals` | `string[]` | — | Numeral glyph substitution |
| `onCompleteCallback` | `function` | — | Callback called when animation completes |
| `onStartCallback` | `function` | — | Callback called when animation starts |
| `plugin` | `CountUpPlugin` | — | Plugin for alternate animations |
| `autoAnimate` | `boolean` | `false` | Trigger animation when target becomes visible |
| `autoAnimateDelay` | `number` | `200` | Animation delay in ms after auto-animate triggers |
| `autoAnimateOnce` | `boolean` | `false` | Run animation only once for auto-animate triggers |
| `enableScrollSpy` | `boolean` | — | *(deprecated)* Use `autoAnimate` instead |
| `scrollSpyDelay` | `number` | — | *(deprecated)* Use `autoAnimateDelay` instead |
| `scrollSpyOnce` | `boolean` | — | *(deprecated)* Use `autoAnimateOnce` instead |
**Example usage**:
```js
const countUp = new CountUp('targetId', 5234);
if (!countUp.error) {
countUp.start();
} else {
console.error(countUp.error);
}
```
Pass options:
```js
const countUp = new CountUp('targetId', 5234, options);
```
with optional complete callback:
```js
const countUp = new CountUp('targetId', 5234, { onCompleteCallback: someMethod });
// or (passing fn to start will override options.onCompleteCallback)
countUp.start(someMethod);
// or
countUp.start(() => console.log('Complete!'));
```
**Other methods**:
Toggle pause/resume:
```js
countUp.pauseResume();
```
Reset the animation:
```js
countUp.reset();
```
Update the end value and animate:
```js
countUp.update(989);
```
Destroy the instance (cancels animation, disconnects observers, clears callbacks):
```js
countUp.onDestroy();
```
---
### **Auto animate when element becomes visible**
Use the `autoAnimate` option to animate when the element is scrolled into view or appears on screen. When using autoAnimate, just initialize CountUp but do not call start().
```js
const countUp = new CountUp('targetId', 989, { autoAnimate: true });
```
**Note** - Auto-animate uses IntersectionObserver which is broadly supported, but if you need to support some very old browsers, v2.9.0 and earlier use a window on-scroll handler when `enableScrollSpy` is set to true.
---
### **Alternate animations with plugins**
Currently there's just one plugin, the **[Odometer Plugin](https://github.com/msoler75/odometer_countup.js)**.
To use a plugin, you'll need to first install the plugin package. Then you can include it and use the plugin option. See each plugin's docs for more detailed info.
```js
const countUp = new CountUp('targetId', 5234, {
plugin: new Odometer({ duration: 2.3, lastDigitDelay: 0 }),
duration: 3.0
});
```
If you'd like to make your own plugin, see [the docs](#creating-animation-plugins) below!
### Tabular nums
To optimize the styling of counting number animations, you can take advantage of an OpenType feature called tabular nums which stabilizes jitteryness by using equal-width numbers.
In my experience, most OpenType fonts already use tabular nums, so this isn't needed. But it may help to add this style if they don't:
```css
font-variant-numeric: tabular-nums;
```
---
## Including CountUp
CountUp is distributed as an ES module, though a UMD module is [also included](#umd-module), along with a separate requestAnimationFrame polyfill (see below).
For the examples below, first install CountUp:
```
npm i countup.js
```
### Example with vanilla js
This is what is used in the demo. Checkout index.html and demo.js.
main.js:
```js
import { CountUp } from './js/countUp.min.js';
window.onload = function() {
var countUp = new CountUp('target', 2000);
countUp.start();
}
```
Include in your html. Notice the `type` attribute:
```html
<script src="./main.js" type="module"></script>
```
If you prefer not to use modules, use the `nomodule` script tag to include separate scripts:
```html
<script nomodule src="js/countUp.umd.js"></script>
<script nomodule src="js/main-for-legacy.js"></script>
```
To run module-enabled scripts locally, you'll need a simple local server setup like [this](https://www.npmjs.com/package/http-server) (test the demo locally by running `npm run serve`) because otherwise you may see a CORS error when your browser tries to load the script as a module.
### For Webpack and other build systems
Import from the package, instead of the file location:
```js
import { CountUp } from 'countup.js';
```
### UMD module
CountUp is also wrapped as a UMD module in `./dist/countUp.umd.js` and it exposes CountUp as a global variable on the window scope. To use it, include `countUp.umd.js` in a script tag, and invoke it like so:
```js
var numAnim = new countUp.CountUp('myTarget', 2000);
numAnim.start()
```
---
## Contributing
Before you make a pull request, please be sure to follow these instructions:
1. Do your work on `src/countUp.ts`
1. Lint: `npm run lint`
1. Run tests: `npm t`
1. Build and serve the demo by running `npm start` then check the demo to make sure it counts.
<!-- PUBLISHING
1. bump version in package.json and countUp.ts
2. npm run build
3. commit changes
4. npm publish
-->
---
## Creating Animation Plugins
CountUp supports plugins as of v2.6.0. Plugins implement their own render method to display each frame's formatted value. A class instance or object can be passed to the `plugin` property of CountUpOptions, and the plugin's render method will be called instead of CountUp's.
```ts
export declare interface CountUpPlugin {
render(elem: HTMLElement, formatted: string): void;
}
```
An example of a plugin:
```ts
export class SomePlugin implements CountUpPlugin {
// ...some properties here
constructor(options: SomePluginOptions) {
// ...setup code here if you need it
}
render(elem: HTMLElement, formatted: string): void {
// render DOM here
}
}
```
If you make a plugin, be sure to create a PR to add it to this README!
+221
View File
@@ -0,0 +1,221 @@
// same as demo.js but with a different instantiation of CountUp,
// and no lambdas
window.onload = function () {
var el = function (id) {
return document.getElementById(id);
};
var code, stars, endVal, options;
var demo = new countUp.CountUp('myTargetElement', 100);
var codeVisualizer = el('codeVisualizer');
var errorSection = el('errorSection');
el('version').innerHTML = demo.version;
var changeEls = document.querySelectorAll('.updateCodeVis');
for (var i = 0, len = changeEls.length; i < len; i++) {
changeEls[i].onchange = updateCodeVisualizer;
}
el('swapValues').onclick = function () {
var oldStartVal = el('startVal').value;
var oldEndVal = el('endVal').value;
el('startVal').value = oldEndVal;
el('endVal').value = oldStartVal;
updateCodeVisualizer();
};
el('start').onclick = createCountUp;
el('apply').onclick = createCountUp;
el('pauseResume').onclick = function () {
code += '<br>demo.pauseResume();';
codeVisualizer.innerHTML = code;
demo.pauseResume();
};
el('reset').onclick = function () {
code += '<br>demo.reset();';
codeVisualizer.innerHTML = code;
demo.reset();
};
el('update').onclick = function () {
var updateVal = el('updateVal').value;
var num = updateVal ? updateVal : 0;
code += "<br>demo.update(" + num + ");";
codeVisualizer.innerHTML = code;
demo.update(num);
};
el('updateVal').onchange = function () {
var updateVal = el('updateVal').value;
var num = updateVal ? updateVal : 0;
code += '<br>demo.update(' + num + ');';
codeVisualizer.innerHTML = code;
};
// OPTION VALUES
var easingFunctions = {
easeOutExpo: function (t, b, c, d) {
return c * (-Math.pow(2, -10 * t / d) + 1) * 1024 / 1023 + b;
},
outQuintic: function (t, b, c, d) {
var ts = (t /= d) * t;
var tc = ts * t;
return b + c * (tc * ts + -5 * ts * ts + 10 * tc + -10 * ts + 5 * t);
},
outCubic: function (t, b, c, d) {
var ts = (t /= d) * t;
var tc = ts * t;
return b + c * (tc + -3 * ts + 3 * t);
}
};
function getEasingFn() {
var fn = el('easingFnsDropdown').value;
if (fn === 'easeOutExpo') {
return null;
}
if (typeof easingFunctions[fn] === 'undefined') {
return undefined;
}
return easingFunctions[fn];
}
function getEasingFnBody(fn) {
fn = typeof fn === 'undefined' ? getEasingFn() : fn;
if (typeof fn === 'undefined') {
return 'undefined function';
}
if (fn !== null) {
return fn.toString().replace(/^ {8}/gm, '');
}
return '';
}
function getNumerals() {
var numeralsCode = el('numeralsDropdown').value;
// optionally provide alternates for 0-9
switch (numeralsCode) {
case 'ea': // Eastern Arabic
return ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'];
case 'fa': // Farsi
return ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];
default:
return null;
}
}
var stringifyArray = function (arr) { return '[\'' + arr.join('\', \'') + '\']'; };
// COUNTUP AND CODE VISUALIZER
function createCountUp() {
establishOptionsFromInputs();
demo = new countUp.CountUp('myTargetElement', endVal, options);
if (!demo.error) {
errorSection.style.display = 'none';
demo.start();
updateCodeVisualizer();
}
else {
errorSection.style.display = 'block';
document.getElementById('error').innerHTML = demo.error;
console.error(demo.error);
}
}
function methodToCallOnComplete() {
console.log('COMPLETE!');
alert('COMPLETE!');
}
function establishOptionsFromInputs() {
endVal = Number(el('endVal').value);
options = {
startVal: el('startVal').value,
decimalPlaces: el('decimalPlaces').value,
duration: Number(el('duration').value),
useEasing: el('useEasing').checked,
useGrouping: el('useGrouping').checked,
useIndianSeparators: el('useIndianSeparators').checked,
easingFn: typeof getEasingFn() === 'undefined' ? null : getEasingFn(),
separator: el('separator').value,
decimal: el('decimal').value,
prefix: el('prefix').value,
suffix: el('suffix').value,
numerals: getNumerals(),
onCompleteCallback: el('useOnComplete').checked ? methodToCallOnComplete : null
};
// unset null values so they don't overwrite defaults
for (var key in options) {
if (options.hasOwnProperty(key)) {
if (options[key] === null) {
delete options[key];
}
}
}
}
function updateCodeVisualizer() {
establishOptionsFromInputs();
code = '';
if (options.useEasing && options.easingFn) {
code += 'const easingFn = ';
var split = getEasingFnBody(options.easingFn).split('\n');
for (var line in split) {
if (split.hasOwnProperty(line)) {
code += split[line].replace(' ', '&nbsp;') + '<br>';
}
}
}
function indentedLine(keyPair, singleLine) {
if (singleLine === void 0) { singleLine = false; }
var delimeter = (singleLine) ? ';' : ',';
return "&emsp;&emsp;" + keyPair + delimeter + "<br>";
}
var opts = '';
opts += (options.startVal !== '0') ? indentedLine("startVal: " + options.startVal) : '';
opts += (options.decimalPlaces !== '0') ? indentedLine("decimalPlaces: " + options.decimalPlaces) : '';
opts += (options.duration !== 2) ? indentedLine("duration: " + options.duration) : '';
opts += (options.useEasing) ? '' : indentedLine("useEasing: " + options.useEasing);
opts += (options.useEasing && options.easingFn) ? indentedLine("easingFn") : '';
opts += (options.useGrouping) ? '' : indentedLine("useGrouping: " + options.useGrouping);
opts += (options.useIndianSeparators) ? indentedLine("useIndianSeparators: " + options.useIndianSeparators) : '';
opts += (options.separator !== ',') ? indentedLine("separator: '" + options.separator + "'") : '';
opts += (options.decimal !== '.') ? indentedLine("decimal: '" + options.decimal + "'") : '';
opts += (options.prefix.length) ? indentedLine("prefix: '" + options.prefix + "'") : '';
opts += (options.suffix.length) ? indentedLine("suffix: '" + options.suffix + "'") : '';
opts += (options.numerals && options.numerals.length) ?
indentedLine("numerals: " + stringifyArray(options.numerals)) : '';
opts += (options.onCompleteCallback) ? indentedLine("onCompleteCallback: methodToCallOnComplete") : '';
if (opts.length) {
code += "const options = {<br>" + opts + "};<br>";
code += "let demo = new CountUp('myTargetElement', " + endVal + ", options);<br>";
}
else {
code += "let demo = new CountUp('myTargetElement', " + endVal + ");<br>";
}
code += 'if (!demo.error) {<br>';
code += indentedLine('demo.start()', true);
code += '} else {<br>';
code += indentedLine('console.error(demo.error)', true);
code += '}';
codeVisualizer.innerHTML = code;
}
// get current star count
var repoInfoUrl = 'https://api.github.com/repos/inorganik/CountUp.js';
var getStars = new XMLHttpRequest();
getStars.open('GET', repoInfoUrl, true);
getStars.timeout = 5000;
getStars.onreadystatechange = function () {
// 2: received headers, 3: loading, 4: done
if (getStars.readyState === 4) {
if (getStars.status === 200) {
if (getStars.responseText !== 'undefined') {
if (getStars.responseText.length > 0) {
var data = JSON.parse(getStars.responseText);
stars = data.stargazers_count;
// change input values
el('endVal').value = stars;
createCountUp();
}
}
}
}
};
getStars.onerror = function () {
console.error('error getting stars:', getStars.status);
stars = getStars.status;
demo.start();
};
getStars.send();
}
+256
View File
@@ -0,0 +1,256 @@
import { CountUp } from '../dist/countUp.js';
const el = (id) => document.getElementById(id);
let code, stars, endVal, options;
let demo = new CountUp('myTargetElement', 100);
let scrollSpyCountUp, hiddenAtInitCountUp, insideModalCountUp;
const codeVisualizer = el('codeVisualizer');
const errorSection = el('errorSection');
let startTime;
el('version').textContent = demo.version;
document.querySelectorAll('.updateCodeVis').forEach((elem) => {
elem.addEventListener('change', updateCodeVisualizer);
});
el('swapValues').addEventListener('click', () => {
const oldStartVal = el('startVal').value;
const oldEndVal = el('endVal').value;
el('startVal').value = oldEndVal;
el('endVal').value = oldStartVal;
updateCodeVisualizer();
});
el('start').addEventListener('click', createCountUp);
el('apply').addEventListener('click', createCountUp);
el('pauseResume').addEventListener('click', () => {
code += '<br>demo.pauseResume();';
codeVisualizer.innerHTML = code;
demo.pauseResume();
});
el('reset').addEventListener('click', () => {
code += '<br>demo.reset();';
codeVisualizer.innerHTML = code;
demo.reset();
});
el('update').addEventListener('click', () => {
const num = el('updateVal').value || 0;
code += `<br>demo.update(${num});`;
codeVisualizer.innerHTML = code;
demo.update(num);
});
el('updateVal').addEventListener('change', () => {
const num = el('updateVal').value || 0;
code += `<br>demo.update(${num});`;
codeVisualizer.innerHTML = code;
});
const easingFunctions = {
easeOutExpo: (t, b, c, d) => c * (-Math.pow(2, -10 * t / d) + 1) * 1024 / 1023 + b,
outQuintic: (t, b, c, d) => {
const ts = (t /= d) * t;
const tc = ts * t;
return b + c * (tc * ts + -5 * ts * ts + 10 * tc + -10 * ts + 5 * t);
},
outCubic: (t, b, c, d) => {
const ts = (t /= d) * t;
const tc = ts * t;
return b + c * (tc + -3 * ts + 3 * t);
}
};
function getEasingFn() {
const fn = el('easingFnsDropdown').value;
if (fn === 'easeOutExpo') return null;
if (easingFunctions[fn] === undefined) return undefined;
return easingFunctions[fn];
}
function getEasingFnBody(fn = getEasingFn()) {
if (fn === undefined) return 'undefined function';
if (fn !== null) return fn.toString().replace(/^ {4}/gm, '');
return '';
}
function getNumerals() {
const numeralsCode = el('numeralsDropdown').value;
switch (numeralsCode) {
case 'ea':
return ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'];
case 'fa':
return ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];
default:
return null;
}
}
const stringifyArray = (arr) => `['${arr.join("', '")}']`;
function createCountUp() {
demo.onDestroy();
establishOptionsFromInputs();
demo = new CountUp('myTargetElement', endVal, options);
if (!demo.error) {
errorSection.style.display = 'none';
startTime = Date.now();
demo.start();
updateCodeVisualizer();
} else {
errorSection.style.display = 'block';
el('error').textContent = demo.error;
console.error(demo.error);
}
}
function calculateAnimationTime() {
const duration = Date.now() - startTime;
console.log('actual animation duration (ms):', duration);
alert('COMPLETE!');
}
function establishOptionsFromInputs() {
endVal = Number(el('endVal').value);
options = {
startVal: el('startVal').value,
decimalPlaces: el('decimalPlaces').value,
duration: Number(el('duration').value),
useGrouping: el('useGrouping').checked,
useIndianSeparators: el('useIndianSeparators').checked,
easingFn: getEasingFn() ?? null,
separator: el('separator').value,
decimal: el('decimal').value,
prefix: el('prefix').value,
numerals: getNumerals(),
onCompleteCallback: el('useOnComplete').checked ? calculateAnimationTime : null
};
for (const [key, value] of Object.entries(options)) {
if (value === null) delete options[key];
}
}
function updateCodeVisualizer() {
establishOptionsFromInputs();
code = '';
if (options.easingFn) {
code += 'const easingFn = ';
for (const line of getEasingFnBody(options.easingFn).split('\n')) {
code += `${line.replace(' ', '&nbsp;')}<br>`;
}
}
const indentedLine = (keyPair, singleLine = false) => {
const delimiter = singleLine ? ';' : ',';
return `&emsp;&emsp;${keyPair}${delimiter}<br>`;
};
let opts = '';
opts += (options.startVal !== '0') ? indentedLine(`startVal: ${options.startVal}`) : '';
opts += (options.decimalPlaces !== '0') ? indentedLine(`decimalPlaces: ${options.decimalPlaces}`) : '';
opts += (options.duration !== 2) ? indentedLine(`duration: ${options.duration}`) : '';
opts += options.easingFn ? indentedLine('easingFn') : '';
opts += options.useGrouping ? '' : indentedLine(`useGrouping: ${options.useGrouping}`);
opts += options.useIndianSeparators ? indentedLine(`useIndianSeparators: ${options.useIndianSeparators}`) : '';
opts += (options.separator !== ',') ? indentedLine(`separator: '${options.separator}'`) : '';
opts += (options.decimal !== '.') ? indentedLine(`decimal: '${options.decimal}'`) : '';
opts += options.prefix.length ? indentedLine(`prefix: '${options.prefix}'`) : '';
opts += (options.numerals && options.numerals.length) ?
indentedLine(`numerals: ${stringifyArray(options.numerals)}`) : '';
opts += options.onCompleteCallback ? indentedLine('onCompleteCallback: methodToCallOnComplete') : '';
if (opts.length) {
code += `const options = {<br>${opts}};<br>`;
code += `const demo = new CountUp('myTargetElement', ${endVal}, options);<br>`;
} else {
code += `const demo = new CountUp('myTargetElement', ${endVal});<br>`;
}
code += 'if (!demo.error) {<br>';
code += indentedLine('demo.start()', true);
code += '} else {<br>';
code += indentedLine('console.error(demo.error)', true);
code += '}';
codeVisualizer.innerHTML = code;
}
// auto animate options
function getAutoAnimateOptions() {
return {
autoAnimate: true,
autoAnimateOnce: el('autoAnimateOnce').checked,
autoAnimateDelay: Number(el('autoAnimateDelay').value),
onCompleteCallback: null,
};
}
function recreateAutoAnimateDemos() {
createScrollSpyCountUp();
createHiddenAtInitCountUp();
createInsideModalCountUp();
}
el('autoAnimateOnce').addEventListener('change', recreateAutoAnimateDemos);
el('autoAnimateDelay').addEventListener('change', recreateAutoAnimateDemos);
// scroll spy
function createScrollSpyCountUp() {
if (scrollSpyCountUp) scrollSpyCountUp.onDestroy();
establishOptionsFromInputs();
const scrollSpyOptions = { ...options, ...getAutoAnimateOptions() };
scrollSpyCountUp = new CountUp('scrollSpyTarget', endVal, scrollSpyOptions);
if (scrollSpyCountUp.error) {
console.error('scrollSpyCountUp error:', scrollSpyCountUp.error);
}
}
createScrollSpyCountUp();
el('apply').addEventListener('click', createScrollSpyCountUp);
el('start').addEventListener('click', createScrollSpyCountUp);
// hidden at init
function createHiddenAtInitCountUp() {
if (hiddenAtInitCountUp) hiddenAtInitCountUp.onDestroy();
establishOptionsFromInputs();
const hiddenOptions = { ...options, ...getAutoAnimateOptions() };
hiddenAtInitCountUp = new CountUp('hiddenAtInitTarget', endVal, hiddenOptions);
if (hiddenAtInitCountUp.error) {
console.error('hiddenAtInitCountUp error:', hiddenAtInitCountUp.error);
}
}
createHiddenAtInitCountUp();
el('apply').addEventListener('click', createHiddenAtInitCountUp);
el('start').addEventListener('click', createHiddenAtInitCountUp);
el('toggleVisibility').addEventListener('click', () => {
const target = el('hiddenAtInitTarget');
target.style.display = target.style.display === 'none' ? '' : 'none';
});
// inside modal
function createInsideModalCountUp() {
if (insideModalCountUp) insideModalCountUp.onDestroy();
establishOptionsFromInputs();
const modalOptions = { ...options, ...getAutoAnimateOptions() };
insideModalCountUp = new CountUp('modalTarget', endVal, modalOptions);
if (insideModalCountUp.error) {
console.error('insideModalCountUp error:', insideModalCountUp.error);
}
}
createInsideModalCountUp();
el('apply').addEventListener('click', createInsideModalCountUp);
el('start').addEventListener('click', createInsideModalCountUp);
el('openModal').addEventListener('click', () => el('modalDialog').showModal());
el('closeModal').addEventListener('click', () => el('modalDialog').close());
// get current star count
try {
const response = await fetch('https://api.github.com/repos/inorganik/CountUp.js');
if (response.ok) {
const data = await response.json();
stars = data.stargazers_count;
el('endVal').value = stars;
createCountUp();
createScrollSpyCountUp();
createHiddenAtInitCountUp();
createInsideModalCountUp();
}
} catch (error) {
console.error('error getting stars:', error);
demo.start();
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

+129
View File
@@ -0,0 +1,129 @@
export interface CountUpOptions {
/** Number to start at @default 0 */
startVal?: number;
/** Number of decimal places @default 0 */
decimalPlaces?: number;
/** Animation duration in seconds @default 2 */
duration?: number;
/** Example: 1,000 vs 1000 @default true */
useGrouping?: boolean;
/** Example: 1,00,000 vs 100,000 @default false */
useIndianSeparators?: boolean;
/** Ease animation @default true */
useEasing?: boolean;
/** Smooth easing for large numbers above this if useEasing @default 999 */
smartEasingThreshold?: number;
/** Amount to be eased for numbers above threshold @default 333 */
smartEasingAmount?: number;
/** Grouping separator @default ',' */
separator?: string;
/** Decimal character @default '.' */
decimal?: string;
/** Easing function for animation @default easeOutExpo */
easingFn?: (t: number, b: number, c: number, d: number) => number;
/** Custom function to format the result */
formattingFn?: (n: number) => string;
/** Text prepended to result */
prefix?: string;
/** Text appended to result */
suffix?: string;
/** Numeral glyph substitution */
numerals?: string[];
/** Callback called when animation completes */
onCompleteCallback?: () => any;
/** Callback called when animation starts */
onStartCallback?: () => any;
/** Plugin for alternate animations */
plugin?: CountUpPlugin;
/** Trigger animation when target becomes visible @default false */
autoAnimate?: boolean;
/** Animation delay in ms after auto-animate triggers @default 200 */
autoAnimateDelay?: number;
/** Run animation only once for auto-animate triggers @default false */
autoAnimateOnce?: boolean;
/** @deprecated Please use autoAnimate instead */
enableScrollSpy?: boolean;
/** @deprecated Please use autoAnimateDelay instead */
scrollSpyDelay?: number;
/** @deprecated Please use autoAnimateOnce instead */
scrollSpyOnce?: boolean;
}
export declare interface CountUpPlugin {
render(elem: HTMLElement, formatted: string): void;
}
/**
* Animates a number by counting to it.
* playground: stackblitz.com/edit/countup-typescript
*
* @param target - id of html element, input, svg text element, or DOM element reference where counting occurs.
* @param endVal - the value you want to arrive at.
* @param options - optional configuration object for fine-grain control
*/
export declare class CountUp {
private endVal?;
options?: CountUpOptions;
version: string;
private static observedElements;
private defaults;
private rAF;
private autoAnimateTimeout;
private startTime;
private remaining;
private finalEndVal;
private useEasing;
private countDown;
private observer;
el: HTMLElement | HTMLInputElement;
formattingFn: (num: number) => string;
easingFn?: (t: number, b: number, c: number, d: number) => number;
error: string;
startVal: number;
duration: number;
paused: boolean;
frameVal: number;
once: boolean;
constructor(target: string | HTMLElement | HTMLInputElement, endVal?: number | null, options?: CountUpOptions);
/** Set up an IntersectionObserver to auto-animate when the target element appears. */
private setupObserver;
/** Disconnect the IntersectionObserver and stop watching this element. */
unobserve(): void;
/** Teardown: cancel animation, disconnect observer, clear callbacks. */
onDestroy(): void;
/**
* Smart easing works by breaking the animation into 2 parts, the second part being the
* smartEasingAmount and first part being the total amount minus the smartEasingAmount. It works
* by disabling easing for the first part and enabling it on the second part. It is used if
* useEasing is true and the total animation amount exceeds the smartEasingThreshold.
*/
private determineDirectionAndSmartEasing;
/** Start the animation. Optionally pass a callback that fires on completion. */
start(callback?: (args?: any) => any): void;
/** Toggle pause/resume on the animation. */
pauseResume(): void;
/** Reset to startVal so the animation can be run again. */
reset(): void;
/** Pass a new endVal and start the animation. */
update(newEndVal: string | number): void;
/** Animation frame callback — advances the value each frame. */
count: (timestamp: number) => void;
/** Format and render the given value to the target element. */
printValue(val: number): void;
/** Return true if the value is a finite number. */
ensureNumber(n: any): boolean;
/** Validate and convert a value to a number, setting an error if invalid. */
validateValue(value: string | number): number;
/** Reset startTime, duration, and remaining to their initial values. */
private resetDuration;
/** Default number formatter with grouping, decimals, prefix/suffix, and numeral substitution. */
formatNumber: (num: number) => string;
/**
* Default easing function (easeOutExpo).
* @param t current time
* @param b beginning value
* @param c change in value
* @param d duration
*/
easeOutExpo: (t: number, b: number, c: number, d: number) => number;
/** Parse a formatted string back to a number using the current separator/decimal options. */
parse(number: string): number;
}
+364
View File
@@ -0,0 +1,364 @@
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
/**
* Animates a number by counting to it.
* playground: stackblitz.com/edit/countup-typescript
*
* @param target - id of html element, input, svg text element, or DOM element reference where counting occurs.
* @param endVal - the value you want to arrive at.
* @param options - optional configuration object for fine-grain control
*/
var CountUp = /** @class */ (function () {
function CountUp(target, endVal, options) {
var _this = this;
this.endVal = endVal;
this.options = options;
this.version = '2.10.0';
this.defaults = {
startVal: 0,
decimalPlaces: 0,
duration: 2,
useEasing: true,
useGrouping: true,
useIndianSeparators: false,
smartEasingThreshold: 999,
smartEasingAmount: 333,
separator: ',',
decimal: '.',
prefix: '',
suffix: '',
autoAnimate: false,
autoAnimateDelay: 200,
autoAnimateOnce: false,
};
this.finalEndVal = null; // for smart easing
this.useEasing = true;
this.countDown = false;
this.error = '';
this.startVal = 0;
this.paused = true;
this.once = false;
/** Animation frame callback — advances the value each frame. */
this.count = function (timestamp) {
if (!_this.startTime) {
_this.startTime = timestamp;
}
var progress = timestamp - _this.startTime;
_this.remaining = _this.duration - progress;
// to ease or not to ease
if (_this.useEasing) {
if (_this.countDown) {
_this.frameVal = _this.startVal - _this.easingFn(progress, 0, _this.startVal - _this.endVal, _this.duration);
}
else {
_this.frameVal = _this.easingFn(progress, _this.startVal, _this.endVal - _this.startVal, _this.duration);
}
}
else {
_this.frameVal = _this.startVal + (_this.endVal - _this.startVal) * (progress / _this.duration);
}
// don't go past endVal since progress can exceed duration in the last frame
var wentPast = _this.countDown ? _this.frameVal < _this.endVal : _this.frameVal > _this.endVal;
_this.frameVal = wentPast ? _this.endVal : _this.frameVal;
// decimal
_this.frameVal = Number(_this.frameVal.toFixed(_this.options.decimalPlaces));
// format and print value
_this.printValue(_this.frameVal);
// whether to continue
if (progress < _this.duration) {
_this.rAF = requestAnimationFrame(_this.count);
}
else if (_this.finalEndVal !== null) {
// smart easing
_this.update(_this.finalEndVal);
}
else {
if (_this.options.onCompleteCallback) {
_this.options.onCompleteCallback();
}
}
};
/** Default number formatter with grouping, decimals, prefix/suffix, and numeral substitution. */
this.formatNumber = function (num) {
var neg = (num < 0) ? '-' : '';
var result, x1, x2, x3;
result = Math.abs(num).toFixed(_this.options.decimalPlaces);
result += '';
var x = result.split('.');
x1 = x[0];
x2 = x.length > 1 ? _this.options.decimal + x[1] : '';
if (_this.options.useGrouping) {
x3 = '';
var factor = 3, j = 0;
for (var i = 0, len = x1.length; i < len; ++i) {
if (_this.options.useIndianSeparators && i === 4) {
factor = 2;
j = 1;
}
if (i !== 0 && (j % factor) === 0) {
x3 = _this.options.separator + x3;
}
j++;
x3 = x1[len - i - 1] + x3;
}
x1 = x3;
}
// optional numeral substitution
if (_this.options.numerals && _this.options.numerals.length) {
x1 = x1.replace(/[0-9]/g, function (w) { return _this.options.numerals[+w]; });
x2 = x2.replace(/[0-9]/g, function (w) { return _this.options.numerals[+w]; });
}
return neg + _this.options.prefix + x1 + x2 + _this.options.suffix;
};
/**
* Default easing function (easeOutExpo).
* @param t current time
* @param b beginning value
* @param c change in value
* @param d duration
*/
this.easeOutExpo = function (t, b, c, d) {
return c * (-Math.pow(2, -10 * t / d) + 1) * 1024 / 1023 + b;
};
this.options = __assign(__assign({}, this.defaults), options);
if (this.options.enableScrollSpy) {
this.options.autoAnimate = true;
}
if (this.options.scrollSpyDelay !== undefined) {
this.options.autoAnimateDelay = this.options.scrollSpyDelay;
}
if (this.options.scrollSpyOnce) {
this.options.autoAnimateOnce = true;
}
this.formattingFn = (this.options.formattingFn) ?
this.options.formattingFn : this.formatNumber;
this.easingFn = (this.options.easingFn) ?
this.options.easingFn : this.easeOutExpo;
this.el = (typeof target === 'string') ? document.getElementById(target) : target;
endVal = endVal == null ? this.parse(this.el.innerHTML) : endVal;
this.startVal = this.validateValue(this.options.startVal);
this.frameVal = this.startVal;
this.endVal = this.validateValue(endVal);
this.options.decimalPlaces = Math.max(0 || this.options.decimalPlaces);
this.resetDuration();
this.options.separator = String(this.options.separator);
this.useEasing = this.options.useEasing;
if (this.options.separator === '') {
this.options.useGrouping = false;
}
if (this.el) {
this.printValue(this.startVal);
}
else {
this.error = '[CountUp] target is null or undefined';
}
if (typeof window !== 'undefined' && this.options.autoAnimate) {
if (!this.error && typeof IntersectionObserver !== 'undefined') {
this.setupObserver();
}
else {
if (this.error) {
console.error(this.error, target);
}
else {
console.error('IntersectionObserver is not supported by this browser');
}
}
}
}
/** Set up an IntersectionObserver to auto-animate when the target element appears. */
CountUp.prototype.setupObserver = function () {
var _this = this;
var existing = CountUp.observedElements.get(this.el);
if (existing) {
existing.unobserve();
}
CountUp.observedElements.set(this.el, this);
this.observer = new IntersectionObserver(function (entries) {
for (var _i = 0, entries_1 = entries; _i < entries_1.length; _i++) {
var entry = entries_1[_i];
if (entry.isIntersecting && _this.paused && !_this.once) {
_this.paused = false;
_this.autoAnimateTimeout = setTimeout(function () { return _this.start(); }, _this.options.autoAnimateDelay);
if (_this.options.autoAnimateOnce) {
_this.once = true;
_this.observer.disconnect();
}
}
else if (!entry.isIntersecting && !_this.paused) {
clearTimeout(_this.autoAnimateTimeout);
_this.reset();
}
}
}, { threshold: 0 });
this.observer.observe(this.el);
};
/** Disconnect the IntersectionObserver and stop watching this element. */
CountUp.prototype.unobserve = function () {
var _a;
clearTimeout(this.autoAnimateTimeout);
(_a = this.observer) === null || _a === void 0 ? void 0 : _a.disconnect();
CountUp.observedElements.delete(this.el);
};
/** Teardown: cancel animation, disconnect observer, clear callbacks. */
CountUp.prototype.onDestroy = function () {
clearTimeout(this.autoAnimateTimeout);
cancelAnimationFrame(this.rAF);
this.paused = true;
this.unobserve();
this.options.onCompleteCallback = null;
this.options.onStartCallback = null;
};
/**
* Smart easing works by breaking the animation into 2 parts, the second part being the
* smartEasingAmount and first part being the total amount minus the smartEasingAmount. It works
* by disabling easing for the first part and enabling it on the second part. It is used if
* useEasing is true and the total animation amount exceeds the smartEasingThreshold.
*/
CountUp.prototype.determineDirectionAndSmartEasing = function () {
var end = (this.finalEndVal) ? this.finalEndVal : this.endVal;
this.countDown = (this.startVal > end);
var animateAmount = end - this.startVal;
if (Math.abs(animateAmount) > this.options.smartEasingThreshold && this.options.useEasing) {
this.finalEndVal = end;
var up = (this.countDown) ? 1 : -1;
this.endVal = end + (up * this.options.smartEasingAmount);
this.duration = this.duration / 2;
}
else {
this.endVal = end;
this.finalEndVal = null;
}
if (this.finalEndVal !== null) {
// setting finalEndVal indicates smart easing
this.useEasing = false;
}
else {
this.useEasing = this.options.useEasing;
}
};
/** Start the animation. Optionally pass a callback that fires on completion. */
CountUp.prototype.start = function (callback) {
if (this.error) {
return;
}
if (this.options.onStartCallback) {
this.options.onStartCallback();
}
if (callback) {
this.options.onCompleteCallback = callback;
}
if (this.duration > 0) {
this.determineDirectionAndSmartEasing();
this.paused = false;
this.rAF = requestAnimationFrame(this.count);
}
else {
this.printValue(this.endVal);
}
};
/** Toggle pause/resume on the animation. */
CountUp.prototype.pauseResume = function () {
if (!this.paused) {
cancelAnimationFrame(this.rAF);
}
else {
this.startTime = null;
this.duration = this.remaining;
this.startVal = this.frameVal;
this.determineDirectionAndSmartEasing();
this.rAF = requestAnimationFrame(this.count);
}
this.paused = !this.paused;
};
/** Reset to startVal so the animation can be run again. */
CountUp.prototype.reset = function () {
clearTimeout(this.autoAnimateTimeout);
cancelAnimationFrame(this.rAF);
this.paused = true;
this.once = false;
this.resetDuration();
this.startVal = this.validateValue(this.options.startVal);
this.frameVal = this.startVal;
this.printValue(this.startVal);
};
/** Pass a new endVal and start the animation. */
CountUp.prototype.update = function (newEndVal) {
cancelAnimationFrame(this.rAF);
this.startTime = null;
this.endVal = this.validateValue(newEndVal);
if (this.endVal === this.frameVal) {
return;
}
this.startVal = this.frameVal;
if (this.finalEndVal == null) {
this.resetDuration();
}
this.finalEndVal = null;
this.determineDirectionAndSmartEasing();
this.rAF = requestAnimationFrame(this.count);
};
/** Format and render the given value to the target element. */
CountUp.prototype.printValue = function (val) {
var _a;
if (!this.el)
return;
var result = this.formattingFn(val);
if ((_a = this.options.plugin) === null || _a === void 0 ? void 0 : _a.render) {
this.options.plugin.render(this.el, result);
return;
}
if (this.el.tagName === 'INPUT') {
var input = this.el;
input.value = result;
}
else if (this.el.tagName === 'text' || this.el.tagName === 'tspan') {
this.el.textContent = result;
}
else {
this.el.innerHTML = result;
}
};
/** Return true if the value is a finite number. */
CountUp.prototype.ensureNumber = function (n) {
return (typeof n === 'number' && !isNaN(n));
};
/** Validate and convert a value to a number, setting an error if invalid. */
CountUp.prototype.validateValue = function (value) {
var newValue = Number(value);
if (!this.ensureNumber(newValue)) {
this.error = "[CountUp] invalid start or end value: ".concat(value);
return null;
}
else {
return newValue;
}
};
/** Reset startTime, duration, and remaining to their initial values. */
CountUp.prototype.resetDuration = function () {
this.startTime = null;
this.duration = Number(this.options.duration) * 1000;
this.remaining = this.duration;
};
/** Parse a formatted string back to a number using the current separator/decimal options. */
CountUp.prototype.parse = function (number) {
// eslint-disable-next-line no-irregular-whitespace
var escapeRegExp = function (s) { return s.replace(/([.,'  ])/g, '\\$1'); };
var sep = escapeRegExp(this.options.separator);
var dec = escapeRegExp(this.options.decimal);
var num = number.replace(new RegExp(sep, 'g'), '').replace(new RegExp(dec, 'g'), '.');
return parseFloat(num);
};
CountUp.observedElements = new WeakMap();
return CountUp;
}());
export { CountUp };
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+26
View File
@@ -0,0 +1,26 @@
// make sure requestAnimationFrame and cancelAnimationFrame are defined
// polyfill for browsers without native support
// by Opera engineer Erik Möller
(function () {
var lastTime = 0;
var vendors = ['webkit', 'moz', 'ms', 'o'];
for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] ||
window[vendors[x] + 'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function (callback) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function () { return callback(currTime + timeToCall); }, timeToCall);
lastTime = currTime + timeToCall;
return id;
};
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = function (id) {
clearTimeout(id);
};
}
})();
+199
View File
@@ -0,0 +1,199 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<meta name="robots" content="index,follow">
<meta name="mobile-web-app-capable" content="yes">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="description" content="A javascript class that animates a numerical value by counting to it.">
<title>CountUp.js</title>
<link rel="stylesheet" type="text/css" href="https://inorganik.github.io/assets/css/style.css">
<style>
h1.jumbo {
line-height: 200px;
}
.dot-matrix {
background-image:radial-gradient(circle, #898989 1px, transparent 1px);
background-size:1em 1em;
}
[type="button"].indigo {
background-color: #4d63bc ;
color: #fff;
}
@media (prefers-color-scheme: dark) {
[type="button"].indigo {
background-color: #6e90da;
}
}
</style>
<script src="demo/demo.js" type="module"></script>
<script nomodule src="dist/countUp.umd.js"></script>
<script nomodule src="demo/demo-nomodule.js"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-QXSCCPQ13N"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-QXSCCPQ13N');
</script>
</head>
<body>
<a class="forkMe" href="https://github.com/inorganik/CountUp.js"><img src="https://inorganik.github.io/assets/img/forkme_custom_indigo.png" alt="Fork me on GitHub"></a>
<div id="wrap">
<header>
<div id="github"><a class="block" href="https://inorganik.github.io"></a></div>
<div class="leaderLine">////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////</div>
<div id="logo"><a class="block" href="https://inorganik.net" alt="inorganik produce, inc"></a></div>
</header>
<section>
<div class="col full">
<h1>CountUp.js &nbsp;<small id="version" class="lt-gray"></small></h1>
</div>
<div class="col full">
<p>CountUp.js is a dependency-free, lightweight JavaScript class that animates a numerical value by counting to it.</p>
<p>Install via npm/yarn using the package name &nbsp;<code class="indigo large">countup.js</code>.</p>
<h3 class="marginTop marginBottom"><a class="lime weight700" href="https://github.com/inorganik/CountUp.js">View on GitHub</a></h3>
</div>
</section>
<section>
<p style="position:absolute; top:5px; left:0;">Current stars:</p>
<h1 class="jumbo" id="myTargetElement">0</h1>
</section>
<section id="errorSection" style="background-color:#FFDCDC; display:none" class="col-inner">
<h4 id="error" style="color: red" class="noMargin"></h4>
</section>
<section id="paramsSection">
<form>
<h4 class="inlineLeft noMargin weight300">Params:</h4>
<div class="inlineLeft marginLeft marginRight">
<input type="text" value="0" id="startVal" style="width:80px" class="updateCodeVis">
<label class="inlineLabel">Start</label>
</div>
<input id="swapValues" type="button" class="inlineLeft marginRight" value="Swap" style="width:80px;">
<div class="inlineLeft marginRight">
<input type="text" value="94.62" id="endVal" style="width:80px" class="updateCodeVis">
<label class="inlineLabel">End</label>
</div>
<div class="inlineLeft marginRight">
<input type="number" value="0" id="decimalPlaces" step="1" style="width:50px" class="updateCodeVis">
<label class="inlineLabel">Decimal places</label>
</div>
<div class="inlineLeft marginRight">
<input type="number" value="2" id="duration" step=".1" style="width:50px" class="updateCodeVis">
<label class="inlineLabel">Duration</label>
</div>
</form>
</section>
<section id="optionsSection">
<form>
<h4 class="inlineLeft noMargin weight300">Options:</h4>
<div class="inlineLeft marginLeft marginRight">
<input id="useGrouping" type="checkbox" checked><label class="inlineLabel updateCodeVis">Use grouping</label>
</div>
<div class="inlineLeft marginRight">
<input id="useIndianSeparators" type="checkbox"><label class="inlineLabel updateCodeVis">Use Indian separators</label>
</div>
<div class="inlineLeft marginRight">
<input type="text" value="," id="separator" style="width:25px; padding:0 5px;" class="updateCodeVis">
<label class="inlineLabel">Separator</label>
</div>
<div class="inlineLeft marginRight">
<input type="text" value="." id="decimal" style="width:25px; padding:0 5px;" class="updateCodeVis">
<label class="inlineLabel">Decimal</label>
</div>
<div class="inlineLeft marginRight">
<input type="text" value="" id="prefix" style="width:50px; padding:0 5px;" class="updateCodeVis">
<label class="inlineLabel">Prefix</label>
</div>
<h4 class="inlineLeft noMargin weight300">+ many more</h4>
</form>
</section>
<section id="methodsSection">
<form>
<h4 class="inlineLeft noMargin weight300">Methods:</h4>
<input type="button" value="Start" id="start" class="inlineLeft marginLeft marginRight indigo">
<input type="button" value="Pause/Resume" id="pauseResume" class="inlineLeft marginRight">
<input type="button" value="Reset" id="reset" class="inlineLeft marginRight">
<input type="button" value="Update:" id="update" class="inlineLeft" style="margin-right:5px">
<div class="inlineLeft marginRight">
<input type="text" value="6789" id="updateVal" style="width:80px">
</div>
<div class="inlineLeft">
<input type="checkbox" id="useOnComplete" class="updateCodeVis"><label class="inlineLabel">Alert on complete</label>
</div>
</form>
</section>
<section id="easingSection">
<form>
<h4 class="inlineLeft noMargin weight300">Custom:</h4>
<div class="inlineLeft marginLeft">
<label class="inlineLabel">Easing: &nbsp;</label>
<select id="easingFnsDropdown" class="marginRight updateCodeVis">
<option value="easeOutExpo" selected>easeOutExpo (default, built-in)</option>
<option value="outQuintic">outQuintic</option>
<option value="outCubic">outCubic</option>
</select>
</div>
<div class="inlineLeft marginRight">
<label class="inlineLabel">Numerals: &nbsp;</label>
<select id="numeralsDropdown" class="updateCodeVis">
<option value="" selected>Default ("1234")</option>
<option value="ea">Eastern Arabic ("١٢٣٤")</option>
<option value="fa">Farsi ("۱۲۳۴")</option>
</select>
</div>
<div class="inlineLeft">
<input type="button" id="apply" value="Apply">
</div>
</form>
</section>
<section id="codeVisualizerSection">
<div class="col full marginBottom marginTop">
<div class="code-contain indigo">
<code id="codeVisualizer" class="indigo"></code>
</div>
</div>
</section>
<section id="scrollSpySection">
<form>
<h3 class="inlineLeft noMargin">Auto animate demos (scroll down)</h3>
<div class="inlineLeft marginLeft marginRight">
<input id="autoAnimateOnce" type="checkbox"><label class="inlineLabel">Once</label>
</div>
<div class="inlineLeft marginRight">
<input type="number" value="0" id="autoAnimateDelay" step="100" min="0" style="width:70px">
<label class="inlineLabel">Delay (ms)</label>
</div>
</form>
<p>Use the <code class="indigo">autoAnimate</code> option to animate when the target element appears.</p>
<div class="dot-matrix" style="height:60vh;"></div>
<h1 class="jumbo" id="scrollSpyTarget">0</h1>
<div class="dot-matrix" style="height:20vh;"></div>
</section>
<section id="hiddenAtInitSection" style="padding-top:20px;">
<h3>Hidden at init</h3>
<p><input type="button" value="Toggle visibility" id="toggleVisibility"></p>
<div style="height: 200px;">
<h1 class="jumbo" id="hiddenAtInitTarget" style="display:none;">0</h1>
</div>
</section>
<section id="modalSection" style="padding-top:20px;" class="marginBottom">
<h3>Inside a modal</h3>
<p><input type="button" value="Open modal" id="openModal"></p>
<dialog id="modalDialog" style="width: 650px; padding:40px 60px; border:1px solid #ccc; border-radius:16px; box-shadow:0 4px 24px rgba(0,0,0,.2); text-align:center;">
<h1 class="jumbo" id="modalTarget">0</h1>
<p><input type="button" value="Close" id="closeModal"></p>
</dialog>
<div class="dot-matrix" style="height:20vh"></div>
</section>
</div>
</body>
</html>
+17
View File
@@ -0,0 +1,17 @@
module.exports = {
roots: [
'<rootDir>/src'
],
transform: {
'^.+\\.ts?$': 'ts-jest'
},
testRegex: '(\\.|/)(test|spec)\\.ts?$',
testEnvironment: 'jsdom',
moduleFileExtensions: [
'ts',
'js',
'jsx',
'json',
'node'
]
}
+44
View File
@@ -0,0 +1,44 @@
{
"name": "countup.js",
"description": "Animates a numerical value by counting to it",
"version": "2.10.0",
"license": "MIT",
"author": "Jamie Perkins",
"main": "./dist/countUp.umd.js",
"module": "./dist/countUp.min.js",
"types": "./dist/countUp.d.ts",
"repository": {
"type": "git",
"url": "git+https://github.com/inorganik/countUp.js.git"
},
"exports": {
"types": "./dist/countUp.d.ts",
"import": "./dist/countUp.min.js",
"require": "./dist/countUp.umd.js"
},
"scripts": {
"build": "npm run clean && tsc && rollup -c rollup.config.mjs",
"clean": "rimraf dist/countUp.*",
"lint": "eslint -c .eslintrc.js --ext .ts ./src",
"serve": "http-server -o -c-1 ./",
"start": "npm run build && npm run serve",
"test": "jest",
"test:watch": "jest --watch"
},
"devDependencies": {
"@eslint/js": "^9.6.0",
"@rollup/plugin-terser": "^0.4.4",
"@types/eslint__js": "^8.42.3",
"@types/jest": "^29.5.12",
"eslint": "^8.57.0",
"eslint-plugin-import": "^2.29.1",
"http-server": "^14.1.1",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"rimraf": "^5.0.9",
"rollup": "^4.18.1",
"ts-jest": "^29.2.2",
"typescript": "^5.5.3",
"typescript-eslint": "^7.16.0"
}
}
+33
View File
@@ -0,0 +1,33 @@
import terser from '@rollup/plugin-terser';
/**
* Regarding "(!) `this` has been rewritten to `undefined`" warning:
* It occurs because of typescript's Object.assign polyfill, which uses
* `this` on the global scope. If you set `context: 'window'` in the rollup
* config, it will silence the warning, but it will cause issues if CountUp
* is not run in the browser. Allowing rollup to rewrite this to undefined
* on just the global scope is harmless and doesn't break anything.
*/
export default [
// minified build
{
input: 'dist/countUp.js',
output: {
file: 'dist/countUp.min.js',
},
plugins: [
terser(), // minify the output
],
},
// UMD build
{
input: 'dist/countUp.js',
output: {
file: 'dist/countUp.umd.js',
name: 'countUp',
format: 'umd',
},
plugins: [
terser(),
],
}
];
+591
View File
@@ -0,0 +1,591 @@
import { CountUp, CountUpPlugin } from './countUp';
type IntersectionCallback = (entries: Partial<IntersectionObserverEntry>[]) => void;
class MockIntersectionObserver {
callback: IntersectionCallback;
elements: Element[] = [];
static instances: MockIntersectionObserver[] = [];
constructor(callback: IntersectionCallback) {
this.callback = callback;
MockIntersectionObserver.instances.push(this);
}
observe(el: Element) { this.elements.push(el); }
unobserve(el: Element) { this.elements = this.elements.filter(e => e !== el); }
disconnect() { this.elements = []; }
trigger(isIntersecting: boolean) {
this.callback(this.elements.map(target => ({ isIntersecting, target } as Partial<IntersectionObserverEntry>)));
}
}
describe('CountUp', () => {
let countUp;
let time;
const getTargetHtml = () => document.getElementById('target')?.innerHTML;
const resetRAF = () => {
time = 0;
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => {
time += 100;
if (time < 2500) {
return cb(time) as any;
}
});
};
beforeEach(() => {
document.body.innerHTML =
'<div>' +
' <h1 id="target"></h1>' +
'</div>';
(window as any).IntersectionObserver = MockIntersectionObserver;
MockIntersectionObserver.instances = [];
countUp = new CountUp('target', 100);
resetRAF();
});
describe('constructor', () => {
it('should create for a valid target, and print startVal', () => {
expect(countUp).toBeTruthy();
expect(countUp.error.length).toBe(0);
expect(getTargetHtml()).toEqual('0');
});
it('should set an error for a bad target', () => {
countUp = new CountUp('notThere', 100);
expect(countUp.error.length).toBeGreaterThan(0);
});
it('should set an error for a bad endVal', () => {
const endVal = '%' as any;
countUp = new CountUp('target', endVal);
expect(countUp.error.length).toBeGreaterThan(0);
});
it('should set an error for a bad startVal', () => {
const startVal = 'oops' as any;
countUp = new CountUp('target', 100, { startVal });
expect(countUp.error.length).toBeGreaterThan(0);
});
it('should return a value for version', () => {
expect(countUp.version).toBeTruthy();
});
it('should support getting endVal from the target element', () => {
document.body.innerHTML =
'<div>' +
' <h1 id="target">1,500</h1>' +
'</div>';
countUp = new CountUp('target');
expect(countUp.endVal).toBe(1500);
});
it('should set an error when endVal is omitted and not in target element', () => {
document.body.innerHTML =
'<div>' +
' <h1 id="target"></h1>' +
'</div>';
countUp = new CountUp('target');
expect(countUp.error.length).toBeGreaterThan(0);
});
it('should not call parse when an endVal is passed to the constructor', () => {
const parseSpy = jest.spyOn(CountUp.prototype, 'parse');
countUp = new CountUp('target', 0, { startVal: 100 });
expect(parseSpy).not.toHaveBeenCalled();
parseSpy.mockRestore();
});
});
describe('class methods', () => {
describe('# start', () => {
it('should count when start method is called', () => {
countUp.start();
expect(getTargetHtml()).toEqual('100');
});
it('should use a callback provided to start', () => {
const cb = jest.fn();
countUp.start(cb);
expect(getTargetHtml()).toEqual('100');
expect(cb).toHaveBeenCalled();
});
});
describe('# pauseResume', () => {
it('should pause when pauseResume is called', () => {
countUp.start();
countUp.pauseResume();
expect(countUp.paused).toBeTruthy();
});
});
describe('# reset', () => {
it('should reset when reset is called', () => {
countUp.start();
countUp.reset();
expect(getTargetHtml()).toEqual('0');
expect(countUp.paused).toBeTruthy();
});
});
describe('# update', () => {
it('should update when update is called', () => {
countUp.start();
expect(getTargetHtml()).toEqual('100');
resetRAF();
countUp.update(200);
expect(getTargetHtml()).toEqual('200');
});
});
describe('# onDestroy', () => {
it('should cancel a running animation', () => {
const cancelSpy = jest.spyOn(window, 'cancelAnimationFrame');
countUp.start();
countUp.onDestroy();
expect(cancelSpy).toHaveBeenCalled();
});
it('should set paused to true', () => {
countUp.start();
expect(countUp.paused).toBe(false);
countUp.onDestroy();
expect(countUp.paused).toBe(true);
});
it('should disconnect the observer', () => {
countUp = new CountUp('target', 100, { autoAnimate: true });
const observer = MockIntersectionObserver.instances[MockIntersectionObserver.instances.length - 1];
const disconnectSpy = jest.spyOn(observer, 'disconnect');
countUp.onDestroy();
expect(disconnectSpy).toHaveBeenCalled();
});
it('should clear onCompleteCallback', () => {
const cb = jest.fn();
countUp = new CountUp('target', 100, { onCompleteCallback: cb });
countUp.onDestroy();
expect(countUp.options.onCompleteCallback).toBeNull();
});
it('should clear onStartCallback', () => {
const cb = jest.fn();
countUp = new CountUp('target', 100, { onStartCallback: cb });
countUp.onDestroy();
expect(countUp.options.onStartCallback).toBeNull();
});
it('should prevent onCompleteCallback from firing after destroy', () => {
const cb = jest.fn();
countUp = new CountUp('target', 100, { onCompleteCallback: cb });
countUp.onDestroy();
resetRAF();
countUp.start();
expect(cb).not.toHaveBeenCalled();
});
it('should be safe to call on a fresh instance', () => {
countUp = new CountUp('target', 100);
expect(() => countUp.onDestroy()).not.toThrow();
expect(countUp.paused).toBe(true);
});
});
describe('# parse', () => {
it('should properly parse numbers', () => {
countUp = new CountUp('target', 0);
const result0 = countUp.parse('14,921.00123');
countUp = new CountUp('target', 0, { separator: '.', decimal: ',' });
const result1 = countUp.parse('1.500,0');
countUp = new CountUp('target', 0, { separator: ' ' });
const result2 = countUp.parse('2 800');
expect(result0).toEqual(14921.00123);
expect(result1).toEqual(1500);
expect(result2).toEqual(2800);
});
});
});
describe('various use-cases', () => {
it('should handle large numbers', () => {
countUp = new CountUp('target', 6000);
const spy = jest.spyOn(countUp, 'determineDirectionAndSmartEasing');
countUp.start();
expect(getTargetHtml()).toEqual('6,000');
expect(spy).toHaveBeenCalled();
});
it('should not use easing when specified with a large number (auto-smooth)', () => {
countUp = new CountUp('target', 6000, { useEasing: false });
const spy = jest.spyOn(countUp, 'easingFn');
countUp.start();
expect(getTargetHtml()).toEqual('6,000');
expect(spy).toHaveBeenCalledTimes(0);
});
it('should count down when endVal is less than startVal', () => {
countUp = new CountUp('target', 10, { startVal: 500 });
expect(getTargetHtml()).toEqual('500');
countUp.start();
expect(getTargetHtml()).toEqual('10');
});
it('should handle negative numbers', () => {
countUp = new CountUp('target', -500);
countUp.start();
expect(getTargetHtml()).toEqual('-500');
});
it('should properly handle a zero duration', () => {
countUp = new CountUp('target', 2000, { duration: 0 });
countUp.start();
expect(getTargetHtml()).toEqual('2,000');
});
it('should call the callback when finished if there is one', () => {
const cb = jest.fn();
countUp.start(cb);
expect(getTargetHtml()).toEqual('100');
expect(cb).toHaveBeenCalled();
});
});
describe('options', () => {
it('should respect the decimalPlaces option', () => {
countUp = new CountUp('target', 100, { decimalPlaces: 2 });
countUp.start();
expect(getTargetHtml()).toEqual('100.00');
});
it('should respect the duration option', () => {
countUp = new CountUp('target', 100, { duration: 1 });
countUp.start();
expect(getTargetHtml()).toEqual('100');
});
it('should respect the useEasing option', () => {
countUp = new CountUp('target', 100, { useEasing: false });
countUp.start();
expect(getTargetHtml()).toEqual('100');
});
it('should respect the useGrouping option', () => {
countUp = new CountUp('target', 100000, { useGrouping: false });
countUp.start();
expect(getTargetHtml()).toEqual('100000');
resetRAF();
countUp = new CountUp('target', 1000000, { useGrouping: true });
countUp.start();
expect(getTargetHtml()).toEqual('1,000,000');
});
it('should respect the useIndianSeparators option', () => {
countUp = new CountUp('target', 100000, { useIndianSeparators: true });
countUp.start();
expect(getTargetHtml()).toEqual('1,00,000');
resetRAF();
countUp = new CountUp('target', 10000000, { useIndianSeparators: true });
countUp.start();
expect(getTargetHtml()).toEqual('1,00,00,000');
});
it('should respect the separator option', () => {
countUp = new CountUp('target', 10000, { separator: ':' });
countUp.start();
expect(getTargetHtml()).toEqual('10:000');
});
it('should respect the decimal option', () => {
countUp = new CountUp('target', 100, { decimal: ',', decimalPlaces: 1 });
countUp.start();
expect(getTargetHtml()).toEqual('100,0');
});
it('should respect the easingFn option', () => {
const easeOutQuintic = jest.fn().mockReturnValue(100);
countUp = new CountUp('target', 100, { easingFn: easeOutQuintic });
countUp.start();
expect(easeOutQuintic).toHaveBeenCalled();
expect(getTargetHtml()).toEqual('100');
});
it('should respect the formattingFn option', () => {
const formatter = jest.fn().mockReturnValue('~100~');
countUp = new CountUp('target', 100, { formattingFn: formatter });
countUp.start();
expect(formatter).toHaveBeenCalled();
expect(getTargetHtml()).toEqual('~100~');
});
it('should respect the prefix option', () => {
countUp = new CountUp('target', 100, { prefix: '$' });
countUp.start();
expect(getTargetHtml()).toEqual('$100');
});
it('should respect the suffix option', () => {
countUp = new CountUp('target', 100, { suffix: '!' });
countUp.start();
expect(getTargetHtml()).toEqual('100!');
});
it('should respect the numerals option', () => {
const numerals = [')', '!', '@', '#', '$', '%', '^', '&', '*', '('];
countUp = new CountUp('target', 100, { numerals });
countUp.start();
expect(getTargetHtml()).toEqual('!))');
});
it('should respect the onCompleteCallback option', () => {
const options = { onCompleteCallback: jest.fn() };
const callbackSpy = jest.spyOn(options, 'onCompleteCallback');
countUp = new CountUp('target', 100, options);
countUp.start();
expect(getTargetHtml()).toEqual('100');
expect(callbackSpy).toHaveBeenCalled();
});
it('should respect the onStartCallback option', () => {
const options = { onStartCallback: jest.fn() };
const callbackSpy = jest.spyOn(options, 'onStartCallback');
countUp = new CountUp('target', 100, options);
countUp.start();
expect(callbackSpy).toHaveBeenCalled();
expect(getTargetHtml()).toEqual('100');
});
it('should respect the plugin option', () => {
const plugin: CountUpPlugin = {
render: (el, result) => {
el.innerHTML = result;
}
};
countUp = new CountUp('target', 1000, {
plugin,
useGrouping: true
});
countUp.start();
expect(getTargetHtml()).toEqual('1,000');
});
});
describe('autoAnimate (IntersectionObserver)', () => {
beforeEach(() => {
jest.useFakeTimers({ doNotFake: ['requestAnimationFrame'] });
});
afterEach(() => {
jest.useRealTimers();
});
it('should create an IntersectionObserver when autoAnimate is true', () => {
countUp = new CountUp('target', 100, { autoAnimate: true });
expect(MockIntersectionObserver.instances.length).toBe(1);
expect(MockIntersectionObserver.instances[0].elements).toContain(countUp.el);
});
it('should not create an observer when autoAnimate is false', () => {
MockIntersectionObserver.instances = [];
countUp = new CountUp('target', 100);
expect(MockIntersectionObserver.instances.length).toBe(0);
});
it('should start animation when element becomes visible', () => {
countUp = new CountUp('target', 100, { autoAnimate: true, autoAnimateDelay: 0 });
resetRAF();
const observer = MockIntersectionObserver.instances[0];
observer.trigger(true);
jest.advanceTimersByTime(0);
expect(getTargetHtml()).toEqual('100');
});
it('should respect autoAnimateDelay before starting', () => {
countUp = new CountUp('target', 100, { autoAnimate: true, autoAnimateDelay: 500 });
resetRAF();
const startSpy = jest.spyOn(countUp, 'start');
const observer = MockIntersectionObserver.instances[0];
observer.trigger(true);
expect(startSpy).not.toHaveBeenCalled();
jest.advanceTimersByTime(500);
expect(startSpy).toHaveBeenCalled();
});
it('should reset when element goes out of view', () => {
countUp = new CountUp('target', 100, { autoAnimate: true, autoAnimateDelay: 0 });
resetRAF();
const observer = MockIntersectionObserver.instances[0];
observer.trigger(true);
jest.advanceTimersByTime(0);
expect(getTargetHtml()).toEqual('100');
observer.trigger(false);
expect(countUp.paused).toBe(true);
expect(getTargetHtml()).toEqual('0');
});
it('should disconnect observer when autoAnimateOnce is true', () => {
countUp = new CountUp('target', 100, { autoAnimate: true, autoAnimateOnce: true, autoAnimateDelay: 0 });
const observer = MockIntersectionObserver.instances[0];
const disconnectSpy = jest.spyOn(observer, 'disconnect');
observer.trigger(true);
jest.advanceTimersByTime(0);
expect(disconnectSpy).toHaveBeenCalled();
expect(countUp.once).toBe(true);
});
it('should not disconnect observer when autoAnimateOnce is false', () => {
countUp = new CountUp('target', 100, { autoAnimate: true, autoAnimateOnce: false, autoAnimateDelay: 0 });
const observer = MockIntersectionObserver.instances[0];
const disconnectSpy = jest.spyOn(observer, 'disconnect');
observer.trigger(true);
jest.advanceTimersByTime(0);
expect(disconnectSpy).not.toHaveBeenCalled();
});
it('should not re-animate after first run when autoAnimateOnce is true', () => {
countUp = new CountUp('target', 100, { autoAnimate: true, autoAnimateOnce: true, autoAnimateDelay: 0 });
resetRAF();
const observer = MockIntersectionObserver.instances[MockIntersectionObserver.instances.length - 1];
observer.trigger(true);
jest.advanceTimersByTime(0);
expect(getTargetHtml()).toEqual('100');
// observer was disconnected so subsequent triggers process no entries
observer.trigger(false);
expect(getTargetHtml()).toEqual('100');
observer.trigger(true);
jest.advanceTimersByTime(0);
expect(getTargetHtml()).toEqual('100');
});
it('should allow re-animation after manual reset when autoAnimateOnce is true', () => {
countUp = new CountUp('target', 100, { autoAnimate: true, autoAnimateOnce: true, autoAnimateDelay: 0 });
resetRAF();
const observer = MockIntersectionObserver.instances[MockIntersectionObserver.instances.length - 1];
observer.trigger(true);
jest.advanceTimersByTime(0);
expect(getTargetHtml()).toEqual('100');
expect(countUp.once).toBe(true);
// manual reset clears the once flag
countUp.reset();
expect(getTargetHtml()).toEqual('0');
expect(countUp.once).toBe(false);
// re-observe and trigger — animation should play again
observer.observe(countUp.el);
resetRAF();
observer.trigger(true);
jest.advanceTimersByTime(0);
expect(getTargetHtml()).toEqual('100');
});
it('should support multiple independent instances', () => {
document.body.innerHTML =
'<h1 id="target1"></h1>' +
'<h1 id="target2"></h1>';
MockIntersectionObserver.instances = [];
const cu1 = new CountUp('target1', 50, { autoAnimate: true, autoAnimateDelay: 0 });
const cu2 = new CountUp('target2', 200, { autoAnimate: true, autoAnimateDelay: 0 });
expect(MockIntersectionObserver.instances.length).toBe(2);
const obs1 = MockIntersectionObserver.instances[0];
const obs2 = MockIntersectionObserver.instances[1];
expect(obs1.elements).toContain(cu1.el);
expect(obs2.elements).toContain(cu2.el);
expect(obs1).not.toBe(obs2);
resetRAF();
obs1.trigger(true);
jest.advanceTimersByTime(0);
expect(document.getElementById('target1')!.innerHTML).toEqual('50');
expect(cu2.paused).toBe(true);
});
it('should allow cleanup via unobserve()', () => {
countUp = new CountUp('target', 100, { autoAnimate: true });
const observer = MockIntersectionObserver.instances[0];
const disconnectSpy = jest.spyOn(observer, 'disconnect');
countUp.unobserve();
expect(disconnectSpy).toHaveBeenCalled();
});
it('should map deprecated enableScrollSpy to autoAnimate', () => {
countUp = new CountUp('target', 100, { enableScrollSpy: true });
expect(countUp.options.autoAnimate).toBe(true);
expect(MockIntersectionObserver.instances.length).toBe(1);
});
});
});
+425
View File
@@ -0,0 +1,425 @@
export interface CountUpOptions {
/** Number to start at @default 0 */
startVal?: number;
/** Number of decimal places @default 0 */
decimalPlaces?: number;
/** Animation duration in seconds @default 2 */
duration?: number;
/** Example: 1,000 vs 1000 @default true */
useGrouping?: boolean;
/** Example: 1,00,000 vs 100,000 @default false */
useIndianSeparators?: boolean;
/** Ease animation @default true */
useEasing?: boolean;
/** Smooth easing for large numbers above this if useEasing @default 999 */
smartEasingThreshold?: number;
/** Amount to be eased for numbers above threshold @default 333 */
smartEasingAmount?: number;
/** Grouping separator @default ',' */
separator?: string;
/** Decimal character @default '.' */
decimal?: string;
/** Easing function for animation @default easeOutExpo */
easingFn?: (t: number, b: number, c: number, d: number) => number;
/** Custom function to format the result */
formattingFn?: (n: number) => string;
/** Text prepended to result */
prefix?: string;
/** Text appended to result */
suffix?: string;
/** Numeral glyph substitution */
numerals?: string[];
/** Callback called when animation completes */
onCompleteCallback?: () => any;
/** Callback called when animation starts */
onStartCallback?: () => any;
/** Plugin for alternate animations */
plugin?: CountUpPlugin;
/** Trigger animation when target becomes visible @default false */
autoAnimate?: boolean;
/** Animation delay in ms after auto-animate triggers @default 200 */
autoAnimateDelay?: number;
/** Run animation only once for auto-animate triggers @default false */
autoAnimateOnce?: boolean;
/** @deprecated Please use autoAnimate instead */
enableScrollSpy?: boolean;
/** @deprecated Please use autoAnimateDelay instead */
scrollSpyDelay?: number;
/** @deprecated Please use autoAnimateOnce instead */
scrollSpyOnce?: boolean;
}
export declare interface CountUpPlugin {
render(elem: HTMLElement, formatted: string): void;
}
/**
* Animates a number by counting to it.
* playground: stackblitz.com/edit/countup-typescript
*
* @param target - id of html element, input, svg text element, or DOM element reference where counting occurs.
* @param endVal - the value you want to arrive at.
* @param options - optional configuration object for fine-grain control
*/
export class CountUp {
version = '2.10.0';
private static observedElements = new WeakMap<HTMLElement, CountUp>();
private defaults: CountUpOptions = {
startVal: 0,
decimalPlaces: 0,
duration: 2,
useEasing: true,
useGrouping: true,
useIndianSeparators: false,
smartEasingThreshold: 999,
smartEasingAmount: 333,
separator: ',',
decimal: '.',
prefix: '',
suffix: '',
autoAnimate: false,
autoAnimateDelay: 200,
autoAnimateOnce: false,
};
private rAF: any;
private autoAnimateTimeout: any;
private startTime: number;
private remaining: number;
private finalEndVal: number = null; // for smart easing
private useEasing = true;
private countDown = false;
private observer: IntersectionObserver;
el: HTMLElement | HTMLInputElement;
formattingFn: (num: number) => string;
easingFn?: (t: number, b: number, c: number, d: number) => number;
error = '';
startVal = 0;
duration: number;
paused = true;
frameVal: number;
once = false;
constructor(
target: string | HTMLElement | HTMLInputElement,
private endVal?: number | null,
public options?: CountUpOptions
) {
this.options = {
...this.defaults,
...options
};
if (this.options.enableScrollSpy) {
this.options.autoAnimate = true;
}
if (this.options.scrollSpyDelay !== undefined) {
this.options.autoAnimateDelay = this.options.scrollSpyDelay;
}
if (this.options.scrollSpyOnce) {
this.options.autoAnimateOnce = true;
}
this.formattingFn = (this.options.formattingFn) ?
this.options.formattingFn : this.formatNumber;
this.easingFn = (this.options.easingFn) ?
this.options.easingFn : this.easeOutExpo;
this.el = (typeof target === 'string') ? document.getElementById(target) : target;
endVal = endVal == null ? this.parse(this.el.innerHTML) : endVal;
this.startVal = this.validateValue(this.options.startVal);
this.frameVal = this.startVal;
this.endVal = this.validateValue(endVal);
this.options.decimalPlaces = Math.max(0 || this.options.decimalPlaces);
this.resetDuration();
this.options.separator = String(this.options.separator);
this.useEasing = this.options.useEasing;
if (this.options.separator === '') {
this.options.useGrouping = false;
}
if (this.el) {
this.printValue(this.startVal);
} else {
this.error = '[CountUp] target is null or undefined';
}
if (typeof window !== 'undefined' && this.options.autoAnimate) {
if (!this.error && typeof IntersectionObserver !== 'undefined') {
this.setupObserver();
} else {
if (this.error) {
console.error(this.error, target);
} else {
console.error('IntersectionObserver is not supported by this browser');
}
}
}
}
/** Set up an IntersectionObserver to auto-animate when the target element appears. */
private setupObserver(): void {
const existing = CountUp.observedElements.get(this.el as HTMLElement);
if (existing) {
existing.unobserve();
}
CountUp.observedElements.set(this.el as HTMLElement, this);
this.observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting && this.paused && !this.once) {
this.paused = false;
this.autoAnimateTimeout = setTimeout(() => this.start(), this.options.autoAnimateDelay);
if (this.options.autoAnimateOnce) {
this.once = true;
this.observer.disconnect();
}
} else if (!entry.isIntersecting && !this.paused) {
clearTimeout(this.autoAnimateTimeout);
this.reset();
}
}
}, { threshold: 0 });
this.observer.observe(this.el);
}
/** Disconnect the IntersectionObserver and stop watching this element. */
unobserve(): void {
clearTimeout(this.autoAnimateTimeout);
this.observer?.disconnect();
CountUp.observedElements.delete(this.el as HTMLElement);
}
/** Teardown: cancel animation, disconnect observer, clear callbacks. */
onDestroy(): void {
clearTimeout(this.autoAnimateTimeout);
cancelAnimationFrame(this.rAF);
this.paused = true;
this.unobserve();
this.options.onCompleteCallback = null;
this.options.onStartCallback = null;
}
/**
* Smart easing works by breaking the animation into 2 parts, the second part being the
* smartEasingAmount and first part being the total amount minus the smartEasingAmount. It works
* by disabling easing for the first part and enabling it on the second part. It is used if
* useEasing is true and the total animation amount exceeds the smartEasingThreshold.
*/
private determineDirectionAndSmartEasing(): void {
const end = (this.finalEndVal) ? this.finalEndVal : this.endVal;
this.countDown = (this.startVal > end);
const animateAmount = end - this.startVal;
if (Math.abs(animateAmount) > this.options.smartEasingThreshold && this.options.useEasing) {
this.finalEndVal = end;
const up = (this.countDown) ? 1 : -1;
this.endVal = end + (up * this.options.smartEasingAmount);
this.duration = this.duration / 2;
} else {
this.endVal = end;
this.finalEndVal = null;
}
if (this.finalEndVal !== null) {
// setting finalEndVal indicates smart easing
this.useEasing = false;
} else {
this.useEasing = this.options.useEasing;
}
}
/** Start the animation. Optionally pass a callback that fires on completion. */
start(callback?: (args?: any) => any): void {
if (this.error) {
return;
}
if (this.options.onStartCallback) {
this.options.onStartCallback();
}
if (callback) {
this.options.onCompleteCallback = callback;
}
if (this.duration > 0) {
this.determineDirectionAndSmartEasing();
this.paused = false;
this.rAF = requestAnimationFrame(this.count);
} else {
this.printValue(this.endVal);
}
}
/** Toggle pause/resume on the animation. */
pauseResume(): void {
if (!this.paused) {
cancelAnimationFrame(this.rAF);
} else {
this.startTime = null;
this.duration = this.remaining;
this.startVal = this.frameVal;
this.determineDirectionAndSmartEasing();
this.rAF = requestAnimationFrame(this.count);
}
this.paused = !this.paused;
}
/** Reset to startVal so the animation can be run again. */
reset(): void {
clearTimeout(this.autoAnimateTimeout);
cancelAnimationFrame(this.rAF);
this.paused = true;
this.once = false;
this.resetDuration();
this.startVal = this.validateValue(this.options.startVal);
this.frameVal = this.startVal;
this.printValue(this.startVal);
}
/** Pass a new endVal and start the animation. */
update(newEndVal: string | number): void {
cancelAnimationFrame(this.rAF);
this.startTime = null;
this.endVal = this.validateValue(newEndVal);
if (this.endVal === this.frameVal) {
return;
}
this.startVal = this.frameVal;
if (this.finalEndVal == null) {
this.resetDuration();
}
this.finalEndVal = null;
this.determineDirectionAndSmartEasing();
this.rAF = requestAnimationFrame(this.count);
}
/** Animation frame callback — advances the value each frame. */
count = (timestamp: number): void => {
if (!this.startTime) { this.startTime = timestamp; }
const progress = timestamp - this.startTime;
this.remaining = this.duration - progress;
// to ease or not to ease
if (this.useEasing) {
if (this.countDown) {
this.frameVal = this.startVal - this.easingFn(progress, 0, this.startVal - this.endVal, this.duration);
} else {
this.frameVal = this.easingFn(progress, this.startVal, this.endVal - this.startVal, this.duration);
}
} else {
this.frameVal = this.startVal + (this.endVal - this.startVal) * (progress / this.duration);
}
// don't go past endVal since progress can exceed duration in the last frame
const wentPast = this.countDown ? this.frameVal < this.endVal : this.frameVal > this.endVal;
this.frameVal = wentPast ? this.endVal : this.frameVal;
// decimal
this.frameVal = Number(this.frameVal.toFixed(this.options.decimalPlaces));
// format and print value
this.printValue(this.frameVal);
// whether to continue
if (progress < this.duration) {
this.rAF = requestAnimationFrame(this.count);
} else if (this.finalEndVal !== null) {
// smart easing
this.update(this.finalEndVal);
} else {
if (this.options.onCompleteCallback) {
this.options.onCompleteCallback();
}
}
}
/** Format and render the given value to the target element. */
printValue(val: number): void {
if (!this.el) return;
const result = this.formattingFn(val);
if (this.options.plugin?.render) {
this.options.plugin.render(this.el, result);
return;
}
if (this.el.tagName === 'INPUT') {
const input = this.el as HTMLInputElement;
input.value = result;
} else if (this.el.tagName === 'text' || this.el.tagName === 'tspan') {
this.el.textContent = result;
} else {
this.el.innerHTML = result;
}
}
/** Return true if the value is a finite number. */
ensureNumber(n: any): boolean {
return (typeof n === 'number' && !isNaN(n));
}
/** Validate and convert a value to a number, setting an error if invalid. */
validateValue(value: string | number): number {
const newValue = Number(value);
if (!this.ensureNumber(newValue)) {
this.error = `[CountUp] invalid start or end value: ${value}`;
return null;
} else {
return newValue;
}
}
/** Reset startTime, duration, and remaining to their initial values. */
private resetDuration(): void {
this.startTime = null;
this.duration = Number(this.options.duration) * 1000;
this.remaining = this.duration;
}
/** Default number formatter with grouping, decimals, prefix/suffix, and numeral substitution. */
formatNumber = (num: number): string => {
const neg = (num < 0) ? '-' : '';
let result: string, x1: string, x2: string, x3: string;
result = Math.abs(num).toFixed(this.options.decimalPlaces);
result += '';
const x = result.split('.');
x1 = x[0];
x2 = x.length > 1 ? this.options.decimal + x[1] : '';
if (this.options.useGrouping) {
x3 = '';
let factor = 3, j = 0;
for (let i = 0, len = x1.length; i < len; ++i) {
if (this.options.useIndianSeparators && i === 4) {
factor = 2;
j = 1;
}
if (i !== 0 && (j % factor) === 0) {
x3 = this.options.separator + x3;
}
j++;
x3 = x1[len - i - 1] + x3;
}
x1 = x3;
}
// optional numeral substitution
if (this.options.numerals && this.options.numerals.length) {
x1 = x1.replace(/[0-9]/g, (w) => this.options.numerals[+w]);
x2 = x2.replace(/[0-9]/g, (w) => this.options.numerals[+w]);
}
return neg + this.options.prefix + x1 + x2 + this.options.suffix;
}
/**
* Default easing function (easeOutExpo).
* @param t current time
* @param b beginning value
* @param c change in value
* @param d duration
*/
easeOutExpo = (t: number, b: number, c: number, d: number): number =>
c * (-Math.pow(2, -10 * t / d) + 1) * 1024 / 1023 + b;
/** Parse a formatted string back to a number using the current separator/decimal options. */
parse(number: string): number {
// eslint-disable-next-line no-irregular-whitespace
const escapeRegExp = (s: string) => s.replace(/([.,'  ])/g, '\\$1');
const sep = escapeRegExp(this.options.separator);
const dec = escapeRegExp(this.options.decimal);
const num = number.replace(new RegExp(sep, 'g'), '').replace(new RegExp(dec, 'g'), '.');
return parseFloat(num)
}
}
+19
View File
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"lib": ["es2017", "dom"],
"module": "esnext",
"moduleResolution": "node",
"declaration": true,
"outDir": "dist",
"target": "es5",
"pretty": true,
"esModuleInterop": true,
"skipLibCheck": true,
},
"compileOnSave": true,
"include": ["src"],
"exclude": [
"node_modules",
"**/*.spec.ts"
]
}