@madj2k/fe-frontend-kit 2.0.0

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.
Files changed (40) hide show
  1. package/index.js +7 -0
  2. package/index.scss +6 -0
  3. package/menus/flyout-menu/flyout-menu-2.0.0.js +331 -0
  4. package/menus/flyout-menu/flyout-menu-2.0.0.scss +47 -0
  5. package/menus/flyout-menu/index.js +2 -0
  6. package/menus/flyout-menu/index.scss +1 -0
  7. package/menus/pulldown-menu/index.js +2 -0
  8. package/menus/pulldown-menu/index.scss +1 -0
  9. package/menus/pulldown-menu/pulldown-menu-2.0.0.js +196 -0
  10. package/menus/pulldown-menu/pulldown-menu-2.0.0.scss +33 -0
  11. package/package.json +31 -0
  12. package/readme.md +218 -0
  13. package/sass/bootstrap-5.3.0/00_mixins/_accessibility.scss +42 -0
  14. package/sass/bootstrap-5.3.0/00_mixins/_colors.scss +99 -0
  15. package/sass/bootstrap-5.3.0/00_mixins/_effects.scss +45 -0
  16. package/sass/bootstrap-5.3.0/00_mixins/_flex-box.scss +104 -0
  17. package/sass/bootstrap-5.3.0/00_mixins/_form.scss +164 -0
  18. package/sass/bootstrap-5.3.0/00_mixins/_format.scss +208 -0
  19. package/sass/bootstrap-5.3.0/00_mixins/_icons.scss +129 -0
  20. package/sass/bootstrap-5.3.0/00_mixins/_nav.scss +327 -0
  21. package/sass/bootstrap-5.3.0/00_mixins/_page.scss +261 -0
  22. package/sass/bootstrap-5.3.0/00_mixins/_section.scss +111 -0
  23. package/sass/bootstrap-5.3.0/00_mixins/_toggle-list.scss +133 -0
  24. package/sass/bootstrap-5.3.0/00_mixins/_unit.scss +51 -0
  25. package/sass/bootstrap-5.3.0/10_config/_colors.scss +17 -0
  26. package/sass/bootstrap-5.3.0/10_config/_font.scss +228 -0
  27. package/sass/bootstrap-5.3.0/10_config/_maps.scss +51 -0
  28. package/sass/bootstrap-5.3.0/index.scss +20 -0
  29. package/tools/owl/index.js +2 -0
  30. package/tools/owl/index.scss +1 -0
  31. package/tools/owl/owl-thumbnail-2.0.0.js +355 -0
  32. package/tools/owl/owl-thumbnail-2.0.0.scss +0 -0
  33. package/tools/resize-end/index.js +2 -0
  34. package/tools/resize-end/index.scss +1 -0
  35. package/tools/resize-end/resize-end-2.0.0.js +108 -0
  36. package/tools/resize-end/resize-end-2.0.0.scss +10 -0
  37. package/tools/scrolling/index.js +2 -0
  38. package/tools/scrolling/index.scss +1 -0
  39. package/tools/scrolling/scrolling-2.0.0.js +244 -0
  40. package/tools/scrolling/scrolling-2.0.0.scss +10 -0
