feat(materials): Фаза 3 (часть 2) — источник «Учебник»

Сервер инжектит в /textbook/<slug> плавающую кнопку «В мои материалы» (js/textbook-clip.js +
material-save.js рядом с deep-link). Сохраняет текущий § как ссылку /textbook/<slug>#sec-<id>
(заголовок = название §, источник = глава). Скрыта в classroom-embed и для неавторизованных.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-04 12:17:08 +03:00
parent 61e30bedf9
commit 43fe90d601
2 changed files with 54 additions and 1 deletions
+5 -1
View File
@@ -424,7 +424,11 @@ const EMBED_INJECT = `
// Always injected (plain + embed): deep-link helper so /textbook/<slug>#sec-pN // Always injected (plain + embed): deep-link helper so /textbook/<slug>#sec-pN
// actually opens § N. Without it the page ignores the hash and shows §1. // actually opens § N. Without it the page ignores the hash and shows §1.
const DEEPLINK_INJECT = `\n<script defer src="/js/textbook-deeplink.js"></script>\n`; const DEEPLINK_INJECT = `
<script defer src="/js/textbook-deeplink.js"></script>
<script defer src="/js/material-save.js"></script>
<script defer src="/js/textbook-clip.js"></script>
`;
function _renderTextbook(filePath, slug, embed) { function _renderTextbook(filePath, slug, embed) {
let stat; try { stat = fs.statSync(filePath); } catch { return null; } let stat; try { stat = fs.statSync(filePath); } catch { return null; }
+49
View File
@@ -0,0 +1,49 @@
'use strict';
/* textbook-clip.js — floating «В мои материалы» button on textbook pages.
* Injected by the server into /textbook/<slug>. Saves the currently open
* paragraph as a link (/textbook/<slug>#sec-<id>) into the student's
* personal materials. Reuses MaterialSave (material-save.js) + LS (api.js).
* Hidden inside the classroom embed (iframe) to avoid clutter. */
(function () {
if (window.parent !== window) return; // skip in classroom embed
if (!window.LS || !LS.getToken || !LS.getToken()) return; // only for logged-in users
function chapterTitle() {
return (document.title || 'Учебник').replace(/\s*[—|].*$/, '').replace(/\s*·\s*LearnSpace.*$/i, '').trim() || 'Учебник';
}
function sectionTitle() {
const h = document.querySelector('.sec.active .sec-h');
if (h && h.textContent.trim()) return h.textContent.trim();
const n = document.querySelector('.psel-card.active .psel-name');
if (n && n.textContent.trim()) return n.textContent.trim();
return (document.title || 'Тема').split('·').pop().trim() || 'Тема';
}
function activeId() {
const c = document.querySelector('.psel-card.active');
return c && c.dataset ? c.dataset.id : null;
}
function save(btn) {
if (!window.MaterialSave) { if (LS.toast) LS.toast('Модуль не загружен', 'error'); return; }
const slug = location.pathname.replace(/^\/textbook\//, '').replace(/\/+$/, '');
if (!slug) return;
const id = activeId();
const hash = id ? '#sec-' + id : (location.hash || '');
MaterialSave.link({ title: sectionTitle(), url: '/textbook/' + slug + hash, sourceTitle: chapterTitle() }, btn);
}
function build() {
if (document.getElementById('__ls_clip') || !document.body) return;
const btn = document.createElement('button');
btn.id = '__ls_clip';
btn.type = 'button';
btn.title = 'Сохранить эту тему в «Мои материалы»';
btn.innerHTML = '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg><span>В мои материалы</span>';
btn.style.cssText = 'position:fixed;right:18px;bottom:18px;z-index:9000;display:inline-flex;align-items:center;gap:7px;padding:10px 14px;border-radius:99px;border:none;background:#8b5cf6;color:#fff;font:600 13px/1 Inter,system-ui,sans-serif;cursor:pointer;box-shadow:0 6px 20px rgba(139,92,246,.4)';
btn.addEventListener('click', function () { save(btn); });
document.body.appendChild(btn);
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { setTimeout(build, 300); });
else setTimeout(build, 300);
})();