jekyll-theme-zer0 0.15.2 → 0.17.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +138 -10
- data/README.md +7 -7
- data/_data/authors.yml +2 -2
- data/_data/features.yml +676 -0
- data/_data/navigation/README.md +54 -0
- data/_data/navigation/about.yml +7 -5
- data/_data/navigation/docs.yml +77 -31
- data/_data/navigation/home.yml +4 -3
- data/_data/navigation/main.yml +16 -7
- data/_data/navigation/quickstart.yml +4 -2
- data/_includes/components/js-cdn.html +2 -2
- data/_includes/components/preview-image.html +20 -3
- data/_includes/content/intro.html +11 -2
- data/_includes/content/seo.html +12 -1
- data/_includes/landing/landing-install-cards.html +8 -8
- data/_includes/landing/landing-quick-links.html +4 -4
- data/_includes/navigation/breadcrumbs.html +29 -6
- data/_includes/navigation/nav-tree.html +181 -0
- data/_includes/navigation/navbar.html +262 -9
- data/_includes/navigation/sidebar-left.html +22 -23
- data/_layouts/default.html +1 -1
- data/_layouts/landing.html +1 -1
- data/_layouts/notebook.html +4 -4
- data/_layouts/root.html +2 -2
- data/_plugins/preview_image_generator.rb +29 -8
- data/_sass/core/_nav-tree.scss +145 -0
- data/_sass/custom.scss +3 -0
- data/assets/images/previews/site-personalization-configuration.png +0 -0
- data/assets/js/modules/navigation/config.js +149 -0
- data/assets/js/modules/navigation/focus.js +189 -0
- data/assets/js/modules/navigation/gestures.js +179 -0
- data/assets/js/modules/navigation/index.js +227 -0
- data/assets/js/modules/navigation/keyboard.js +237 -0
- data/assets/js/modules/navigation/scroll-spy.js +219 -0
- data/assets/js/modules/navigation/sidebar-state.js +267 -0
- data/assets/js/modules/navigation/smooth-scroll.js +153 -0
- data/scripts/README.md +8 -1
- data/scripts/lib/preview_generator.py +164 -8
- data/scripts/migrate-nav-modes.sh +146 -0
- data/scripts/update-preview-paths.sh +145 -0
- metadata +17 -3
- data/assets/js/sidebar.js +0 -511
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ===================================================================
|
|
3
|
+
* SWIPE GESTURES - Mobile Offcanvas Control
|
|
4
|
+
* ===================================================================
|
|
5
|
+
*
|
|
6
|
+
* File: gestures.js
|
|
7
|
+
* Path: assets/js/modules/navigation/gestures.js
|
|
8
|
+
* Purpose: Touch gesture support for sidebar navigation on mobile
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Swipe from left edge to open left sidebar
|
|
12
|
+
* - Swipe from right edge to open TOC sidebar
|
|
13
|
+
* - Configurable threshold and edge zones
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* import { SwipeGestures } from './gestures.js';
|
|
17
|
+
* const gestures = new SwipeGestures();
|
|
18
|
+
*
|
|
19
|
+
* ===================================================================
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { config, isBelowBreakpoint } from './config.js';
|
|
23
|
+
|
|
24
|
+
export class SwipeGestures {
|
|
25
|
+
constructor() {
|
|
26
|
+
if (!config.gestures.enabled) {
|
|
27
|
+
console.log('SwipeGestures: Disabled via config');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.startX = 0;
|
|
32
|
+
this.startY = 0;
|
|
33
|
+
this.distX = 0;
|
|
34
|
+
this.distY = 0;
|
|
35
|
+
|
|
36
|
+
// Bound handlers for cleanup
|
|
37
|
+
this._boundTouchStart = this._handleTouchStart.bind(this);
|
|
38
|
+
this._boundTouchMove = this._handleTouchMove.bind(this);
|
|
39
|
+
this._boundTouchEnd = this._handleTouchEnd.bind(this);
|
|
40
|
+
|
|
41
|
+
this._init();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Initialize touch event listeners
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
48
|
+
_init() {
|
|
49
|
+
document.addEventListener('touchstart', this._boundTouchStart, { passive: true });
|
|
50
|
+
document.addEventListener('touchmove', this._boundTouchMove, { passive: true });
|
|
51
|
+
document.addEventListener('touchend', this._boundTouchEnd);
|
|
52
|
+
|
|
53
|
+
console.log('SwipeGestures: Initialized');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Handle touch start
|
|
58
|
+
* @private
|
|
59
|
+
* @param {TouchEvent} event
|
|
60
|
+
*/
|
|
61
|
+
_handleTouchStart(event) {
|
|
62
|
+
const touch = event.touches[0];
|
|
63
|
+
this.startX = touch.clientX;
|
|
64
|
+
this.startY = touch.clientY;
|
|
65
|
+
this.distX = 0;
|
|
66
|
+
this.distY = 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Handle touch move
|
|
71
|
+
* @private
|
|
72
|
+
* @param {TouchEvent} event
|
|
73
|
+
*/
|
|
74
|
+
_handleTouchMove(event) {
|
|
75
|
+
if (!this.startX || !this.startY) return;
|
|
76
|
+
|
|
77
|
+
const touch = event.touches[0];
|
|
78
|
+
this.distX = touch.clientX - this.startX;
|
|
79
|
+
this.distY = touch.clientY - this.startY;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Handle touch end
|
|
84
|
+
* @private
|
|
85
|
+
* @param {TouchEvent} event
|
|
86
|
+
*/
|
|
87
|
+
_handleTouchEnd(event) {
|
|
88
|
+
const { threshold, edgeZone } = config.gestures;
|
|
89
|
+
|
|
90
|
+
// Check if swipe distance meets threshold
|
|
91
|
+
if (Math.abs(this.distX) < threshold) {
|
|
92
|
+
this._reset();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Only handle horizontal swipes (not vertical scroll)
|
|
97
|
+
if (Math.abs(this.distX) > Math.abs(this.distY)) {
|
|
98
|
+
if (this.distX > 0) {
|
|
99
|
+
this._handleSwipeRight();
|
|
100
|
+
} else {
|
|
101
|
+
this._handleSwipeLeft();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this._reset();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Handle swipe right (open left sidebar)
|
|
110
|
+
* @private
|
|
111
|
+
*/
|
|
112
|
+
_handleSwipeRight() {
|
|
113
|
+
const { edgeZone } = config.gestures;
|
|
114
|
+
|
|
115
|
+
// Only if swipe started from left edge
|
|
116
|
+
if (this.startX > edgeZone) return;
|
|
117
|
+
|
|
118
|
+
// Only on mobile
|
|
119
|
+
if (!isBelowBreakpoint('lg')) return;
|
|
120
|
+
|
|
121
|
+
const leftSidebar = document.querySelector(config.selectors.leftSidebar);
|
|
122
|
+
if (leftSidebar && typeof bootstrap !== 'undefined') {
|
|
123
|
+
const bsOffcanvas = bootstrap.Offcanvas.getOrCreateInstance(leftSidebar);
|
|
124
|
+
bsOffcanvas.show();
|
|
125
|
+
|
|
126
|
+
document.dispatchEvent(new CustomEvent('navigation:swipe', {
|
|
127
|
+
detail: { direction: 'right', sidebar: 'left' }
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Handle swipe left (open right/TOC sidebar)
|
|
134
|
+
* @private
|
|
135
|
+
*/
|
|
136
|
+
_handleSwipeLeft() {
|
|
137
|
+
const { edgeZone } = config.gestures;
|
|
138
|
+
const windowWidth = window.innerWidth;
|
|
139
|
+
|
|
140
|
+
// Only if swipe started from right edge
|
|
141
|
+
if (this.startX < windowWidth - edgeZone) return;
|
|
142
|
+
|
|
143
|
+
// Only on mobile
|
|
144
|
+
if (!isBelowBreakpoint('lg')) return;
|
|
145
|
+
|
|
146
|
+
const rightSidebar = document.querySelector(config.selectors.rightSidebar);
|
|
147
|
+
if (rightSidebar && typeof bootstrap !== 'undefined') {
|
|
148
|
+
const bsOffcanvas = bootstrap.Offcanvas.getOrCreateInstance(rightSidebar);
|
|
149
|
+
bsOffcanvas.show();
|
|
150
|
+
|
|
151
|
+
document.dispatchEvent(new CustomEvent('navigation:swipe', {
|
|
152
|
+
detail: { direction: 'left', sidebar: 'toc' }
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Reset touch tracking state
|
|
159
|
+
* @private
|
|
160
|
+
*/
|
|
161
|
+
_reset() {
|
|
162
|
+
this.startX = 0;
|
|
163
|
+
this.startY = 0;
|
|
164
|
+
this.distX = 0;
|
|
165
|
+
this.distY = 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Cleanup event listeners
|
|
170
|
+
*/
|
|
171
|
+
destroy() {
|
|
172
|
+
document.removeEventListener('touchstart', this._boundTouchStart);
|
|
173
|
+
document.removeEventListener('touchmove', this._boundTouchMove);
|
|
174
|
+
document.removeEventListener('touchend', this._boundTouchEnd);
|
|
175
|
+
console.log('SwipeGestures: Destroyed');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export default SwipeGestures;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ===================================================================
|
|
3
|
+
* NAVIGATION - Main Entry Point
|
|
4
|
+
* ===================================================================
|
|
5
|
+
*
|
|
6
|
+
* File: index.js
|
|
7
|
+
* Path: assets/js/modules/navigation/index.js
|
|
8
|
+
* Purpose: Main orchestrator for all navigation modules
|
|
9
|
+
*
|
|
10
|
+
* This module imports and initializes all navigation-related modules:
|
|
11
|
+
* - ScrollSpy: Highlights current section in TOC
|
|
12
|
+
* - SmoothScroll: Smooth anchor navigation
|
|
13
|
+
* - KeyboardShortcuts: Keyboard navigation
|
|
14
|
+
* - SwipeGestures: Mobile gesture support
|
|
15
|
+
* - FocusManager: Accessibility focus management
|
|
16
|
+
* - SidebarState: Persistence of sidebar state
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* <script type="module" src="/assets/js/modules/navigation/index.js"></script>
|
|
20
|
+
*
|
|
21
|
+
* Or import programmatically:
|
|
22
|
+
* import { Navigation } from './modules/navigation/index.js';
|
|
23
|
+
* const nav = new Navigation();
|
|
24
|
+
*
|
|
25
|
+
* ===================================================================
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { config } from './config.js';
|
|
29
|
+
import { ScrollSpy } from './scroll-spy.js';
|
|
30
|
+
import { SmoothScroll } from './smooth-scroll.js';
|
|
31
|
+
import { KeyboardShortcuts } from './keyboard.js';
|
|
32
|
+
import { SwipeGestures } from './gestures.js';
|
|
33
|
+
import { FocusManager } from './focus.js';
|
|
34
|
+
import { SidebarState } from './sidebar-state.js';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Navigation Controller - Orchestrates all navigation modules
|
|
38
|
+
*/
|
|
39
|
+
export class Navigation {
|
|
40
|
+
constructor() {
|
|
41
|
+
this.modules = {};
|
|
42
|
+
this._initialized = false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Initialize all navigation modules
|
|
47
|
+
* @returns {Navigation} this instance for chaining
|
|
48
|
+
*/
|
|
49
|
+
init() {
|
|
50
|
+
if (this._initialized) {
|
|
51
|
+
console.warn('Navigation: Already initialized');
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// Initialize sidebar state first (other modules may depend on it)
|
|
57
|
+
this.modules.state = new SidebarState();
|
|
58
|
+
|
|
59
|
+
// Initialize TOC-related modules only if TOC exists
|
|
60
|
+
const toc = document.querySelector(config.selectors.toc);
|
|
61
|
+
if (toc) {
|
|
62
|
+
this.modules.scrollSpy = new ScrollSpy();
|
|
63
|
+
this.modules.smoothScroll = new SmoothScroll();
|
|
64
|
+
} else {
|
|
65
|
+
console.log('Navigation: No TOC found, skipping scroll spy and smooth scroll');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Initialize keyboard shortcuts
|
|
69
|
+
this.modules.keyboard = new KeyboardShortcuts();
|
|
70
|
+
|
|
71
|
+
// Initialize gesture support
|
|
72
|
+
this.modules.gestures = new SwipeGestures();
|
|
73
|
+
|
|
74
|
+
// Initialize focus management
|
|
75
|
+
this.modules.focus = new FocusManager();
|
|
76
|
+
|
|
77
|
+
this._initialized = true;
|
|
78
|
+
|
|
79
|
+
// Dispatch ready event
|
|
80
|
+
document.dispatchEvent(new CustomEvent('navigation:ready', {
|
|
81
|
+
detail: {
|
|
82
|
+
modules: Object.keys(this.modules)
|
|
83
|
+
}
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
console.log('Navigation: Successfully initialized modules:', Object.keys(this.modules));
|
|
87
|
+
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('Navigation: Initialization error', error);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get a specific module instance
|
|
97
|
+
* @param {string} name - Module name
|
|
98
|
+
* @returns {Object|undefined}
|
|
99
|
+
*/
|
|
100
|
+
getModule(name) {
|
|
101
|
+
return this.modules[name];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get configuration
|
|
106
|
+
* @returns {Object}
|
|
107
|
+
*/
|
|
108
|
+
getConfig() {
|
|
109
|
+
return config;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Scroll to a specific element
|
|
114
|
+
* @param {string|Element} target - Element ID or Element
|
|
115
|
+
* @param {number} [offset] - Optional scroll offset
|
|
116
|
+
*/
|
|
117
|
+
scrollTo(target, offset) {
|
|
118
|
+
if (!this.modules.smoothScroll) return;
|
|
119
|
+
|
|
120
|
+
if (typeof target === 'string') {
|
|
121
|
+
this.modules.smoothScroll.scrollToId(target, offset);
|
|
122
|
+
} else if (target instanceof Element) {
|
|
123
|
+
this.modules.smoothScroll.scrollToElement(target, offset);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Expand sidebar tree to show a specific item
|
|
129
|
+
* @param {string} targetId - ID of element to reveal
|
|
130
|
+
*/
|
|
131
|
+
expandTo(targetId) {
|
|
132
|
+
if (this.modules.state) {
|
|
133
|
+
this.modules.state.expandPathTo(targetId);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Expand all sidebar tree nodes
|
|
139
|
+
*/
|
|
140
|
+
expandAll() {
|
|
141
|
+
if (this.modules.state) {
|
|
142
|
+
this.modules.state.expandAll();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Collapse all sidebar tree nodes
|
|
148
|
+
*/
|
|
149
|
+
collapseAll() {
|
|
150
|
+
if (this.modules.state) {
|
|
151
|
+
this.modules.state.collapseAll();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get keyboard shortcuts for help display
|
|
157
|
+
* @returns {Object|null}
|
|
158
|
+
*/
|
|
159
|
+
getShortcuts() {
|
|
160
|
+
if (this.modules.keyboard && this.modules.keyboard.getShortcuts) {
|
|
161
|
+
return this.modules.keyboard.getShortcuts();
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Destroy all modules and cleanup
|
|
168
|
+
*/
|
|
169
|
+
destroy() {
|
|
170
|
+
Object.values(this.modules).forEach(module => {
|
|
171
|
+
if (module && typeof module.destroy === 'function') {
|
|
172
|
+
module.destroy();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
this.modules = {};
|
|
177
|
+
this._initialized = false;
|
|
178
|
+
|
|
179
|
+
document.dispatchEvent(new CustomEvent('navigation:destroyed'));
|
|
180
|
+
console.log('Navigation: Destroyed all modules');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ===================================================================
|
|
185
|
+
// AUTO-INITIALIZATION
|
|
186
|
+
// ===================================================================
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Create and initialize navigation when DOM is ready
|
|
190
|
+
*/
|
|
191
|
+
function initNavigation() {
|
|
192
|
+
// Create global instance
|
|
193
|
+
window.zer0Navigation = new Navigation();
|
|
194
|
+
|
|
195
|
+
// Check if Bootstrap is available
|
|
196
|
+
if (typeof bootstrap === 'undefined') {
|
|
197
|
+
console.warn('Navigation: Bootstrap not found, waiting for load event');
|
|
198
|
+
window.addEventListener('load', () => {
|
|
199
|
+
window.zer0Navigation.init();
|
|
200
|
+
});
|
|
201
|
+
} else {
|
|
202
|
+
window.zer0Navigation.init();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Wait for DOM to be ready
|
|
207
|
+
if (document.readyState === 'loading') {
|
|
208
|
+
document.addEventListener('DOMContentLoaded', initNavigation);
|
|
209
|
+
} else {
|
|
210
|
+
// DOM already loaded
|
|
211
|
+
initNavigation();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ===================================================================
|
|
215
|
+
// EXPORTS
|
|
216
|
+
// ===================================================================
|
|
217
|
+
|
|
218
|
+
// Export individual modules for advanced usage
|
|
219
|
+
export { config } from './config.js';
|
|
220
|
+
export { ScrollSpy } from './scroll-spy.js';
|
|
221
|
+
export { SmoothScroll } from './smooth-scroll.js';
|
|
222
|
+
export { KeyboardShortcuts } from './keyboard.js';
|
|
223
|
+
export { SwipeGestures } from './gestures.js';
|
|
224
|
+
export { FocusManager } from './focus.js';
|
|
225
|
+
export { SidebarState } from './sidebar-state.js';
|
|
226
|
+
|
|
227
|
+
export default Navigation;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ===================================================================
|
|
3
|
+
* KEYBOARD SHORTCUTS - Navigation Enhancements
|
|
4
|
+
* ===================================================================
|
|
5
|
+
*
|
|
6
|
+
* File: keyboard.js
|
|
7
|
+
* Path: assets/js/modules/navigation/keyboard.js
|
|
8
|
+
* Purpose: Keyboard shortcuts for navigation and accessibility
|
|
9
|
+
*
|
|
10
|
+
* Default Shortcuts:
|
|
11
|
+
* - [ : Navigate to previous section
|
|
12
|
+
* - ] : Navigate to next section
|
|
13
|
+
* - / : Focus search (when implemented)
|
|
14
|
+
* - b : Toggle left sidebar
|
|
15
|
+
* - t : Toggle TOC sidebar
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* import { KeyboardShortcuts } from './keyboard.js';
|
|
19
|
+
* const shortcuts = new KeyboardShortcuts();
|
|
20
|
+
*
|
|
21
|
+
* ===================================================================
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { config, isBelowBreakpoint } from './config.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get all elements safely
|
|
28
|
+
* @param {string} selector - CSS selector
|
|
29
|
+
* @returns {NodeList}
|
|
30
|
+
*/
|
|
31
|
+
function getElements(selector) {
|
|
32
|
+
try {
|
|
33
|
+
return document.querySelectorAll(selector);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class KeyboardShortcuts {
|
|
40
|
+
constructor() {
|
|
41
|
+
if (!config.keyboard.enabled) {
|
|
42
|
+
console.log('KeyboardShortcuts: Disabled via config');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.tocLinks = Array.from(getElements(config.selectors.tocLinks));
|
|
47
|
+
this.currentIndex = -1;
|
|
48
|
+
this._boundHandler = this._handleKeydown.bind(this);
|
|
49
|
+
|
|
50
|
+
this._init();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Initialize keyboard event listeners
|
|
55
|
+
* @private
|
|
56
|
+
*/
|
|
57
|
+
_init() {
|
|
58
|
+
document.addEventListener('keydown', this._boundHandler);
|
|
59
|
+
console.log('KeyboardShortcuts: Initialized');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Handle keydown events
|
|
64
|
+
* @private
|
|
65
|
+
* @param {KeyboardEvent} event
|
|
66
|
+
*/
|
|
67
|
+
_handleKeydown(event) {
|
|
68
|
+
// Ignore if user is typing in an input
|
|
69
|
+
if (event.target.matches('input, textarea, select, [contenteditable="true"]')) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { keys } = config.keyboard;
|
|
74
|
+
|
|
75
|
+
switch(event.key) {
|
|
76
|
+
case keys.previousSection:
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
this._navigatePrevious();
|
|
79
|
+
break;
|
|
80
|
+
|
|
81
|
+
case keys.nextSection:
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
this._navigateNext();
|
|
84
|
+
break;
|
|
85
|
+
|
|
86
|
+
case keys.search:
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
this._focusSearch();
|
|
89
|
+
break;
|
|
90
|
+
|
|
91
|
+
case keys.toggleSidebar:
|
|
92
|
+
// Only handle if not typing
|
|
93
|
+
if (!event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
this._toggleSidebar();
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
|
|
99
|
+
case keys.toggleToc:
|
|
100
|
+
// Only handle if not typing
|
|
101
|
+
if (!event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
this._toggleToc();
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Navigate to previous section
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
_navigatePrevious() {
|
|
114
|
+
this._updateCurrentIndex();
|
|
115
|
+
|
|
116
|
+
if (this.currentIndex > 0) {
|
|
117
|
+
this.currentIndex--;
|
|
118
|
+
this._navigateToIndex(this.currentIndex);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Navigate to next section
|
|
124
|
+
* @private
|
|
125
|
+
*/
|
|
126
|
+
_navigateNext() {
|
|
127
|
+
this._updateCurrentIndex();
|
|
128
|
+
|
|
129
|
+
if (this.currentIndex < this.tocLinks.length - 1) {
|
|
130
|
+
this.currentIndex++;
|
|
131
|
+
this._navigateToIndex(this.currentIndex);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Update current index based on active link
|
|
137
|
+
* @private
|
|
138
|
+
*/
|
|
139
|
+
_updateCurrentIndex() {
|
|
140
|
+
const activeLink = document.querySelector(`${config.selectors.tocLinks}.active`);
|
|
141
|
+
if (activeLink) {
|
|
142
|
+
this.currentIndex = this.tocLinks.indexOf(activeLink);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Navigate to a specific index
|
|
148
|
+
* @private
|
|
149
|
+
* @param {number} index
|
|
150
|
+
*/
|
|
151
|
+
_navigateToIndex(index) {
|
|
152
|
+
const link = this.tocLinks[index];
|
|
153
|
+
if (link) {
|
|
154
|
+
link.click();
|
|
155
|
+
|
|
156
|
+
// Dispatch event for tracking
|
|
157
|
+
document.dispatchEvent(new CustomEvent('navigation:keyboardNav', {
|
|
158
|
+
detail: {
|
|
159
|
+
direction: index > this.currentIndex ? 'next' : 'previous',
|
|
160
|
+
index: index
|
|
161
|
+
}
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Focus search input
|
|
168
|
+
* @private
|
|
169
|
+
*/
|
|
170
|
+
_focusSearch() {
|
|
171
|
+
const searchInput = document.querySelector('#search-input, [data-search-input]');
|
|
172
|
+
if (searchInput) {
|
|
173
|
+
searchInput.focus();
|
|
174
|
+
} else {
|
|
175
|
+
console.log('KeyboardShortcuts: Search not yet implemented');
|
|
176
|
+
// Dispatch event so other modules can handle
|
|
177
|
+
document.dispatchEvent(new CustomEvent('navigation:searchRequest'));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Toggle left sidebar
|
|
183
|
+
* @private
|
|
184
|
+
*/
|
|
185
|
+
_toggleSidebar() {
|
|
186
|
+
const sidebar = document.querySelector(config.selectors.leftSidebar);
|
|
187
|
+
if (sidebar && typeof bootstrap !== 'undefined') {
|
|
188
|
+
const bsOffcanvas = bootstrap.Offcanvas.getOrCreateInstance(sidebar);
|
|
189
|
+
bsOffcanvas.toggle();
|
|
190
|
+
|
|
191
|
+
document.dispatchEvent(new CustomEvent('navigation:sidebarToggle', {
|
|
192
|
+
detail: { sidebar: 'left' }
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Toggle TOC sidebar
|
|
199
|
+
* @private
|
|
200
|
+
*/
|
|
201
|
+
_toggleToc() {
|
|
202
|
+
const toc = document.querySelector(config.selectors.rightSidebar);
|
|
203
|
+
if (toc && typeof bootstrap !== 'undefined') {
|
|
204
|
+
const bsOffcanvas = bootstrap.Offcanvas.getOrCreateInstance(toc);
|
|
205
|
+
bsOffcanvas.toggle();
|
|
206
|
+
|
|
207
|
+
document.dispatchEvent(new CustomEvent('navigation:sidebarToggle', {
|
|
208
|
+
detail: { sidebar: 'toc' }
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get available shortcuts for help display
|
|
215
|
+
* @returns {Object} Map of key to description
|
|
216
|
+
*/
|
|
217
|
+
getShortcuts() {
|
|
218
|
+
const { keys } = config.keyboard;
|
|
219
|
+
return {
|
|
220
|
+
[keys.previousSection]: 'Previous section',
|
|
221
|
+
[keys.nextSection]: 'Next section',
|
|
222
|
+
[keys.search]: 'Focus search',
|
|
223
|
+
[keys.toggleSidebar]: 'Toggle sidebar',
|
|
224
|
+
[keys.toggleToc]: 'Toggle table of contents'
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Cleanup event listeners
|
|
230
|
+
*/
|
|
231
|
+
destroy() {
|
|
232
|
+
document.removeEventListener('keydown', this._boundHandler);
|
|
233
|
+
console.log('KeyboardShortcuts: Destroyed');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export default KeyboardShortcuts;
|