@natachah/vanilla-frontend 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitlab-ci.yml +40 -0
- package/LICENSE.md +7 -0
- package/README.md +11 -0
- package/docs/index.html +36 -0
- package/docs/main.js +32 -0
- package/docs/pages/components/badge.html +154 -0
- package/docs/pages/components/button.html +186 -0
- package/docs/pages/components/card.html +184 -0
- package/docs/pages/components/dialog.html +334 -0
- package/docs/pages/components/disclosure.html +310 -0
- package/docs/pages/components/dropdown.html +255 -0
- package/docs/pages/components/form.html +331 -0
- package/docs/pages/components/list.html +140 -0
- package/docs/pages/components/loading.html +58 -0
- package/docs/pages/components/media.html +130 -0
- package/docs/pages/components/nav.html +119 -0
- package/docs/pages/components/progress.html +47 -0
- package/docs/pages/components/slider.html +311 -0
- package/docs/pages/components/table.html +168 -0
- package/docs/pages/javascript/autofill.html +170 -0
- package/docs/pages/javascript/checkall.html +59 -0
- package/docs/pages/javascript/comfort.html +134 -0
- package/docs/pages/javascript/consent.html +112 -0
- package/docs/pages/javascript/cookie.html +81 -0
- package/docs/pages/javascript/form.html +199 -0
- package/docs/pages/javascript/scroll.html +209 -0
- package/docs/pages/javascript/sidebar.html +53 -0
- package/docs/pages/javascript/sortable.html +148 -0
- package/docs/pages/javascript/toggle.html +191 -0
- package/docs/pages/javascript/tree.html +221 -0
- package/docs/pages/layout/grid.html +201 -0
- package/docs/pages/layout/reset.html +53 -0
- package/docs/pages/layout/typography.html +324 -0
- package/docs/pages/quick-start/conventions.html +112 -0
- package/docs/pages/quick-start/customization.html +187 -0
- package/docs/pages/quick-start/installation.html +95 -0
- package/docs/pages/quick-start/mixins.html +228 -0
- package/docs/pages/test.html +15 -0
- package/docs/src/js/demo.js +98 -0
- package/docs/src/js/doc-code.js +102 -0
- package/docs/src/js/doc-demo.js +14 -0
- package/docs/src/js/doc-layout.js +108 -0
- package/docs/src/scss/demo.scss +77 -0
- package/docs/src/scss/layout.scss +160 -0
- package/docs/src/scss/style.scss +278 -0
- package/docs/vite.config.mjs +23 -0
- package/esbuild.mjs +25 -0
- package/js/_autofill.js +131 -0
- package/js/_check-all.js +77 -0
- package/js/_comfort.js +174 -0
- package/js/_consent.js +84 -0
- package/js/_dialog.js +164 -0
- package/js/_dropdown.js +101 -0
- package/js/_scroll.js +184 -0
- package/js/_sidebar.js +97 -0
- package/js/_slider.js +249 -0
- package/js/_sortable.js +143 -0
- package/js/_tabpanel.js +88 -0
- package/js/_toggle.js +123 -0
- package/js/_tree.js +85 -0
- package/js/tests/autofill.test.js +157 -0
- package/js/tests/base-component.test.js +108 -0
- package/js/tests/check-all.test.js +88 -0
- package/js/tests/comfort.test.js +219 -0
- package/js/tests/consent.test.js +84 -0
- package/js/tests/cookie.test.js +102 -0
- package/js/tests/dialog.test.js +189 -0
- package/js/tests/dropdown.test.js +115 -0
- package/js/tests/form-helper.test.js +155 -0
- package/js/tests/scroll.test.js +203 -0
- package/js/tests/sidebar.test.js +99 -0
- package/js/tests/slider.test.js +307 -0
- package/js/tests/sortable.test.js +124 -0
- package/js/tests/tabpanel.test.js +114 -0
- package/js/tests/toggle.test.js +190 -0
- package/js/tests/tree.test.js +165 -0
- package/js/utilities/_base-component.js +101 -0
- package/js/utilities/_cookie.js +98 -0
- package/js/utilities/_error.js +80 -0
- package/js/utilities/_form-helper.js +101 -0
- package/package.json +42 -0
- package/scss/_badge.scss +37 -0
- package/scss/_button.scss +34 -0
- package/scss/_card.scss +122 -0
- package/scss/_dialog.scss +116 -0
- package/scss/_disclosure.scss +101 -0
- package/scss/_dropdown.scss +68 -0
- package/scss/_form.scss +197 -0
- package/scss/_grid.scss +40 -0
- package/scss/_group.scss +57 -0
- package/scss/_list.scss +18 -0
- package/scss/_loading.scss +49 -0
- package/scss/_media.scss +37 -0
- package/scss/_nav.scss +72 -0
- package/scss/_progress.scss +40 -0
- package/scss/_slider.scss +35 -0
- package/scss/_table.scss +36 -0
- package/scss/utilities/_mixin.scss +322 -0
- package/scss/utilities/_reset.scss +145 -0
- package/scss/utilities/_typography.scss +107 -0
- package/scss/vanilla-frontend.scss +23 -0
- package/scss/variables/_root.scss +70 -0
- package/scss/variables/_setting.scss +63 -0
- package/vitest.config.js +7 -0
package/js/_dropdown.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ------------------------------------------------------------------
|
|
3
|
+
* Dropdown
|
|
4
|
+
* ------------------------------------------------------------------
|
|
5
|
+
* This class enable the functionality to un/collapse some element by another
|
|
6
|
+
* This component is a simpler version of the Toggle
|
|
7
|
+
*
|
|
8
|
+
* @author Natacha Herth
|
|
9
|
+
* @version 0.0.1
|
|
10
|
+
* @copyright Natacha Herth, design & web development
|
|
11
|
+
*
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import BaseComponent from './utilities/_base-component'
|
|
15
|
+
import ErrorMessage from './utilities/_error'
|
|
16
|
+
|
|
17
|
+
export default class Dropdown extends BaseComponent {
|
|
18
|
+
|
|
19
|
+
static OPTIONS = {
|
|
20
|
+
closable: true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates an instance
|
|
25
|
+
*
|
|
26
|
+
* @param {HTMLElement} el - The HTML element
|
|
27
|
+
* @param {object} options - The custom options
|
|
28
|
+
* @constructor
|
|
29
|
+
*/
|
|
30
|
+
constructor(el, options = {}) {
|
|
31
|
+
|
|
32
|
+
// Check for errors
|
|
33
|
+
if (options.closable && typeof options.closable !== 'boolean') throw new Error(ErrorMessage.typeOf('options.closable', 'boolean'))
|
|
34
|
+
|
|
35
|
+
// Run the SUPER constructor from BaseComponent
|
|
36
|
+
super(el, options)
|
|
37
|
+
|
|
38
|
+
// Define the properties
|
|
39
|
+
this._button = this._element.querySelector('[aria-controls]')
|
|
40
|
+
this._content = document.getElementById(this._button.getAttribute('aria-controls'))
|
|
41
|
+
|
|
42
|
+
// Init the event listener
|
|
43
|
+
this.#init()
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Init the event listener
|
|
49
|
+
*
|
|
50
|
+
* @private
|
|
51
|
+
*/
|
|
52
|
+
#init() {
|
|
53
|
+
|
|
54
|
+
// Toggle the content on click on the button
|
|
55
|
+
this._button.addEventListener('click', () => this.#toggle())
|
|
56
|
+
|
|
57
|
+
// Close the content is click outside
|
|
58
|
+
if (this._options.closable) {
|
|
59
|
+
this._content.addEventListener('blur', (e) => {
|
|
60
|
+
if (e.relatedTarget !== this._button) this.#toggle()
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Define the value
|
|
68
|
+
*
|
|
69
|
+
* @return {string}
|
|
70
|
+
* @private
|
|
71
|
+
*/
|
|
72
|
+
get value() {
|
|
73
|
+
return this._button.getAttribute('aria-pressed') === 'true'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Toggle the [hidden] and [aria-expanded] attributes
|
|
78
|
+
*
|
|
79
|
+
* @private
|
|
80
|
+
*/
|
|
81
|
+
#toggle() {
|
|
82
|
+
|
|
83
|
+
// Define new value
|
|
84
|
+
const value = !this.value
|
|
85
|
+
|
|
86
|
+
// Change the [aria-pressed] and [aria-expanded] attribute on button
|
|
87
|
+
this._button.setAttribute('aria-pressed', value)
|
|
88
|
+
this._button.setAttribute('aria-expanded', value)
|
|
89
|
+
|
|
90
|
+
// Change visibility of the content
|
|
91
|
+
this._content.hidden = !value
|
|
92
|
+
|
|
93
|
+
// Put the focus on the content
|
|
94
|
+
if (value) this._content.focus()
|
|
95
|
+
|
|
96
|
+
// Emmit event
|
|
97
|
+
this.emmitEvent('changed', { isOpen: !this._content.hidden })
|
|
98
|
+
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
}
|
package/js/_scroll.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ------------------------------------------------------------------
|
|
3
|
+
* Scroll
|
|
4
|
+
* ------------------------------------------------------------------
|
|
5
|
+
* This script enable an HTML element to scroll to a position and to spy some buttons
|
|
6
|
+
*
|
|
7
|
+
* @author Natacha Herth
|
|
8
|
+
* @version 0.0.1
|
|
9
|
+
* @copyright Natacha Herth, design & web development
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import BaseComponent from "./utilities/_base-component"
|
|
13
|
+
import ErrorMessage from "./utilities/_error"
|
|
14
|
+
|
|
15
|
+
export default class Scroll extends BaseComponent {
|
|
16
|
+
|
|
17
|
+
static OPTIONS = {
|
|
18
|
+
behavior: 'smooth', // Can be auto, smooth or instant
|
|
19
|
+
navigation: null, // Id of the navigation
|
|
20
|
+
gaps: {
|
|
21
|
+
top: 100,
|
|
22
|
+
bottom: 100,
|
|
23
|
+
spy: 100
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates an instance
|
|
29
|
+
*
|
|
30
|
+
* @param {HTMLElement} el - The HTML element
|
|
31
|
+
* @param {object} options - The custom options
|
|
32
|
+
* @constructor
|
|
33
|
+
*/
|
|
34
|
+
constructor(el, options = {}) {
|
|
35
|
+
|
|
36
|
+
// Check for errors
|
|
37
|
+
if (options.gaps) {
|
|
38
|
+
if (!(options.gaps instanceof Object)) throw new Error(ErrorMessage.instanceOf('options.gaps', 'Object'))
|
|
39
|
+
if (options.gaps.top && typeof options.gaps.top !== 'number') throw new Error(ErrorMessage.typeOf('options.gaps.top', 'number'))
|
|
40
|
+
if (options.gaps.bottom && typeof options.gaps.bottom !== 'number') throw new Error(ErrorMessage.typeOf('options.gaps.bottom', 'number'))
|
|
41
|
+
if (options.gaps.spy && typeof options.gaps.spy !== 'number') throw new Error(ErrorMessage.typeOf('options.gaps.spy', 'number'))
|
|
42
|
+
}
|
|
43
|
+
if (options.behavior && !['auto', 'smooth', 'instant'].includes(options.behavior)) throw new Error(ErrorMessage.enumOf('options.behavior', 'auto|smooth|instant'))
|
|
44
|
+
if (options.navigation && !document.getElementById(options.navigation)) throw new Error(ErrorMessage.existById('element', options.navigation))
|
|
45
|
+
|
|
46
|
+
// Run the SUPER constructor from BaseComponent
|
|
47
|
+
super(el, options)
|
|
48
|
+
|
|
49
|
+
// Reduce animation
|
|
50
|
+
const isReduced = window.matchMedia(`(prefers-reduced-motion: reduce)`) === true || window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true
|
|
51
|
+
if (isReduced) this._options.behavior = 'instant'
|
|
52
|
+
|
|
53
|
+
// Define the properties
|
|
54
|
+
this._buttons = {
|
|
55
|
+
top: document.querySelector(`[data-scroll-top="${this._element.id}"]`) ?? null,
|
|
56
|
+
bottom: document.querySelector(`[data-scroll-bottom="${this._element.id}"]`) ?? null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const links = [...document.querySelectorAll(`#${this._options.navigation} a[href^="#"]`)].filter((a) => document.getElementById(a.getAttribute('href').substring(1)))
|
|
60
|
+
this._spy = {
|
|
61
|
+
enable: this._options.navigation && links.length ? true : false,
|
|
62
|
+
links: links,
|
|
63
|
+
targets: links.map(a => document.getElementById(a.getAttribute('href').substring(1))),
|
|
64
|
+
current: 0
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Init the event listener
|
|
68
|
+
this.#init()
|
|
69
|
+
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get the height of the element
|
|
74
|
+
* It's a get method because the height can change by screen size, CSS and more !
|
|
75
|
+
*
|
|
76
|
+
* @returns {int}
|
|
77
|
+
*/
|
|
78
|
+
get _height() {
|
|
79
|
+
return this._element.scrollHeight - this._element.clientHeight
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Init the event listener
|
|
84
|
+
*
|
|
85
|
+
* @private
|
|
86
|
+
*/
|
|
87
|
+
#init() {
|
|
88
|
+
|
|
89
|
+
// SCROLL
|
|
90
|
+
if (this._element.nodeName === 'HTML') {
|
|
91
|
+
window.onscroll = () => this.#scroll()
|
|
92
|
+
} else {
|
|
93
|
+
this._element.onscroll = () => this.#scroll()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// CLICK on links
|
|
97
|
+
[...this._spy.links].forEach((link, index) => link.addEventListener('click', (e) => {
|
|
98
|
+
e.preventDefault()
|
|
99
|
+
this.goTo(this._spy.targets[index].offsetTop)
|
|
100
|
+
}))
|
|
101
|
+
|
|
102
|
+
// CLICK on buttons
|
|
103
|
+
if (this._buttons.top) this._buttons.top.addEventListener('click', () => this.scrollTop())
|
|
104
|
+
if (this._buttons.bottom) this._buttons.bottom.addEventListener('click', () => this.scrollBottom())
|
|
105
|
+
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Define what to do on scroll
|
|
110
|
+
*
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
#scroll() {
|
|
114
|
+
|
|
115
|
+
// If Spy, run the method #spy
|
|
116
|
+
if (this._spy.enable) this.#spy()
|
|
117
|
+
|
|
118
|
+
// Toggle the top button
|
|
119
|
+
if (this._buttons.top) this._buttons.top.hidden = this._element.scrollTop <= this._options.gaps.top
|
|
120
|
+
|
|
121
|
+
// Toggle the bottom button
|
|
122
|
+
if (this._buttons.bottom) this._buttons.bottom.hidden = this._element.scrollTop >= this._height - this._options.gaps.bottom
|
|
123
|
+
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Spy if current section changed, and toggle the [aria-current] attribute on links
|
|
128
|
+
*
|
|
129
|
+
* @private
|
|
130
|
+
*/
|
|
131
|
+
#spy() {
|
|
132
|
+
|
|
133
|
+
const current = this._spy.targets.length - [...this._spy.targets].reverse().findIndex((target) => this._element.scrollTop >= target.offsetTop - this._options.gaps.spy) - 1
|
|
134
|
+
|
|
135
|
+
if (current !== this._spy.current) {
|
|
136
|
+
|
|
137
|
+
// Remove [aria-current] on each links
|
|
138
|
+
this._spy.links.forEach(link => link.removeAttribute('aria-current'))
|
|
139
|
+
|
|
140
|
+
// Define current section
|
|
141
|
+
this._spy.current = current
|
|
142
|
+
|
|
143
|
+
// Set [aria-current] on link
|
|
144
|
+
if (this._spy.links[this._spy.current]) this._spy.links[this._spy.current].setAttribute('aria-current', 'location')
|
|
145
|
+
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Scroll to a certain distance
|
|
152
|
+
*
|
|
153
|
+
* @param {number} distance
|
|
154
|
+
*/
|
|
155
|
+
goTo(distance) {
|
|
156
|
+
|
|
157
|
+
// Check for errors
|
|
158
|
+
if (typeof distance !== 'number') throw new Error(ErrorMessage.typeOf('distance', 'number'))
|
|
159
|
+
|
|
160
|
+
// Scroll to
|
|
161
|
+
this._element.scrollTo({
|
|
162
|
+
top: distance,
|
|
163
|
+
behavior: this._options.behavior
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Scroll to the top
|
|
170
|
+
*
|
|
171
|
+
*/
|
|
172
|
+
scrollTop() {
|
|
173
|
+
this.goTo(0)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Scroll to the bottom
|
|
178
|
+
*
|
|
179
|
+
*/
|
|
180
|
+
scrollBottom() {
|
|
181
|
+
this.goTo(this._height)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
}
|
package/js/_sidebar.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ------------------------------------------------------------------
|
|
3
|
+
* Sidebar
|
|
4
|
+
* ------------------------------------------------------------------
|
|
5
|
+
* This class enable the functionality to toggles multiple tabpanel
|
|
6
|
+
*
|
|
7
|
+
* @author Natacha Herth
|
|
8
|
+
* @version 0.0.1
|
|
9
|
+
* @copyright Natacha Herth, design & web development
|
|
10
|
+
*
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import BaseComponent from './utilities/_base-component'
|
|
14
|
+
import ErrorMessage from "./utilities/_error"
|
|
15
|
+
|
|
16
|
+
export default class Sidebar extends BaseComponent {
|
|
17
|
+
|
|
18
|
+
static OPTIONS = {
|
|
19
|
+
breakpoint: 960,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates an instance
|
|
24
|
+
*
|
|
25
|
+
* @param {HTMLElement} el - The HTML element
|
|
26
|
+
* @param {object} options - The custom options
|
|
27
|
+
* @constructor
|
|
28
|
+
*/
|
|
29
|
+
constructor(el, options = {}) {
|
|
30
|
+
|
|
31
|
+
if (options.breakpoint && typeof options.breakpoint !== 'number') throw new Error(ErrorMessage.typeOf('options.breakpoint', 'number'))
|
|
32
|
+
|
|
33
|
+
// Run the SUPER constructor from BaseComponent
|
|
34
|
+
super(el, options)
|
|
35
|
+
|
|
36
|
+
// Define the properties
|
|
37
|
+
this._buttons = document.querySelectorAll(`[aria-controls=${this._element.id}]`) ?? []
|
|
38
|
+
|
|
39
|
+
this._backdrop = document.getElementById('backdrop')
|
|
40
|
+
|
|
41
|
+
this._timeout = false
|
|
42
|
+
|
|
43
|
+
this._isOpen = !this._element.hidden
|
|
44
|
+
|
|
45
|
+
// Init the event listener
|
|
46
|
+
this.#init()
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Init the event listener
|
|
52
|
+
*
|
|
53
|
+
* @private
|
|
54
|
+
*/
|
|
55
|
+
#init() {
|
|
56
|
+
|
|
57
|
+
// On load check if need to open/close the sidebar
|
|
58
|
+
if (window.innerWidth > this._options.breakpoint && !this._isOpen) this.#toggle()
|
|
59
|
+
|
|
60
|
+
// On resize check if need to open/close the sidebar
|
|
61
|
+
window.onresize = () => {
|
|
62
|
+
clearTimeout(this._timeout)
|
|
63
|
+
this._timeout = setTimeout(() => {
|
|
64
|
+
if ((window.innerWidth <= this._options.breakpoint && this._isOpen) || (window.innerWidth > this._options.breakpoint && !this._isOpen)) this.#toggle()
|
|
65
|
+
}, 250)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// On click on the button toggle the sidebar
|
|
69
|
+
this._buttons.forEach((button) => button.addEventListener('click', () => this.#toggle()))
|
|
70
|
+
|
|
71
|
+
// On click on the backdrop, close the sidebar
|
|
72
|
+
this._backdrop.addEventListener('click', () => this.#toggle())
|
|
73
|
+
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Toggle the sidebar
|
|
78
|
+
*
|
|
79
|
+
* @private
|
|
80
|
+
*/
|
|
81
|
+
#toggle() {
|
|
82
|
+
|
|
83
|
+
// Change the state
|
|
84
|
+
this._isOpen = !this._isOpen
|
|
85
|
+
|
|
86
|
+
// Change the [aria-pressed] & [aria-expanded] attribute on the <button>
|
|
87
|
+
this._buttons.forEach((button) => {
|
|
88
|
+
button.setAttribute('aria-pressed', this._isOpen)
|
|
89
|
+
button.setAttribute('aria-expanded', this._isOpen)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// Change the [hidden] attribute on the sidebar
|
|
93
|
+
this._element.hidden = !this._isOpen
|
|
94
|
+
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
}
|
package/js/_slider.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ------------------------------------------------------------------
|
|
3
|
+
* Slider
|
|
4
|
+
* ------------------------------------------------------------------
|
|
5
|
+
* This class enable the functionality to make an element slider
|
|
6
|
+
*
|
|
7
|
+
* @author Natacha Herth
|
|
8
|
+
* @version 0.0.1
|
|
9
|
+
* @copyright Natacha Herth, design & web development
|
|
10
|
+
*
|
|
11
|
+
* * keep check on the scrollend event: https://caniuse.com/?search=scrollend
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import BaseComponent from './utilities/_base-component'
|
|
15
|
+
import ErrorMessage from "./utilities/_error"
|
|
16
|
+
|
|
17
|
+
export default class Slider extends BaseComponent {
|
|
18
|
+
|
|
19
|
+
static OPTIONS = {
|
|
20
|
+
behavior: 'smooth', // Can be auto, smooth or instant
|
|
21
|
+
loop: false,
|
|
22
|
+
autoplay: false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates an instance
|
|
27
|
+
*
|
|
28
|
+
* @param {HTMLElement} el - The HTML element
|
|
29
|
+
* @param {object} options - The custom options
|
|
30
|
+
* @constructor
|
|
31
|
+
*/
|
|
32
|
+
constructor(el, options = {}) {
|
|
33
|
+
|
|
34
|
+
// Check for errors
|
|
35
|
+
if (options.behavior && !['auto', 'smooth', 'instant'].includes(options.behavior)) throw new Error(ErrorMessage.enumOf('options.behavior', 'auto|smooth|instant'))
|
|
36
|
+
if (options.loop && typeof options.loop !== 'boolean') throw new Error(ErrorMessage.typeOf('options.loop', 'boolean'))
|
|
37
|
+
if (options.autoplay && (typeof options.autoplay !== 'boolean' && typeof options.autoplay !== 'number')) throw new Error(ErrorMessage.typeOf('options.autoplay', 'boolean|number'))
|
|
38
|
+
|
|
39
|
+
// Run the SUPER constructor from BaseComponent
|
|
40
|
+
super(el, options)
|
|
41
|
+
|
|
42
|
+
// Reduce animation
|
|
43
|
+
const isReduced = window.matchMedia(`(prefers-reduced-motion: reduce)`) === true || window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true
|
|
44
|
+
if (isReduced) this._options.behavior = 'instant'
|
|
45
|
+
|
|
46
|
+
// If loop, clone first and last slides (needed in case or scrolling/grabbing and slide effect)
|
|
47
|
+
if (this._options.loop) {
|
|
48
|
+
|
|
49
|
+
const cloneFirst = this._element.firstElementChild.cloneNode(true)
|
|
50
|
+
const cloneLast = this._element.lastElementChild.cloneNode(true)
|
|
51
|
+
|
|
52
|
+
cloneFirst.removeAttribute('id')
|
|
53
|
+
cloneFirst.removeAttribute('aria-hidden')
|
|
54
|
+
cloneFirst.removeAttribute('role')
|
|
55
|
+
this._element.append(cloneFirst)
|
|
56
|
+
|
|
57
|
+
cloneLast.removeAttribute('id')
|
|
58
|
+
cloneLast.removeAttribute('aria-hidden')
|
|
59
|
+
cloneLast.removeAttribute('role')
|
|
60
|
+
this._element.prepend(cloneLast)
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Define the properties (don't use .children because of clones in loop)
|
|
65
|
+
this._slides = this._element.querySelectorAll('[role=tabpanel]')
|
|
66
|
+
|
|
67
|
+
this._buttons = {
|
|
68
|
+
prev: document.querySelector(`[aria-controls=${this._element.id}][data-slider-prev]`),
|
|
69
|
+
next: document.querySelector(`[aria-controls=${this._element.id}][data-slider-next]`),
|
|
70
|
+
tabs: document.querySelectorAll(`[aria-controls=${this._element.id}][role=tablist] [role=tab]`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this._current = 0
|
|
74
|
+
|
|
75
|
+
this._interval = null
|
|
76
|
+
|
|
77
|
+
this.#init()
|
|
78
|
+
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Init the event listener
|
|
83
|
+
*
|
|
84
|
+
* @private
|
|
85
|
+
*/
|
|
86
|
+
#init() {
|
|
87
|
+
|
|
88
|
+
// AUTOPLAY
|
|
89
|
+
if (this._options.autoplay) this._interval = setInterval(() => this.next(), this._options.autoplay)
|
|
90
|
+
|
|
91
|
+
// CLICK next
|
|
92
|
+
if (this._buttons.next) this._buttons.next.addEventListener('click', () => this.next())
|
|
93
|
+
|
|
94
|
+
// CLICK prev
|
|
95
|
+
if (this._buttons.prev) this._buttons.prev.addEventListener('click', () => this.prev())
|
|
96
|
+
|
|
97
|
+
// CLICK tabs
|
|
98
|
+
if (this._buttons.tabs.length) this._buttons.tabs.forEach((tab, index) => tab.addEventListener('click', () => this.goTo(index)))
|
|
99
|
+
|
|
100
|
+
// CLICK scroll
|
|
101
|
+
this._element.addEventListener('scroll', () => {
|
|
102
|
+
|
|
103
|
+
// Clear timeout to avoid multiple request
|
|
104
|
+
clearTimeout(this._element.scrollTimeout)
|
|
105
|
+
|
|
106
|
+
// Run event before changed
|
|
107
|
+
this.emmitEvent('changing', { current: this._current })
|
|
108
|
+
|
|
109
|
+
// Set the timeout
|
|
110
|
+
this._element.scrollTimeout = setTimeout(() => {
|
|
111
|
+
|
|
112
|
+
// Check loop on scrolling
|
|
113
|
+
if (this._options.loop) this.#loop()
|
|
114
|
+
|
|
115
|
+
// Toggle the attributes
|
|
116
|
+
this.#change()
|
|
117
|
+
|
|
118
|
+
}, 150)
|
|
119
|
+
|
|
120
|
+
// Make sure to reset the interval to keep the slide duration when manually change
|
|
121
|
+
if (this._options.autoplay) {
|
|
122
|
+
clearInterval(this._interval)
|
|
123
|
+
this._interval = setInterval(() => this.next(), this._options.autoplay)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Change the current slide and toggle the attributes
|
|
132
|
+
*
|
|
133
|
+
* @private
|
|
134
|
+
*/
|
|
135
|
+
#change() {
|
|
136
|
+
|
|
137
|
+
// Define the index
|
|
138
|
+
const index = this._slides.length - [...this._slides].reverse().findIndex((slide) => this._element.scrollLeft >= slide.offsetLeft) - 1
|
|
139
|
+
|
|
140
|
+
// Check if index change to avoid multiple call
|
|
141
|
+
if (index !== this._current) {
|
|
142
|
+
|
|
143
|
+
// Define the new current
|
|
144
|
+
this._current = index
|
|
145
|
+
|
|
146
|
+
// Change the [aria-selected] attribute on tabs
|
|
147
|
+
this._buttons.tabs.forEach((tab, index) => tab.setAttribute('aria-selected', index === this._current))
|
|
148
|
+
|
|
149
|
+
// Change the [aria-hidden] attribute on slide
|
|
150
|
+
this._slides.forEach((slide, index) => slide.setAttribute('aria-hidden', index !== this._current))
|
|
151
|
+
|
|
152
|
+
// Toggle [disabled] attribute on the next button
|
|
153
|
+
if (this._buttons.next && !this._options.loop) this._buttons.next.disabled = this._current === this._slides.length - 1
|
|
154
|
+
|
|
155
|
+
// Toggle [disabled] attribute on the prev button
|
|
156
|
+
if (this._buttons.prev && !this._options.loop) this._buttons.prev.disabled = this._current === 0
|
|
157
|
+
|
|
158
|
+
// Run event after changed
|
|
159
|
+
this.emmitEvent('changed', { current: this._current })
|
|
160
|
+
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Loop scrolling when reach the start/end of the slide
|
|
167
|
+
*
|
|
168
|
+
* @returns
|
|
169
|
+
* @private
|
|
170
|
+
*/
|
|
171
|
+
#loop() {
|
|
172
|
+
|
|
173
|
+
// Going to much left => go to the last slide
|
|
174
|
+
if (this._element.scrollLeft <= this._element.offsetWidth) {
|
|
175
|
+
this._element.scrollTo(this._slides[this._slides.length - 1].offsetLeft, 0)
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Going to much right => go to the first slide
|
|
180
|
+
if (this._element.scrollWidth - this._element.scrollLeft <= this._element.offsetWidth) {
|
|
181
|
+
this._element.scrollTo(this._slides[0].offsetLeft, 0)
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Go to a slide by index
|
|
189
|
+
*
|
|
190
|
+
* @param {int} index - The index number of the slide
|
|
191
|
+
*/
|
|
192
|
+
goTo(index) {
|
|
193
|
+
|
|
194
|
+
// Check for errors
|
|
195
|
+
if (typeof index !== 'number') throw new Error(ErrorMessage.typeOf('index', 'number'))
|
|
196
|
+
|
|
197
|
+
// Define the offset, if loop get the first or last slide offset
|
|
198
|
+
let offset
|
|
199
|
+
|
|
200
|
+
if (this._options.loop && (index < 0 || index > this._slides.length - 1)) {
|
|
201
|
+
offset = this._element.children[index + 1].offsetLeft
|
|
202
|
+
} else {
|
|
203
|
+
index = index < 0 ? this._slides.length - 1 : index % this._slides.length
|
|
204
|
+
offset = this._slides[index].offsetLeft
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Scroll to position
|
|
208
|
+
this._element.scrollTo({
|
|
209
|
+
left: offset,
|
|
210
|
+
behavior: this._options.behavior
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Go to the next slide
|
|
217
|
+
*
|
|
218
|
+
*/
|
|
219
|
+
next() {
|
|
220
|
+
|
|
221
|
+
// Define the new _current
|
|
222
|
+
const index = this._current + 1
|
|
223
|
+
|
|
224
|
+
// If last item => return
|
|
225
|
+
if (!this._options.loop && !this._options.autoplay && index === this._slides.length) return
|
|
226
|
+
|
|
227
|
+
// Run method goTo()
|
|
228
|
+
this.goTo(index)
|
|
229
|
+
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Go to the previous slide
|
|
234
|
+
*
|
|
235
|
+
*/
|
|
236
|
+
prev() {
|
|
237
|
+
|
|
238
|
+
// Define the new _current
|
|
239
|
+
const index = this._current - 1
|
|
240
|
+
|
|
241
|
+
// If first item => return
|
|
242
|
+
if (!this._options.loop && index < 0) return
|
|
243
|
+
|
|
244
|
+
// Run method goTo()
|
|
245
|
+
this.goTo(index)
|
|
246
|
+
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
}
|