Skip to content

Commit 5283c20

Browse files
committed
feat: add loading buttons
1 parent ee68ee3 commit 5283c20

File tree

5 files changed

+365
-0
lines changed

5 files changed

+365
-0
lines changed

js/index.esm.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Button from './src/button'
1010
import Carousel from './src/carousel'
1111
import Collapse from './src/collapse'
1212
import Dropdown from './src/dropdown'
13+
import LoadingButton from './src/loading-button'
1314
import Modal from './src/modal'
1415
import MultiSelect from './src/multi-select'
1516
import Navigation from './src/navigation'
@@ -26,6 +27,7 @@ export {
2627
Carousel,
2728
Collapse,
2829
Dropdown,
30+
LoadingButton,
2931
Modal,
3032
MultiSelect,
3133
Navigation,

js/index.umd.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Button from './src/button'
1010
import Carousel from './src/carousel'
1111
import Collapse from './src/collapse'
1212
import Dropdown from './src/dropdown'
13+
import LoadingButton from './src/loading-button'
1314
import Modal from './src/modal'
1415
import MultiSelect from './src/multi-select'
1516
import Navigation from './src/navigation'
@@ -26,6 +27,7 @@ export default {
2627
Carousel,
2728
Collapse,
2829
Dropdown,
30+
LoadingButton,
2931
Modal,
3032
MultiSelect,
3133
Navigation,

js/src/loading-button.js

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
/**
2+
* --------------------------------------------------------------------------
3+
* CoreUI (v4.0.0-alpha.0): alert.js
4+
* Licensed under MIT (https://coreui.io/license)
5+
*
6+
* This component is a modified version of the Bootstrap's alert.js
7+
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
8+
* --------------------------------------------------------------------------
9+
*/
10+
11+
import {
12+
defineJQueryPlugin,
13+
emulateTransitionEnd,
14+
getTransitionDurationFromElement,
15+
typeCheckConfig
16+
} from './util/index'
17+
import Data from './dom/data'
18+
import EventHandler from './dom/event-handler'
19+
import Manipulator from './dom/manipulator'
20+
// import SelectorEngine from './dom/selector-engine'
21+
import BaseComponent from './base-component'
22+
23+
/**
24+
* ------------------------------------------------------------------------
25+
* Constants
26+
* ------------------------------------------------------------------------
27+
*/
28+
29+
const NAME = 'loading-button'
30+
const DATA_KEY = 'coreui.loading-button'
31+
const EVENT_KEY = `.${DATA_KEY}`
32+
// const DATA_API_KEY = '.data-api'
33+
34+
const MAX_PERCENT = 100
35+
const MILLISECONDS = 10
36+
const PROGRESS_BAR_BG_COLOR_LIGHT = 'rgba(255, 255, 255, .2)'
37+
const PROGRESS_BAR_BG_COLOR_DARK = 'rgba(0, 0, 0, .2)'
38+
39+
// const SELECTOR_COMPONENT = '[data-coreui="loading-button"]'
40+
41+
const EVENT_START = `start${EVENT_KEY}`
42+
const EVENT_STOP = `stop${EVENT_KEY}`
43+
const EVENT_COMPLETE = `complete${EVENT_KEY}`
44+
// const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
45+
46+
const CLASS_NAME_LOADING_BUTTON_LOADING = 'loading-button-loading'
47+
const CLASS_NAME_LOADING_BUTTON_PROGRESS = 'loading-button-progress'
48+
const CLASS_NAME_LOADING_BUTTON_SPINNER = 'loading-button-spinner'
49+
50+
const Default = {
51+
percent: 0,
52+
progress: false,
53+
spinner: true,
54+
spinnerType: 'border',
55+
timeout: 1000
56+
}
57+
58+
const DefaultType = {
59+
percent: 'number',
60+
progress: 'boolean',
61+
spinner: 'boolean',
62+
spinnerType: 'string',
63+
timeout: 'number'
64+
}
65+
66+
/**
67+
* ------------------------------------------------------------------------
68+
* Class Definition
69+
* ------------------------------------------------------------------------
70+
*/
71+
72+
class LoadingButton extends BaseComponent {
73+
constructor(element, config) {
74+
// eslint-disable-next-line no-console
75+
console.log('created')
76+
super(element)
77+
78+
this._config = this._getConfig(config)
79+
this._pause = false
80+
this._percent = this._config.percent
81+
this._timeout = this._config.timeout
82+
this._progressBar = null
83+
this._spinner = null
84+
this._state = 'idle'
85+
86+
if (this._element) {
87+
Data.setData(element, DATA_KEY, this)
88+
}
89+
}
90+
91+
// Getters
92+
93+
static get Default() {
94+
return Default
95+
}
96+
97+
static get DefaultType() {
98+
return DefaultType
99+
}
100+
101+
static get DATA_KEY() {
102+
return DATA_KEY
103+
}
104+
105+
// Public
106+
107+
start() {
108+
if (this._state !== 'loading') {
109+
this._createSpinner()
110+
this._createProgressBar()
111+
112+
setTimeout(() => {
113+
this._element.classList.add(CLASS_NAME_LOADING_BUTTON_LOADING)
114+
this._loading()
115+
EventHandler.trigger(this._element, EVENT_START)
116+
}, 1)
117+
}
118+
}
119+
120+
stop() {
121+
this._element.classList.remove(CLASS_NAME_LOADING_BUTTON_LOADING)
122+
const stoped = () => {
123+
this._removeSpinner()
124+
this._removeProgressBar()
125+
this._state = 'idle'
126+
127+
EventHandler.trigger(this._element, EVENT_STOP)
128+
if (this._percent >= 100) {
129+
EventHandler.trigger(this._element, EVENT_COMPLETE)
130+
}
131+
132+
this._percent = this._config.percent
133+
this._timeout = this._config.timeout
134+
}
135+
136+
if (this._spinner) {
137+
const transitionDuration = getTransitionDurationFromElement(this._spinner)
138+
139+
EventHandler.one(this._spinner, 'transitionend', stoped)
140+
emulateTransitionEnd(this._spinner, transitionDuration)
141+
return
142+
}
143+
144+
stoped()
145+
}
146+
147+
pause() {
148+
this._pause = true
149+
this._state = 'pause'
150+
}
151+
152+
resume() {
153+
this._pause = false
154+
this._loading()
155+
}
156+
157+
complete() {
158+
this._timeout = 1000
159+
}
160+
161+
updatePercent(percent) {
162+
const diff = (this._percent - percent) / 100
163+
this._timeout *= (1 + diff)
164+
this._percent = percent
165+
}
166+
167+
dispose() {
168+
Data.removeData(this._element, DATA_KEY)
169+
this._element = null
170+
}
171+
172+
update(config) { // public method
173+
this._config = this._getConfig(config)
174+
}
175+
176+
_getConfig(config) {
177+
config = {
178+
...Default,
179+
...Manipulator.getDataAttributes(this._element),
180+
...config
181+
}
182+
typeCheckConfig(NAME, config, DefaultType)
183+
184+
return config
185+
}
186+
187+
_loading() {
188+
const progress = setInterval(() => {
189+
this._state = 'loading'
190+
if (this._percent >= MAX_PERCENT) {
191+
this.stop()
192+
clearInterval(progress)
193+
return
194+
}
195+
196+
if (this._pause) {
197+
clearInterval(progress)
198+
return
199+
}
200+
201+
const frames = this._timeout / (MAX_PERCENT - this._percent) / MILLISECONDS
202+
this._percent = Math.round((this._percent + (1 / frames)) * 100) / 100
203+
this._timeout -= MILLISECONDS
204+
this._animateProgressBar()
205+
}, MILLISECONDS)
206+
}
207+
208+
_createProgressBar() {
209+
if (this._config.progress) {
210+
const progress = document.createElement('div')
211+
progress.classList.add(CLASS_NAME_LOADING_BUTTON_PROGRESS)
212+
progress.setAttribute('role', 'progressbar')
213+
progress.setAttribute('aria-hidden', 'true')
214+
progress.style.backgroundColor = this._progressBarBg()
215+
216+
this._element.insertBefore(progress, this._element.firstChild)
217+
this._progressBar = progress
218+
}
219+
}
220+
221+
_createSpinner() {
222+
if (this._config.spinner) {
223+
const spinner = document.createElement('span')
224+
const type = this._config.spinnerType
225+
spinner.classList.add(CLASS_NAME_LOADING_BUTTON_SPINNER, `spinner-${type}`, `spinner-${type}-sm`)
226+
spinner.setAttribute('role', 'status')
227+
spinner.setAttribute('aria-hidden', 'true')
228+
this._element.insertBefore(spinner, this._element.firstChild)
229+
this._spinner = spinner
230+
}
231+
}
232+
233+
_removeProgressBar() {
234+
if (this._config.progress) {
235+
this._progressBar.remove()
236+
this._progressBar = null
237+
}
238+
}
239+
240+
_removeSpinner() {
241+
if (this._config.spinner) {
242+
this._spinner.remove()
243+
this._spinner = null
244+
}
245+
}
246+
247+
_progressBarBg() {
248+
// The yiq lightness value that determines when the lightness of color changes from "dark" to "light". Acceptable values are between 0 and 255.
249+
const yiqContrastedThreshold = 150
250+
const color = window.getComputedStyle(this._element).getPropertyValue('background-color') === 'rgba(0, 0, 0, 0)' ? 'rgb(255, 255, 255)' : window.getComputedStyle(this._element).getPropertyValue('background-color')
251+
252+
const rgb = color.match(/^rgb?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i)
253+
254+
const r = Number.parseInt(rgb[1], 10)
255+
const g = Number.parseInt(rgb[2], 10)
256+
const b = Number.parseInt(rgb[3], 10)
257+
258+
const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000
259+
260+
if (yiq > yiqContrastedThreshold) {
261+
return PROGRESS_BAR_BG_COLOR_DARK
262+
}
263+
264+
return PROGRESS_BAR_BG_COLOR_LIGHT
265+
}
266+
267+
_animateProgressBar() {
268+
if (this._config.progress) {
269+
this._progressBar.style.width = `${this._percent}%`
270+
}
271+
}
272+
273+
// Static
274+
275+
static loadingButtonInterface(element, config) {
276+
let data = Data.getData(element, DATA_KEY)
277+
let _config = {
278+
...Default,
279+
...Manipulator.getDataAttributes(element)
280+
}
281+
282+
if (typeof config === 'object') {
283+
_config = {
284+
..._config,
285+
...config
286+
}
287+
}
288+
289+
if (!data) {
290+
data = new LoadingButton(element, _config)
291+
}
292+
293+
if (typeof config === 'string') {
294+
if (typeof data[config] === 'undefined') {
295+
throw new TypeError(`No method named "${config}"`)
296+
}
297+
298+
data[config]()
299+
}
300+
}
301+
302+
static jQueryInterface(config, par) {
303+
return this.each(function () {
304+
LoadingButton.loadingButtonInterface(this, config, par)
305+
})
306+
}
307+
}
308+
309+
/**
310+
* ------------------------------------------------------------------------
311+
* Data Api implementation
312+
* ------------------------------------------------------------------------
313+
*/
314+
// EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
315+
// SelectorEngine.find(SELECTOR_COMPONENT).forEach(element => {
316+
// LoadingButton.loadingButtonInterface(element, Manipulator.getDataAttributes(element))
317+
// })
318+
// })
319+
320+
/**
321+
* ------------------------------------------------------------------------
322+
* jQuery
323+
* ------------------------------------------------------------------------
324+
* add .Alert to jQuery only if jQuery is present
325+
*/
326+
327+
defineJQueryPlugin(NAME, LoadingButton)
328+
329+
export default LoadingButton

scss/_loading-button.scss

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//
2+
// Loading button
3+
//
4+
5+
.loading-button {
6+
position: relative;
7+
overflow: hidden;
8+
}
9+
10+
.loading-button-progress {
11+
position: absolute;
12+
top: 0;
13+
left: 0;
14+
width: 0%;
15+
height: 100%;
16+
}
17+
18+
.loading-button-spinner {
19+
margin-right: $spacer;
20+
margin-left: - ($spacer * 2);
21+
opacity: 0;
22+
@include transition(margin .15s, opacity .15s, border .15s);
23+
}
24+
25+
.loading-button-loading {
26+
.loading-button-spinner {
27+
width: 1rem;
28+
margin-left: 0;
29+
opacity: 1;
30+
}
31+
}

0 commit comments

Comments
 (0)