Skip to content

Commit 1c94128

Browse files
committed
Improve mobile layout
1 parent a70bffb commit 1c94128

File tree

3 files changed

+876
-2
lines changed

3 files changed

+876
-2
lines changed

docs/_static/mobile.js

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
/**
2+
* Mobile Menu and Responsive Enhancements for ChatterBot Documentation
3+
* Provides mobile-friendly navigation and touch interactions
4+
*/
5+
6+
(function() {
7+
'use strict';
8+
9+
var state = {
10+
sidebar: null,
11+
toggleButton: null,
12+
overlay: null,
13+
isInitialized: false,
14+
clickOutsideHandler: null,
15+
sidebarLinkHandlers: []
16+
};
17+
18+
/**
19+
* Create overlay for mobile menu
20+
*/
21+
function createOverlay() {
22+
if (state.overlay) return state.overlay;
23+
24+
var overlay = document.createElement('div');
25+
overlay.className = 'mobile-sidebar-overlay';
26+
overlay.setAttribute('aria-hidden', 'true');
27+
document.body.appendChild(overlay);
28+
29+
overlay.addEventListener('click', closeMobileMenu);
30+
31+
return overlay;
32+
}
33+
34+
/**
35+
* Open mobile menu
36+
*/
37+
function openMobileMenu() {
38+
if (!state.sidebar || !state.toggleButton) return;
39+
40+
state.sidebar.classList.add('mobile-open');
41+
state.overlay.classList.add('active');
42+
state.toggleButton.setAttribute('aria-expanded', 'true');
43+
state.toggleButton.innerHTML = '✕ Close';
44+
document.body.style.overflow = 'hidden'; // Prevent background scrolling
45+
}
46+
47+
/**
48+
* Close mobile menu
49+
*/
50+
function closeMobileMenu() {
51+
if (!state.sidebar || !state.toggleButton) return;
52+
53+
state.sidebar.classList.remove('mobile-open');
54+
state.overlay.classList.remove('active');
55+
state.toggleButton.setAttribute('aria-expanded', 'false');
56+
state.toggleButton.innerHTML = '☰ Menu';
57+
document.body.style.overflow = ''; // Restore scrolling
58+
}
59+
60+
/**
61+
* Toggle mobile menu
62+
*/
63+
function toggleMobileMenu(event) {
64+
event.preventDefault();
65+
event.stopPropagation();
66+
67+
var isOpen = state.sidebar.classList.contains('mobile-open');
68+
69+
if (isOpen) {
70+
closeMobileMenu();
71+
} else {
72+
openMobileMenu();
73+
}
74+
}
75+
76+
/**
77+
* Initialize mobile menu functionality
78+
*/
79+
function initMobileMenu() {
80+
// Only initialize once
81+
if (state.isInitialized) return;
82+
83+
state.sidebar = document.querySelector('div.sphinxsidebar');
84+
if (!state.sidebar) return;
85+
86+
// Create overlay
87+
state.overlay = createOverlay();
88+
89+
// Create mobile menu toggle button
90+
state.toggleButton = document.createElement('button');
91+
state.toggleButton.className = 'mobile-menu-toggle';
92+
state.toggleButton.innerHTML = '☰ Menu';
93+
state.toggleButton.setAttribute('aria-label', 'Toggle navigation menu');
94+
state.toggleButton.setAttribute('aria-expanded', 'false');
95+
state.toggleButton.setAttribute('type', 'button');
96+
document.body.appendChild(state.toggleButton);
97+
98+
// Add click event listener
99+
state.toggleButton.addEventListener('click', toggleMobileMenu);
100+
101+
// Close menu when clicking a link inside sidebar
102+
var sidebarLinks = state.sidebar.querySelectorAll('a');
103+
sidebarLinks.forEach(function(link) {
104+
var handler = function(e) {
105+
// Small delay to allow navigation to start
106+
setTimeout(closeMobileMenu, 100);
107+
};
108+
link.addEventListener('click', handler);
109+
state.sidebarLinkHandlers.push({ element: link, handler: handler });
110+
});
111+
112+
// Handle escape key
113+
document.addEventListener('keydown', function(e) {
114+
if (e.key === 'Escape' && state.sidebar.classList.contains('mobile-open')) {
115+
closeMobileMenu();
116+
state.toggleButton.focus();
117+
}
118+
});
119+
120+
state.isInitialized = true;
121+
}
122+
123+
/**
124+
* Clean up mobile menu
125+
*/
126+
function cleanupMobileMenu() {
127+
if (!state.isInitialized) return;
128+
129+
// Remove toggle button
130+
if (state.toggleButton && state.toggleButton.parentNode) {
131+
state.toggleButton.parentNode.removeChild(state.toggleButton);
132+
}
133+
134+
// Remove overlay
135+
if (state.overlay && state.overlay.parentNode) {
136+
state.overlay.parentNode.removeChild(state.overlay);
137+
}
138+
139+
// Remove sidebar classes
140+
if (state.sidebar) {
141+
state.sidebar.classList.remove('mobile-open');
142+
}
143+
144+
// Remove event listeners from sidebar links
145+
state.sidebarLinkHandlers.forEach(function(item) {
146+
item.element.removeEventListener('click', item.handler);
147+
});
148+
149+
// Restore body overflow
150+
document.body.style.overflow = '';
151+
152+
// Reset state
153+
state.isInitialized = false;
154+
state.toggleButton = null;
155+
state.overlay = null;
156+
state.sidebarLinkHandlers = [];
157+
}
158+
159+
/**
160+
* Improve table responsiveness
161+
*/
162+
function makeTablesResponsive() {
163+
var tables = document.querySelectorAll('table.docutils');
164+
165+
tables.forEach(function(table) {
166+
// Skip if already wrapped
167+
if (table.parentNode.classList.contains('table-wrapper')) {
168+
return;
169+
}
170+
171+
// Create wrapper for horizontal scrolling
172+
var wrapper = document.createElement('div');
173+
wrapper.className = 'table-wrapper';
174+
wrapper.style.overflowX = 'auto';
175+
wrapper.style.webkitOverflowScrolling = 'touch';
176+
wrapper.style.marginBottom = '1em';
177+
178+
table.parentNode.insertBefore(wrapper, table);
179+
wrapper.appendChild(table);
180+
});
181+
}
182+
183+
/**
184+
* Add touch-friendly behavior to code blocks
185+
*/
186+
function enhanceCodeBlocks() {
187+
var codeBlocks = document.querySelectorAll('div.highlight');
188+
189+
codeBlocks.forEach(function(block) {
190+
// Add visual indicator for scrollable content
191+
var pre = block.querySelector('pre');
192+
if (pre && pre.scrollWidth > pre.clientWidth) {
193+
block.classList.add('scrollable');
194+
block.setAttribute('title', 'Swipe to scroll code');
195+
}
196+
});
197+
}
198+
199+
/**
200+
* Handle window resize events
201+
*/
202+
function handleResize() {
203+
var isMobile = window.innerWidth <= 480;
204+
205+
if (isMobile && !state.isInitialized) {
206+
// Mobile view - initialize
207+
initMobileMenu();
208+
} else if (!isMobile && state.isInitialized) {
209+
// Desktop view - cleanup
210+
cleanupMobileMenu();
211+
}
212+
}
213+
214+
/**
215+
* Improve accessibility for mobile users
216+
*/
217+
function improveAccessibility() {
218+
// Add skip to content link
219+
var skipLink = document.createElement('a');
220+
skipLink.href = '#document';
221+
skipLink.className = 'skip-to-content';
222+
skipLink.textContent = 'Skip to content';
223+
skipLink.style.position = 'absolute';
224+
skipLink.style.top = '-40px';
225+
skipLink.style.left = '0';
226+
skipLink.style.background = '#300a24';
227+
skipLink.style.color = '#e8ffca';
228+
skipLink.style.padding = '8px';
229+
skipLink.style.textDecoration = 'none';
230+
skipLink.style.zIndex = '1001';
231+
232+
skipLink.addEventListener('focus', function() {
233+
this.style.top = '0';
234+
});
235+
236+
skipLink.addEventListener('blur', function() {
237+
this.style.top = '-40px';
238+
});
239+
240+
document.body.insertBefore(skipLink, document.body.firstChild);
241+
}
242+
243+
/**
244+
* Initialize all mobile enhancements
245+
*/
246+
function init() {
247+
// Wait for DOM to be ready
248+
if (document.readyState === 'loading') {
249+
document.addEventListener('DOMContentLoaded', function() {
250+
// Check if mobile on initial load (480px breakpoint for actual phones)
251+
if (window.innerWidth <= 480) {
252+
initMobileMenu();
253+
}
254+
makeTablesResponsive();
255+
enhanceCodeBlocks();
256+
improveAccessibility();
257+
});
258+
} else {
259+
// Check if mobile on initial load (480px breakpoint for actual phones)
260+
if (window.innerWidth <= 480) {
261+
initMobileMenu();
262+
}
263+
makeTablesResponsive();
264+
enhanceCodeBlocks();
265+
improveAccessibility();
266+
}
267+
268+
// Handle window resize with debouncing
269+
var resizeTimer;
270+
window.addEventListener('resize', function() {
271+
clearTimeout(resizeTimer);
272+
resizeTimer = setTimeout(handleResize, 250);
273+
});
274+
275+
// Handle orientation change on mobile devices
276+
window.addEventListener('orientationchange', function() {
277+
clearTimeout(resizeTimer);
278+
resizeTimer = setTimeout(handleResize, 300);
279+
});
280+
}
281+
282+
// Initialize
283+
init();
284+
285+
})();

0 commit comments

Comments
 (0)