@madj2k/fe-frontend-kit 2.0.7 → 2.0.9

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/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // Tools
2
2
  export { Madj2kBanner } from './tools/banner';
3
+ export { Madj2kBetterResizeEvent } from './tools/better-resize-event';
3
4
  export { Madj2kOwlThumbnail } from './tools/owl-thumbnail';
4
5
  export { Madj2kResizeEnd } from './tools/resize-end';
5
6
  export { Madj2kScrolling } from './tools/scrolling';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@madj2k/fe-frontend-kit",
3
- "version": "2.0.7",
3
+ "version": "2.0.9",
4
4
  "description": "Shared frontend utilities, menus and mixins for projects",
5
5
  "main": "index.js",
6
6
  "style": "index.scss",
package/readme.md CHANGED
@@ -30,6 +30,45 @@ Each menu component can be used separately.
30
30
  A lightweight class to show a full-page overlay (banner, popup, hint or cookie layer),
31
31
  with opening and closing animation and optional cookie persistence.
32
32
 
33
+ Init:
34
+ ```
35
+ import { Madj2kBanner } from '@madj2k/frontend-kit/tools/banner';
36
+ const banner = new Madj2kBanner({
37
+ bannerId: 'my-banner',
38
+ activeClass: 'active',
39
+ openClass: 'open',
40
+ closingClass: 'closing',
41
+ openingClass: 'opening',
42
+ timeout: 1000,
43
+ cookieName: 'myBannerCookie',
44
+ cookieDays: 30,
45
+ debug: false
46
+ });
47
+ ```
48
+
49
+ HTML:
50
+ ```
51
+ <div id="my-banner" class="banner my-banner">
52
+ <div class="my-banner-content">
53
+ <button class="my-banner-close" aria-controls="my-banner">Close</button>
54
+ <p>Your overlay content here ...</p>
55
+ </div>
56
+ </div>
57
+ ```
58
+
59
+ CSS:
60
+ ```
61
+ .my-banner {
62
+ position: fixed;
63
+ top: 0;
64
+ left: 0;
65
+ width: 100%;
66
+ height: 100%;
67
+ background: rgba(0, 0, 0, 0.8);
68
+ z-index: 9999;
69
+ }
70
+ ```
71
+
33
72
  # JS: OWL-Thumbnails
34
73
  A JavaScript helper class to create a main OWL carousel with a synchronized thumbnail navigation carousel.
35
74
 
@@ -45,12 +84,67 @@ including:
45
84
  This is especially useful for image galleries or product carousels in CMS setups (e.g. TYPO3, WordPress, etc.)
46
85
  where content may change dynamically.
47
86
 
48
- # JS: Resize-End
49
- A lightweight helper class that triggers a debounced 'madj2k-resize-end' event
87
+ Init with available options:
88
+ ```
89
+ const owlThumbnail = new Madj2kOwlThumbnail('.js-main-carousel', '.js-thumbs-carousel', {
90
+ main: {
91
+ items: 1,
92
+ margin: 20,
93
+ dots: true,
94
+ nav: true,
95
+ autoHeight: true
96
+ },
97
+ thumbs: {
98
+ items: 3,
99
+ margin: 10,
100
+ dots: false,
101
+ nav: true,
102
+ center: true
103
+ },
104
+ resizeEvent: 'custom.resize',
105
+ equalizeThumbHeights: true,
106
+ noStageOffset: true
107
+ }
108
+ });
109
+ ```
110
+
111
+ HTML:
112
+ ```
113
+ <div class="js-main-carousel owl-carousel">
114
+ <div class="item"><img src="image1.jpg" alt=""></div>
115
+ <div class="item"><img src="image2.jpg" alt=""></div>
116
+ <div class="item"><img src="image3.jpg" alt=""></div>
117
+ </div>
118
+ <div class="js-thumbs-carousel owl-carousel">
119
+ <div class="item" data-index="0"><img src="thumb1.jpg" alt=""></div>
120
+ <div class="item" data-index="1"><img src="thumb2.jpg" alt=""></div>
121
+ <div class="item" data-index="2"><img src="thumb3.jpg" alt=""></div>
122
+ </div>
123
+ ```
124
+
125
+ # JS: Better Resize Event
126
+ A lightweight helper class that triggers a debounced 'madj2k-better-resize-event' event
50
127
  when the user finishes resizing the browser window.
51
128
 
52
129
  It also manages a scrolling detection state (via body attribute) to avoid triggering
53
- resize-end during user scrolling, and respects active input fields (useful on mobile).
130
+ resize-events during user scrolling on mobile, and respects active input fields (keyboard-issue on mobile).
131
+
132
+ Init with available options:
133
+ ```
134
+ import { Madj2kBetterResizeEvent } from '@madj2k/frontend-kit/tools/better-resize-event';
135
+ const betterResizeEvent = new Madj2kBetterResizeEvent({
136
+ resizeEndTimeout: 300,
137
+ scrollingEndTimeout: 600,
138
+ viewportDeltaThreshold: 50
139
+ });
140
+ ```
141
+ Usage:
142
+ ```
143
+ document.addEventListener('madj2k-better-resize-event', () => {
144
+ console.log('Resize fired');
145
+ });
146
+ ```
147
+
54
148
 
