@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.
- package/index.js +7 -0
- package/index.scss +6 -0
- package/menus/flyout-menu/flyout-menu-2.0.0.js +331 -0
- package/menus/flyout-menu/flyout-menu-2.0.0.scss +47 -0
- package/menus/flyout-menu/index.js +2 -0
- package/menus/flyout-menu/index.scss +1 -0
- package/menus/pulldown-menu/index.js +2 -0
- package/menus/pulldown-menu/index.scss +1 -0
- package/menus/pulldown-menu/pulldown-menu-2.0.0.js +196 -0
- package/menus/pulldown-menu/pulldown-menu-2.0.0.scss +33 -0
- package/package.json +31 -0
- package/readme.md +218 -0
- package/sass/bootstrap-5.3.0/00_mixins/_accessibility.scss +42 -0
- package/sass/bootstrap-5.3.0/00_mixins/_colors.scss +99 -0
- package/sass/bootstrap-5.3.0/00_mixins/_effects.scss +45 -0
- package/sass/bootstrap-5.3.0/00_mixins/_flex-box.scss +104 -0
- package/sass/bootstrap-5.3.0/00_mixins/_form.scss +164 -0
- package/sass/bootstrap-5.3.0/00_mixins/_format.scss +208 -0
- package/sass/bootstrap-5.3.0/00_mixins/_icons.scss +129 -0
- package/sass/bootstrap-5.3.0/00_mixins/_nav.scss +327 -0
- package/sass/bootstrap-5.3.0/00_mixins/_page.scss +261 -0
- package/sass/bootstrap-5.3.0/00_mixins/_section.scss +111 -0
- package/sass/bootstrap-5.3.0/00_mixins/_toggle-list.scss +133 -0
- package/sass/bootstrap-5.3.0/00_mixins/_unit.scss +51 -0
- package/sass/bootstrap-5.3.0/10_config/_colors.scss +17 -0
- package/sass/bootstrap-5.3.0/10_config/_font.scss +228 -0
- package/sass/bootstrap-5.3.0/10_config/_maps.scss +51 -0
- package/sass/bootstrap-5.3.0/index.scss +20 -0
- package/tools/owl/index.js +2 -0
- package/tools/owl/index.scss +1 -0
- package/tools/owl/owl-thumbnail-2.0.0.js +355 -0
- package/tools/owl/owl-thumbnail-2.0.0.scss +0 -0
- package/tools/resize-end/index.js +2 -0
- package/tools/resize-end/index.scss +1 -0
- package/tools/resize-end/resize-end-2.0.0.js +108 -0
- package/tools/resize-end/resize-end-2.0.0.scss +10 -0
- package/tools/scrolling/index.js +2 -0
- package/tools/scrolling/index.scss +1 -0
- package/tools/scrolling/scrolling-2.0.0.js +244 -0
- 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,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 @@
|
|
|
1
|
+
@forward './flyout-menu-2.0.0';
|
|
@@ -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
|
+
}
|