Skip to content

Commit 397aad8

Browse files
committed
feat: Loading Buttons initial version
1 parent f1c2f60 commit 397aad8

File tree

5 files changed

+383
-0
lines changed

5 files changed

+383
-0
lines changed

js/index.esm.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Carousel from './src/carousel'
1212
import ClassToggler from './src/class-toggler'
1313
import Collapse from './src/collapse'
1414
import Dropdown from './src/dropdown'
15+
import LoadingButton from './src/loading-button'
1516
import Modal from './src/modal'
1617
import Popover from './src/popover'
1718
import Scrollspy from './src/scrollspy'
@@ -28,6 +29,7 @@ export {
2829
ClassToggler,
2930
Collapse,
3031
Dropdown,
32+
LoadingButton,
3133
Modal,
3234
Popover,
3335
Scrollspy,

js/index.umd.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Carousel from './src/carousel'
1212
import ClassToggler from './src/class-toggler'
1313
import Collapse from './src/collapse'
1414
import Dropdown from './src/dropdown'
15+
import LoadingButton from './src/loading-button'
1516
import Modal from './src/modal'
1617
import Popover from './src/popover'
1718
import Scrollspy from './src/scrollspy'
@@ -29,6 +30,7 @@ export default {
2930
ClassToggler,
3031
Collapse,
3132
Dropdown,
33+
LoadingButton,
3234
Modal,
3335
Popover,
3436
Scrollspy,

js/src/loading-button.js

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

0 commit comments

Comments
 (0)