55
149
  # JS: Scrolling
56
150
  A lightweight scrolling helper class that enables:
@@ -62,11 +156,52 @@ A lightweight scrolling helper class that enables:
62
156
  The class is fully configurable via options and is designed to be used in CMS contexts
63
157
  where elements can be added, removed or re-ordered dynamically.
64
158
 
159
+ Init with available options:
160
+ ```
161
+ import { Madj2kScrolling } from '@madj2k/frontend-kit/tools/scrolling';
162
+ const scrolling = new Madj2kScrolling({
163
+ anchorScrollingCollapsibleSelector: ['.collapse', '.custom-collapse'],
164
+ anchorScrollingSelector: ['a[href^="#"]', '.btn-scroll'],
165
+ anchorScrollingOffsetSelector: '#siteheader',
166
+ anchorScrollingDisableSelector: '.js-no-scroll',
167
+ appearOnScrollSelector: ['.js-appear-on-scroll'],
168
+ appearOnScrollTimeout: 500,
169
+ appearOnScrollThreshold: 25,
170
+ debug: true
171
+ });
172
+ ```
173
+ Usage with Appear-On-Scroll (HTML):
174
+ ```
175
+ <div class="js-appear-on-scroll">
176
+ <h2>Animated content</h2>
177
+ <p>This will fade and move in when scrolled into view.</p>
178
+ </div>
179
+ ```
180
+
65
181
  # JS: Toggled Overlay
66
182
  This class toggles the visibility of any target element referenced by the `aria-controls`
67
183
  attribute of a trigger element (button, link, etc.). It manages ARIA attributes for accessibility
68
184
  and allows overlays to be closed externally via a custom event.
69
185
 
186
+ Init with available options:
187
+ ```
188
+ import { Madj2kToggledOverlay } from '@madj2k/frontend-kit/tools/toggled-overlay';
189
+ const overlayToggle = new Madj2kToggledOverlay('.js-toggled-overlay');
190
+ ```
191
+
192
+ HTML:
193
+ ```
194
+ <button class="js-toggled-overlay toggled-overlay-button"
195
+ aria-label="Open overlay"
196
+ aria-controls="my-overlay"
197
+ aria-expanded="false">
198
+ <span class="icon icon-info"></span>
199
+ </button>
200
+
201
+ <div id="my-overlay" class="toggled-overlay" aria-hidden="true">
202
+ <!-- Overlay content -->
203
+ </div>
204
+ ```
70
205
 
71
206
  # Flyout-Navigation
72
207
  ## Usage
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Overlay (Vanilla JS)
2
+ * Banner / Overlay (Vanilla JS)
3
3
  *
4
4
  * A lightweight class to show a full-page overlay (banner, popup, hint or cookie layer),
5
5
  * with opening and closing animation and optional cookie persistence.
@@ -15,23 +15,33 @@
15
15
  *
16
16
  * @author Steffen Kroggel <developer@steffenkroggel.de>
17
17
  * @copyright 2025 Steffen Kroggel
18
- * @version 2.0.0
18
+ * @version 2.0.1 – legacy bannerCloseId fallback
19
19
  * @license GNU General Public License v3.0
20
20
  * @see https://www.gnu.org/licenses/gpl-3.0.en.html
21
21
  *
22
+ * @example
23
+ * // Advanced usage with config:
24
+ * const banner = new Madj2kBanner({
25
+ * bannerId: 'my-banner',
26
+ * activeClass: 'active',
27
+ * openClass: 'open',
28
+ * closingClass: 'closing',
29
+ * openingClass: 'opening',
30
+ * timeout: 1000,
31
+ * cookieName: 'myBannerCookie',
32
+ * cookieDays: 30,
33
+ * debug: false
34
+ * });
22
35
  *
23
- * Basic HTML structure:
24
- *
25
- * <div id="my-banner" class="my-banner">
36
+ * @example
37
+ * <div id="my-banner" class="banner my-banner">
26
38
  * <div class="my-banner-content">
27
- * <button id="my-banner-close" class="my-banner-close">Close</button>
39
+ * <button class="my-banner-close" aria-controls="my-banner">Close</button>
28
40
  * <p>Your overlay content here ...</p>
29
41
  * </div>
30
42
  * </div>
31
43
  *
32
- *
33
- * Example CSS:
34
- *
44
+ * @example
35
45
  * .my-banner {
36
46
  * position: fixed;
37
47
  * top: 0;
@@ -39,58 +49,23 @@
39
49
  * width: 100%;
40
50
  * height: 100%;
41
51
  * background: rgba(0, 0, 0, 0.8);
42
- * opacity: 0;
43
- * visibility: hidden;
44
- * transition: opacity 0.5s ease, visibility 0.5s ease;
45
52
  * z-index: 9999;
46
53
  * }
