├── .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 | [](https://www.npmjs.com/package/rsup-progress)
9 | [](https://bundlephobia.com/result?p=rsup-progress)
10 | [](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 |
20 |
21 | Rsup
22 | Progress
23 |
24 | A lightweight (1KB) progress bar with promise support
25 |
26 |
View on Github
29 |
30 |
31 | progress.start()
32 |
33 |
34 | progress.end()
35 |
36 |
37 | progress.end(true )
38 |
39 |
40 | progress.promise(delay(300) )
41 |
42 |
46 | progress.promise(delay(3000) , { waitAnimation: true })
49 |
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 |
--------------------------------------------------------------------------------