Files
Maxim Dolgolyov be4d43105e LearnSpace: full-stack educational whiteboard platform
Node.js/Express backend + vanilla JS frontend.
Features: real-time collaborative whiteboard (SSE), multi-page support,
LaTeX formulas, shapes/connectors, coordinate systems, number lines,
compass, zoom/pan, Catmull-Rom pencil smoothing, ruler/protractor with
rotation & resize controls, minimap navigation overlay, auto-measurements,
multi-page thumbnails sidebar, PNG export, page templates.
Student/teacher workflows: classes, assignments, library, dashboard.
Mobile responsive. SQLite (better-sqlite3).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 10:10:37 +03:00

156 lines
5.7 KiB
JavaScript

/* ═══════════════════════════════════════════════════════
LearnSpace — Mobile Drawer & Topbar
Injected into all sidebar-layout pages
═══════════════════════════════════════════════════════ */
(function () {
const MOBILE_BP = 768;
function isMobile() { return window.innerWidth <= MOBILE_BP; }
/* ── Inject mob-bar and backdrop ── */
function injectMobBar() {
if (document.getElementById('ls-mob-bar')) return;
const layout = document.querySelector('.app-layout');
if (!layout) return;
/* Backdrop overlay */
const backdrop = document.createElement('div');
backdrop.className = 'sb-backdrop';
backdrop.id = 'ls-sb-backdrop';
document.body.appendChild(backdrop);
/* Top bar */
const bar = document.createElement('div');
bar.className = 'mob-bar';
bar.id = 'ls-mob-bar';
bar.innerHTML = `
<a href="/" class="mob-bar-logo">Learn<span>Space</span></a>
<div class="mob-bar-actions">
<button class="mob-icon-btn" id="mob-notif-btn" title="Уведомления" style="display:none">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span class="mob-notif-dot" id="mob-notif-dot" style="display:none;position:absolute;top:5px;right:5px;width:8px;height:8px;border-radius:50%;background:#F15BB5;border:2px solid #EEF2FF;"></span>
</button>
<button class="mob-icon-btn" id="mob-hamburger" title="Меню">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
</div>
`;
document.body.insertBefore(bar, document.body.firstChild);
}
/* ── Open / Close drawer ── */
function openDrawer() {
const layout = document.querySelector('.app-layout');
const backdrop = document.getElementById('ls-sb-backdrop');
if (!layout) return;
layout.classList.add('sb-open');
if (backdrop) backdrop.classList.add('open');
document.body.style.overflow = 'hidden';
}
function closeDrawer() {
const layout = document.querySelector('.app-layout');
const backdrop = document.getElementById('ls-sb-backdrop');
if (!layout) return;
layout.classList.remove('sb-open');
if (backdrop) backdrop.classList.remove('open');
document.body.style.overflow = '';
}
/* ── Wire up events ── */
function wireEvents() {
/* Hamburger */
document.addEventListener('click', function (e) {
if (e.target.closest('#mob-hamburger')) {
const layout = document.querySelector('.app-layout');
if (layout && layout.classList.contains('sb-open')) {
closeDrawer();
} else {
openDrawer();
}
return;
}
/* Backdrop tap */
if (e.target.id === 'ls-sb-backdrop') {
closeDrawer();
return;
}
/* Sidebar nav link tap — close drawer */
if (isMobile() && e.target.closest('.sb-link')) {
setTimeout(closeDrawer, 80);
return;
}
/* Mob notif button — delegate to LS.notif.toggle or legacy toggleNotifDrop */
if (e.target.closest('#mob-notif-btn')) {
if (typeof LS !== 'undefined' && LS.notif && LS.notif.toggle) {
LS.notif.toggle();
} else if (typeof toggleNotifDrop === 'function') {
toggleNotifDrop();
}
return;
}
});
/* Escape key */
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') closeDrawer();
});
/* Close drawer on resize to desktop */
window.addEventListener('resize', function () {
if (!isMobile()) closeDrawer();
});
/* Close drawer on orientation change */
window.addEventListener('orientationchange', function () {
setTimeout(function () {
if (!isMobile()) closeDrawer();
}, 150);
});
}
/* ── Sync mob notif button visibility with page's notif button ── */
function syncNotifBtn() {
const mobBtn = document.getElementById('mob-notif-btn');
if (!mobBtn) return;
/* Look for existing notif button in sidebar */
const sbNotif = document.querySelector('.sb-link[onclick*="Notif"], .sb-link[onclick*="notif"], #notif-btn, [data-notif]');
if (sbNotif) {
mobBtn.style.display = 'flex';
/* Mirror badge count */
const badge = sbNotif.querySelector('.sb-badge');
const dot = document.getElementById('mob-notif-dot');
if (badge && dot) {
const observer = new MutationObserver(function () {
dot.style.display = (badge.textContent.trim() !== '0' && badge.style.display !== 'none') ? 'block' : 'none';
});
observer.observe(badge, { childList: true, attributes: true, attributeFilter: ['style'] });
}
}
}
/* ── Init ── */
function init() {
if (window._lsMobileInited) return;
window._lsMobileInited = true;
injectMobBar();
wireEvents();
/* Defer notif sync until page JS has run */
setTimeout(syncNotifBtn, 600);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();