47
- *
48
- * .my-banner.active {
49
- * visibility: visible;
50
- * }
51
- *
52
- * .my-banner.open {
53
- * opacity: 1;
54
- * }
55
- *
56
- * .my-banner.closing {
57
- * opacity: 0;
58
- * }
59
- *
60
- *
61
- * Basic usage:
62
- *
63
- * const banner = new Madj2kBanner();
64
- *
65
- *
66
- * Advanced usage with config:
67
- *
68
- * const overlay = new Madj2kBanner({
69
- * overlayId: 'my-banner',
70
- * overlayCloseId: 'my-banner-close',
71
- * activeClass: 'active',
72
- * openClass: 'open',
73
- * closingClass: 'closing',
74
- * openingClass: 'opening',
75
- * timeout: 1000,
76
- * cookieName: 'myOverlayCookie',
77
- * cookieDays: 30
78
- * });
79
- *
80
54
  */
55
+
81
56
  class Madj2kBanner {
82
57
 
83
58
  config = {
84
- 'overlayId' : 'overlay',
85
- 'overlayCloseId' : 'overlay-close',
59
+ 'bannerId' : 'banner',
60
+ 'bannerCloseId': 'banner-close', // fallback legacy
86
61
  'activeClass': 'active',
87
62
  'openClass': 'open',
88
63
  'closingClass': 'closing',
89
64
  'openingClass': 'opening',
90
65
  'timeout': '1000',
91
- 'cookieName': 'overlay',
92
- 'cookieDays': '365'
93
-
66
+ 'cookieName': 'banner',
67
+ 'cookieDays': '365',
68
+ 'debug': false
94
69
  };
95
70
 
96
71
  /**
@@ -99,44 +74,76 @@ class Madj2kBanner {
99
74
  */
100
75
  constructor(config) {
101
76
  this.config = {...this.config, ...config }
102
- this.initOverlay();
77
+ this._log('Init Madj2kBanner');
78
+ this.initBanner();
103
79
  }
104
80
 
105
81
  /**
106
- * Init overlay if no cookie is set!
82
+ * Init banner if no cookie is set!
107
83
  */
108
- initOverlay () {
109
- const overlay = document.getElementById(this.config.overlayId);
84
+ initBanner () {
85
+ const banner = document.getElementById(this.config.bannerId);
110
86
  const self = this;
111
87
 
112
- if (overlay && ! this.getCookie()) {
88
+ if (banner && ! this.getCookie()) {
89
+ this._log('No cookie found, showing banner');
113
90
 
114
- overlay.classList.add(self.config.activeClass);
91
+ banner.classList.add(self.config.activeClass);
115
92
  setTimeout(function(){
116
- overlay.classList.add(self.config.openingClass);
93
+ banner.classList.add(self.config.openingClass);
117
94
  setTimeout(function(){
118
- overlay.classList.add(self.config.openClass);
119
- overlay.classList.remove(self.config.openingClass);
95
+ banner.classList.add(self.config.openClass);
96
+ banner.classList.remove(self.config.openingClass);
97
+ self._log('Banner opened');
120
98
  }, self.config.timeout);
121
- }, 1 ); // minimum timeout for transitions!
99
+ }, 1 );
122
100
 
123
- const overlayClose = document.getElementById(this.config.overlayCloseId);
124
- if (overlayClose) {
125
- overlayClose.addEventListener('click', (ee) => {
101
+ // Add close button(s) via aria-controls
102
+ let bannerCloseButtons = document.querySelectorAll('[aria-controls="' + this.config.bannerId + '"]');
126
103
 
127
- this.setCookie();
104
+ // fallback to legacy ID
105
+ if (bannerCloseButtons.length === 0 && this.config.bannerCloseId) {
106
+ const legacyButton = document.getElementById(this.config.bannerCloseId);
107
+ if (legacyButton) {
108
+ this._log('Legacy close button fallback used:', this.config.bannerCloseId);
109
+ legacyButton.addEventListener('click', (ee) => {
110
+ this._closeBanner(banner);
111
+ });
112
+ }
113
+ } else {
128
114
 
129
- overlay.classList.add(self.config.closingClass);
130
- overlay.classList.remove(self.config.openClass);
131
- setTimeout(function(){
132
- overlay.classList.remove(self.config.closingClass);
133
- overlay.classList.remove(self.config.activeClass);
134
- }, self.config.timeout);
115
+ bannerCloseButtons.forEach(button => {
116
+ button.addEventListener('click', (ee) => {
117
+ this._closeBanner(banner);
118
+ });
135
119
  });
136
120
  }
121
+
122
+ } else {
123
+ if (banner) {
124
+ this._log('Banner not shown (cookie found)');
125
+ }
137
126
  }
138
127
  }
139
128
 
129
+ /**
130
+ * Closes the banner and sets cookie
131
+ * @param banner
132
+ * @private
133
+ */
134
+ _closeBanner(banner) {
135
+ this.setCookie();
136
+
137
+ banner.classList.add(this.config.closingClass);
138
+ banner.classList.remove(this.config.openClass);
139
+ this._log('Banner closing');
140
+
141
+ setTimeout(() => {
142
+ banner.classList.remove(this.config.closingClass);
143
+ banner.classList.remove(this.config.activeClass);
144
+ this._log('Banner closed');
145
+ }, this.config.timeout);
146
+ }
140
147
 
141
148
  /**
142
149
  * Sets cookie
@@ -146,9 +153,9 @@ class Madj2kBanner {
146
153
  d.setTime(d.getTime() + (this.config.cookieDays * 24 * 60 * 60 * 1000));
147
154
  let expires = "expires=" + d.toUTCString();
148
155
  document.cookie = (this.config.cookieName + '=1;' + expires + ';path=/' + '; SameSite=Strict');
156
+ this._log('Cookie set:', this.config.cookieName);
149
157
  }
150
158
 
151
-
152
159
  /**
153
160
  * Gets cookie values
154
161
  * @returns {string}
@@ -163,10 +170,21 @@ class Madj2kBanner {
163
170
  c = c.substring(1);
164
171
  }
165
172
  if (c.indexOf(name) === 0) {
173
+ this._log('Cookie found:', this.config.cookieName);
166
174
  return c.substring(name.length, c.length);
167
175
  }
168
176
  }
169
177
 
170
178
  return '';
171
179
  }
180
+
181
+ /**
182
+ * Debug logging helper
183
+ * @private
184
+ */
185
+ _log(...args) {
186
+ if (this.config.debug) {
187
+ console.log('[Madj2kBanner]', ...args);
188
+ }
189
+ }
172
190
  }
@@ -1,14 +1,7 @@
1
- .my-banner {
2
- position: fixed;
3
- top: 0;
4
- left: 0;
5
- width: 100%;
6
- height: 100%;
7
- background: rgba(0, 0, 0, 0.8);
1
+ .banner {
8
2
  opacity: 0;
9
3
  visibility: hidden;
10
4
  transition: opacity 0.5s ease, visibility 0.5s ease;
11
- z-index: 9999;
12
5
 
13
6
  &.active {
14
7
  visibility: visible;
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Madj2kBetterResizeEvent
3
+ *
4
+ * A lightweight helper class that triggers a debounced 'madj2k-better-resize-event' event
5
+ * when the user finishes resizing the browser window.
6
+ *
7
+ * In some cases, you need to be able to react to the window's resize event. Unfortunately, however, the resize event is not only triggered at the end of the
8
+ * resizing of the browser window, but every time the size is changed in between. This can lead to an overhead if the event is handled every
9
+ * the event every time. With the JS-module, a final event is only triggered when the browser window has been resized.
10
+ *
11
+ * The script also considers edge-cases such as showing the software-keyboard and showing / hiding the navigation bar on mobile devices
12
+ * (both trigger a resize event).
13
+ *
14
+ * Notes:
15
+ * - On iOS (Safari), viewport height and width can change during scroll bounce or keyboard animation.
16
+ * - This version adds "viewport delta" detection to prevent false-positive resize events.
17
+ * - Scrolling state is now internal (no more data-resizeend-scrolling attribute).
18
+ * - New event: 'madj2k-better-resize-event' (legacy event still supported).
19
+ *
20
+ * @author Steffen Kroggel <developer@steffenkroggel.de>
21
+ * @copyright 2025 Steffen Kroggel
22
+ * @version 2.0.1
23
+ * @license GNU General Public License v3.0
24
+ * @see https://www.gnu.org/licenses/gpl-3.0.en.html
25
+ *
26
+ * @example
27
+ * // Initialize with defaults
28
+ * const betterResizeEvent = new Madj2kBetterResizeEvent();
29
+ *
30
+ * @example
31
+ * // Initialize with custom config
32
+ * const betterResizeEvent = new Madj2kBetterResizeEvent({
33
+ * resizeEndTimeout: 300,
34
+ * scrollingEndTimeout: 600,
35
+ * viewportDeltaThreshold: 50
36
+ * });
37
+ *
38
+ * @example
39
+ * // Listen to resize-end event
40
+ * document.addEventListener('madj2k-better-resize-event', () => {
41
+ * console.log('Resize fired');
42
+ * });
43
+ */
44
+
45
+ class Madj2kBetterResizeEvent {
46
+
47
+ config = {
48
+ scrollingEndTimeout: 500,
49
+ resizeEndTimeout: 200,
50
+ viewportDeltaThreshold: 50 // px threshold to filter small height/width changes (default: 50)
51
+ };
52
+
53
+ finalEventTimers = {};
54
+ lastViewportHeight = window.innerHeight;
55
+ lastViewportWidth = window.innerWidth;
56
+ isScrolling = false;
57
+
58
+ /**
59
+ * Constructor
60
+ * @param {Object} config - Optional config overrides
61
+ */
62
+ constructor(config = {}) {
63
+ this.config = { ...this.config, ...config };
64
+
65
+ this.lastViewportHeight = window.innerHeight;
66
+ this.lastViewportWidth = window.innerWidth;
67
+ this.isScrolling = false;
68
+
69
+ this.initScrollingDetection();
70
+ this.initResizeEvent();
71
+ }
72
+
73
+ /**
74
+ * Init resize event
75
+ */
76
+ initResizeEvent() {
77
+ window.addEventListener('resize', () => {
78
+ const currentHeight = window.innerHeight;
79
+ const currentWidth = window.innerWidth;
80
+
81
+ const deltaH = Math.abs(this.lastViewportHeight - currentHeight);
82
+ const deltaW = Math.abs(this.lastViewportWidth - currentWidth);
83
+
84
+ // Skip if height/width change is below threshold (keyboard open/close or bounce)
85
+ if (deltaH < this.config.viewportDeltaThreshold && deltaW < this.config.viewportDeltaThreshold) {
86
+ return;
87
+ }
88
+
89
+ this.lastViewportHeight = currentHeight;
90
+ this.lastViewportWidth = currentWidth;
91
+
92
+ if (!this.isScrolling) {
93
+ this.waitForFinalEvent(() => {
94
+ // Skip if input is focused (e.g. keyboard open on mobile)
95
+ const active = document.activeElement;
96
+ if (!(active && active.tagName === 'INPUT')) {
97
+
98
+ // New event
99
+ const newEvent = new CustomEvent('madj2k-better-resize-event');
100
+ document.dispatchEvent(newEvent);
101
+
102
+ // Legacy event for backwards compatibility
103
+ const legacyEvent = new CustomEvent('madj2k-resize-end');
104
+ document.dispatchEvent(legacyEvent);
105
+ }
106
+ }, this.config.resizeEndTimeout, 'resize');
107
+ }
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Init scrolling detection
113
+ */
114
+ initScrollingDetection() {
115
+ const handler = () => {
116
+ this.isScrolling = true;
117
+
118
+ this.waitForFinalEvent(() => {
119
+ this.isScrolling = false;
120
+ }, this.config.scrollingEndTimeout, 'scrolling');
121
+ };
122
+
123
+ window.addEventListener('scroll', handler, { passive: true });
124
+ window.addEventListener('touchmove', handler, { passive: true });
125
+ }
126
+
127
+ /**
128
+ * Debounced final event dispatcher
129
+ */
130
+ waitForFinalEvent(callback, ms, uniqueId = "default") {
131
+ if (this.finalEventTimers[uniqueId]) {
132
+ clearTimeout(this.finalEventTimers[uniqueId]);
133
+ }
134
+ this.finalEventTimers[uniqueId] = setTimeout(callback, ms);
135
+ }
136
+ }
@@ -0,0 +1,2 @@
1
+ import Madj2kBetterResizeEvent from './better-resize-event-2.0.js';
2
+ export { Madj2kBetterResizeEvent };
@@ -0,0 +1 @@
1
+ @forward './better-resize-event-2.0';
@@ -21,6 +21,27 @@
21
21
  * @license GNU General Public License v3.0
22
22
  * @see https://www.gnu.org/licenses/gpl-3.0.en.html
23
23
  *
24
+ * @example:
25
+ * const owlThumbnail = new Madj2kOwlThumbnail('.js-main-carousel', '.js-thumbs-carousel', {
26
+ * main: {
27
+ * items: 1,
28
+ * margin: 20,
29
+ * dots: true,
30
+ * nav: true,
31
+ * autoHeight: true
32
+ * },
33
+ * thumbs: {
34
+ * items: 3,
35
+ * margin: 10,
36
+ * dots: false,
37
+ * nav: true,
38
+ * center: true
39
+ * },
40
+ * resizeEvent: 'custom.resize',
41
+ * equalizeThumbHeights: true,
42
+ * noStageOffset: true
43
+ * }
44
+ * });
24
45
  * HTML example without data attributes:
25
46
  * <div class="js-main-carousel owl-carousel">
26
47
  * <div class="item"><img src="image1.jpg" alt=""></div>
@@ -45,28 +66,6 @@
45
66
  * <div class="item" data-index="2"><img src="thumb3.jpg" alt=""></div>
46
67
  * </div>
47
68
  *
48
- * @example:
49
- *
50
- * const owlThumbnail = new Madj2kOwlThumbnail('.js-main-carousel', '.js-thumbs-carousel', {
51
- * main: {
52
- * items: 1,
53
- * margin: 20,
54
- * dots: true,
55
- * nav: true,
56
- * autoHeight: true
57
- * },
58
- * thumbs: {
59
- * items: 3,
60
- * margin: 10,
61
- * dots: false,
62
- * nav: true,
63
- * center: true
64
- * },
65
- * resizeEvent: 'custom.resize',
66
- * equalizeThumbHeights: true,
67
- * noStageOffset: true
68
- * }
69
- * });
70
69
  */
71
70
  class Madj2kOwlThumbnail {
72
71
  constructor(mainSelector, thumbSelector, options = {}, debug = false) {
@@ -1,2 +1,6 @@
1
- import Madj2kResizeEnd from './resize-end-2.0.js';
2
- export { Madj2kResizeEnd };
1
+ /**
2
+ * Legacy Wrapper: ResizeEnd
3
+ */
4
+ import Madj2kBetterResizeEvent from '../better-resize-event/better-resize-event-2.0.js';
5
+ class Madj2kResizeEnd extends Madj2kBetterResizeEvent {}
6
+ export { Madj2kResizeEnd };
@@ -1 +1 @@
1
- @forward './resize-end-2.0';
1
+ @forward '../better-resize-event/index';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Scrolling-Events (Vanilla JS)
2
+ * Scrolling-Events
3
3
  *
4
4
  * A lightweight scrolling helper class that enables:
5
5
  * 1. Body classes based on scroll direction (scroll-up / scroll-down)
@@ -12,24 +12,25 @@
12
12
  *
13
13
  * @author Steffen Kroggel <developer@steffenkroggel.de>
14
14
  * @copyright 2025 Steffen Kroggel
15
- * @version 2.0.0
15
+ * @version 2.0.1 – Added debug option + log helper, changed anchorScrollingDisableSelector
16
16
  * @license GNU General Public License v3.0
17
17
  * @see https://www.gnu.org/licenses/gpl-3.0.en.html
18
18
  *
19
19
  * @example
20
20
  * // Initialize with defaults
21
- * const scrolling = new Scrolling();
21
+ * const scrolling = new Madj2kScrolling();
22
22
  *
23
23
  * @example
24
24
  * // Initialize with custom config
25
25
  * const scrolling = new Madj2kScrolling({
26
26
  * anchorScrollingCollapsibleSelector: ['.collapse', '.custom-collapse'],
27
- * anchorScrollingSelector: ['a[href^="#"]', '.btn-scroll'],*
27
+ * anchorScrollingSelector: ['a[href^="#"]', '.btn-scroll'],
28
28
  * anchorScrollingOffsetSelector: '#siteheader',
29
- * anchorScrollingDisableClass: 'js-no-scroll',
29
+ * anchorScrollingDisableSelector: '.js-no-scroll',
30
30
  * appearOnScrollSelector: ['.js-appear-on-scroll'],
31
31
  * appearOnScrollTimeout: 500,
32
- * appearOnScrollThreshold: 25*
32
+ * appearOnScrollThreshold: 25,
33
+ * debug: true
33
34
  * });
34
35
  *
35
36
  * @example
@@ -66,66 +67,70 @@ class Madj2kScrolling {
66
67
  anchorScrollingSelector: ['a[href^="#"]'],
67
68
  anchorScrollingOffsetSelector: '',
68
69
  anchorScrollingScriptScrollTimeout: 800,
69
- anchorScrollingDisableClass: 'js-no-scroll',
70
+ anchorScrollingDisableSelector: '.js-no-scroll',
70
71
  anchorScrollingCollapsibleSelector: ['.collapse'],
71
72
  anchorScrollingBehavior: 'smooth',
72
73
  appearOnScrollSelector: ['.js-appear-on-scroll'],
73
74
  appearOnScrollTimeout: 500,
74
- appearOnScrollThreshold: 25
75
+ appearOnScrollThreshold: 25,
76
+ debug: false
75
77
  };
76
78
 
77
79
  constructor(config) {
78
80
  this.config = { ...this.config, ...config };
79
- this.scrollTimer = null;
81
+
82
+ this.lastScrollTop = window.scrollY;
83
+ this.lastContentHeight = document.documentElement.scrollHeight;
84
+
85
+ this._log('Initialized with config:', this.config);
80
86
 
81
87
  this.initScrollClassesForBody();
82
88
  this.initAnchorScrolling();
83
89
  this.initAppearOnScroll();
84
90
  }
85
91
 
92
+ /**
93
+ * Adds scroll classes to body based on scroll direction
94
+ * @private
95
+ */
86
96
  initScrollClassesForBody() {
87
- const body = document.body;
88
- let lastScrollTop = window.scrollY;
89
- let lastContentHeight = document.documentElement.scrollHeight;
90
-
91
- const setContentHeight = () => {
92
- lastContentHeight = document.documentElement.scrollHeight;
93
- body.setAttribute('data-last-content-height', lastContentHeight);
94
- };
95
-
96
97
  const addScrollClasses = () => {
97
98
  const scrollTop = window.scrollY;
98
99
  const contentHeight = document.documentElement.scrollHeight;
99
100
 
100
- if (parseInt(body.getAttribute('data-last-content-height')) !== contentHeight) {
101
+ if (this.lastContentHeight !== contentHeight) {
102
+ this._log('Content height changed, skipping scroll class update');
103
+ this.lastContentHeight = contentHeight;
101
104
  return;
102
105
  }
103
106
 
104
- if (!body.classList.contains('block-scroll-classes')) {
107
+ if (!document.body.classList.contains('block-scroll-classes')) {
105
108
  if (scrollTop > 0) {
106
- body.classList.remove('scroll-up', 'scroll-down');
107
- if (scrollTop > lastScrollTop) {
108
- body.classList.add('scroll-down');
109
- } else if (scrollTop < lastScrollTop) {
110
- body.classList.add('scroll-up');
109
+ document.body.classList.remove('scroll-up', 'scroll-down');
110
+ if (scrollTop > this.lastScrollTop) {
111
+ document.body.classList.add('scroll-down');
112
+ this._log('Scroll direction: down');
113
+ } else if (scrollTop < this.lastScrollTop) {
114
+ document.body.classList.add('scroll-up');
115
+ this._log('Scroll direction: up');
111
116
  }
112
117
  } else {
113
- body.classList.remove('scroll-down');
118
+ document.body.classList.remove('scroll-down');
114
119
  }
115
120
  }
116
121
 
117
- body.setAttribute('data-last-scroll-top', Math.max(scrollTop, 0));
118
- lastScrollTop = scrollTop;
122
+ this.lastScrollTop = scrollTop;
119
123
  };
120
124
 
121
- setContentHeight();
122
-
123
125
  window.addEventListener('scroll', () => {
124
- setContentHeight();
125
126
  addScrollClasses();
126
127
  });
127
128
  }
128
129
 
130
+ /**
131
+ * Initializes anchor scrolling with optional offset
132
+ * @private
133
+ */
129
134
  initAnchorScrolling() {
130
135
  const offsetElement = document.querySelector(this.config.anchorScrollingOffsetSelector);
131
136
  let scriptScrollTimer = null;
@@ -140,7 +145,6 @@ class Madj2kScrolling {
140
145
  }
141
146
 
142
147
  document.body.classList.add('block-scroll-classes');
143
-
144
148
  if (scriptScrollTimer) {
145
149
  clearTimeout(scriptScrollTimer);
146
150
  }
@@ -153,6 +157,8 @@ class Madj2kScrolling {
153
157
  top: scrollTo,
154
158
  behavior: this.config.anchorScrollingBehavior
155
159
  });
160
+
161
+ this._log('Anchor scroll to:', element);
156
162
  }
157
163
  };
158
164
 
@@ -168,7 +174,6 @@ class Madj2kScrolling {
168
174
  event.preventDefault();
169
175
 
170
176
  const anchorId = event.currentTarget.getAttribute('href');
171
-
172
177
  if (anchorId && anchorId.startsWith('#')) {
173
178
  const anchor = document.querySelector(anchorId);
174
179
  if (anchor) scrollToElement(anchor);
@@ -177,13 +182,13 @@ class Madj2kScrolling {
177
182
 
178
183
  const getAnchorSelector = () => {
179
184
  return this.config.anchorScrollingSelector
180
- .map(sel => `${sel}:not(.visually-hidden-focusable):not(.${this.config.anchorScrollingDisableClass})`)
185
+ .map(sel => `${sel}:not(.visually-hidden-focusable):not(${this.config.anchorScrollingDisableSelector})`)
181
186
  .join(', ');
182
187
  };
183
188
 
184
189
  const getCollapsibleSelector = () => {
185
190
  return this.config.anchorScrollingCollapsibleSelector
186
- .map(sel => `${sel}:not(.${this.config.anchorScrollingDisableClass})`)
191
+ .map(sel => `${sel}:not(${this.config.anchorScrollingDisableSelector})`)
187
192
  .join(', ');
188
193
  };
189
194
 
@@ -195,6 +200,7 @@ class Madj2kScrolling {
195
200
  document.querySelectorAll(getCollapsibleSelector())
196
201
  .forEach(el => {
197
202
  el.addEventListener('shown.bs.collapse', e => {
203
+ this._log('Collapse shown, scroll to:', e.target);
198
204
  scrollToElement(e.target);
199
205
  });
200
206
  });
@@ -202,6 +208,10 @@ class Madj2kScrolling {
202
208
  jumpToAnchorByUrl();
203
209
  }
204
210
 
211
+ /**
212
+ * Initializes appear-on-scroll animations
213
+ * @private
214
+ */
205
215
  initAppearOnScroll() {
206
216
  const initElement = (element) => {
207
217
  const rect = element.getBoundingClientRect();
@@ -210,6 +220,7 @@ class Madj2kScrolling {
210
220
 
211
221
  if (windowBottom > elementTop) {
212
222
  element.setAttribute('data-appear-on-scroll', 0);
223
+ this._log('Appear on scroll (init):', element);
213
224
  } else {
214
225
  element.setAttribute('data-appear-on-scroll', 1);
215
226
  }
@@ -220,8 +231,9 @@ class Madj2kScrolling {
220
231
  const windowBottom = window.scrollY + window.innerHeight;
221
232
  const elementTop = window.scrollY + rect.top;
222
233
 
223
- if (windowBottom > elementTop) {
234
+ if (windowBottom > elementTop && element.getAttribute('data-appear-on-scroll') !== '0') {
224
235
  element.setAttribute('data-appear-on-scroll', 0);
236
+ this._log('Appear on scroll (show):', element);
225
237
  }
226
238
  };
227
239
 
@@ -241,4 +253,14 @@ class Madj2kScrolling {
241
253
 
242
254
  window.addEventListener('scroll', updateOnScroll);
243
255
  }
256
+
257
+ /**
258
+ * Debug logging helper
259
+ * @private
260
+ */
261
+ _log(...args) {
262
+ if (this.config.debug) {
263
+ console.log('[Madj2kScrolling]', ...args);
264
+ }
265
+ }
244
266
  }
@@ -1,16 +1,16 @@
1
1
  /**!
2
2
  * ToggledOverlay
3
3
  *
4
+ * This class toggles the visibility of any target element referenced by the `aria-controls`
5
+ * attribute of a trigger element (button, link, etc.). It manages ARIA attributes for accessibility
6
+ * and allows overlays to be closed externally via a custom event.
7
+ *
4
8
  * @author Steffen Kroggel <developer@steffenkroggel.de>
5
9
  * @copyright 2025 Steffen Kroggel
6
- * @version 2.0.0
10
+ * @version 2.0.1
7
11
  * @license GNU General Public License v3.0
8
12
  * @see https://www.gnu.org/licenses/gpl-3.0.en.html
9
13
  *
10
- * This class toggles the visibility of any target element referenced by the `aria-controls`
11
- * attribute of a trigger element (button, link, etc.). It manages ARIA attributes for accessibility
12
- * and allows overlays to be closed externally via a custom event.
13
- *
14
14
  * @example
15
15
  * Example HTML:
16
16
  * <button class="js-toggled-overlay toggled-overlay-button"
@@ -1,108 +0,0 @@
1
- /**
2
- * ResizeEnd Events (Vanilla JS)
3
- **
4
- * A lightweight helper class that triggers a debounced 'madj2k-resize-end' event
5
- * when the user finishes resizing the browser window.
6
- *
7
- * It also manages a scrolling detection state (via body attribute) to avoid triggering
8
- * resize-end during user scrolling, and respects active input fields (useful on mobile).
9
- *
10
- * @author Steffen Kroggel <developer@steffenkroggel.de>
11
- * @copyright 2025 Steffen Kroggel
12
- * @version 2.0.0
13
- * @license GNU General Public License v3.0
14
- * @see https://www.gnu.org/licenses/gpl-3.0.en.html
15
- *
16
- * @example
17
- * // Initialize with defaults
18
- * const resizeEnd = new Madj2kResizeEnd();
19
- *
20
- * @example
21
- * // Initialize with custom config
22
- * const resizeEnd = new Madj2kResizeEnd({
23
- * resizeEndTimeout: 300,
24
- * scrollingEndTimeout: 600
25
- * });
26
- *
27
- * @example
28
- * // Listen to resize-end event
29
- * document.addEventListener('madj2k-resize-end', () => {
30
- * console.log('Resize-End fired');
31
- * });
32
- *
33
- * @example
34
- * // Example use case: reload masonry grid after resize ends
35
- * document.addEventListener('madj2k-resize-end', () => {
36
- * myMasonry.reloadItems();
37
- * myMasonry.layout();
38
- * });
39
- */
40
- class Madj2kResizeEnd {
41
-
42
- config = {
43
- scrollingEndTimeout: 500,
44
- resizeEndTimeout: 200,
45
- scrollingDataAttr: 'data-resizeend-scrolling'
46
- };
47
-
48
- finalEventTimers = {};
49
-
50
- /**
51
- * Constructor
52
- * @param {Object} config - Optional config overrides
53
- */
54
- constructor(config = {}) {
55
- this.config = { ...this.config, ...config };
56
- this.initScrollingDetection();
57
- this.initResizeEndEvent();
58
- }
59
-
60
- /**
61
- * Init resizeEnd custom event
62
- */
63
- initResizeEndEvent() {
64
- window.addEventListener('resize', () => {
65
- const body = document.body;
66
-
67
- if (
68
- !body.hasAttribute(this.config.scrollingDataAttr) ||
69
- body.getAttribute(this.config.scrollingDataAttr) === '0'
70
- ) {
71
- this.waitForFinalEvent(() => {
72
- // Skip if input is focused (e.g. keyboard open on mobile)
73
- const active = document.activeElement;
74
- if (!(active && active.tagName === 'INPUT')) {
75
- const event = new CustomEvent('madj2k-resize-end');
76
- document.dispatchEvent(event);
77
- }
78
- }, this.config.resizeEndTimeout, 'resize');
79
- }
80
- });
81
- }
82
-
83
- /**
84
- * Init scrolling detection
85
- */
86
- initScrollingDetection() {
87
- const handler = () => {
88
- document.body.setAttribute(this.config.scrollingDataAttr, '1');
89
-
90
- this.waitForFinalEvent(() => {
91
- document.body.setAttribute(this.config.scrollingDataAttr, '0');
92
- }, this.config.scrollingEndTimeout, 'scrolling');
93
- };
94
-
95
- window.addEventListener('scroll', handler, { passive: true });
96
- window.addEventListener('touchmove', handler, { passive: true });
97
- }
98
-
99
- /**
100
- * Debounced final event dispatcher
101
- */
102
- waitForFinalEvent(callback, ms, uniqueId = "default") {
103
- if (this.finalEventTimers[uniqueId]) {
104
- clearTimeout(this.finalEventTimers[uniqueId]);
105
- }
106
- this.finalEventTimers[uniqueId] = setTimeout(callback, ms);
107
- }
108
- }
@@ -1,10 +0,0 @@
1
- .js-appear-on-scroll {
2
- opacity: 0;
3
- transition: opacity 0.5s ease-out, transform 0.5s ease-out;
4
- transform: translateY(1rem);
5
-
6
- &[data-appear-on-scroll="0"] {
7
- opacity: 1;
8
- transform: translateY(0);
9
- }
10
- }