'use strict'; /* textbook-clip.js — floating actions on textbook pages (/textbook/). * Injected by the server. Two actions for logged-in students: * • «В мои материалы» — save the current paragraph as a LINK * • «Вырезать область» — drag a rectangle on the page, capture it as an * IMAGE (html2canvas, lazy-loaded) and save it. * Reuses MaterialSave (material-save.js) + LS (api.js). Hidden in 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; // logged-in users only var H2C_SRC = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js'; var CROP_SVG = ''; var BOOKMARK_SVG = ''; function chapterTitle() { return (document.title || 'Учебник').replace(/\s*[—|].*$/, '').replace(/\s*·\s*LearnSpace.*$/i, '').trim() || 'Учебник'; } function sectionTitle() { var h = document.querySelector('.sec.active .sec-h'); if (h && h.textContent.trim()) return h.textContent.trim(); var 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() { var c = document.querySelector('.psel-card.active'); return c && c.dataset ? c.dataset.id : null; } function toast(msg, type) { if (LS.toast) LS.toast(msg, type); } /* ── Save current paragraph as a link ── */ function saveLink(btn) { if (!window.MaterialSave) { toast('Модуль не загружен', 'error'); return; } var slug = location.pathname.replace(/^\/textbook\//, '').replace(/\/+$/, ''); if (!slug) return; var id = activeId(); var hash = id ? '#sec-' + id : (location.hash || ''); MaterialSave.link({ title: sectionTitle(), url: '/textbook/' + slug + hash, sourceTitle: chapterTitle() }, btn); } /* ── Lazy html2canvas loader ── */ var _h2c = null; function ensureH2C() { if (window.html2canvas) return Promise.resolve(window.html2canvas); if (_h2c) return _h2c; _h2c = new Promise(function (res, rej) { var s = document.createElement('script'); s.src = H2C_SRC; s.async = true; s.onload = function () { res(window.html2canvas); }; s.onerror = function () { _h2c = null; rej(new Error('Не удалось загрузить модуль снимков')); }; document.head.appendChild(s); }); return _h2c; } /* ── Styles for selection + preview overlays ── */ function ensureStyles() { if (document.getElementById('__lsclip-style')) return; var s = document.createElement('style'); s.id = '__lsclip-style'; s.textContent = [ '.__lsclip-sel-ov{position:fixed;inset:0;z-index:2147483600;cursor:crosshair;background:rgba(15,12,30,.22);touch-action:none}', '.__lsclip-box{position:absolute;border:2px dashed #fff;background:rgba(139,92,246,.18);box-shadow:0 0 0 1px rgba(139,92,246,.9);pointer-events:none}', '.__lsclip-hint{position:fixed;top:16px;left:50%;transform:translateX(-50%);z-index:2147483601;background:rgba(15,12,30,.92);color:#fff;padding:8px 16px;border-radius:99px;font:600 13px/1 Manrope,system-ui,sans-serif;box-shadow:0 6px 20px rgba(0,0,0,.3);pointer-events:none}', '.__lsclip-pv-ov{position:fixed;inset:0;z-index:2147483600;background:rgba(15,12,30,.72);display:flex;align-items:center;justify-content:center;padding:16px}', '.__lsclip-pv{background:#fff;border-radius:14px;width:440px;max-width:92vw;max-height:92vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.4)}', '.__lsclip-pv-h{padding:14px 16px;font:700 .95rem/1.2 Manrope,system-ui,sans-serif;color:#1a1433;border-bottom:1px solid #eee}', '.__lsclip-pv-body{padding:14px 16px;display:flex;flex-direction:column;gap:12px;overflow:auto}', '.__lsclip-pv-img{max-width:100%;max-height:46vh;object-fit:contain;border:1px solid #eee;border-radius:8px;background:#f8fafc;align-self:center}', '.__lsclip-pv-input{padding:9px 12px;border:1px solid #e2e8f0;border-radius:9px;font:inherit;width:100%;box-sizing:border-box}', '.__lsclip-pv-actions{display:flex;justify-content:flex-end;gap:8px;padding:12px 16px;border-top:1px solid #eee}', '.__lsclip-pv-actions button{padding:8px 16px;border-radius:9px;border:1px solid #e2e8f0;background:#fff;font:700 .85rem Manrope,system-ui,sans-serif;cursor:pointer;color:#475569}', '.__lsclip-pv-actions button.primary{background:#8b5cf6;border-color:#8b5cf6;color:#fff}', '.__lsclip-pv-actions button:disabled{opacity:.5;cursor:default}' ].join(''); document.head.appendChild(s); } /* ── Drag a rectangle on the live page ── */ function startRegion() { ensureStyles(); var ov = document.createElement('div'); ov.className = '__lsclip-sel-ov'; ov.setAttribute('data-h2c-ignore', ''); var box = document.createElement('div'); box.className = '__lsclip-box'; box.style.display = 'none'; box.setAttribute('data-h2c-ignore', ''); var hint = document.createElement('div'); hint.className = '__lsclip-hint'; hint.setAttribute('data-h2c-ignore', ''); hint.textContent = 'Выделите область мышью — она сохранится картинкой. Esc — отмена'; ov.appendChild(box); document.body.appendChild(ov); document.body.appendChild(hint); var dragging = false, sx = 0, sy = 0, rect = null; function cleanup() { ov.remove(); hint.remove(); document.removeEventListener('keydown', onKey); } function onKey(e) { if (e.key === 'Escape') cleanup(); } document.addEventListener('keydown', onKey); ov.addEventListener('pointerdown', function (e) { e.preventDefault(); dragging = true; sx = e.clientX; sy = e.clientY; rect = null; box.style.display = 'block'; box.style.left = sx + 'px'; box.style.top = sy + 'px'; box.style.width = '0'; box.style.height = '0'; try { ov.setPointerCapture(e.pointerId); } catch (err) {} }); ov.addEventListener('pointermove', function (e) { if (!dragging) return; var x = Math.min(e.clientX, sx), y = Math.min(e.clientY, sy); var w = Math.abs(e.clientX - sx), h = Math.abs(e.clientY - sy); rect = { left: x, top: y, width: w, height: h }; box.style.left = x + 'px'; box.style.top = y + 'px'; box.style.width = w + 'px'; box.style.height = h + 'px'; }); ov.addEventListener('pointerup', function () { dragging = false; var r = rect; cleanup(); // remove overlay BEFORE rasterizing if (!r || r.width < 8 || r.height < 8) return; capture(r); }); } /* ── Rasterize the selected region → PNG blob ── */ function capture(r) { var scrollX = window.scrollX || window.pageXOffset || 0; var scrollY = window.scrollY || window.pageYOffset || 0; var br = document.body.getBoundingClientRect(); var ox = br.left + scrollX, oy = br.top + scrollY; // body origin in document coords document.body.style.cursor = 'progress'; ensureH2C().then(function (h2c) { return h2c(document.body, { x: Math.round((r.left + scrollX) - ox), y: Math.round((r.top + scrollY) - oy), width: Math.round(r.width), height: Math.round(r.height), scale: Math.min(2, window.devicePixelRatio || 1), useCORS: true, backgroundColor: '#ffffff', logging: false, ignoreElements: function (el) { return !!(el && el.hasAttribute && el.hasAttribute('data-h2c-ignore')); } }); }).then(function (canvas) { document.body.style.cursor = ''; canvas.toBlob(function (blob) { if (!blob) { toast('Не удалось снять область', 'error'); return; } preview(blob); }, 'image/png'); }).catch(function (e) { document.body.style.cursor = ''; toast((e && e.message) || 'Ошибка снимка', 'error'); }); } /* ── Preview + title, then save as image material ── */ function preview(blob) { ensureStyles(); var url = URL.createObjectURL(blob); var ov = document.createElement('div'); ov.className = '__lsclip-pv-ov'; ov.innerHTML = '
' + '
Сохранить область в «Мои материалы»
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
'; document.body.appendChild(ov); ov.querySelector('.__lsclip-pv-img').src = url; var input = ov.querySelector('.__lsclip-pv-input'); input.value = sectionTitle(); var saveBtn = ov.querySelector('[data-a="save"]'); function close() { ov.remove(); URL.revokeObjectURL(url); } ov.querySelector('[data-a="cancel"]').onclick = close; ov.addEventListener('click', function (e) { if (e.target === ov) close(); }); saveBtn.onclick = async function () { saveBtn.disabled = true; try { var fd = new FormData(); fd.append('file', blob, 'textbook-region.png'); var up = await LS.uploadMaterialFile(fd); await LS.saveMaterial({ kind: 'image', title: input.value.trim() || sectionTitle(), url: up.url, sourceTitle: chapterTitle() }); toast('Сохранено в «Мои материалы»', 'success'); close(); } catch (e) { toast((e && e.message) || 'Ошибка сохранения', 'error'); saveBtn.disabled = false; } }; setTimeout(function () { try { input.focus(); input.select(); } catch (e) {} }, 30); } /* ── Floating buttons ── */ function makeBtn(id, label, svg, solid) { var b = document.createElement('button'); b.id = id; b.type = 'button'; b.innerHTML = svg + '' + label + ''; var base = 'display:inline-flex;align-items:center;gap:7px;padding:10px 14px;border-radius:99px;font:600 13px/1 Manrope,system-ui,sans-serif;cursor:pointer;box-shadow:0 6px 20px rgba(139,92,246,.30);'; b.style.cssText = base + (solid ? 'border:none;background:#8b5cf6;color:#fff;' : 'border:1px solid rgba(139,92,246,.45);background:#fff;color:#7c3aed;'); return b; } function build() { if (document.getElementById('__ls_clip') || !document.body) return; var grp = document.createElement('div'); grp.id = '__ls_clip'; grp.setAttribute('data-h2c-ignore', ''); grp.style.cssText = 'position:fixed;right:18px;bottom:18px;z-index:9000;display:flex;flex-direction:column;gap:10px;align-items:flex-end'; var bRegion = makeBtn('__ls_clip_region', 'Вырезать область', CROP_SVG, false); bRegion.title = 'Выделить часть страницы и сохранить картинкой'; bRegion.addEventListener('click', function () { startRegion(); }); var bLink = makeBtn('__ls_clip_link', 'В мои материалы', BOOKMARK_SVG, true); bLink.title = 'Сохранить эту тему ссылкой в «Мои материалы»'; bLink.addEventListener('click', function () { saveLink(bLink); }); grp.appendChild(bRegion); grp.appendChild(bLink); document.body.appendChild(grp); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { setTimeout(build, 300); }); else setTimeout(build, 300); })();