mtrl 0.2.8 → 0.2.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.ts +2 -0
- package/package.json +1 -1
- package/src/components/navigation/api.ts +131 -96
- package/src/components/navigation/features/controller.ts +273 -0
- package/src/components/navigation/features/items.ts +133 -64
- package/src/components/navigation/navigation.ts +17 -2
- package/src/components/navigation/system-types.ts +124 -0
- package/src/components/navigation/system.ts +776 -0
- package/src/components/slider/config.ts +20 -2
- package/src/components/slider/features/controller.ts +761 -0
- package/src/components/slider/features/handlers.ts +18 -15
- package/src/components/slider/features/index.ts +3 -2
- package/src/components/slider/features/range.ts +104 -0
- package/src/components/slider/slider.ts +34 -14
- package/src/components/slider/structure.ts +152 -0
- package/src/components/textfield/api.ts +53 -0
- package/src/components/textfield/features.ts +322 -0
- package/src/components/textfield/textfield.ts +8 -0
- package/src/components/textfield/types.ts +12 -3
- package/src/components/timepicker/clockdial.ts +1 -4
- package/src/core/compose/features/textinput.ts +15 -2
- package/src/core/composition/features/dom.ts +33 -0
- package/src/core/composition/features/icon.ts +131 -0
- package/src/core/composition/features/index.ts +11 -0
- package/src/core/composition/features/label.ts +156 -0
- package/src/core/composition/features/structure.ts +22 -0
- package/src/core/composition/index.ts +26 -0
- package/src/core/index.ts +1 -1
- package/src/core/structure.ts +288 -0
- package/src/index.ts +1 -0
- package/src/styles/components/_navigation-mobile.scss +244 -0
- package/src/styles/components/_navigation-system.scss +151 -0
- package/src/styles/components/_textfield.scss +250 -11
- package/demo/build.ts +0 -349
- package/demo/index.html +0 -110
- package/demo/main.js +0 -448
- package/demo/styles.css +0 -239
- package/server.ts +0 -86
- package/src/components/slider/features/slider.ts +0 -318
- package/src/components/slider/features/structure.ts +0 -181
- package/src/components/slider/features/ui.ts +0 -388
- package/src/components/textfield/constants.ts +0 -100
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
// src/components/navigation/system.ts
|
|
2
|
+
|
|
3
|
+
import createNavigation from './navigation';
|
|
4
|
+
import {
|
|
5
|
+
isMobileDevice,
|
|
6
|
+
hasTouchSupport,
|
|
7
|
+
TOUCH_CONFIG,
|
|
8
|
+
TOUCH_TARGETS,
|
|
9
|
+
normalizeEvent
|
|
10
|
+
} from '../../core/utils/mobile';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates a complete navigation system with synchronized rail and drawer components
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} options - System configuration options
|
|
16
|
+
* @returns {Object} Navigation system API
|
|
17
|
+
*/
|
|
18
|
+
export const createNavigationSystem = (options = {}) => {
|
|
19
|
+
// Determine mobile configuration
|
|
20
|
+
const mobileConfig = {
|
|
21
|
+
breakpoint: options.breakpoint || 960,
|
|
22
|
+
lockBodyScroll: options.lockBodyScroll !== false,
|
|
23
|
+
hideOnClickOutside: options.hideOnClickOutside !== false,
|
|
24
|
+
enableSwipeGestures: options.enableSwipeGestures !== false,
|
|
25
|
+
optimizeForTouch: options.optimizeForTouch !== false,
|
|
26
|
+
overlayClass: options.overlayClass || 'mtrl-nav-overlay',
|
|
27
|
+
closeButtonClass: options.closeButtonClass || 'mtrl-nav-close-btn',
|
|
28
|
+
bodyLockClass: options.bodyLockClass || 'mtrl-body-drawer-open'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Internal state
|
|
32
|
+
const state = {
|
|
33
|
+
rail: null,
|
|
34
|
+
drawer: null,
|
|
35
|
+
activeSection: options.activeSection || null,
|
|
36
|
+
activeSubsection: options.activeSubsection || null,
|
|
37
|
+
items: options.items || {},
|
|
38
|
+
mouseInDrawer: false,
|
|
39
|
+
mouseInRail: false,
|
|
40
|
+
hoverTimer: null,
|
|
41
|
+
closeTimer: null,
|
|
42
|
+
processingChange: false,
|
|
43
|
+
isMobile: false,
|
|
44
|
+
overlayElement: null,
|
|
45
|
+
closeButtonElement: null,
|
|
46
|
+
resizeObserver: null,
|
|
47
|
+
outsideClickHandler: null,
|
|
48
|
+
outsideClickHandlerSet: false
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Config with defaults
|
|
52
|
+
const config = {
|
|
53
|
+
// Display options
|
|
54
|
+
animateDrawer: true,
|
|
55
|
+
showLabelsOnRail: options.showLabelsOnRail !== false,
|
|
56
|
+
hideDrawerOnClick: options.hideDrawerOnClick || false,
|
|
57
|
+
expanded: options.expanded === true,
|
|
58
|
+
|
|
59
|
+
// Timing options (ms)
|
|
60
|
+
hoverDelay: options.hoverDelay || 200,
|
|
61
|
+
closeDelay: options.closeDelay || 100,
|
|
62
|
+
|
|
63
|
+
// Component options
|
|
64
|
+
railOptions: options.railOptions || {},
|
|
65
|
+
drawerOptions: options.drawerOptions || {}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Update drawer content for a specific section WITHOUT changing visibility
|
|
70
|
+
*/
|
|
71
|
+
const updateDrawerContent = (sectionId) => {
|
|
72
|
+
if (!state.drawer || !sectionId || !state.items[sectionId]) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Get section items
|
|
77
|
+
const sectionData = state.items[sectionId];
|
|
78
|
+
const items = sectionData.items || [];
|
|
79
|
+
|
|
80
|
+
// If no items, hide drawer and exit
|
|
81
|
+
if (items.length === 0) {
|
|
82
|
+
hideDrawer();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Clear existing drawer items first using the API
|
|
87
|
+
const currentItems = state.drawer.getAllItems();
|
|
88
|
+
if (currentItems?.length > 0) {
|
|
89
|
+
currentItems.forEach(item => {
|
|
90
|
+
state.drawer.removeItem(item.config.id);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Add new items to drawer through the API
|
|
95
|
+
items.forEach(item => {
|
|
96
|
+
state.drawer.addItem(item);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Show the drawer
|
|
100
|
+
showDrawer();
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Create the rail navigation component
|
|
105
|
+
*/
|
|
106
|
+
const createRailNavigation = () => {
|
|
107
|
+
// Build rail items from sections
|
|
108
|
+
const railItems = Object.keys(state.items || {}).map(sectionId => ({
|
|
109
|
+
id: sectionId,
|
|
110
|
+
label: state.items[sectionId]?.label || sectionId,
|
|
111
|
+
icon: state.items[sectionId]?.icon || '',
|
|
112
|
+
active: sectionId === state.activeSection
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
// Create the rail component
|
|
116
|
+
const rail = createNavigation({
|
|
117
|
+
variant: 'rail',
|
|
118
|
+
position: 'left',
|
|
119
|
+
showLabels: config.showLabelsOnRail,
|
|
120
|
+
items: railItems,
|
|
121
|
+
...config.railOptions
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
document.body.appendChild(rail.element);
|
|
125
|
+
|
|
126
|
+
// Register for change events - will listen for when rail items are clicked
|
|
127
|
+
rail.on('change', (event) => {
|
|
128
|
+
console.log('rail.on change', event)
|
|
129
|
+
// Extract ID from event data
|
|
130
|
+
const id = event?.id;
|
|
131
|
+
|
|
132
|
+
if (!id || state.processingChange) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check if this is a user action
|
|
137
|
+
const isUserAction = event?.source === 'userAction';
|
|
138
|
+
|
|
139
|
+
// Set processing flag to prevent loops
|
|
140
|
+
state.processingChange = true;
|
|
141
|
+
|
|
142
|
+
// Update active section
|
|
143
|
+
state.activeSection = id;
|
|
144
|
+
|
|
145
|
+
// Handle internally first - update drawer content
|
|
146
|
+
updateDrawerContent(id);
|
|
147
|
+
|
|
148
|
+
// Then notify external handlers
|
|
149
|
+
if (system.onSectionChange && isUserAction) {
|
|
150
|
+
system.onSectionChange(id, { source: isUserAction ? 'userClick' : 'programmatic' });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Clear the processing flag after a delay
|
|
154
|
+
setTimeout(() => {
|
|
155
|
+
state.processingChange = false;
|
|
156
|
+
}, 50);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
rail.on('mouseover', (event) => {
|
|
160
|
+
const id = event?.id;
|
|
161
|
+
|
|
162
|
+
// Set rail mouse state
|
|
163
|
+
state.mouseInRail = true;
|
|
164
|
+
|
|
165
|
+
// Clear any existing hover timer
|
|
166
|
+
clearTimeout(state.hoverTimer);
|
|
167
|
+
state.hoverTimer = null;
|
|
168
|
+
|
|
169
|
+
// Only schedule drawer operations if there's an ID
|
|
170
|
+
if (id) {
|
|
171
|
+
// Check if this section has items
|
|
172
|
+
if (state.items[id]?.items?.length > 0) {
|
|
173
|
+
// Has items - schedule drawer opening
|
|
174
|
+
state.hoverTimer = setTimeout(() => {
|
|
175
|
+
updateDrawerContent(id);
|
|
176
|
+
}, config.hoverDelay);
|
|
177
|
+
} else {
|
|
178
|
+
// No items - hide drawer after a delay to prevent flickering
|
|
179
|
+
state.closeTimer = setTimeout(() => {
|
|
180
|
+
// Only hide if we're still in the rail but not in the drawer
|
|
181
|
+
if (state.mouseInRail && !state.mouseInDrawer) {
|
|
182
|
+
hideDrawer();
|
|
183
|
+
}
|
|
184
|
+
}, config.hoverDelay);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
rail.on('mouseenter', () => {
|
|
190
|
+
state.mouseInRail = true;
|
|
191
|
+
|
|
192
|
+
// Clear any pending drawer close timer when entering rail
|
|
193
|
+
clearTimeout(state.closeTimer);
|
|
194
|
+
state.closeTimer = null;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
rail.on('mouseleave', () => {
|
|
198
|
+
state.mouseInRail = false;
|
|
199
|
+
|
|
200
|
+
// Clear any existing hover timer
|
|
201
|
+
clearTimeout(state.hoverTimer);
|
|
202
|
+
state.hoverTimer = null;
|
|
203
|
+
|
|
204
|
+
// Only set timer to hide drawer if we're not in drawer either
|
|
205
|
+
if (!state.mouseInDrawer) {
|
|
206
|
+
state.closeTimer = setTimeout(() => {
|
|
207
|
+
// Double-check we're still not in rail or drawer before hiding
|
|
208
|
+
if (!state.mouseInRail && !state.mouseInDrawer) {
|
|
209
|
+
hideDrawer();
|
|
210
|
+
}
|
|
211
|
+
}, config.closeDelay);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return rail;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Create the drawer navigation component
|
|
220
|
+
*/
|
|
221
|
+
const createDrawerNavigation = () => {
|
|
222
|
+
// Create the drawer component (initially empty)
|
|
223
|
+
const drawer = createNavigation({
|
|
224
|
+
variant: 'drawer',
|
|
225
|
+
position: 'left',
|
|
226
|
+
items: [], // Start empty
|
|
227
|
+
...config.drawerOptions
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
document.body.appendChild(drawer.element);
|
|
231
|
+
|
|
232
|
+
// Mark drawer with identifier
|
|
233
|
+
drawer.element.dataset.id = 'drawer';
|
|
234
|
+
|
|
235
|
+
// IMPORTANT: Make drawer initially hidden unless explicitly expanded
|
|
236
|
+
if (!config.expanded) {
|
|
237
|
+
drawer.element.classList.add('mtrl-nav--hidden');
|
|
238
|
+
drawer.element.setAttribute('aria-hidden', 'true');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Use the component's native event system
|
|
242
|
+
if (typeof drawer.on === 'function') {
|
|
243
|
+
// Handle item selection
|
|
244
|
+
drawer.on('change', (event) => {
|
|
245
|
+
const id = event.id;
|
|
246
|
+
|
|
247
|
+
state.activeSubsection = id;
|
|
248
|
+
|
|
249
|
+
// If configuration specifies to hide drawer on click, do so
|
|
250
|
+
if (config.hideDrawerOnClick) {
|
|
251
|
+
hideDrawer();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Emit item selection event
|
|
255
|
+
if (system.onItemSelect) {
|
|
256
|
+
system.onItemSelect(event);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Handle mouseenter/mouseleave for drawer
|
|
261
|
+
drawer.on('mouseenter', () => {
|
|
262
|
+
state.mouseInDrawer = true;
|
|
263
|
+
|
|
264
|
+
// Clear any hover and close timers
|
|
265
|
+
clearTimeout(state.hoverTimer);
|
|
266
|
+
clearTimeout(state.closeTimer);
|
|
267
|
+
state.hoverTimer = null;
|
|
268
|
+
state.closeTimer = null;
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
drawer.on('mouseleave', () => {
|
|
272
|
+
state.mouseInDrawer = false;
|
|
273
|
+
|
|
274
|
+
// Only set timer to hide drawer if we're not in rail
|
|
275
|
+
if (!state.mouseInRail) {
|
|
276
|
+
state.closeTimer = setTimeout(() => {
|
|
277
|
+
// Double-check we're still not in drawer or rail before hiding
|
|
278
|
+
if (!state.mouseInDrawer && !state.mouseInRail) {
|
|
279
|
+
hideDrawer();
|
|
280
|
+
}
|
|
281
|
+
}, config.closeDelay);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return drawer;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Checks and updates the mobile state
|
|
291
|
+
*/
|
|
292
|
+
const checkMobileState = () => {
|
|
293
|
+
const prevState = state.isMobile;
|
|
294
|
+
state.isMobile = window.innerWidth <= mobileConfig.breakpoint || isMobileDevice();
|
|
295
|
+
|
|
296
|
+
// If state changed, adjust UI
|
|
297
|
+
if (prevState !== state.isMobile) {
|
|
298
|
+
if (state.isMobile) {
|
|
299
|
+
// Switched to mobile mode
|
|
300
|
+
setupMobileMode();
|
|
301
|
+
} else {
|
|
302
|
+
// Switched to desktop mode
|
|
303
|
+
teardownMobileMode();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Emit a view change event
|
|
307
|
+
if (system.onViewChange) {
|
|
308
|
+
system.onViewChange({
|
|
309
|
+
mobile: state.isMobile,
|
|
310
|
+
previousMobile: prevState,
|
|
311
|
+
width: window.innerWidth
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Creates and appends overlay element for mobile
|
|
319
|
+
*/
|
|
320
|
+
const createOverlay = () => {
|
|
321
|
+
if (state.overlayElement) return state.overlayElement;
|
|
322
|
+
|
|
323
|
+
state.overlayElement = document.createElement('div');
|
|
324
|
+
state.overlayElement.className = mobileConfig.overlayClass;
|
|
325
|
+
state.overlayElement.setAttribute('aria-hidden', 'true');
|
|
326
|
+
document.body.appendChild(state.overlayElement);
|
|
327
|
+
|
|
328
|
+
state.overlayElement.addEventListener('click', (event) => {
|
|
329
|
+
if (event.target === state.overlayElement) {
|
|
330
|
+
hideDrawer();
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
return state.overlayElement;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Creates and adds close button to the drawer
|
|
339
|
+
*/
|
|
340
|
+
const createCloseButton = () => {
|
|
341
|
+
if (!state.drawer || state.closeButtonElement) return null;
|
|
342
|
+
|
|
343
|
+
state.closeButtonElement = document.createElement('button');
|
|
344
|
+
state.closeButtonElement.className = mobileConfig.closeButtonClass;
|
|
345
|
+
state.closeButtonElement.setAttribute('aria-label', 'Close navigation');
|
|
346
|
+
state.closeButtonElement.innerHTML = `
|
|
347
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
348
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
349
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
350
|
+
</svg>
|
|
351
|
+
`;
|
|
352
|
+
|
|
353
|
+
// Handle click event
|
|
354
|
+
state.closeButtonElement.addEventListener('click', () => {
|
|
355
|
+
hideDrawer();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Apply touch-friendly styles if needed
|
|
359
|
+
if (hasTouchSupport() && mobileConfig.optimizeForTouch) {
|
|
360
|
+
state.closeButtonElement.style.minWidth = `${TOUCH_TARGETS.COMFORTABLE}px`;
|
|
361
|
+
state.closeButtonElement.style.minHeight = `${TOUCH_TARGETS.COMFORTABLE}px`;
|
|
362
|
+
|
|
363
|
+
// Add touch feedback
|
|
364
|
+
state.closeButtonElement.addEventListener('touchstart', () => {
|
|
365
|
+
state.closeButtonElement.classList.add('active');
|
|
366
|
+
}, { passive: true });
|
|
367
|
+
|
|
368
|
+
state.closeButtonElement.addEventListener('touchend', () => {
|
|
369
|
+
setTimeout(() => {
|
|
370
|
+
state.closeButtonElement.classList.remove('active');
|
|
371
|
+
}, TOUCH_CONFIG.FEEDBACK_DURATION);
|
|
372
|
+
}, { passive: true });
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
state.drawer.element.appendChild(state.closeButtonElement);
|
|
376
|
+
return state.closeButtonElement;
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Sets up mobile mode features
|
|
381
|
+
*/
|
|
382
|
+
const setupMobileMode = () => {
|
|
383
|
+
const drawer = state.drawer;
|
|
384
|
+
const rail = state.rail;
|
|
385
|
+
|
|
386
|
+
if (!drawer || !rail) return;
|
|
387
|
+
|
|
388
|
+
// Create mobile UI elements
|
|
389
|
+
createOverlay();
|
|
390
|
+
createCloseButton();
|
|
391
|
+
|
|
392
|
+
// Setup outside click handling
|
|
393
|
+
setupOutsideClickHandling();
|
|
394
|
+
|
|
395
|
+
// Setup touch gestures if enabled
|
|
396
|
+
if (mobileConfig.enableSwipeGestures && hasTouchSupport()) {
|
|
397
|
+
setupTouchGestures();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Hide drawer initially in mobile mode
|
|
401
|
+
hideDrawer();
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Tears down mobile-specific features
|
|
406
|
+
*/
|
|
407
|
+
const teardownMobileMode = () => {
|
|
408
|
+
// Hide overlay
|
|
409
|
+
if (state.overlayElement) {
|
|
410
|
+
state.overlayElement.classList.remove('active');
|
|
411
|
+
state.overlayElement.setAttribute('aria-hidden', 'true');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Hide close button
|
|
415
|
+
if (state.closeButtonElement) {
|
|
416
|
+
state.closeButtonElement.style.display = 'none';
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Remove body scroll lock if applied
|
|
420
|
+
document.body.classList.remove(mobileConfig.bodyLockClass);
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Sets up outside click handling for mobile
|
|
425
|
+
*/
|
|
426
|
+
const setupOutsideClickHandling = () => {
|
|
427
|
+
if (!mobileConfig.hideOnClickOutside) return;
|
|
428
|
+
|
|
429
|
+
// Only set up once
|
|
430
|
+
if (state.outsideClickHandlerSet) return;
|
|
431
|
+
state.outsideClickHandlerSet = true;
|
|
432
|
+
|
|
433
|
+
// Use either click or touchend event depending on device capability
|
|
434
|
+
const eventType = hasTouchSupport() ? 'touchend' : 'click';
|
|
435
|
+
|
|
436
|
+
// The handler function
|
|
437
|
+
const handleOutsideClick = (event) => {
|
|
438
|
+
if (!state.isMobile || !isDrawerVisible()) return;
|
|
439
|
+
|
|
440
|
+
const normalizedEvent = normalizeEvent(event);
|
|
441
|
+
const target = normalizedEvent.target as HTMLElement;
|
|
442
|
+
|
|
443
|
+
// Don't close if clicking on drawer, rail, or excluded elements
|
|
444
|
+
if (state.drawer.element.contains(target) ||
|
|
445
|
+
state.rail.element.contains(target)) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Close drawer - it's an outside click/touch
|
|
450
|
+
hideDrawer();
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// Store handler for cleanup
|
|
454
|
+
state.outsideClickHandler = handleOutsideClick;
|
|
455
|
+
|
|
456
|
+
// Add listener
|
|
457
|
+
document.addEventListener(eventType, handleOutsideClick,
|
|
458
|
+
hasTouchSupport() ? { passive: true } : false);
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Sets up touch gestures for mobile
|
|
463
|
+
*/
|
|
464
|
+
const setupTouchGestures = () => {
|
|
465
|
+
const drawer = state.drawer;
|
|
466
|
+
const rail = state.rail;
|
|
467
|
+
|
|
468
|
+
if (!drawer || !rail) return;
|
|
469
|
+
|
|
470
|
+
let touchStartX = 0;
|
|
471
|
+
let touchStartY = 0;
|
|
472
|
+
|
|
473
|
+
// Rail swipe right to open drawer
|
|
474
|
+
rail.element.addEventListener('touchstart', (event) => {
|
|
475
|
+
const touch = event.touches[0];
|
|
476
|
+
touchStartX = touch.clientX;
|
|
477
|
+
touchStartY = touch.clientY;
|
|
478
|
+
}, { passive: true });
|
|
479
|
+
|
|
480
|
+
rail.element.addEventListener('touchmove', (event) => {
|
|
481
|
+
if (!state.isMobile || isDrawerVisible()) return;
|
|
482
|
+
|
|
483
|
+
const touch = event.touches[0];
|
|
484
|
+
const deltaX = touch.clientX - touchStartX;
|
|
485
|
+
const deltaY = touch.clientY - touchStartY;
|
|
486
|
+
|
|
487
|
+
// Only consider horizontal swipes
|
|
488
|
+
if (Math.abs(deltaX) > Math.abs(deltaY) &&
|
|
489
|
+
deltaX > TOUCH_CONFIG.SWIPE_THRESHOLD) {
|
|
490
|
+
showDrawer();
|
|
491
|
+
}
|
|
492
|
+
}, { passive: true });
|
|
493
|
+
|
|
494
|
+
// Drawer swipe left to close
|
|
495
|
+
drawer.element.addEventListener('touchstart', (event) => {
|
|
496
|
+
const touch = event.touches[0];
|
|
497
|
+
touchStartX = touch.clientX;
|
|
498
|
+
touchStartY = touch.clientY;
|
|
499
|
+
}, { passive: true });
|
|
500
|
+
|
|
501
|
+
// Use touchmove with transform for visual feedback
|
|
502
|
+
drawer.element.addEventListener('touchmove', (event) => {
|
|
503
|
+
if (!state.isMobile || !isDrawerVisible()) return;
|
|
504
|
+
|
|
505
|
+
const touch = event.touches[0];
|
|
506
|
+
const deltaX = touch.clientX - touchStartX;
|
|
507
|
+
|
|
508
|
+
// Only apply transform for leftward swipes
|
|
509
|
+
if (deltaX < 0) {
|
|
510
|
+
// Apply transform with resistance
|
|
511
|
+
drawer.element.style.transform = `translateX(${deltaX / 2}px)`;
|
|
512
|
+
|
|
513
|
+
// Close if threshold reached
|
|
514
|
+
if (deltaX < -TOUCH_CONFIG.SWIPE_THRESHOLD) {
|
|
515
|
+
hideDrawer();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}, { passive: true });
|
|
519
|
+
|
|
520
|
+
// Reset transforms when touch ends
|
|
521
|
+
drawer.element.addEventListener('touchend', () => {
|
|
522
|
+
if (drawer.element.style.transform) {
|
|
523
|
+
drawer.element.style.transition = 'transform 0.2s ease';
|
|
524
|
+
drawer.element.style.transform = '';
|
|
525
|
+
|
|
526
|
+
setTimeout(() => {
|
|
527
|
+
drawer.element.style.transition = '';
|
|
528
|
+
}, 200);
|
|
529
|
+
}
|
|
530
|
+
}, { passive: true });
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Show the drawer with mobile-specific behaviors
|
|
535
|
+
*/
|
|
536
|
+
const showDrawer = () => {
|
|
537
|
+
if (!state.drawer) return;
|
|
538
|
+
|
|
539
|
+
state.drawer.element.classList.remove('mtrl-nav--hidden');
|
|
540
|
+
state.drawer.element.setAttribute('aria-hidden', 'false');
|
|
541
|
+
|
|
542
|
+
// Apply mobile-specific behaviors
|
|
543
|
+
if (state.isMobile) {
|
|
544
|
+
if (state.overlayElement) {
|
|
545
|
+
state.overlayElement.classList.add('active');
|
|
546
|
+
state.overlayElement.setAttribute('aria-hidden', 'false');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Lock body scroll if enabled
|
|
550
|
+
if (mobileConfig.lockBodyScroll) {
|
|
551
|
+
document.body.classList.add(mobileConfig.bodyLockClass);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Ensure close button is visible
|
|
555
|
+
if (state.closeButtonElement) {
|
|
556
|
+
state.closeButtonElement.style.display = 'flex';
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Hide the drawer with mobile-specific behaviors
|
|
563
|
+
*/
|
|
564
|
+
const hideDrawer = () => {
|
|
565
|
+
if (!state.drawer) return;
|
|
566
|
+
|
|
567
|
+
state.drawer.element.classList.add('mtrl-nav--hidden');
|
|
568
|
+
state.drawer.element.setAttribute('aria-hidden', 'true');
|
|
569
|
+
|
|
570
|
+
// Remove mobile-specific effects
|
|
571
|
+
if (state.overlayElement) {
|
|
572
|
+
state.overlayElement.classList.remove('active');
|
|
573
|
+
state.overlayElement.setAttribute('aria-hidden', 'true');
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Unlock body scroll
|
|
577
|
+
if (mobileConfig.lockBodyScroll) {
|
|
578
|
+
document.body.classList.remove(mobileConfig.bodyLockClass);
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Check if drawer is visible
|
|
584
|
+
*/
|
|
585
|
+
const isDrawerVisible = () => {
|
|
586
|
+
if (!state.drawer) return false;
|
|
587
|
+
return !state.drawer.element.classList.contains('mtrl-nav--hidden');
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Initialize the navigation system
|
|
592
|
+
*/
|
|
593
|
+
const initialize = () => {
|
|
594
|
+
// Create rail component
|
|
595
|
+
state.rail = createRailNavigation();
|
|
596
|
+
|
|
597
|
+
// Create drawer component
|
|
598
|
+
state.drawer = createDrawerNavigation();
|
|
599
|
+
|
|
600
|
+
// Setup responsive behavior
|
|
601
|
+
if (window.ResizeObserver) {
|
|
602
|
+
// Use ResizeObserver for better performance
|
|
603
|
+
state.resizeObserver = new ResizeObserver(() => {
|
|
604
|
+
checkMobileState();
|
|
605
|
+
});
|
|
606
|
+
state.resizeObserver.observe(document.body);
|
|
607
|
+
} else {
|
|
608
|
+
// Fallback to window resize event
|
|
609
|
+
window.addEventListener('resize', checkMobileState);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Listen for orientation changes on mobile
|
|
613
|
+
window.addEventListener('orientationchange', () => {
|
|
614
|
+
// Small delay to ensure dimensions have updated
|
|
615
|
+
setTimeout(checkMobileState, 100);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// Set active section if specified
|
|
619
|
+
if (options.activeSection && state.items[options.activeSection]) {
|
|
620
|
+
state.activeSection = options.activeSection;
|
|
621
|
+
|
|
622
|
+
if (state.rail) {
|
|
623
|
+
state.rail.setActive(options.activeSection);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Update drawer content without showing it
|
|
627
|
+
updateDrawerContent(options.activeSection);
|
|
628
|
+
|
|
629
|
+
// Only show drawer if expanded is explicitly true
|
|
630
|
+
if (options.expanded === true) {
|
|
631
|
+
showDrawer();
|
|
632
|
+
} else {
|
|
633
|
+
// Explicitly ensure drawer is hidden
|
|
634
|
+
hideDrawer();
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Check initial mobile state
|
|
639
|
+
checkMobileState();
|
|
640
|
+
|
|
641
|
+
return system;
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Clean up resources
|
|
646
|
+
*/
|
|
647
|
+
const cleanup = () => {
|
|
648
|
+
// Clean up resize observer
|
|
649
|
+
if (state.resizeObserver) {
|
|
650
|
+
state.resizeObserver.disconnect();
|
|
651
|
+
state.resizeObserver = null;
|
|
652
|
+
} else {
|
|
653
|
+
window.removeEventListener('resize', checkMobileState);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Remove orientation change listener
|
|
657
|
+
window.removeEventListener('orientationchange', checkMobileState);
|
|
658
|
+
|
|
659
|
+
// Remove outside click handler
|
|
660
|
+
if (state.outsideClickHandler) {
|
|
661
|
+
const eventType = hasTouchSupport() ? 'touchend' : 'click';
|
|
662
|
+
document.removeEventListener(eventType, state.outsideClickHandler);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Clean up overlay
|
|
666
|
+
if (state.overlayElement && state.overlayElement.parentNode) {
|
|
667
|
+
state.overlayElement.parentNode.removeChild(state.overlayElement);
|
|
668
|
+
state.overlayElement = null;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Clear timers
|
|
672
|
+
clearTimeout(state.hoverTimer);
|
|
673
|
+
clearTimeout(state.closeTimer);
|
|
674
|
+
state.hoverTimer = null;
|
|
675
|
+
state.closeTimer = null;
|
|
676
|
+
|
|
677
|
+
// Destroy components
|
|
678
|
+
if (state.rail) {
|
|
679
|
+
state.rail.destroy();
|
|
680
|
+
state.rail = null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (state.drawer) {
|
|
684
|
+
state.drawer.destroy();
|
|
685
|
+
state.drawer = null;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Reset state
|
|
689
|
+
state.activeSection = null;
|
|
690
|
+
state.activeSubsection = null;
|
|
691
|
+
state.mouseInDrawer = false;
|
|
692
|
+
state.mouseInRail = false;
|
|
693
|
+
state.processingChange = false;
|
|
694
|
+
state.isMobile = false;
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Navigate to a specific section and subsection
|
|
699
|
+
*/
|
|
700
|
+
const navigateTo = (section, subsection, silent) => {
|
|
701
|
+
console.error('navigateTo', section, subsection, silent)
|
|
702
|
+
// Skip if section doesn't exist
|
|
703
|
+
if (!section || !state.items[section]) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Check if we're already on this section and subsection
|
|
708
|
+
if (state.activeSection === section && state.activeSubsection === subsection) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Update active section
|
|
713
|
+
state.activeSection = section;
|
|
714
|
+
|
|
715
|
+
// Update rail if it exists
|
|
716
|
+
if (state.rail) {
|
|
717
|
+
state.rail.setActive(section, silent);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Update drawer content
|
|
721
|
+
// updateDrawerContent(section);
|
|
722
|
+
|
|
723
|
+
// Update active subsection if specified
|
|
724
|
+
if (subsection && state.drawer) {
|
|
725
|
+
state.activeSubsection = subsection;
|
|
726
|
+
state.drawer.setActive(subsection, silent);
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
// Create the public API
|
|
731
|
+
const system = {
|
|
732
|
+
initialize,
|
|
733
|
+
cleanup,
|
|
734
|
+
navigateTo,
|
|
735
|
+
|
|
736
|
+
// Component access
|
|
737
|
+
getRail: () => state.rail,
|
|
738
|
+
getDrawer: () => state.drawer,
|
|
739
|
+
|
|
740
|
+
// State getters
|
|
741
|
+
getActiveSection: () => state.activeSection,
|
|
742
|
+
getActiveSubsection: () => state.activeSubsection,
|
|
743
|
+
|
|
744
|
+
// Drawer control
|
|
745
|
+
showDrawer,
|
|
746
|
+
hideDrawer,
|
|
747
|
+
isDrawerVisible,
|
|
748
|
+
|
|
749
|
+
// Configure
|
|
750
|
+
configure: (newConfig) => {
|
|
751
|
+
Object.assign(options, newConfig);
|
|
752
|
+
return system;
|
|
753
|
+
},
|
|
754
|
+
|
|
755
|
+
// State processing management
|
|
756
|
+
setProcessingChange: (isProcessing) => {
|
|
757
|
+
state.processingChange = isProcessing;
|
|
758
|
+
},
|
|
759
|
+
|
|
760
|
+
isProcessingChange: () => state.processingChange,
|
|
761
|
+
|
|
762
|
+
// Mobile-specific methods
|
|
763
|
+
isMobile: () => state.isMobile,
|
|
764
|
+
checkMobileState,
|
|
765
|
+
|
|
766
|
+
// Event handlers (to be set by user)
|
|
767
|
+
onSectionChange: null,
|
|
768
|
+
onItemSelect: null,
|
|
769
|
+
onViewChange: null
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
// Return the uninitialized system
|
|
773
|
+
return system;
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
export default createNavigationSystem;
|