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>
This commit is contained in:
+155
@@ -0,0 +1,155 @@
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
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();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user