@natachah/vanilla-frontend 0.1.14 → 0.1.16

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/js/_slider.js CHANGED
@@ -2,12 +2,12 @@
2
2
  * ------------------------------------------------------------------
3
3
  * Slider
4
4
  * ------------------------------------------------------------------
5
- * This class enable the functionality to make an element slider
5
+ * This class enables the functionality to make an element slider
6
6
  *
7
7
  * @author Natacha Herth
8
8
  * @copyright Natacha Herth, design & web development
9
9
  *
10
- * * keep check on the scrollend event: https://caniuse.com/?search=scrollend
10
+ * Keep check on the scrollend event: https://caniuse.com/?search=scrollend
11
11
  */
12
12
 
13
13
  import BaseComponent from './utilities/_base-component'
@@ -16,34 +16,52 @@ import ErrorMessage from "./utilities/_error"
16
16
  export default class Slider extends BaseComponent {
17
17
 
18
18
  static OPTIONS = {
19
- behavior: 'smooth', // Can be auto, smooth or instant
19
+ behavior: 'smooth', // Can be auto, smooth, or instant
20
20
  loop: false,
21
21
  autoplay: false
22
22
  }
23
23
 
24
24
  /**
25
- * Creates an instance
25
+ * Creates an instance of Slider.
26
26
  *
27
27
  * @param {HTMLElement} el - The HTML element
28
- * @param {object} options - The custom options
28
+ * @param {object} options - Custom options
29
29
  * @constructor
30
30
  */
31
31
  constructor(el, options = {}) {
32
32
 
33
- // Check for errors
34
- if (options.behavior && !['auto', 'smooth', 'instant'].includes(options.behavior)) throw new Error(ErrorMessage.enumOf('options.behavior', 'auto|smooth|instant'))
35
- if (options.loop && typeof options.loop !== 'boolean') throw new Error(ErrorMessage.typeOf('options.loop', 'boolean'))
36
- if (options.autoplay && (typeof options.autoplay !== 'boolean' && typeof options.autoplay !== 'number')) throw new Error(ErrorMessage.typeOf('options.autoplay', 'boolean|number'))
33
+ // Check for errors in the options
34
+ if (options.behavior && !['auto', 'smooth', 'instant'].includes(options.behavior)) {
35
+ throw new Error(ErrorMessage.enumOf('options.behavior', 'auto|smooth|instant'))
36
+ }
37
+ if (options.loop && typeof options.loop !== 'boolean') {
38
+ throw new Error(ErrorMessage.typeOf('options.loop', 'boolean'))
39
+ }
40
+ if (options.autoplay && (typeof options.autoplay !== 'boolean' && typeof options.autoplay !== 'number')) {
41
+ throw new Error(ErrorMessage.typeOf('options.autoplay', 'boolean|number'))
42
+ }
37
43
 
38
44
  // Run the SUPER constructor from BaseComponent
39
45
  super(el, options, 'slider')
40
46
 
41
- // Reduce animation
42
- const isReduced = window.matchMedia(`(prefers-reduced-motion: reduce)`) === true || window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true
47
+ // Handle reduced motion preferences
48
+ const isReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
43
49
  if (isReduced) this._options.behavior = 'instant'
44
50
 
45
- // If loop, clone first and last slides (needed in case or scrolling/grabbing and slide effect)
46
- if (this._options.loop) {
51
+ // Get slides and buttons
52
+ this._slides = Array.from(this._element.querySelectorAll('[role=tabpanel]'))
53
+
54
+ this._buttons = {
55
+ prev: document.querySelector(`[aria-controls=${this._element.id}][data-slider-prev]`),
56
+ next: document.querySelector(`[aria-controls=${this._element.id}][data-slider-next]`),
57
+ tabs: document.querySelectorAll(`[aria-controls=${this._element.id}][role=tablist] [role=tab]`)
58
+ }
59
+
60
+ this._current = 0
61
+ this._interval = null
62
+
63
+ // Clone first and last slides if loop is enabled
64
+ if (this._options.loop && this._options.behavior == 'smooth') {
47
65
 
48
66
  const cloneFirst = this._element.firstElementChild.cloneNode(true)
49
67
  const cloneLast = this._element.lastElementChild.cloneNode(true)
@@ -60,189 +78,152 @@ export default class Slider extends BaseComponent {
60
78
 
61
79
  }
62
80
 
63
- // Define the properties (don't use .children because of clones in loop)
64
- this._slides = this._element.querySelectorAll('[role=tabpanel]')
65
-
66
- this._buttons = {
67
- prev: document.querySelector(`[aria-controls=${this._element.id}][data-slider-prev]`),
68
- next: document.querySelector(`[aria-controls=${this._element.id}][data-slider-next]`),
69
- tabs: document.querySelectorAll(`[aria-controls=${this._element.id}][role=tablist] [role=tab]`)
70
- }
71
-
72
- this._current = 0
73
-
74
- this._interval = null
75
-
81
+ // Initialize event listeners and functionality
76
82
  this.#init()
77
-
78
83
  }
79
84
 
80
85
  /**
81
- * Init the event listener
86
+ * Initialize event listeners and autoplay functionality.
82
87
  *
83
88
  * @private
84
89
  */
85
90
  #init() {
86
91
 
87
- // AUTOPLAY
88
- if (this._options.autoplay) this._interval = setInterval(() => this.next(), this._options.autoplay)
92
+ // Handle autoplay
93
+ if (this._options.autoplay) {
94
+ this._interval = setInterval(() => this.next(), this._options.autoplay)
95
+ }
89
96
 
90
- // CLICK next
97
+ // Add event listeners for next and prev buttons
91
98
  if (this._buttons.next) this._buttons.next.addEventListener('click', () => this.next())
92
-
93
- // CLICK prev
94
99
  if (this._buttons.prev) this._buttons.prev.addEventListener('click', () => this.prev())
95
100
 
96
- // CLICK tabs
97
- if (this._buttons.tabs.length) this._buttons.tabs.forEach((tab, index) => tab.addEventListener('click', () => this.goTo(index)))
98
-
99
- // CLICK scroll
100
- this._element.addEventListener('scroll', () => {
101
-
102
- // Clear timeout to avoid multiple request
103
- clearTimeout(this._element.scrollTimeout)
104
-
105
- // Run event before changed
106
- this.emmitEvent('changing', { current: this._current })
107
-
108
- // Set the timeout
109
- this._element.scrollTimeout = setTimeout(() => {
110
-
111
- // Check loop on scrolling
112
- if (this._options.loop) this.#loop()
113
-
114
- // Toggle the attributes
115
- this.#change()
116
-
117
- }, 150)
101
+ // Add event listeners for tabs
102
+ if (this._buttons.tabs.length) {
103
+ this._buttons.tabs.forEach((tab, index) => tab.addEventListener('click', () => this.goTo(index)))
104
+ }
118
105
 
119
- // Make sure to reset the interval to keep the slide duration when manually change
120
- if (this._options.autoplay) {
106
+ // Handle autoplay reset after user interaction
107
+ if (this._options.autoplay) {
108
+ this._element.addEventListener('click', () => {
121
109
  clearInterval(this._interval)
122
110
  this._interval = setInterval(() => this.next(), this._options.autoplay)
123
- }
124
-
125
- })
126
-
127
- }
128
-
129
- /**
130
- * Change the current slide and toggle the attributes
131
- *
132
- * @private
133
- */
134
- #change() {
135
-
136
- // Define the index
137
- const index = this._slides.length - [...this._slides].reverse().findIndex((slide) => this._element.scrollLeft >= slide.offsetLeft) - 1
138
-
139
- // Check if index change to avoid multiple call
140
- if (index !== this._current) {
141
-
142
- // Define the new current
143
- this._current = index
144
-
145
- // Change the [aria-selected] attribute on tabs
146
- this._buttons.tabs.forEach((tab, index) => tab.setAttribute('aria-selected', index === this._current))
147
-
148
- // Change the [aria-hidden] attribute on slide
149
- this._slides.forEach((slide, index) => slide.setAttribute('aria-hidden', index !== this._current))
111
+ })
112
+ }
150
113
 
151
- // Toggle [disabled] attribute on the next button
152
- if (this._buttons.next && !this._options.loop) this._buttons.next.disabled = this._current === this._slides.length - 1
114
+ // Enable swipe functionality on touch devices.
115
+ if (!this._options.autoplay) {
116
+ let startX = 0
153
117
 
154
- // Toggle [disabled] attribute on the prev button
155
- if (this._buttons.prev && !this._options.loop) this._buttons.prev.disabled = this._current === 0
118
+ const onTouchStart = (event) => {
119
+ startX = event.touches[0].clientX
120
+ }
156
121
 
157
- // Run event after changed
158
- this.emmitEvent('changed', { current: this._current })
122
+ const onTouchEnd = (event) => {
123
+ const endX = event.changedTouches[0].clientX
124
+ if (startX > endX + 50) {
125
+ this.next()
126
+ } else if (startX < endX - 50) {
127
+ this.prev()
128
+ }
129
+ }
159
130
 
131
+ this._element.addEventListener('touchstart', onTouchStart)
132
+ this._element.addEventListener('touchend', onTouchEnd)
160
133
  }
161
-
162
134
  }
163
135
 
164
136
  /**
165
- * Loop scrolling when reach the start/end of the slide
137
+ * Go to a specific slide by index.
166
138
  *
167
- * @returns
168
- * @private
139
+ * @param {int} index - The index of the slide
169
140
  */
170
- #loop() {
141
+ goTo(index) {
171
142
 
172
- // Going to much left => go to the last slide
173
- if (this._element.scrollLeft <= this._element.offsetWidth) {
174
- this._element.scrollTo(this._slides[this._slides.length - 1].offsetLeft, 0)
175
- return
143
+ if (typeof index !== 'number') {
144
+ throw new Error(ErrorMessage.typeOf('index', 'number'))
176
145
  }
177
146
 
178
- // Going to much right => go to the first slide
179
- if (this._element.scrollWidth - this._element.scrollLeft <= this._element.offsetWidth) {
180
- this._element.scrollTo(this._slides[0].offsetLeft, 0)
181
- return
182
- }
147
+ const previous = this._current
183
148
 
184
- }
149
+ let clone
185
150
 
186
- /**
187
- * Go to a slide by index
188
- *
189
- * @param {int} index - The index number of the slide
190
- */
191
- goTo(index) {
151
+ if (this._options.loop && (index < 0 || index >= this._slides.length)) {
192
152
 
193
- // Check for errors
194
- if (typeof index !== 'number') throw new Error(ErrorMessage.typeOf('index', 'number'))
153
+ // Go to the correct slide index
154
+ this._current = index < 0 ? this._slides.length - 1 : 0
195
155
 
196
- // Define the offset, if loop get the first or last slide offset
197
- let offset
156
+ // If scroll behavior go to the clone offset
157
+ if (this._options.behavior == 'smooth') {
158
+ clone = this._current == 0 ? this._element.lastElementChild : this._element.firstElementChild
159
+ }
198
160
 
199
- if (this._options.loop && (index < 0 || index > this._slides.length - 1)) {
200
- offset = this._element.children[index + 1].offsetLeft
201
161
  } else {
202
- index = index < 0 ? this._slides.length - 1 : index % this._slides.length
203
- offset = this._slides[index].offsetLeft
162
+ // Otherwise go to the index
163
+ this._current = index
204
164
  }
205
165
 
206
- // Scroll to position
166
+ // Define the offset
167
+ const offset = clone ? clone.offsetLeft : this._slides[this._current].offsetLeft
168
+
169
+ // Go to the slide
207
170
  this._element.scrollTo({
208
171
  left: offset,
209
172
  behavior: this._options.behavior
210
173
  })
211
174
 
175
+ // If clone offset, then go to the right slide instant
176
+ if (clone) {
177
+ setTimeout(() => {
178
+ this._element.scrollTo({
179
+ left: this._slides[this._current].offsetLeft,
180
+ behavior: 'instant'
181
+ })
182
+ }, 500)
183
+ }
184
+
185
+ // Update the [aria-selected] attribute on tabs
186
+ this._buttons.tabs.forEach((tab, idx) => tab.setAttribute('aria-selected', idx === this._current))
187
+
188
+ // Update the [aria-hidden] attribute on slides
189
+ this._slides.forEach((slide, idx) => slide.setAttribute('aria-hidden', idx !== this._current))
190
+
191
+ // Disable next and prev buttons if necessary
192
+ if (this._buttons.next && !this._options.loop) {
193
+ this._buttons.next.disabled = this._current === this._slides.length - 1
194
+ }
195
+ if (this._buttons.prev && !this._options.loop) {
196
+ this._buttons.prev.disabled = this._current === 0
197
+ }
198
+
199
+ // Emit "changed" event after the slide has changed
200
+ this.emmitEvent('changed', { current: this._current, previous: previous })
212
201
  }
213
202
 
214
203
  /**
215
- * Go to the next slide
216
- *
204
+ * Go to the next slide.
217
205
  */
218
206
  next() {
207
+ const nextIndex = this._current + 1
219
208
 
220
- // Define the new _current
221
- const index = this._current + 1
222
-
223
- // If last item => return
224
- if (!this._options.loop && !this._options.autoplay && index === this._slides.length) return
225
-
226
- // Run method goTo()
227
- this.goTo(index)
209
+ // Prevent scrolling if at the last slide and looping is disabled
210
+ if (!this._options.loop && nextIndex >= this._slides.length) return
228
211
 
212
+ // Move to the next slide
213
+ this.goTo(nextIndex)
229
214
  }
230
215
 
231
216
  /**
232
- * Go to the previous slide
233
- *
217
+ * Go to the previous slide.
234
218
  */
235
219
  prev() {
220
+ const prevIndex = this._current - 1
236
221
 
237
- // Define the new _current
238
- const index = this._current - 1
239
-
240
- // If first item => return
241
- if (!this._options.loop && index < 0) return
242
-
243
- // Run method goTo()
244
- this.goTo(index)
222
+ // Prevent scrolling if at the first slide and looping is disabled
223
+ if (!this._options.loop && prevIndex < 0) return
245
224
 
225
+ // Move to the previous slide
226
+ this.goTo(prevIndex)
246
227
  }
247
228
 
248
- }
229
+ }
package/js/_sortable.js CHANGED
@@ -26,9 +26,17 @@ export default class Sortable extends BaseComponent {
26
26
  super(el, options, 'sortable')
27
27
 
28
28
  // Define the properties
29
+ this._items = null
29
30
  this._withHandle = this._element.querySelector('[data-handle=sortable]') ? true : false
30
31
  this._current = null
31
32
 
33
+ // Lier une seule fois les méthodes à `this`
34
+ this.handleMouseDown = this.handleMouseDown.bind(this)
35
+ this.handleMouseUp = this.handleMouseUp.bind(this)
36
+ this.drag = this.drag.bind(this)
37
+ this.dragging = this.dragging.bind(this)
38
+ this.drop = this.drop.bind(this)
39
+
32
40
  // Init the event listener
33
41
  this.#init()
34
42
 
@@ -44,41 +52,88 @@ export default class Sortable extends BaseComponent {
44
52
  // Prevent default animation at the drop
45
53
  this._element.addEventListener('dragover', (e) => e.preventDefault())
46
54
 
47
- // Events directly on the items
48
- this.items.forEach((item) => {
49
-
50
- // Toggle the [dragable] attribute
51
- item.addEventListener('mousedown', (e) => { if (!this._withHandle || (e.target.hasAttribute('data-handle') && e.target.getAttribute('data-handle') === 'sortable')) item.draggable = true })
52
- item.addEventListener('mouseup', () => item.draggable = false)
55
+ // Init the events on the items
56
+ this.#initEvents()
53
57
 
54
- // Drag and drop events
55
- item.addEventListener('dragstart', () => this.#drag(item))
56
- item.addEventListener('dragenter', () => this.#dragging(item))
57
- item.addEventListener('dragend', () => this.#drop(item))
58
+ }
58
59
 
59
- const btns = item.querySelectorAll('[data-go]')
60
- if (btns) btns.forEach(btn => btn.addEventListener('click', () => this.#move(item, btn.getAttribute('data-go') == 'up')))
60
+ /**
61
+ * Init the items and the event listeners
62
+ *
63
+ * @private
64
+ */
65
+ #initEvents() {
66
+
67
+ // Get the items
68
+ this._items = this._element.querySelectorAll('[draggable]')
69
+
70
+ // Add the events
71
+ this._items.forEach(item => {
72
+ item.addEventListener('mousedown', this.handleMouseDown)
73
+ item.addEventListener('mouseup', this.handleMouseUp)
74
+ item.addEventListener('dragstart', this.drag)
75
+ item.addEventListener('dragenter', this.dragging)
76
+ item.addEventListener('dragend', this.drop)
77
+ })
78
+ }
61
79
 
80
+ /**
81
+ * Reset the event listeners
82
+ *
83
+ * @private
84
+ */
85
+ resetEvents() {
86
+
87
+ // Remove the listeners
88
+ this._items.forEach((item) => {
89
+ item.removeEventListener('mousedown', this.handleMouseDown)
90
+ item.removeEventListener('mouseup', this.handleMouseUp)
91
+ item.removeEventListener('dragstart', this.drag)
92
+ item.removeEventListener('dragenter', this.dragging)
93
+ item.removeEventListener('dragend', this.drop)
62
94
  })
63
95
 
96
+ // Re-initialise the items
97
+ this.#initEvents()
98
+ }
99
+
100
+ /**
101
+ * Handle the mousedown event
102
+ *
103
+ * @private
104
+ */
105
+ handleMouseDown(e) {
106
+ // Avoid if not an element
107
+ if (!(e.target instanceof Element)) return
108
+ const target = e.target.closest('[draggable]')
109
+ if (!this._withHandle || (e.target.hasAttribute('data-handle') && e.target.getAttribute('data-handle') === 'sortable')) target.draggable = true
64
110
  }
65
111
 
66
112
  /**
67
- * Get the list of the items
113
+ * Handle the mouseup event
68
114
  *
69
- * @return {object}
115
+ * @private
70
116
  */
71
- get items() {
72
- return this._element.querySelectorAll('[draggable]')
117
+ handleMouseUp(e) {
118
+ // Avoid if not an element
119
+ if (!(e.target instanceof Element)) return
120
+ const target = e.target.closest('[draggable]')
121
+ target.draggable = false
73
122
  }
74
123
 
75
124
  /**
76
125
  * Drag an item
77
126
  *
78
- * @param {HTMLElement} item - The current item
79
127
  * @private
80
128
  */
81
- #drag(item) {
129
+ drag(e) {
130
+
131
+ // Avoid if not an element
132
+ if (!(e.target instanceof Element)) return
133
+
134
+ // Get the item
135
+ // * Bug with event listeners reset if passing some data
136
+ const item = e.target.closest('[draggable]')
82
137
 
83
138
  // Check for errors
84
139
  if (!(item instanceof HTMLElement)) throw new Error(ErrorMessage.instanceOf('item', 'HTMLElement'))
@@ -100,10 +155,16 @@ export default class Sortable extends BaseComponent {
100
155
  /**
101
156
  * When dragging an item, move it before or after an element
102
157
  *
103
- * @param {HTMLElement} item - The current item
104
158
  * @private
105
159
  */
106
- #dragging(item) {
160
+ dragging(e) {
161
+
162
+ // Avoid if not an element
163
+ if (!(e.target instanceof Element)) return
164
+
165
+ // Get the item
166
+ // * Bug with event listeners reset if passing some data
167
+ const item = e.target.closest('[draggable]')
107
168
 
108
169
  // Check for errors
109
170
  if (!(item instanceof HTMLElement)) throw new Error(ErrorMessage.instanceOf('item', 'HTMLElement'))
@@ -120,10 +181,16 @@ export default class Sortable extends BaseComponent {
120
181
  /**
121
182
  * Drop an item
122
183
  *
123
- * @param {HTMLElement} item - The current item
124
184
  * @private
125
185
  */
126
- #drop(item) {
186
+ drop(e) {
187
+
188
+ // Avoid if not an element
189
+ if (!(e.target instanceof Element)) return
190
+
191
+ // Get the item
192
+ // * Bug with event listeners reset if passing some data
193
+ const item = e.target.closest('[draggable]')
127
194
 
128
195
  // Check for errors
129
196
  if (!(item instanceof HTMLElement)) throw new Error(ErrorMessage.instanceOf('item', 'HTMLElement'))
@@ -145,26 +212,4 @@ export default class Sortable extends BaseComponent {
145
212
 
146
213
  }
147
214
 
148
- /**
149
- * Manually move up and down
150
- *
151
- * @param {HTMLElement} item - The current item
152
- * @private
153
- */
154
- #move(item, goUp = false) {
155
-
156
- const sibling = goUp ? item.previousElementSibling : item.nextElementSibling
157
-
158
- if (sibling) {
159
- this._element.insertBefore(item, goUp ? sibling : sibling.nextElementSibling)
160
- } else if (!goUp) {
161
- this._element.appendChild(item)
162
- }
163
-
164
- // Run custom event
165
- this.emmitEvent('moved', { items: this.items, current: item })
166
-
167
- }
168
-
169
-
170
215
  }