@numiko/drilldown 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/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # Drilldown
2
+
3
+ The Drilldown Package provides an easy-to-use solution for creating animated, hierarchical menus using the GreenSock Animation Platform (GSAP). It allows for smooth transitions between menu levels with configurable options for customization and callbacks for advanced integrations.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Features](#markdown-header-features)
8
+ - [Dependencies](#markdown-header-dependencies)
9
+ - [Installation](#markdown-header-installation)
10
+ - [Usage](#markdown-header-usage)
11
+ - [API](#markdown-header-api)
12
+ - [Release Notes](#markdown-header-release-notes)
13
+
14
+ ## Features
15
+
16
+ - **Dynamic Hierarchical Menus**: Easily create multi-level drilldown menus to efficiently manage complex site navigation structures.
17
+ - **GSAP Animations**: Utilizes the GreenSock Animation Platform (GSAP) to provide smooth, high-performance transitions between menu levels.
18
+ - **Configurable Selectors**: Customize selectors for menu items, back buttons, submenus, and more to match your HTML structure.
19
+ - **Customizable Animation Callbacks**: Offers hooks for animation start and complete events at both child and parent levels, allowing for fine-tuned control over menu behavior.
20
+ - **Automatic Height Adjustment**: Features a ResizeObserver to automatically adjust the height of the drilldown menu, ensuring it matches the content for a seamless user experience.
21
+ - **Lifecycle Management**: Features methods for initializing and destroying drilldown instances, enabling proper setup and teardown of components to prevent memory leaks and ensure clean transitions between states.
22
+
23
+ ## Dependencies
24
+
25
+ - GSAP
26
+
27
+
28
+ ## Installation
29
+
30
+ Install from npm:
31
+
32
+ ```shell
33
+ npm install @numiko/drilldown
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ### JS initialisation
39
+
40
+ ```ts
41
+ import { Drilldown, type DrilldownOptions } from '@numiko/drilldown';
42
+
43
+ const menuElement = document.getElementById('menu') as HTMLElement;
44
+ const options: DrilldownOptions = {
45
+ backSelector: '[data-js-menu-back]',
46
+ nextSelector: '[data-js-menu-next]',
47
+ submenuSelector: '[data-js-menu-container]',
48
+ itemSelector: '[data-js-menu-item]',
49
+ };
50
+
51
+ const menu = new Drilldown(menuElement, options);
52
+ menu.init();
53
+
54
+ ```
55
+
56
+ ### HTML Structure (with appropriate tailwind styles)
57
+
58
+ **Note** The menu markup will be generated by the menu twig template for drupal builds - this is just a basic example of the needed structure.
59
+
60
+ ```HTML
61
+ <div id="menu" class="overflow-hidden bg-black text-white relative top-full left-0 w-full" data-js-menu-container data-js-menu-container-level="0" aria-hidden="false">
62
+ <ul class="flex flex-col">
63
+ <li data-js-menu-item data-js-menu-item-level="0">
64
+ <a class="flex gap-4 w-full text-left items-center justify-between px-9 py-7 group" href="http://www.example.com" data-js-menu-link data-js-menu-link-level="1">
65
+ <span>Top level 1</span>
66
+ </a>
67
+ </li>
68
+ <li data-js-menu-item data-js-menu-item-level="0">
69
+ <button class="flex gap-4 w-full text-left items-center justify-between px-9 py-7 group" type="button" data-js-menu-link data-js-menu-link-level="0" data-js-menu-next aria-controls="menu_1">
70
+ <span>Top level 2 &raquo;</span>
71
+ </button>
72
+ <div id="menu_1" class="invisible absolute z-10 w-full top-0" data-js-menu-container data-js-menu-container-level="1">
73
+ <button class="flex gap-4 w-full text-left items-center justify-start px-9 py-7 group" type="button" data-js-menu-back>
74
+ <span> &laquo; Go back</span>
75
+ </button>
76
+ <div>
77
+ <a class="flex w-full text-left items-center justify-between px-9 py-7 group" data-js-menu-parent href="http://www.example.com">Top level 2</a>
78
+ </div>
79
+ <ul class="flex flex-col">
80
+ <li data-js-menu-item data-js-menu-item-level="1">
81
+ <a class="flex gap-4 w-full text-left items-center justify-between px-9 py-7 group" href="http://www.example.com" data-js-menu-link data-js-menu-link-level="1">
82
+ <span>Second level 1</span>
83
+ </a>
84
+ </li>
85
+ <li data-js-menu-item data-js-menu-item-level="1">
86
+ <button class="flex gap-4 w-full text-left items-center justify-between px-9 py-7 group" href="http://www.example.com" data-js-menu-link data-js-menu-link-level="1" data-js-menu-next aria-controls="menu_2">
87
+ <span>Second level 2 &raquo;</span>
88
+ </button>
89
+ <div id="menu_2" class="invisible absolute z-10 w-full top-0" data-js-menu-container data-js-menu-container-level="2">
90
+ <button class="flex gap-4 w-full text-left items-center justify-start px-9 py-7 group" type="button" data-js-menu-back>
91
+ <span>&laquo; Go back</span>
92
+ </button>
93
+ <div>
94
+ <a class="flex w-full text-left items-center justify-between px-9 py-7 group" data-js-menu-parent href="http://www.example.com">Second level 2</a>
95
+ </div>
96
+ <ul class="flex flex-col">
97
+ <li data-js-menu-item data-js-menu-item-level="2">
98
+ <a class="flex gap-4 w-full text-left items-center justify-between px-9 py-7 group" href="http://www.example.com" data-js-menu-link data-js-menu-link-level="2">
99
+ <span>Third level 1</span>
100
+ </a>
101
+ </li>
102
+ </ul>
103
+ </div>
104
+ </li>
105
+ <li data-js-menu-item data-js-menu-item-level="1">
106
+ <a class="flex gap-4 w-full text-left items-center justify-between px-9 py-7 group" href="http://www.example.com" data-js-menu-link data-js-menu-link-level="1">
107
+ <span>Second level 3</span>
108
+ </a>
109
+ </li>
110
+ </ul>
111
+ </div>
112
+ </li>
113
+ </ul>
114
+ </div>
115
+ ```
116
+
117
+ ## Release notes
118
+
119
+ ### v1.0.0
120
+
121
+ - Inital version created to work with SiteKit menus
@@ -0,0 +1,179 @@
1
+ export declare class Drilldown {
2
+ private readonly context;
3
+ private readonly options;
4
+ private activeMenuItem;
5
+ private menuResizeObserver;
6
+ private isAnimating;
7
+ /**
8
+ * Initializes a new instance of the Drilldown class.
9
+ * @param context - The element to initialize the drilldown on.
10
+ * @param options
11
+ */
12
+ constructor(context: HTMLElement, options?: DrilldownOptions);
13
+ /**
14
+ * Binds instance to member functions to preserve the `this` context.
15
+ */
16
+ private bindThisToMemberFunctions;
17
+ init(): void;
18
+ /**
19
+ * Handles click events on the menu, determining whether a next or back button was clicked,
20
+ * and initiates the appropriate animation to transition between menus.
21
+ * This function prevents navigation if animations are currently running or if navigation is disabled.
22
+ * It also ensures that the default action for the click event is prevented.
23
+ */
24
+ private handleMenuClick;
25
+ /**
26
+ * Determines the target menu based on the element that triggered the event.
27
+ * If the action is to navigate to a next (child) menu, it finds the submenu within the active menu item.
28
+ * If the action is to go back, it finds the parent menu of the current submenu.
29
+ *
30
+ * @param element - The element (typically a button or link) that triggered the navigation action.
31
+ * @param isNextButton - A flag indicating whether the navigation action is moving forward to a child menu.
32
+ * @returns The target menu to navigate to, or null if no target menu can be determined.
33
+ */
34
+ private getTargetMenu;
35
+ /**
36
+ * Creates the animation for transitioning between menus.
37
+ * This function initializes a GSAP tween for either showing a child menu (next) or a parent menu (back),
38
+ * depending on the direction of navigation. It also sets up onStart and onComplete callbacks for the animation.
39
+ *
40
+ * @param currentMenu - The currently active menu that is being transitioned from.
41
+ * @param targetMenu - The target menu that is being transitioned to.
42
+ * @param isChild - Determines the type of animation to create; true for child menu, false for parent menu.
43
+ * @returns The GSAP timeline instance for the created animation.
44
+ */
45
+ private createAnimation;
46
+ /**
47
+ * Sets up GSAP tween callbacks for menu animations. This method configures onStart and onComplete callbacks
48
+ * for the tween animation based on whether it's an animation for showing a child menu or a parent menu.
49
+ * It integrates both custom callback functions provided via options and internal logic defined in the class.
50
+ *
51
+ * @param tween - The GSAP tween instance that controls the menu animation.
52
+ * @param currentMenu - The currently active menu element that is being transitioned from.
53
+ * @param targetMenu - The target menu element that is being transitioned to.
54
+ * @param isChild - Flag to determine if the animation is for showing a child menu (true) or a parent menu (false).
55
+ */
56
+ private setupTweenCallbacks;
57
+ /**
58
+ * Creates a ResizeObserver to adjust the height of the drilldown menu.
59
+ * This ensures the menu height matches the content, especially useful when content changes size.
60
+ */
61
+ private createMenuResizeObserver;
62
+ /**
63
+ * Returns the main list items for a given menu. It does not include supplementary items, for
64
+ * example, the parent link or the back button.
65
+ * @param menu
66
+ */
67
+ private getMenuListItems;
68
+ /**
69
+ * Returns the elements that will be hidden when a menu is transitioning.
70
+ * @param menu
71
+ */
72
+ private getElementsToHideForMenuTransition;
73
+ /**
74
+ * The callback that is fired when the menu navigates down a level.
75
+ */
76
+ private onShowChildStart;
77
+ /**
78
+ * The callback that is fired after the menu has moved down a level.
79
+ */
80
+ private onShowChildComplete;
81
+ /**
82
+ * The callback that is fired when the menu navigates up a level.
83
+ */
84
+ private onShowParentStart;
85
+ /**
86
+ * The callback that is fired after the menu has moved up a level.
87
+ */
88
+ private onShowParentComplete;
89
+ /**
90
+ * Generates the tween animation for showing a child menu.
91
+ * This method configures the animation sequence for transitioning to a submenu.
92
+ * @param currentMenu - The currently active menu.
93
+ * @param targetMenu - The submenu to be displayed.
94
+ */
95
+ private onShowChildTween;
96
+ /**
97
+ * Generates the tween animation for showing a parent menu.
98
+ * This method configures the animation sequence for transitioning back to a parent menu.
99
+ * @param currentMenu - The currently active submenu.
100
+ * @param targetMenu - The parent menu to be displayed.
101
+ */
102
+ private onShowParentTween;
103
+ /**
104
+ * Removes all inline styles from elements within a given context.
105
+ * This is particularly useful for cleaning up after dynamic style changes during menu animations.
106
+ * @param context - The context within which to remove inline styles.
107
+ */
108
+ private removeInlineStyles;
109
+ /**
110
+ * Calculates the natural height of an element, disregarding any explicitly set heights.
111
+ * This is useful for animations that need to adjust to the content's natural size.
112
+ * @param element - The element to measure.
113
+ */
114
+ private getNaturalHeight;
115
+ /**
116
+ * Resets the menu items to their initial state, removing any dynamic modifications.
117
+ * This method is typically called when the menu is being reset or destroyed.
118
+ */
119
+ private resetMenuListItems;
120
+ /**
121
+ * Resets the drilldown menu to its initial state, removing any dynamic styles or selections.
122
+ * This is useful for cleaning up and preparing the menu for another interaction.
123
+ */
124
+ reset(): void;
125
+ /**
126
+ * Cleans up event listeners and resets the menu, preparing the instance for garbage collection.
127
+ * This method should be called to prevent memory leaks when the drilldown menu is no longer needed.
128
+ */
129
+ destroy(): void;
130
+ }
131
+
132
+ /**
133
+ * Configuration options for initializing a Drilldown instance.
134
+ * Defines selectors for menu elements and animation behavior customization.
135
+ */
136
+ export declare type DrilldownOptions = {
137
+ backSelector?: string;
138
+ nextSelector?: string;
139
+ submenuSelector?: string;
140
+ itemSelector?: string;
141
+ /**
142
+ * Callback function executed when the drilldown menu is reset.
143
+ * Useful for custom cleanup or state management.
144
+ */
145
+ onReset?: (() => void) | null;
146
+ /**
147
+ * Animation configuration (options) for child menu transitions.
148
+ * Partial TweenOptions allowing selective override of default values.
149
+ */
150
+ childTweenOptions?: Partial<TweenOptions>;
151
+ /**
152
+ * Animation configuration (options) for parent menu transitions.
153
+ * Partial TweenOptions allowing selective override of default values.
154
+ */
155
+ parentTweenOptions?: Partial<TweenOptions>;
156
+ };
157
+
158
+ export declare type TweenCallback = (currentMenu: HTMLElement, targetMenu: HTMLElement) => void;
159
+
160
+ export declare type TweenOptions = {
161
+ outDuration: number;
162
+ outEase: string;
163
+ outStagger: number;
164
+ inDuration: number;
165
+ inEase: string;
166
+ inStagger: number;
167
+ /**
168
+ * Duration in seconds for height transition animations.
169
+ */
170
+ heightDuration: number;
171
+ /**
172
+ * Easing function for height transition animations.
173
+ */
174
+ heightEase: string;
175
+ onStart?: TweenCallback | null;
176
+ onComplete?: TweenCallback | null;
177
+ };
178
+
179
+ export { }
@@ -0,0 +1,275 @@
1
+ import { gsap as m } from "gsap";
2
+ const u = 0.2, l = "power1.out";
3
+ class f {
4
+ /**
5
+ * Initializes a new instance of the Drilldown class.
6
+ * @param context - The element to initialize the drilldown on.
7
+ * @param options
8
+ */
9
+ constructor(i, s = {}) {
10
+ this.activeMenuItem = null, this.menuResizeObserver = null, this.isAnimating = !1, this.handleMenuClick = (o) => {
11
+ if (this.isAnimating) return;
12
+ const n = o.target, r = n.closest(this.options.nextSelector) ?? n.closest(this.options.backSelector);
13
+ if (!r) return;
14
+ o.preventDefault();
15
+ const a = !!r.closest(this.options.nextSelector), h = r.closest(this.options.submenuSelector);
16
+ if (!h) return;
17
+ const c = this.getTargetMenu(r, a);
18
+ if (!c) return;
19
+ (a ? this.createAnimation(h, c, !0) : this.createAnimation(h, c, !1)).play();
20
+ }, this.onShowParentTween = (o, n) => {
21
+ const r = this.getElementsToHideForMenuTransition(n), a = this.getElementsToHideForMenuTransition(o), h = m.timeline();
22
+ return h.to(a, {
23
+ x: 20,
24
+ autoAlpha: 0,
25
+ duration: this.options.parentTweenOptions.outDuration,
26
+ ease: this.options.parentTweenOptions.outEase,
27
+ stagger: this.options.parentTweenOptions.outStagger
28
+ }), h.to(this.context, {
29
+ // This is calculated at runtime because the height of the parent menu changes
30
+ // just before we animate it in: see the 'onShowParentStart' callback.
31
+ height: () => `${this.getNaturalHeight(n)}px`,
32
+ duration: this.options.parentTweenOptions.heightDuration,
33
+ ease: this.options.parentTweenOptions.heightEase
34
+ }), h.set(o, {
35
+ autoAlpha: 0
36
+ }), h.to(r, {
37
+ autoAlpha: 1,
38
+ x: 0,
39
+ duration: this.options.parentTweenOptions.inDuration,
40
+ ease: this.options.parentTweenOptions.inEase,
41
+ stagger: this.options.parentTweenOptions.inStagger
42
+ }), h;
43
+ }, this.bindThisToMemberFunctions(), this.context = i;
44
+ const t = s.childTweenOptions, e = s.parentTweenOptions;
45
+ this.options = {
46
+ backSelector: s.backSelector || "[data-js-menu-back]",
47
+ nextSelector: s.nextSelector || "[data-js-menu-next]",
48
+ submenuSelector: s.submenuSelector || "[data-js-menu-container]",
49
+ itemSelector: s.itemSelector || "[data-js-menu-item]",
50
+ onReset: s.onReset || null,
51
+ childTweenOptions: {
52
+ outDuration: (t == null ? void 0 : t.outDuration) || u,
53
+ outEase: (t == null ? void 0 : t.outEase) || l,
54
+ outStagger: (t == null ? void 0 : t.outStagger) || 0,
55
+ inDuration: (t == null ? void 0 : t.inDuration) || u,
56
+ inEase: (t == null ? void 0 : t.inEase) || l,
57
+ inStagger: (t == null ? void 0 : t.inStagger) || 0,
58
+ heightDuration: (t == null ? void 0 : t.heightDuration) || u,
59
+ heightEase: (t == null ? void 0 : t.heightEase) || l,
60
+ onStart: (t == null ? void 0 : t.onStart) || null,
61
+ onComplete: (t == null ? void 0 : t.onComplete) || null
62
+ },
63
+ parentTweenOptions: {
64
+ outDuration: (e == null ? void 0 : e.outDuration) || u,
65
+ outEase: (e == null ? void 0 : e.outEase) || l,
66
+ outStagger: (e == null ? void 0 : e.outStagger) || 0,
67
+ inDuration: (e == null ? void 0 : e.inDuration) || u,
68
+ inEase: (e == null ? void 0 : e.inEase) || l,
69
+ inStagger: (e == null ? void 0 : e.inStagger) || 0,
70
+ heightDuration: (e == null ? void 0 : e.heightDuration) || u,
71
+ heightEase: (e == null ? void 0 : e.heightEase) || l,
72
+ onStart: (e == null ? void 0 : e.onStart) || null,
73
+ onComplete: (e == null ? void 0 : e.onComplete) || null
74
+ }
75
+ }, this.activeMenuItem = null, this.menuResizeObserver = null;
76
+ }
77
+ /**
78
+ * Binds instance to member functions to preserve the `this` context.
79
+ */
80
+ bindThisToMemberFunctions() {
81
+ const i = Object.getOwnPropertyNames(Object.getPrototypeOf(this)), s = this;
82
+ i.forEach((t) => {
83
+ const e = s[t];
84
+ typeof e == "function" && (s[t] = e.bind(this));
85
+ });
86
+ }
87
+ init() {
88
+ this.context.addEventListener("click", this.handleMenuClick);
89
+ }
90
+ /**
91
+ * Determines the target menu based on the element that triggered the event.
92
+ * If the action is to navigate to a next (child) menu, it finds the submenu within the active menu item.
93
+ * If the action is to go back, it finds the parent menu of the current submenu.
94
+ *
95
+ * @param element - The element (typically a button or link) that triggered the navigation action.
96
+ * @param isNextButton - A flag indicating whether the navigation action is moving forward to a child menu.
97
+ * @returns The target menu to navigate to, or null if no target menu can be determined.
98
+ */
99
+ getTargetMenu(i, s) {
100
+ return this.activeMenuItem = i.closest(this.options.itemSelector), this.activeMenuItem ? s ? this.activeMenuItem.querySelector(this.options.submenuSelector) : this.activeMenuItem.closest(this.options.submenuSelector) : null;
101
+ }
102
+ /**
103
+ * Creates the animation for transitioning between menus.
104
+ * This function initializes a GSAP tween for either showing a child menu (next) or a parent menu (back),
105
+ * depending on the direction of navigation. It also sets up onStart and onComplete callbacks for the animation.
106
+ *
107
+ * @param currentMenu - The currently active menu that is being transitioned from.
108
+ * @param targetMenu - The target menu that is being transitioned to.
109
+ * @param isChild - Determines the type of animation to create; true for child menu, false for parent menu.
110
+ * @returns The GSAP timeline instance for the created animation.
111
+ */
112
+ createAnimation(i, s, t) {
113
+ const e = t ? this.onShowChildTween(i, s) : this.onShowParentTween(i, s);
114
+ return this.setupTweenCallbacks(e, i, s, t), e.pause(), e;
115
+ }
116
+ /**
117
+ * Sets up GSAP tween callbacks for menu animations. This method configures onStart and onComplete callbacks
118
+ * for the tween animation based on whether it's an animation for showing a child menu or a parent menu.
119
+ * It integrates both custom callback functions provided via options and internal logic defined in the class.
120
+ *
121
+ * @param tween - The GSAP tween instance that controls the menu animation.
122
+ * @param currentMenu - The currently active menu element that is being transitioned from.
123
+ * @param targetMenu - The target menu element that is being transitioned to.
124
+ * @param isChild - Flag to determine if the animation is for showing a child menu (true) or a parent menu (false).
125
+ */
126
+ setupTweenCallbacks(i, s, t, e) {
127
+ const o = e ? this.options.childTweenOptions : this.options.parentTweenOptions, n = e ? this.onShowChildStart : this.onShowParentStart, r = e ? this.onShowChildComplete : this.onShowParentComplete;
128
+ i.eventCallback("onStart", () => {
129
+ var a;
130
+ (a = o.onStart) == null || a.call(o, s, t), n(s, t);
131
+ }), i.eventCallback("onComplete", () => {
132
+ var a;
133
+ (a = o.onComplete) == null || a.call(o, s, t), r(s, t);
134
+ });
135
+ }
136
+ /**
137
+ * Creates a ResizeObserver to adjust the height of the drilldown menu.
138
+ * This ensures the menu height matches the content, especially useful when content changes size.
139
+ */
140
+ createMenuResizeObserver() {
141
+ return this.menuResizeObserver || (this.menuResizeObserver = new ResizeObserver((i) => {
142
+ for (const s of i) {
143
+ const t = s.target;
144
+ this.context.style.height = `${t.offsetHeight}px`;
145
+ }
146
+ })), this.menuResizeObserver;
147
+ }
148
+ /**
149
+ * Returns the main list items for a given menu. It does not include supplementary items, for
150
+ * example, the parent link or the back button.
151
+ * @param menu
152
+ */
153
+ getMenuListItems(i) {
154
+ const s = Array.from(i.children).filter((e) => e.matches("ul"))[0];
155
+ return s ? Array.from(s.children).filter((e) => e.matches(this.options.itemSelector)) : [];
156
+ }
157
+ /**
158
+ * Returns the elements that will be hidden when a menu is transitioning.
159
+ * @param menu
160
+ */
161
+ getElementsToHideForMenuTransition(i) {
162
+ const t = this.getMenuListItems(i).map((r) => r.querySelector("[data-js-menu-link]")), e = i.querySelector(this.options.backSelector), o = i.querySelector("[data-js-menu-parent]");
163
+ return [e, o, ...t];
164
+ }
165
+ /**
166
+ * The callback that is fired when the menu navigates down a level.
167
+ */
168
+ onShowChildStart(i, s) {
169
+ i.scrollTop = 0, s.scrollTop = 0, this.isAnimating = !0;
170
+ }
171
+ /**
172
+ * The callback that is fired after the menu has moved down a level.
173
+ */
174
+ onShowChildComplete(i, s) {
175
+ this.isAnimating = !1, this.getMenuListItems(i).forEach((o) => {
176
+ o !== this.activeMenuItem && o.classList.add("hidden");
177
+ }), this.menuResizeObserver && this.menuResizeObserver.unobserve(i), this.createMenuResizeObserver().observe(s);
178
+ }
179
+ /**
180
+ * The callback that is fired when the menu navigates up a level.
181
+ */
182
+ onShowParentStart(i, s) {
183
+ this.isAnimating = !0, this.getMenuListItems(s).forEach((e) => {
184
+ e.classList.remove("hidden");
185
+ });
186
+ }
187
+ /**
188
+ * The callback that is fired after the menu has moved up a level.
189
+ */
190
+ onShowParentComplete(i, s) {
191
+ this.isAnimating = !1, this.menuResizeObserver && this.menuResizeObserver.unobserve(i), s === this.context ? this.context.style.height = "auto" : this.createMenuResizeObserver().observe(s);
192
+ }
193
+ /**
194
+ * Generates the tween animation for showing a child menu.
195
+ * This method configures the animation sequence for transitioning to a submenu.
196
+ * @param currentMenu - The currently active menu.
197
+ * @param targetMenu - The submenu to be displayed.
198
+ */
199
+ onShowChildTween(i, s) {
200
+ const t = s.offsetHeight, e = this.getElementsToHideForMenuTransition(i), o = this.getElementsToHideForMenuTransition(s), n = m.timeline();
201
+ return n.to(e, {
202
+ autoAlpha: 0,
203
+ x: -20,
204
+ duration: this.options.childTweenOptions.outDuration,
205
+ ease: this.options.childTweenOptions.outEase,
206
+ stagger: this.options.childTweenOptions.outStagger
207
+ }), n.to(this.context, {
208
+ height: `${t}px`,
209
+ duration: this.options.childTweenOptions.heightDuration,
210
+ ease: this.options.childTweenOptions.heightEase
211
+ }), n.set(s, {
212
+ autoAlpha: 1
213
+ }), n.fromTo(o, {
214
+ autoAlpha: 0,
215
+ x: 20
216
+ }, {
217
+ autoAlpha: 1,
218
+ x: 0,
219
+ duration: this.options.childTweenOptions.inDuration,
220
+ ease: this.options.childTweenOptions.inEase,
221
+ stagger: this.options.childTweenOptions.inStagger
222
+ }), n;
223
+ }
224
+ /**
225
+ * Removes all inline styles from elements within a given context.
226
+ * This is particularly useful for cleaning up after dynamic style changes during menu animations.
227
+ * @param context - The context within which to remove inline styles.
228
+ */
229
+ removeInlineStyles(i) {
230
+ const s = Array.from(i.querySelectorAll("[style]"));
231
+ i.hasAttribute("style") && s.push(i), s.forEach((t) => {
232
+ m.set(t, {
233
+ clearProps: "all",
234
+ overwrite: !0
235
+ });
236
+ });
237
+ }
238
+ /**
239
+ * Calculates the natural height of an element, disregarding any explicitly set heights.
240
+ * This is useful for animations that need to adjust to the content's natural size.
241
+ * @param element - The element to measure.
242
+ */
243
+ getNaturalHeight(i) {
244
+ const s = i.offsetHeight;
245
+ i.style.height = "auto";
246
+ const t = i.offsetHeight;
247
+ return i.style.height = `${s}px`, t;
248
+ }
249
+ /**
250
+ * Resets the menu items to their initial state, removing any dynamic modifications.
251
+ * This method is typically called when the menu is being reset or destroyed.
252
+ */
253
+ resetMenuListItems() {
254
+ this.context.querySelectorAll(this.options.itemSelector).forEach((s) => {
255
+ s.classList.remove("hidden");
256
+ });
257
+ }
258
+ /**
259
+ * Resets the drilldown menu to its initial state, removing any dynamic styles or selections.
260
+ * This is useful for cleaning up and preparing the menu for another interaction.
261
+ */
262
+ reset() {
263
+ this.isAnimating = !1, typeof this.options.onReset == "function" && this.options.onReset(), this.resetMenuListItems(), this.removeInlineStyles(this.context), this.menuResizeObserver && this.menuResizeObserver.disconnect();
264
+ }
265
+ /**
266
+ * Cleans up event listeners and resets the menu, preparing the instance for garbage collection.
267
+ * This method should be called to prevent memory leaks when the drilldown menu is no longer needed.
268
+ */
269
+ destroy() {
270
+ this.context.removeEventListener("click", this.handleMenuClick), this.reset();
271
+ }
272
+ }
273
+ export {
274
+ f as Drilldown
275
+ };
@@ -0,0 +1 @@
1
+ (function(u,l){typeof exports=="object"&&typeof module<"u"?l(exports,require("gsap")):typeof define=="function"&&define.amd?define(["exports","gsap"],l):(u=typeof globalThis<"u"?globalThis:u||self,l(u.Drilldown={},u.gsap))})(this,function(u,l){"use strict";const c="power1.out";class g{constructor(i,s={}){this.activeMenuItem=null,this.menuResizeObserver=null,this.isAnimating=!1,this.handleMenuClick=o=>{if(this.isAnimating)return;const n=o.target,r=n.closest(this.options.nextSelector)??n.closest(this.options.backSelector);if(!r)return;o.preventDefault();const a=!!r.closest(this.options.nextSelector),h=r.closest(this.options.submenuSelector);if(!h)return;const m=this.getTargetMenu(r,a);if(!m)return;(a?this.createAnimation(h,m,!0):this.createAnimation(h,m,!1)).play()},this.onShowParentTween=(o,n)=>{const r=this.getElementsToHideForMenuTransition(n),a=this.getElementsToHideForMenuTransition(o),h=l.gsap.timeline();return h.to(a,{x:20,autoAlpha:0,duration:this.options.parentTweenOptions.outDuration,ease:this.options.parentTweenOptions.outEase,stagger:this.options.parentTweenOptions.outStagger}),h.to(this.context,{height:()=>`${this.getNaturalHeight(n)}px`,duration:this.options.parentTweenOptions.heightDuration,ease:this.options.parentTweenOptions.heightEase}),h.set(o,{autoAlpha:0}),h.to(r,{autoAlpha:1,x:0,duration:this.options.parentTweenOptions.inDuration,ease:this.options.parentTweenOptions.inEase,stagger:this.options.parentTweenOptions.inStagger}),h},this.bindThisToMemberFunctions(),this.context=i;const t=s.childTweenOptions,e=s.parentTweenOptions;this.options={backSelector:s.backSelector||"[data-js-menu-back]",nextSelector:s.nextSelector||"[data-js-menu-next]",submenuSelector:s.submenuSelector||"[data-js-menu-container]",itemSelector:s.itemSelector||"[data-js-menu-item]",onReset:s.onReset||null,childTweenOptions:{outDuration:(t==null?void 0:t.outDuration)||.2,outEase:(t==null?void 0:t.outEase)||c,outStagger:(t==null?void 0:t.outStagger)||0,inDuration:(t==null?void 0:t.inDuration)||.2,inEase:(t==null?void 0:t.inEase)||c,inStagger:(t==null?void 0:t.inStagger)||0,heightDuration:(t==null?void 0:t.heightDuration)||.2,heightEase:(t==null?void 0:t.heightEase)||c,onStart:(t==null?void 0:t.onStart)||null,onComplete:(t==null?void 0:t.onComplete)||null},parentTweenOptions:{outDuration:(e==null?void 0:e.outDuration)||.2,outEase:(e==null?void 0:e.outEase)||c,outStagger:(e==null?void 0:e.outStagger)||0,inDuration:(e==null?void 0:e.inDuration)||.2,inEase:(e==null?void 0:e.inEase)||c,inStagger:(e==null?void 0:e.inStagger)||0,heightDuration:(e==null?void 0:e.heightDuration)||.2,heightEase:(e==null?void 0:e.heightEase)||c,onStart:(e==null?void 0:e.onStart)||null,onComplete:(e==null?void 0:e.onComplete)||null}},this.activeMenuItem=null,this.menuResizeObserver=null}bindThisToMemberFunctions(){const i=Object.getOwnPropertyNames(Object.getPrototypeOf(this)),s=this;i.forEach(t=>{const e=s[t];typeof e=="function"&&(s[t]=e.bind(this))})}init(){this.context.addEventListener("click",this.handleMenuClick)}getTargetMenu(i,s){return this.activeMenuItem=i.closest(this.options.itemSelector),this.activeMenuItem?s?this.activeMenuItem.querySelector(this.options.submenuSelector):this.activeMenuItem.closest(this.options.submenuSelector):null}createAnimation(i,s,t){const e=t?this.onShowChildTween(i,s):this.onShowParentTween(i,s);return this.setupTweenCallbacks(e,i,s,t),e.pause(),e}setupTweenCallbacks(i,s,t,e){const o=e?this.options.childTweenOptions:this.options.parentTweenOptions,n=e?this.onShowChildStart:this.onShowParentStart,r=e?this.onShowChildComplete:this.onShowParentComplete;i.eventCallback("onStart",()=>{var a;(a=o.onStart)==null||a.call(o,s,t),n(s,t)}),i.eventCallback("onComplete",()=>{var a;(a=o.onComplete)==null||a.call(o,s,t),r(s,t)})}createMenuResizeObserver(){return this.menuResizeObserver||(this.menuResizeObserver=new ResizeObserver(i=>{for(const s of i){const t=s.target;this.context.style.height=`${t.offsetHeight}px`}})),this.menuResizeObserver}getMenuListItems(i){const s=Array.from(i.children).filter(e=>e.matches("ul"))[0];return s?Array.from(s.children).filter(e=>e.matches(this.options.itemSelector)):[]}getElementsToHideForMenuTransition(i){const t=this.getMenuListItems(i).map(r=>r.querySelector("[data-js-menu-link]")),e=i.querySelector(this.options.backSelector),o=i.querySelector("[data-js-menu-parent]");return[e,o,...t]}onShowChildStart(i,s){i.scrollTop=0,s.scrollTop=0,this.isAnimating=!0}onShowChildComplete(i,s){this.isAnimating=!1,this.getMenuListItems(i).forEach(o=>{o!==this.activeMenuItem&&o.classList.add("hidden")}),this.menuResizeObserver&&this.menuResizeObserver.unobserve(i),this.createMenuResizeObserver().observe(s)}onShowParentStart(i,s){this.isAnimating=!0,this.getMenuListItems(s).forEach(e=>{e.classList.remove("hidden")})}onShowParentComplete(i,s){this.isAnimating=!1,this.menuResizeObserver&&this.menuResizeObserver.unobserve(i),s===this.context?this.context.style.height="auto":this.createMenuResizeObserver().observe(s)}onShowChildTween(i,s){const t=s.offsetHeight,e=this.getElementsToHideForMenuTransition(i),o=this.getElementsToHideForMenuTransition(s),n=l.gsap.timeline();return n.to(e,{autoAlpha:0,x:-20,duration:this.options.childTweenOptions.outDuration,ease:this.options.childTweenOptions.outEase,stagger:this.options.childTweenOptions.outStagger}),n.to(this.context,{height:`${t}px`,duration:this.options.childTweenOptions.heightDuration,ease:this.options.childTweenOptions.heightEase}),n.set(s,{autoAlpha:1}),n.fromTo(o,{autoAlpha:0,x:20},{autoAlpha:1,x:0,duration:this.options.childTweenOptions.inDuration,ease:this.options.childTweenOptions.inEase,stagger:this.options.childTweenOptions.inStagger}),n}removeInlineStyles(i){const s=Array.from(i.querySelectorAll("[style]"));i.hasAttribute("style")&&s.push(i),s.forEach(t=>{l.gsap.set(t,{clearProps:"all",overwrite:!0})})}getNaturalHeight(i){const s=i.offsetHeight;i.style.height="auto";const t=i.offsetHeight;return i.style.height=`${s}px`,t}resetMenuListItems(){this.context.querySelectorAll(this.options.itemSelector).forEach(s=>{s.classList.remove("hidden")})}reset(){this.isAnimating=!1,typeof this.options.onReset=="function"&&this.options.onReset(),this.resetMenuListItems(),this.removeInlineStyles(this.context),this.menuResizeObserver&&this.menuResizeObserver.disconnect()}destroy(){this.context.removeEventListener("click",this.handleMenuClick),this.reset()}}u.Drilldown=g,Object.defineProperty(u,Symbol.toStringTag,{value:"Module"})});
package/package.json ADDED
@@ -0,0 +1,101 @@
1
+ {
2
+ "name": "@numiko/drilldown",
3
+ "version": "2.0.0",
4
+ "description": "A package to create a menu system enabling you to drilldown to deeper levels and navigate back up",
5
+ "author": "Numiko",
6
+ "private": false,
7
+ "license": "ISC",
8
+ "homepage": "https://bitbucket.org/numiko/drilldown#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+ssh://git@bitbucket.org/numiko/drilldown.git"
12
+ },
13
+ "type": "module",
14
+ "scripts": {
15
+ "dev": "vite",
16
+ "build": "vite build",
17
+ "preview": "vite preview",
18
+ "test": "vitest",
19
+ "test:coverage": "vitest --coverage",
20
+ "test:ci": "vitest --run --reporter junit --outputFile ./reports/results.xml --coverage.enabled --coverage.include lib --coverage.reporter=html --coverage.reportsDirectory ./reports/",
21
+ "generate-docs": "typedoc lib/Drilldown.ts",
22
+ "deploy-docs": "netlify deploy --dir=docs --alias=v$(node -p \"require('./package.json').version\")",
23
+ "release": "echo 'Releasing package. Ensure the module is linked to the Netlify project using `netlify link`.'; release-it release"
24
+ },
25
+ "main": "./dist/Drilldown.umd.js",
26
+ "files": [
27
+ "dist",
28
+ "package.json",
29
+ "README.md"
30
+ ],
31
+ "exports": {
32
+ ".": {
33
+ "import": {
34
+ "types": "./dist/Drilldown.es.d.ts",
35
+ "module": "./dist/Drilldown.es.js"
36
+ },
37
+ "require": "./dist/Drilldown.umd.js"
38
+ }
39
+ },
40
+ "devDependencies": {
41
+ "@eslint/js": "^9.38.0",
42
+ "@j-ulrich/release-it-regex-bumper": "^5.3.0",
43
+ "@stylistic/eslint-plugin": "^2.9.0",
44
+ "@types/gsap": "^3.0.0",
45
+ "@types/node": "^24.9.1",
46
+ "@typescript-eslint/eslint-plugin": "^8.46.2",
47
+ "@typescript-eslint/parser": "^8.46.2",
48
+ "@vitest/coverage-v8": "^2.1.3",
49
+ "eslint": "^9.38.0",
50
+ "globals": "^15.11.0",
51
+ "happy-dom": "^20.0.11",
52
+ "netlify-cli": "^23.13.3",
53
+ "release-it": "^19.0.5",
54
+ "typedoc": "^0.28.14",
55
+ "typescript": "^5.9.3",
56
+ "typescript-eslint": "^8.46.2",
57
+ "vite": "^5.4.9",
58
+ "vite-plugin-dts": "^4.5.4",
59
+ "vite-plugin-eslint": "^1.8.1",
60
+ "vitest": "^2.1.3"
61
+ },
62
+ "dependencies": {
63
+ "gsap": "^3.13.0"
64
+ },
65
+ "optionalDependencies": {
66
+ "@rollup/rollup-darwin-arm64": "^4.18.0",
67
+ "@rollup/rollup-darwin-x64": "^4.18.0",
68
+ "@rollup/rollup-linux-x64-gnu": "^4.52.5"
69
+ },
70
+ "release-it": {
71
+ "$schema": "https://unpkg.com/release-it/schema/release-it.json",
72
+ "plugins": {
73
+ "@j-ulrich/release-it-regex-bumper": {
74
+ "out": [
75
+ {
76
+ "file": "README.md",
77
+ "search": "\\d+-\\d+-\\d+",
78
+ "replace": "{{major}}-{{minor}}-{{patch}}"
79
+ }
80
+ ]
81
+ }
82
+ },
83
+ "npm": {
84
+ "publish": true,
85
+ "publishArgs": [
86
+ "--access",
87
+ "public"
88
+ ]
89
+ },
90
+ "hooks": {
91
+ "before:init": [
92
+ "vitest run"
93
+ ],
94
+ "after:bump": [
95
+ "npm run generate-docs",
96
+ "npm run build",
97
+ "npm run deploy-docs"
98
+ ]
99
+ }
100
+ }
101
+ }