package/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // Tools
2
+ export { Madj2kOwlThumbnail } from './tools/owl/owl-thumbnail.js';
3
+ export { Madj2kScrolling } from './tools/scrolling/scrolling.js';
4
+
5
+ // Menus
6
+ export { Madj2kFlyoutMenu } from './menus/flyout-menu-1.0';
7
+ export { Madj2kPulldownMenu } from './menus/pulldown-menu-1.0';
package/index.scss ADDED
@@ -0,0 +1,6 @@
1
+ // Forward all mixins
2
+ @forward './sass/bootstrap-5.3.0/index';
3
+
4
+ // Forward all menu styles
5
+ @forward './menus/flyout-menu';
6
+ @forward './menus/pulldown-menu';
@@ -0,0 +1,331 @@
1
+ /**!
2
+ * Madj2kFlyoutMenu
3
+ *
4
+ * A JavaScript class that implements a flyout menu system with smooth animations,
5
+ * keyboard navigation, and scroll management. It provides a responsive menu that
6
+ * slides in from the top of the viewport.
7
+ *
8
+ * @author Steffen Kroggel <developer@steffenkroggel.de>
9
+ * @copyright 2025 Steffen Kroggel
10
+ * @version 2.0.0
11
+ * @license GNU General Public License v3.0
12
+ * @see https://www.gnu.org/licenses/gpl-3.0.en.html
13
+ *
14
+ * @example
15
+ * // HTML structure
16
+ * <button class="js-flyout-toggle" aria-controls="flyout-menu">Menu</button>
17
+ * <div id="flyout-menu" class="js-flyout" data-position-ref="main-content" data-padding-ref="content">
18
+ * <div class="js-flyout-container">
19
+ * <div class="js-flyout-inner">Menu content</div>
20
+ * <button class="js-flyout-close">Close</button>
21
+ * </div>
22
+ * </div>
23
+ *
24
+ * // JavaScript initialization
25
+ * const menuTrigger = document.querySelector('.js-flyout-toggle');
26
+ * const flyoutMenu = new Madj2kFlyoutMenu(menuTrigger, {
27
+ * animationDuration: 300,
28
+ * heightMode: 'maxContent'
29
+ * });
30
+ */
31
+
32
+ class Madj2kFlyoutMenu {
33
+ /**
34
+ * Initializes the flyout menu with given element and options
35
+ * @param {HTMLElement} element - The trigger element for the menu
36
+ * @param {Object} options - Configuration options for the menu
37
+ */
38
+ constructor(element, options = {}) {
39
+ const defaults = {
40
+ openStatusClass: 'open',
41
+ animationOpenStatusClass: 'opening',
42
+ animationCloseStatusClass: 'closing',
43
+ animationBodyClassPrefix: 'flyout',
44
+ openStatusBodyClass: 'flyout-open',
45
+ openStatusBodyClassOverflow: 'flyout-open-overflow',
46
+ contentSectionClass: 'js-main-content',
47
+ menuClass: 'js-flyout',
48
+ menuToggleClass: "js-flyout-toggle",
49
+ menuCloseClass: "js-flyout-close",
50
+ menuContainerClass: "js-flyout-container",
51
+ menuInnerClass: "js-flyout-inner",
52
+ heightMode: 'maxContent',
53
+ paddingBehavior: 0,
54
+ paddingViewPortMinWidth: 0,
55
+ animationDuration: 500
56
+ };
57
+
58
+ this.settings = Object.assign({}, defaults, options);
59
+ this.$element = element;
60
+ this.settings.$element = element;
61
+
62
+ const controls = element.getAttribute('aria-controls');
63
+ this.settings.$menu = document.getElementById(controls);
64
+
65
+ const posRef = this.settings.$menu.getAttribute('data-position-ref');
66
+ this.settings.$positionReference = document.getElementById(posRef);
67
+
68
+ const padRef = this.settings.$menu.getAttribute('data-padding-ref');
69
+ this.settings.$paddingReference = document.getElementById(padRef);
70
+
71
+ this.settings.$closeBtn = this.settings.$menu.querySelector(`.${this.settings.menuCloseClass}`);
72
+ this.settings.$menuContainer = this.settings.$menu.querySelector(`.${this.settings.menuContainerClass}`);
73
+ this.settings.$menuInner = this.settings.$menu.querySelector(`.${this.settings.menuInnerClass}`);
74
+
75
+ this.initNoScrollHelper();
76
+ this.resizeAndPositionMenu();
77
+ this.paddingMenu();
78
+ this.bindEvents();
79
+ }
80
+
81
+ /**
82
+ * Binds all necessary event listeners
83
+ */
84
+ bindEvents() {
85
+ if (this.settings.$closeBtn) {
86
+ this.settings.$closeBtn.addEventListener('click', e => this.closeEvent(e));
87
+ this.settings.$closeBtn.addEventListener('keydown', e => this.keyboardEvent(e));
88
+ }
89
+
90
+ this.$element.addEventListener('click', e => this.toggleEvent(e));
91
+ this.$element.addEventListener('keydown', e => this.keyboardEvent(e));
92
+
93
+ this.settings.$menu.querySelectorAll('a,button,input,textarea,select')
94
+ .forEach(el => el.addEventListener('keydown', e => this.keyboardEvent(e)));
95
+
96
+ document.addEventListener('madj2k-flyoutmenu-close', e => this.closeEvent(e));
97
+ document.addEventListener('madj2k-flyoutmenu-resize', e => this.resizeAndPositionMenuEvent(e));
98
+ }
99
+
100
+ /**
101
+ * Handles keyboard navigation events
102
+ * @param {KeyboardEvent} e - The keyboard event
103
+ */
104
+ keyboardEvent(e) {
105
+ const element = e.target;
106
+
107
+ switch (e.key) {
108
+ case 'ArrowUp':
109
+ if (element === this.$element) this.close();
110
+ break;
111
+ case 'Enter':
112
+ if (element === this.$element) {
113
+ e.preventDefault();
114
+ this.toggle();
115
+ }
116
+ break;
117
+ case 'ArrowDown':
118
+ if (element === this.$element) {
119
+ e.preventDefault();
120
+ this.open();
121
+ }
122
+ break;
123
+ case 'Escape':
124
+ e.preventDefault();
125
+ this.close();
126
+ this.focusToggle();
127
+ break;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Handles toggle click event
133
+ * @param {Event} e - The click event
134
+ */
135
+ toggleEvent(e) {
136
+ e.preventDefault();
137
+ this.toggle();
138
+ }
139
+
140
+ /**
141
+ * Toggles the menu open/closed state
142
+ */
143
+ toggle() {
144
+ const others = document.querySelectorAll(`.${this.settings.menuToggleClass}`);
145
+ others.forEach(btn => {
146
+ if (btn !== this.$element) btn.dispatchEvent(new CustomEvent('madj2k-flyoutmenu-close'));
147
+ });
148
+
149
+ if (this.$element.classList.contains(this.settings.openStatusClass)) {
150
+ this.close();
151
+ } else {
152
+ this.open();
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Opens the flyout menu
158
+ */
159
+ open() {
160
+ const {$menu, $element, animationOpenStatusClass, openStatusClass} = this.settings;
161
+ if (!$menu.classList.contains(openStatusClass) && !$menu.classList.contains(animationOpenStatusClass)) {
162
+ document.dispatchEvent(new CustomEvent('madj2k-slidemenu-close'));
163
+ document.dispatchEvent(new CustomEvent('madj2k-pulldownmenu-close'));
164
+ document.dispatchEvent(new CustomEvent('madj2k-flyoutmenu-opening'));
165
+
166
+ this.toggleNoScroll();
167
+ this.resizeAndPositionMenu();
168
+ this.paddingMenu();
169
+
170
+ $menu.classList.add(openStatusClass, animationOpenStatusClass);
171
+ $element.classList.add(openStatusClass, animationOpenStatusClass);
172
+ $element.setAttribute('aria-expanded', true);
173
+ document.body.classList.add(`${this.settings.animationBodyClassPrefix}-${animationOpenStatusClass}`);
174
+
175
+ this.settings.$menuContainer.style.transition = `top ${this.settings.animationDuration}ms`;
176
+ this.settings.$menuContainer.style.top = '0';
177
+
178
+ setTimeout(() => {
179
+ $menu.classList.remove(animationOpenStatusClass);
180
+ $element.classList.remove(animationOpenStatusClass);
181
+ document.body.classList.remove(`${this.settings.animationBodyClassPrefix}-${animationOpenStatusClass}`);
182
+ document.dispatchEvent(new CustomEvent('madj2k-flyoutmenu-opened'));
183
+ }, this.settings.animationDuration);
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Handles close event
189
+ * @param {Event} e - The close event
190
+ */
191
+ closeEvent(e) {
192
+ e.preventDefault();
193
+ if (document.activeElement.tagName !== 'INPUT') {
194
+ this.close();
195
+ if (e.target === this.settings.$closeBtn) {
196
+ this.focusToggle();
197
+ }
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Closes the flyout menu
203
+ */
204
+ close() {
205
+ const {$menu, $element, animationCloseStatusClass, openStatusClass} = this.settings;
206
+ if ($menu.classList.contains(openStatusClass) && !$menu.classList.contains(animationCloseStatusClass)) {
207
+ document.dispatchEvent(new CustomEvent('madj2k-flyoutmenu-closing'));
208
+
209
+ $menu.classList.add(animationCloseStatusClass);
210
+ $element.classList.add(animationCloseStatusClass);
211
+ document.body.classList.add(`${this.settings.animationBodyClassPrefix}-${animationCloseStatusClass}`);
212
+ $element.classList.remove(openStatusClass);
213
+ $element.setAttribute('aria-expanded', false);
214
+
215
+ this.toggleNoScroll();
216
+
217
+ this.settings.$menuContainer.style.transition = `top ${this.settings.animationDuration}ms`;
218
+ this.settings.$menuContainer.style.top = '-100%';
219
+
220
+ setTimeout(() => {
221
+ $menu.classList.remove(openStatusClass, animationCloseStatusClass);
222
+ $element.classList.remove(animationCloseStatusClass);
223
+ document.body.classList.remove(`${this.settings.animationBodyClassPrefix}-${animationCloseStatusClass}`);
224
+ document.dispatchEvent(new CustomEvent('madj2k-flyoutmenu-closed'));
225
+ }, this.settings.animationDuration);
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Handles menu resize event
231
+ * @param {Event} e - The resize event
232
+ */
233
+ resizeAndPositionMenuEvent(e) {
234
+ if (document.activeElement.tagName !== 'INPUT') {
235
+ this.resizeAndPositionMenu();
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Resizes and positions the menu based on reference elements
241
+ */
242
+ resizeAndPositionMenu() {
243
+ const refObj = this.settings.$positionReference || this.$element;
244
+ const refPos = refObj.getBoundingClientRect();
245
+ const flyoutTop = refPos.top + refObj.offsetHeight;
246
+ let height = this.settings.$menuInner.offsetHeight || this.settings.$menu.offsetHeight;
247
+
248
+ this.settings.$menu.style.top = `${flyoutTop}px`;
249
+
250
+ // deprecated fullHeight-setting as fallback
251
+ if (this.settings.heightMode === 'full' || this.settings.fullHeight === true) {
252
+ height = window.innerHeight - refPos.top - refObj.offsetHeight;
253
+ this.settings.$menu.style.height = `${height}px`;
254
+
255
+ } else if (this.settings.heightMode === 'maxContent'){
256
+ this.settings.$menu.style.height = `max-content`;
257
+
258
+ } else {
259
+ this.settings.$menu.style.height = `${height}px`;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Adjusts menu padding based on settings
265
+ */
266
+ paddingMenu() {
267
+ if (!this.settings.$paddingReference) return;
268
+ if (this.settings.paddingBehavior === 0) return;
269
+ if (this.settings.paddingBehavior === 1 && this.settings.$menuInner.hasAttribute('data-padding-set')) return;
270
+
271
+ let left = this.settings.$paddingReference.getBoundingClientRect().left;
272
+ if (window.innerWidth < this.settings.paddingViewPortMinWidth) left = 0;
273
+
274
+ this.settings.$menuInner.style.paddingLeft = `${left}px`;
275
+ this.settings.$menuInner.setAttribute('data-padding-set', 'true');
276
+ }
277
+
278
+ /**
279
+ * Toggles scroll behavior of the page
280
+ */
281
+ toggleNoScroll() {
282
+ const body = document.body;
283
+ const helper = body.querySelector('.no-scroll-helper');
284
+ const inner = body.querySelector('.no-scroll-helper-inner');
285
+ let noScrollClass = this.settings.openStatusBodyClass;
286
+
287
+ if (document.documentElement.scrollHeight > window.innerHeight) {
288
+ noScrollClass += ' ' + this.settings.openStatusBodyClassOverflow;
289
+ }
290
+
291
+ if (!body.classList.contains(this.settings.openStatusBodyClass)) {
292
+ const scrollTop = -document.documentElement.scrollTop;
293
+ helper.setAttribute('data-scroll-top', scrollTop);
294
+ helper.style.cssText = 'position:relative;overflow:hidden;height:100vh;width:100%';
295
+ inner.style.cssText = `position:absolute;top:${scrollTop}px;height:100%;width:100%`;
296
+ body.classList.add(...noScrollClass.split(' '));
297
+ window.scrollTo({top: 0, behavior: 'instant'});
298
+ } else {
299
+ const scrollTop = parseInt(helper.getAttribute('data-scroll-top') || '0') * -1;
300
+ helper.removeAttribute('style');
301
+ inner.removeAttribute('style');
302
+ body.classList.remove(this.settings.openStatusBodyClass, this.settings.openStatusBodyClassOverflow);
303
+ window.scrollTo({top: scrollTop, behavior: 'instant'});
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Initializes the no-scroll helper elements
309
+ */
310
+ initNoScrollHelper() {
311
+ const body = document.body;
312
+ let helper = body.querySelector('.no-scroll-helper');
313
+ const content = document.querySelector(`.${this.settings.contentSectionClass}`);
314
+
315
+ if (!helper) {
316
+ if (content) {
317
+ content.innerHTML = `<div class="no-scroll-helper"><div class="no-scroll-helper-inner">${content.innerHTML}</div></div>`;
318
+ } else {
319
+ body.innerHTML = `<div class="no-scroll-helper"><div class="no-scroll-helper-inner">${body.innerHTML}</div></div>`;
320
+ }
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Sets focus to the toggle element
326
+ * @param {number} timeout - Delay before focusing
327
+ */
328
+ focusToggle(timeout = 0) {
329
+ setTimeout(() => this.$element.focus(), timeout);
330
+ }
331
+ }
@@ -0,0 +1,47 @@
1
+ .flyout {
2
+ position: absolute;
3
+ left: 0;
4
+ top: 0;
5
+
6
+ width: 100%;
7
+ z-index: -1;
8
+ overflow:hidden;
9
+ visibility: hidden;
10
+
11
+ &.open {
12
+ z-index: 890;
13
+ visibility: visible;
14
+
15
+ .flyout-container {
16
+ pointer-events: auto;
17
+ }
18
+ }
19
+
20
+ &.opening {
21
+ .flyout-container {
22
+ pointer-events: none;
23
+ }
24
+ }
25
+
26
+ &.closing {
27
+ .flyout-container {
28
+ pointer-events: none;
29
+ }
30
+ }
31
+
32
+ a, button {
33
+ span {
34
+ pointer-events: none;
35
+ }
36
+ }
37
+
38
+ &-container {
39
+ position: relative;
40
+ top: -100%;
41
+ left: 0;
42
+ width: 100%;
43
+ height: 100%;
44
+ background-color: #fff;
45
+ overflow: hidden;
46
+ }
47
+ }
@@ -0,0 +1,2 @@
1
+ import Madj2kFlyoutMenu from './flyout-menu-2.0.0.js';
2
+ export { Madj2kFlyoutMenu };
@@ -0,0 +1 @@
1
+ @forward './flyout-menu-2.0.0';
@@ -0,0 +1,2 @@
1
+ import Madj2kPulldownMenu from './pulldown-menu-2.0.0.js';
2
+ export { Madj2kPulldownMenu };
@@ -0,0 +1 @@
1
+ @forward './pulldown-menu-2.0.0';
@@ -0,0 +1,196 @@
1
+ /**!
2
+ * Madj2kFlyoutMenu
3
+ *
4
+ * A JavaScript class that implements a flyout menu system with smooth animations,
5
+ * keyboard navigation, and scroll management. It provides a responsive menu that
6
+ * slides in from the top of the viewport.
7
+ *
8
+ * @author Steffen Kroggel <developer@steffenkroggel.de>
9
+ * @copyright 2025 Steffen Kroggel
10
+ * @version 2.0.0
11
+ * @license GNU General Public License v3.0
12
+ * @see https://www.gnu.org/licenses/gpl-3.0.en.html
13
+ *
14
+ * @example
15
+ * // HTML structure
16
+ * <button class="js-flyout-toggle" aria-controls="flyout-menu">Menu</button>
17
+ * <div id="flyout-menu" class="js-flyout" data-position-ref="main-content" data-padding-ref="content">
18
+ * <div class="js-flyout-container">
19
+ * <div class="js-flyout-inner">Menu content</div>
20
+ * <button class="js-flyout-close">Close</button>
21
+ * </div>
22
+ * </div>
23
+ *
24
+ * // JavaScript initialization
25
+ * const menuTrigger = document.querySelector('.js-flyout-toggle');
26
+ * const flyoutMenu = new Madj2kFlyoutMenu(menuTrigger, {
27
+ * animationDuration: 300,
28
+ * fullHeight: true
29
+ * });
30
+ */
31
+ class Madj2kPulldownMenu {
32
+
33
+ static defaultConfig = {
34
+ openStatusClass: 'open',
35
+ menuClass: 'js-pulldown',
36
+ menuToggleClass: 'js-pulldown-toggle',
37
+ menuWrapClass: 'js-pulldown-wrap',
38
+ animationDuration: 500,
39
+ };
40
+
41
+ /**
42
+ * Initializes a new pulldown menu instance
43
+ * @param {HTMLElement} toggleElement - The button/element that toggles the menu
44
+ * @param {Object} options - Optional configuration settings
45
+ */
46
+ constructor(toggleElement, options = {}) {
47
+ this.settings = { ...Madj2kPulldownMenu.defaultConfig, ...options };
48
+ this.toggleElement = toggleElement;
49
+
50
+ const controlsId = toggleElement.getAttribute('aria-controls');
51
+ this.menu = document.getElementById(controlsId);
52
+ this.menuWrap = toggleElement.closest(`.${this.settings.menuWrapClass}`);
53
+
54
+ this.init();
55
+ }
56
+
57
+ /**
58
+ * Initializes event listeners for the menu
59
+ */
60
+ init() {
61
+ this.toggleElement.addEventListener('click', (e) => this.toggleEvent(e));
62
+ this.toggleElement.addEventListener('keydown', (e) => this.keyboardEvent(e));
63
+
64
+ const focusable = this.menu.querySelectorAll('a,button,input,textarea,select');
65
+ focusable.forEach((el) =>
66
+ el.addEventListener('keydown', (e) => this.keyboardEvent(e))
67
+ );
68
+
69
+ window.addEventListener('resize', (e) => this.closeEvent(e));
70
+ document.addEventListener('click', (e) => this.closeViaDocumentClickEvent(e));
71
+ document.addEventListener('madj2k-pulldownmenu-close', (e) => this.closeEvent(e));
72
+ }
73
+
74
+ /**
75
+ * Handles click events on the toggle element
76
+ * @param {Event} e - The click event object
77
+ */
78
+ toggleEvent(e) {
79
+ e.preventDefault();
80
+ e.stopPropagation();
81
+ this.toggle();
82
+ }
83
+
84
+ /**
85
+ * Toggles the menu open/closed state
86
+ */
87
+ toggle() {
88
+ const isOpen = this.toggleElement.classList.contains(this.settings.openStatusClass);
89
+
90
+ // Close other menus
91
+ document.querySelectorAll(`.${this.settings.menuToggleClass}`).forEach((el) => {
92
+ if (el !== this.toggleElement) {
93
+ el.dispatchEvent(new Event('madj2k-pulldownmenu-close'));
94
+ }
95
+ });
96
+
97
+ isOpen ? this.close() : this.open();
98
+ }
99
+
100
+ /**
101
+ * Opens the pulldown menu
102
+ */
103
+ open() {
104
+ if (!this.menu.classList.contains(this.settings.openStatusClass)) {
105
+ document.dispatchEvent(new Event('madj2k-slidemenu-close'));
106
+ document.dispatchEvent(new Event('madj2k-flyoutmenu-close'));
107
+
108
+ this.menu.classList.add(this.settings.openStatusClass);
109
+ this.menuWrap?.classList.add(this.settings.openStatusClass);
110
+ this.toggleElement.classList.add(this.settings.openStatusClass);
111
+ this.toggleElement.setAttribute('aria-expanded', 'true');
112
+
113
+ document.dispatchEvent(new Event('madj2k-pulldownmenu-opened'));
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Closes the pulldown menu
119
+ */
120
+ close() {
121
+ if (this.menu.classList.contains(this.settings.openStatusClass)) {
122
+ this.menu.classList.remove(this.settings.openStatusClass);
123
+ this.menuWrap?.classList.remove(this.settings.openStatusClass);
124
+ this.toggleElement.classList.remove(this.settings.openStatusClass);
125
+ this.toggleElement.setAttribute('aria-expanded', 'false');
126
+
127
+ document.dispatchEvent(new Event('madj2k-pulldownmenu-closed'));
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Handles closing the menu when clicking outside
133
+ * @param {Event} e - The click event object
134
+ */
135
+ closeViaDocumentClickEvent(e) {
136
+ if (!this.menu.contains(e.target) && !this.toggleElement.contains(e.target)) {
137
+ if (document.activeElement.tagName !== 'INPUT') {
138
+ this.close();
139
+ }
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Handles generic close events
145
+ * @param {Event} e - The event object
146
+ */
147
+ closeEvent(e) {
148
+ if (document.activeElement.tagName !== 'INPUT') {
149
+ e.preventDefault();
150
+ this.close();
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Handles keyboard navigation events
156
+ * @param {KeyboardEvent} e - The keyboard event object
157
+ */
158
+ keyboardEvent(e) {
159
+ const key = e.key;
160
+ const target = e.target;
161
+
162
+ switch (key) {
163
+ case 'ArrowUp':
164
+ if (target === this.toggleElement) this.close();
165
+ break;
166
+
167
+ case 'Enter':
168
+ if (target === this.toggleElement) {
169
+ e.preventDefault();
170
+ this.toggle();
171
+ }
172
+ break;
173
+
174
+ case 'ArrowDown':
175
+ if (target === this.toggleElement) {
176
+ e.preventDefault();
177
+ this.open();
178
+ }
179
+ break;
180
+
181
+ case 'Escape':
182
+ e.preventDefault();
183
+ this.close();
184
+ this.focusToggle();
185
+ break;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Sets focus to the toggle element
191
+ * @param {number} timeout - Optional delay in milliseconds
192
+ */
193
+ focusToggle(timeout = 0) {
194
+ setTimeout(() => this.toggleElement.focus(), timeout);
195
+ }
196
+ }
@@ -0,0 +1,33 @@
1
+ .pulldown {
2
+ position: absolute;
3
+ top:0;
4
+ background-color:#fff;
5
+ display:none;
6
+
7
+ a, button {
8
+ span {
9
+ pointer-events: none;
10
+ }
11
+ }
12
+
13
+ /** used to simulate the right padding-top for the pulldown **/
14
+ &-hide {
15
+ visibility:hidden;
16
+ }
17
+
18
+ &.open {
19
+ z-index:900;
20
+ display: block;
21
+ }
22
+
23
+ &-wrap {
24
+ position:relative;
25
+
26
+ &.open {
27
+ .pulldown-toggle {
28
+ z-index: 901;
29
+ position: relative;
30
+ }
31
+ }
32
+ }
33
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@madj2k/fe-frontend-kit",
3
+ "version": "2.0.0",
4
+ "description": "Shared frontend utilities, menus and mixins for projects",
5
+ "main": "index.js",
6
+ "style": "index.scss",
7
+ "files": [
8
+ "tools/",
9
+ "menus/",
10
+ "sass/",
11
+ "index.js",
12
+ "index.scss"
13
+ ],
14
+ "author": {
15
+ "name": "Steffen Kroggel",
16
+ "email": "developer@steffenkroggel.de",
17
+ "url": "https://www.steffenkroggel.de"
18
+ },
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/skroggel/fe-frontend-kit.git"
23
+ },
24
+ "optionalDependencies": {
25
+ "bootstrap": "^5.3.0"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "keywords": ["scss", "mixins", "frontend", "menu", "pulldown menu", "offcanvas menu", "slide menu", "utilities"]
31
+ }