├── .editorconfig ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── logo.png ├── package.json ├── src └── index.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json ├── tsconfig.json ├── vite.config.ts └── website ├── app.ts ├── favicon.ico ├── index.html ├── logo.svg └── style.scss /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 4 5 | indent_style = space 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | .*/ 5 | !.github/ 6 | *-lock.yaml 7 | *-lock.json 8 | *.lock 9 | *.log 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": false, 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "tabWidth": 4 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | ----------- 3 | 4 | Copyright (c) 2019 skt-t1-byungi 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

5 | 6 | > A lightweight (1KB) progress bar with promise support 7 | 8 | [![npm](https://flat.badgen.net/npm/v/rsup-progress)](https://www.npmjs.com/package/rsup-progress) 9 | [![npm](https://flat.badgen.net/bundlephobia/minzip/rsup-progress)](https://bundlephobia.com/result?p=rsup-progress) 10 | [![npm](https://flat.badgen.net/npm/license/rsup-progress)](https://github.com/skt-t1-byungi/rsup-progress/blob/master/LICENSE) 11 | 12 | The progress bar starts quickly but decelerates over time. Invoke the `end` function to finish the animation, providing a natural user experience without an exact percentage of progress. 13 | 14 | https://skt-t1-byungi.github.io/rsup-progress/ 15 | 16 | ## Example 17 | 18 | Using `start` and `end` methods. 19 | 20 | ```js 21 | progress.start() 22 | 23 | fetch('/data.json').then(response => { 24 | progress.end() 25 | }) 26 | ``` 27 | 28 | Using `promise` method. 29 | 30 | ```js 31 | const response = await progress.promise(fetch('/data.json')) 32 | ``` 33 | 34 | ## Install 35 | 36 | ```sh 37 | npm install rsup-progress 38 | ``` 39 | 40 | ```js 41 | import { Progress } from 'rsup-progress' 42 | ``` 43 | 44 | ### Browser ESM 45 | 46 | ```html 47 | 51 | ``` 52 | 53 | ## API 54 | 55 | ### new Progress(options?) 56 | 57 | Create an instance. 58 | 59 | ```js 60 | const progress = new Progress({ 61 | height: 5, 62 | color: '#33eafd', 63 | }) 64 | ``` 65 | 66 | #### options 67 | 68 | - `height` - Progress bar height. Default is `4px`. 69 | - `className` - `class` attribute for the progress bar. 70 | - `color` - Progress bar color. Default is `#ff1a59`. 71 | - `container` - Element to append a progress bar. Default is `document.body`. 72 | - `maxWidth` - Maximum width before completion. Default is `99.8%`. 73 | - `position` - Position to be placed. Default is `top` (There are `top`, `bottom`, `none`). 74 | - `duration` - Time to reach maxWidth. Default is `60000`(ms). 75 | - `hideDuration` - Time to hide when completion. Default is `400`(ms). 76 | - `zIndex` - CSS z-index property. Default is `9999`. 77 | - `timing` - CSS animation timing function. Default is `cubic-bezier(0,1,0,1)`. 78 | 79 | ### progress.setOptions(options) 80 | 81 | Change the options. 82 | 83 | ```js 84 | progress.setOptions({ 85 | color: 'red', 86 | className: 'class1 class2', 87 | }) 88 | ``` 89 | 90 | ### progress.isInProgress 91 | 92 | Check whether the progress bar is active. 93 | 94 | ```js 95 | console.log(progress.isInProgress) // => false 96 | 97 | progress.start() 98 | 99 | console.log(progress.isInProgress) // => true 100 | ``` 101 | 102 | ### progress.start() 103 | 104 | Activate the progress bar. 105 | 106 | ### progress.end(immediately = false) 107 | 108 | Complete the progress bar. If `immediately` is set to true, the element is removed instantly. 109 | 110 | ### progress.promise(promise, options?) 111 | 112 | Automatically call start and end methods based on the given promise. 113 | 114 | ```js 115 | const response = await progress.promise(fetch('/data.json')) 116 | ``` 117 | 118 | #### options.min 119 | 120 | Minimum time to display and maintain the progress bar. Default is `100` ms. If `0` is set and the promise is already resolved, the progress bar won't appear. 121 | 122 | ```js 123 | progress.promise(Promise.resolve(), { min: 0 }) // => Progress bar does not appear. 124 | ``` 125 | 126 | #### options.delay 127 | 128 | If `options.delay` is set, the progress bar will start after the specified delay. 129 | 130 | ```js 131 | progress.promise(delay(500), { delay: 200 }) // => It starts 200ms later. 132 | ``` 133 | 134 | If the promise resolves before the delay, the progress bar won't appear. 135 | 136 | ```js 137 | progress.promise(delay(500), { delay: 600 }) // => Progress bar does not appear. 138 | ``` 139 | 140 | This is useful to prevent "flashing" of the progress bar for short-lived promises. 141 | 142 | #### options.waitAnimation 143 | 144 | If `options.waitAnimation` is set, the returned promise waits for the hide animation to complete. 145 | 146 | ```js 147 | await progress.promise(fetch('/data.json'), { waitAnimation: true }) 148 | 149 | alert('Complete!') 150 | ``` 151 | 152 | Useful for immediate actions like `alert` or `confirm`. Default is `false`. 153 | 154 | ## License 155 | 156 | MIT License ❤️📝 skt-t1-byungi 157 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skt-t1-byungi/rsup-progress/065d2c05b41089401ed22c8ae55a4f8cf95c9a77/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rsup-progress", 3 | "description": "A lightweight (1KB) progress bar with promise support", 4 | "version": "3.2.0", 5 | "repository": "https://github.com/skt-t1-byungi/rsup-progress.git", 6 | "author": "skt-t1-byungi ", 7 | "files": [ 8 | "dist/" 9 | ], 10 | "workspaces": [ 11 | "website/" 12 | ], 13 | "source": "src/index.ts", 14 | "main": "dist/cjs/index.js", 15 | "module": "dist/esm/index.js", 16 | "types": "dist/esm/index.d.ts", 17 | "exports": { 18 | ".": { 19 | "require": "./dist/cjs/index.js", 20 | "import": "./dist/esm/index.js" 21 | } 22 | }, 23 | "keywords": [ 24 | "progress", 25 | "progressbar", 26 | "loader", 27 | "promise", 28 | "1kb" 29 | ], 30 | "license": "MIT", 31 | "scripts": { 32 | "dev": "vite", 33 | "test": "echo skip", 34 | "build": "rm -rf dist && npm run esm && npm run cjs", 35 | "esm": "tsc -p tsconfig.esm.json", 36 | "cjs": "tsc -p tsconfig.cjs.json", 37 | "website": "vite build", 38 | "deploy": "rm -rf dist && npm run website && gh-pages -d dist", 39 | "prepublishOnly": "npm run test && npm run build" 40 | }, 41 | "engines": { 42 | "node": ">= 6" 43 | }, 44 | "devDependencies": { 45 | "@vitejs/plugin-react": "^4.2.1", 46 | "gh-pages": "^6.1.1", 47 | "include-media": "^2.0.0", 48 | "minireset.css": "^0.0.7", 49 | "prettier": "^3.2.5", 50 | "sass": "^1.75.0", 51 | "thejungle": "^3.0.0", 52 | "typescript": "^5.4.5", 53 | "vite": "^5.2.10" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export interface ProgressOptions { 2 | maxWidth?: number | string 3 | height?: number | string 4 | duration?: number 5 | hideDuration?: number 6 | zIndex?: number | string 7 | className?: string 8 | color?: string 9 | timing?: string 10 | position?: 'top' | 'bottom' | 'none' 11 | container?: HTMLElement 12 | } 13 | 14 | const STATE = { 15 | DISAPPEAR: 0, 16 | NONE: 1, 17 | APPEAR: 2, 18 | PENDING: 3, 19 | DISAPPEAR_RESTART: 4, 20 | } as const 21 | 22 | const PERSIST_TIME = 150 23 | 24 | export class Progress { 25 | private _el = document.createElement('div') 26 | private _state = STATE.NONE as (typeof STATE)[keyof typeof STATE] 27 | private _opts = { 28 | maxWidth: '99.8%', 29 | height: '4px', 30 | duration: 60_000, 31 | hideDuration: 400, 32 | zIndex: '9999', 33 | color: '#ff1a59', 34 | className: '', 35 | timing: 'cubic-bezier(0,1,0,1)', 36 | position: 'top', 37 | container: document.body, 38 | } 39 | private _appearRafId: number | null = null 40 | private _disappearTid: ReturnType | null = null 41 | private _pendingPromises: Promise[] = [] 42 | private _delayTimers: ReturnType[] = [] 43 | private _detachListeners: (() => void)[] = [] 44 | 45 | constructor(options: ProgressOptions = {}) { 46 | this.setOptions(options) 47 | } 48 | 49 | setOptions(newOptions: ProgressOptions) { 50 | const opts = assign(this._opts, newOptions) 51 | 52 | if (!isNaN(opts.maxWidth as any)) (opts.maxWidth as any) += 'px' 53 | if (!isNaN(opts.height as any)) (opts.height as any) += 'px' 54 | opts.zIndex = String(opts.zIndex) 55 | 56 | this._el.className = opts.className 57 | this._css( 58 | assign( 59 | { 60 | height: opts.height, 61 | background: opts.color, 62 | zIndex: opts.zIndex, 63 | position: '', 64 | left: '', 65 | top: '', 66 | bottom: '', 67 | }, 68 | { 69 | top: { 70 | position: 'fixed', 71 | top: '0', 72 | left: '0', 73 | }, 74 | bottom: { 75 | position: 'fixed', 76 | bottom: '0', 77 | left: '0', 78 | }, 79 | }[opts.position], 80 | ), 81 | ) 82 | } 83 | 84 | private _css(style: Partial) { 85 | assign(this._el.style, style) 86 | } 87 | 88 | get isInProgress() { 89 | return this._state !== 0 90 | } 91 | 92 | start() { 93 | switch (this._state) { 94 | case STATE.APPEAR: 95 | case STATE.PENDING: 96 | case STATE.DISAPPEAR_RESTART: 97 | return 98 | case STATE.DISAPPEAR: 99 | this._state = STATE.DISAPPEAR_RESTART 100 | return 101 | } 102 | this._state = STATE.APPEAR 103 | 104 | const opts = this._opts 105 | const transition = `width ${opts.duration}ms ${opts.timing}` 106 | this._css({ 107 | width: '0', 108 | opacity: '1', 109 | transition, 110 | webkitTransition: transition, 111 | }) 112 | opts.container.appendChild(this._el) 113 | 114 | this._appearRafId = requestAnimationFrame(() => { 115 | this._appearRafId = requestAnimationFrame(() => { 116 | this._appearRafId = null 117 | this._state = STATE.PENDING 118 | this._css({ width: this._opts.maxWidth }) 119 | }) 120 | }) 121 | } 122 | 123 | end(immediately = false) { 124 | this._pendingPromises = [] 125 | this._delayTimers.splice(0).forEach(clearTimeout) 126 | 127 | switch (this._state) { 128 | case STATE.NONE: 129 | return 130 | case STATE.APPEAR: 131 | this._state = STATE.NONE 132 | cancelAnimationFrame(this._appearRafId) 133 | this._appearRafId = null 134 | this._detach() 135 | return 136 | case STATE.DISAPPEAR: 137 | case STATE.DISAPPEAR_RESTART: 138 | if (immediately) { 139 | this._state = STATE.NONE 140 | clearTimeout(this._disappearTid) 141 | this._disappearTid = null 142 | this._detach() 143 | } else { 144 | this._state = STATE.DISAPPEAR 145 | } 146 | return 147 | } 148 | 149 | if (immediately) { 150 | this._state = STATE.NONE 151 | this._detach() 152 | return 153 | } 154 | this._state = STATE.DISAPPEAR 155 | 156 | const opts = this._opts 157 | const transition = `width 50ms, opacity ${opts.hideDuration}ms ${PERSIST_TIME}ms` 158 | this._css({ 159 | width: '100%', 160 | opacity: '0', 161 | transition, 162 | webkitTransition: transition, 163 | }) 164 | 165 | this._disappearTid = setTimeout(() => { 166 | this._disappearTid = null 167 | const restart = this._state === STATE.DISAPPEAR_RESTART 168 | this._state = STATE.NONE 169 | 170 | this._detach() 171 | if (restart) { 172 | this.start() 173 | } 174 | }, opts.hideDuration + PERSIST_TIME) 175 | } 176 | 177 | private _detach() { 178 | this._detachListeners.splice(0).forEach(fn => fn()) 179 | this._el.parentNode?.removeChild(this._el) 180 | } 181 | 182 | promise(p: Promise, { delay = 0, min = 100, waitAnimation = false } = {}) { 183 | let delayTid: ReturnType | null 184 | 185 | const start = () => { 186 | if (min > 0) { 187 | p = Promise.all([p, new Promise(res => setTimeout(res, min))]).then(([v]) => v) 188 | } 189 | this._pendingPromises.push(p) 190 | this.start() 191 | } 192 | 193 | const clearDelayTimer = () => { 194 | const timers = this._delayTimers 195 | timers.splice(timers.indexOf(delayTid) >>> 0, 1) 196 | delayTid = null 197 | } 198 | 199 | if (delay > 0) { 200 | delayTid = setTimeout(() => { 201 | clearDelayTimer() 202 | start() 203 | }, delay) 204 | this._delayTimers.push(delayTid) 205 | } else { 206 | start() 207 | } 208 | 209 | const next = (val: T | Promise) => { 210 | if (delayTid) { 211 | clearTimeout(delayTid) 212 | clearDelayTimer() 213 | return val 214 | } 215 | 216 | const ret = 217 | waitAnimation && this._state !== STATE.NONE 218 | ? new Promise(r => this._detachListeners.push(r)).then(() => val) 219 | : val 220 | 221 | const arr = this._pendingPromises 222 | const idx = arr.indexOf(p) 223 | if (~idx) { 224 | arr.splice(idx, 1) 225 | if (arr.length === 0) this.end() 226 | } 227 | 228 | return ret 229 | } 230 | 231 | return p.then(next, err => next(Promise.reject(err))) 232 | } 233 | } 234 | 235 | function assign(target: T1, src: T2): T1 & T2 { 236 | for (const k in src) { 237 | if (Object.prototype.hasOwnProperty.call(src, k)) (target as any)[k] = src[k] 238 | } 239 | return target as T1 & T2 240 | } 241 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "outDir": "dist/cjs" 6 | }, 7 | "include": ["./src"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist/esm" 5 | }, 6 | "include": ["./src"] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "ES2015", 5 | "lib": ["DOM", "ES2015"], 6 | "declaration": true, 7 | "baseUrl": ".", 8 | "paths": { 9 | "rsup-progress": ["./src/index.ts"] 10 | } 11 | }, 12 | "include": ["src/", "website/"] 13 | } 14 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | root: 'website', 7 | }) 8 | -------------------------------------------------------------------------------- /website/app.ts: -------------------------------------------------------------------------------- 1 | import { Progress } from 'rsup-progress' 2 | ;(window as any).progress = new Progress({ height: 5 }) 3 | ;(window as any).delay = (ms: number) => new Promise(res => setTimeout(res, ms)) 4 | -------------------------------------------------------------------------------- /website/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skt-t1-byungi/rsup-progress/065d2c05b41089401ed22c8ae55a4f8cf95c9a77/website/favicon.ico -------------------------------------------------------------------------------- /website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Rsup Progress 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

19 | logo 20 |
21 | Rsup 22 | Progress 23 |
24 |

A lightweight (1KB) progress bar with promise support

25 |

26 | View on Github 29 |
30 | 33 | 36 | 39 | 42 | 50 |
51 |
52 |
53 |
54 |

55 | The progress bar starts quickly but decelerates over time. Invoke the 56 | end function to finish the animation. 57 |

58 |

Providing a natural user experience without an exact percentage of progress.

59 |
60 |
MIT License ❤️📝 skt-t1-byungi
61 |
62 | 63 | 64 | -------------------------------------------------------------------------------- /website/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /website/style.scss: -------------------------------------------------------------------------------- 1 | @use 'minireset.css/minireset.sass'; 2 | @use 'include-media/dist/include-media' as *; 3 | @use 'thejungle' as *; 4 | 5 | ::-moz-selection { 6 | background: #f0a4a7; 7 | } 8 | ::selection { 9 | background: #f0a4a7; 10 | } 11 | 12 | html, 13 | body { 14 | background: #cfc4c3; 15 | text-align: center; 16 | font-family: Arial, Helvetica, sans-serif; 17 | font-size: 16px; 18 | 19 | @include media('<=tablet') { 20 | font-size: 14px; 21 | } 22 | @include media('<=phone') { 23 | font-size: 12px; 24 | } 25 | } 26 | 27 | .top { 28 | font-size: 1rem; 29 | padding: 8vh em(20px) em(60px); 30 | background: #f9f8f2; 31 | 32 | &__logo { 33 | margin-bottom: em(60px); 34 | } 35 | 36 | &__github { 37 | font-size: rem(18px); 38 | margin-bottom: em(50px, 18px); 39 | } 40 | 41 | &__btns { 42 | margin: 0 auto; 43 | display: flex; 44 | justify-content: center; 45 | max-width: em(1000px); 46 | flex-wrap: wrap; 47 | 48 | @include media('<=tablet') { 49 | flex-flow: column; 50 | } 51 | } 52 | 53 | &__btn { 54 | font-size: em(18px); 55 | margin: em(5px, 18px); 56 | cursor: pointer; 57 | } 58 | } 59 | 60 | .logo { 61 | color: #2d2f32; 62 | font-size: 1rem; 63 | 64 | @include media('<=tablet') { 65 | font-size: 0.85rem; 66 | } 67 | 68 | &__img { 69 | width: em(160px); 70 | margin-bottom: em(10px); 71 | } 72 | 73 | &__title { 74 | font-family: 'Josefin Sans', sans-serif; 75 | display: flex; 76 | justify-content: center; 77 | margin-bottom: em(20px); 78 | } 79 | 80 | &__txt1 { 81 | font-size: em(72px); 82 | font-weight: bold; 83 | } 84 | 85 | &__txt2 { 86 | font-size: em(48px); 87 | letter-spacing: 0.275em; 88 | margin-left: em(-70px, 48px); 89 | margin-top: em(80px, 48px); 90 | } 91 | 92 | &__desc { 93 | color: #707271; 94 | font-size: em(24px); 95 | letter-spacing: 0.06em; 96 | } 97 | } 98 | 99 | .full-btn { 100 | text-decoration: none; 101 | color: #aa999d; 102 | background: #5a4545; 103 | padding: em(12px) em(28px); 104 | border-radius: #{em(20px)} / 50%; 105 | display: inline-block; 106 | box-shadow: 1px 1px 10px 1px rgba(black, 0.3); 107 | transition: background 0.2s; 108 | 109 | &__bold { 110 | font-weight: bold; 111 | color: white; 112 | } 113 | 114 | &:hover { 115 | background: darken(#5a4545, 8); 116 | transition-duration: 0.4s; 117 | } 118 | } 119 | 120 | .gh-btn { 121 | -webkit-appearance: none; 122 | appearance: none; 123 | border: 2px solid #574142; 124 | background: transparent; 125 | color: #604b4c; 126 | padding: em(14px) em(12px); 127 | outline: none; 128 | transition: background 0.2s; 129 | 130 | &:hover { 131 | background: #e3d8b9; 132 | transition-duration: 0.4s; 133 | text-decoration: underline; 134 | } 135 | 136 | &__bold { 137 | font-weight: bold; 138 | } 139 | &__param { 140 | color: #0d9b8c; 141 | } 142 | } 143 | 144 | .bottom { 145 | text-align: center; 146 | color: #7e696d; 147 | font-size: 1rem; 148 | padding: em(40px) em(20px); 149 | 150 | &__txt { 151 | margin: 0 auto; 152 | line-height: 1.5; 153 | font-size: em(18px); 154 | margin-bottom: em(50px); 155 | } 156 | &__em { 157 | background: #f4c6c5; 158 | padding: 0 em(4px); 159 | font-weight: bold; 160 | } 161 | &__license { 162 | font-size: em(18px); 163 | } 164 | &__author { 165 | font-size: em(18px); 166 | font-family: 'Josefin Sans', sans-serif; 167 | color: darken(#7e696d, 8); 168 | } 169 | } 170 | --------------------------------------------------------------------------------