be4d43105e
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>
156 lines
5.7 KiB
JavaScript
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();
|
|
}
|
|
})();
|