/** * GetCourse Bookmarks — Unified Script * v5.0.0 — Единый скрипт (виджеты + страница /_bookmarks) * * Подключается через файловое хранилище GetCourse. * Query-параметры: * ?max=50 — максимум закладок у пользователя, 0/нет — безлимитно * ?css=/fileservice/.../ХЕШ.css — путь к CSS после https://fs.getcourse.ru */ (function () { 'use strict'; // =================================================================== // DEBUG // =================================================================== var GCBM_DEBUG = true; // поставь false перед публикацией function glog() { if (!GCBM_DEBUG) return; try { console.log.apply(console, ['[GCBM]'].concat(Array.prototype.slice.call(arguments))); } catch (e) {} } function gwarn() { try { console.warn.apply(console, ['[GCBM]'].concat(Array.prototype.slice.call(arguments))); } catch (e) {} } function gerr() { try { console.error.apply(console, ['[GCBM]'].concat(Array.prototype.slice.call(arguments))); } catch (e) {} } // =================================================================== // CONFIG // =================================================================== var Config = (function () { var PAGE_PATH = '/_bookmarks'; var STORAGE_KEYS = { FIELD_ID: 'gcbm_field_id', PROCESS_ID: 'gcbm_process_id', SESSION_PARAMS: 'gcbm_session_params', BOOKMARKS_PREFIX: 'gcbm_bookmarks' }; var _fieldId = null, _processId = null, _userId = null; function parseIdsFromHTML(html) { var r = {}; var m = html.match(/process[?]id=(\d+)/); if (m) r.processId = m[1]; var f = html.match(/formParams\[userCustomFields\]\[(\d+)\]/); if (f) r.fieldId = f[1]; return r; } async function ensureIds() { if (_fieldId && _processId) { glog('Config.ensureIds: из памяти, fieldId =', _fieldId, 'processId =', _processId); return { fieldId: _fieldId, processId: _processId }; } var sf = localStorage.getItem(STORAGE_KEYS.FIELD_ID), sp = localStorage.getItem(STORAGE_KEYS.PROCESS_ID); if (sf && sp) { _fieldId = sf; _processId = sp; glog('Config.ensureIds: из localStorage, fieldId =', sf, 'processId =', sp); return { fieldId: _fieldId, processId: _processId }; } try { glog('Config.ensureIds: GET страницы', PAGE_PATH); var resp = await fetch(PAGE_PATH, { credentials: 'include' }); var html = await resp.text(); glog('Config.ensureIds: статус =', resp.status, 'длина html =', html.length); var p = parseIdsFromHTML(html); if (p.fieldId && p.processId) { _fieldId = p.fieldId; _processId = p.processId; localStorage.setItem(STORAGE_KEYS.FIELD_ID, _fieldId); localStorage.setItem(STORAGE_KEYS.PROCESS_ID, _processId); glog('Config.ensureIds: определены fieldId =', _fieldId, 'processId =', _processId); return { fieldId: _fieldId, processId: _processId }; } gerr('Config.ensureIds: ID не найдены в HTML. fieldId =', p.fieldId, 'processId =', p.processId); throw new Error('ID not found'); } catch (err) { gerr('Config.ensureIds: ошибка', err); throw err; } } function getFieldId() { return _fieldId; } function getProcessId() { return _processId; } function setIds(fieldId, processId) { if (fieldId) { _fieldId = String(fieldId); try { localStorage.setItem(STORAGE_KEYS.FIELD_ID, _fieldId); } catch (e) {} } if (processId) { _processId = String(processId); try { localStorage.setItem(STORAGE_KEYS.PROCESS_ID, _processId); } catch (e) {} } glog('Config.setIds: fieldId =', _fieldId, 'processId =', _processId); } function refreshIdsFromHtml(html) { var p = parseIdsFromHTML(html); if (p.fieldId && p.processId) { setIds(p.fieldId, p.processId); return true; } gwarn('Config.refreshIdsFromHtml: не удалось определить ID из HTML'); return false; } function detectUserId() { if (_userId) return _userId; if (typeof accountUserId !== 'undefined' && accountUserId) { _userId = String(accountUserId); return _userId; } if (window.gcUser && window.gcUser.id) { _userId = String(window.gcUser.id); return _userId; } var m = document.cookie.match(/(?:^|;\s*)userId=(\d+)/); if (m) { _userId = m[1]; return _userId; } var b = document.body.getAttribute('data-user-id'); if (b) { _userId = b; return _userId; } if (window.gc && window.gc.userId) { _userId = String(window.gc.userId); return _userId; } _userId = window.location.hostname.replace(/\./g, '_'); return _userId; } function getBookmarksCacheKey() { return STORAGE_KEYS.BOOKMARKS_PREFIX + '_' + (detectUserId() || 'unknown'); } return { PAGE_PATH: PAGE_PATH, STORAGE_KEYS: STORAGE_KEYS, ensureIds: ensureIds, getFieldId: getFieldId, getProcessId: getProcessId, setIds: setIds, refreshIdsFromHtml: refreshIdsFromHtml, detectUserId: detectUserId, getBookmarksCacheKey: getBookmarksCacheKey }; })(); // Сколько последних закладок показывать в виджете и боковой панели (фикс. 10) var MAX_VISIBLE_BOOKMARKS = 10; // Максимум закладок у пользователя (0 = безлимитно). Задаётся параметром ?max= var MAX_BOOKMARKS = 0; (function () { try { var params = new URL(document.currentScript.src).searchParams; var mb = parseInt(params.get('max')); if (!isNaN(mb) && mb > 0) MAX_BOOKMARKS = mb; var css = params.get('css'); if (css) { var link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'https://fs.getcourse.ru' + css; document.head.appendChild(link); } } catch (e) {} })(); // =================================================================== // EVENT BUS // =================================================================== var EventBus = (function () { var _listeners = {}; function on(event, cb) { if (!_listeners[event]) _listeners[event] = []; _listeners[event].push(cb); } function off(event, cb) { if (!_listeners[event]) return; _listeners[event] = _listeners[event].filter(function (c) { return c !== cb; }); } function emit(event, data) { if (!_listeners[event]) return; _listeners[event].forEach(function (cb) { try { cb(data); } catch (e) {} }); } return { on: on, off: off, emit: emit }; })(); // =================================================================== // DATA LAYER // =================================================================== var DataLayer = (function () { var _cache = null; function _decodeValue(raw) { if (!raw) return []; raw = raw.trim(); if (raw === '' || raw === '""') return []; if (raw.indexOf('\\"') === 0 && raw.lastIndexOf('\\"') === raw.length - 2) { try { var p = JSON.parse(raw.substring(2, raw.length - 2).replace(/'/g, '"')); if (Array.isArray(p)) return p; } catch (e) {} } try { var p2 = JSON.parse(raw); if (Array.isArray(p2)) return p2; } catch (e) {} return []; } function _encodeValue(bookmarks) { return '\\"' + JSON.stringify(bookmarks).replace(/"/g, "'") + '\\"'; } async function getAll() { try { glog('DataLayer.getAll: старт'); await Config.ensureIds(); var resp = await fetch(Config.PAGE_PATH, { credentials: 'include' }); var html = await resp.text(); glog('DataLayer.getAll: GET', Config.PAGE_PATH, 'статус =', resp.status, 'длина html =', html.length); Config.refreshIdsFromHtml(html); var fid = Config.getFieldId(); glog('DataLayer.getAll: fieldId =', fid); var value = null; var m0 = html.match(new RegExp(']*name="formParams\\[userCustomFields\\]\\[' + fid + '\\]"[^>]*>([\\s\\S]*?)<\\/textarea>', 'i')); if (m0) value = m0[1].trim(); if (!value) { var m1 = html.match(new RegExp('data-id="' + fid + '"[\\s\\S]*?]*>([\\s\\S]*?)<\\/textarea>', 'i')); if (m1) value = m1[1].trim(); } if (!value) { var m2 = html.match(new RegExp('name="formParams\\\\[userCustomFields\\\\]\\\\[' + fid + '\\\\]"[\\s\\S]*?]*>([\\s\\S]*?)<\\/textarea>', 'i')); if (m2) value = m2[1].trim(); } if (!value) { var m3 = /]*id="user_bookmarks"[^>]*>([\s\S]*?)<\/div>/i.exec(html); if (m3) { var ta = /]*>([\s\S]*?)<\/textarea>/i.exec(m3[1]); if (ta) value = ta[1].trim(); } } if (!value) { glog('DataLayer.getAll: значение не найдено — диагностика HTML:'); glog(' "userCustomFields" =', html.indexOf('userCustomFields'), '| "user_bookmarks" =', html.indexOf('user_bookmarks'), '| fid ' + fid + ' =', html.indexOf(fid)); var dtas = html.match(/]*>[\s\S]*?<\/textarea>/gi) || []; glog(' найдено '; UIFactory.createDialog('Комментарий', taHtml, [ { label: 'Сохранить', class: CSS_PREFIX + 'dialog-btn--primary', callback: async function () { var ta = document.getElementById(CSS_PREFIX + 'comment-textarea'); if (!ta) return; try { await DataLayer.updateComment(currentId, ta.value.trim()); UIFactory.createToast('Комментарий сохранён', 'success'); } catch (err) { UIFactory.createToast('Ошибка: ' + err.message, 'error'); } } }, { label: 'Отмена', class: '', callback: function () {} } ]); } function _showDeleteConfirm(icon, currentId) { UIFactory.createDialog('Удаление закладки', '

Вы уверены, что хотите удалить закладку?

', [ { label: 'Удалить', class: CSS_PREFIX + 'dialog-btn--danger', callback: async function () { try { await DataLayer.remove(currentId); _updateIconState(icon, 'outline'); UIFactory.createToast('Закладка удалена', 'info'); } catch (err) { UIFactory.createToast('Ошибка: ' + err.message, 'error'); } } }, { label: 'Отмена', class: '', callback: function () {} } ]); } var _currentTooltip = null, _tooltipHideTimer = null; function _showTooltip(icon, currentId, parentEl) { if ('ontouchstart' in window) return; if (_currentTooltip && _currentTooltip.getAttribute('data-tooltip-id') === currentId) return; _hideTooltip(true); var tooltip = document.createElement('div'); tooltip.className = CSS_PREFIX + 'bookmark-tooltip'; tooltip.setAttribute('data-tooltip-id', currentId); tooltip.innerHTML = '
Добавить комментарий
Удалить закладку
'; tooltip.querySelector('.' + CSS_PREFIX + 'bookmark-tooltip__comment').addEventListener('click', function (e) { e.stopPropagation(); _hideTooltip(true); _showCommentDialog(icon, currentId, DataLayer.find(currentId)); }); tooltip.querySelector('.' + CSS_PREFIX + 'bookmark-tooltip__delete').addEventListener('click', function (e) { e.stopPropagation(); _hideTooltip(true); _showDeleteConfirm(icon, currentId); }); tooltip.addEventListener('mouseenter', function () { if (_tooltipHideTimer) { clearTimeout(_tooltipHideTimer); _tooltipHideTimer = null; } }); tooltip.addEventListener('mouseleave', function () { _scheduleTooltipHide(); }); document.body.appendChild(tooltip); var ir = icon.getBoundingClientRect(); var top = ir.top + window.scrollY, left = ir.right + window.scrollX + 6; if (left + 180 > window.innerWidth + window.scrollX) left = ir.left + window.scrollX - 186; tooltip.style.cssText = 'position:absolute;top:' + top + 'px;left:' + left + 'px;z-index:99999;'; requestAnimationFrame(function () { tooltip.classList.add(CSS_PREFIX + 'bookmark-tooltip--visible'); }); _currentTooltip = tooltip; } function _scheduleTooltipHide() { if (_tooltipHideTimer) clearTimeout(_tooltipHideTimer); _tooltipHideTimer = setTimeout(function () { _hideTooltip(true); }, 150); } function _hideTooltip(immediate) { if (_tooltipHideTimer) { clearTimeout(_tooltipHideTimer); _tooltipHideTimer = null; } if (!_currentTooltip) return; var t = _currentTooltip; _currentTooltip = null; if (immediate) { if (t.parentNode) t.parentNode.removeChild(t); } else { t.classList.remove(CSS_PREFIX + 'bookmark-tooltip--visible'); setTimeout(function () { if (t.parentNode) t.parentNode.removeChild(t); }, 200); } } function _attachClickHandler(icon, parentEl) { icon.addEventListener('mouseenter', function () { var id = icon.getAttribute('data-lesson-id'); if (!id || !DataLayer.has(id)) return; _showTooltip(icon, id, parentEl); }); icon.addEventListener('mouseleave', _scheduleTooltipHide); icon.addEventListener('click', async function (e) { e.preventDefault(); e.stopPropagation(); var id = icon.getAttribute('data-lesson-id'); if (!id) return; icon.style.pointerEvents = 'none'; glog('BookmarkIcon: клик, lessonId =', id, 'уже в закладках =', DataLayer.has(id)); try { if (DataLayer.has(id)) { glog('BookmarkIcon: удаляю закладку'); await DataLayer.remove(id); _updateIconState(icon, 'outline'); UIFactory.createToast('Закладка удалена', 'info'); } else { if (MAX_BOOKMARKS > 0 && DataLayer.countVisible() >= MAX_BOOKMARKS) { gwarn('BookmarkIcon: достигнут лимит', MAX_BOOKMARKS); UIFactory.createToast('Достигнут лимит сохранённых закладок', 'error'); return; } glog('BookmarkIcon: добавляю закладку'); var meta = extractLessonMeta(parentEl, id); glog('BookmarkIcon: meta =', meta); await DataLayer.add(meta); _updateIconState(icon, 'filled'); UIFactory.createToast('Урок добавлен в закладки', 'success'); } } catch (err) { gerr('BookmarkIcon: ошибка операции', err); UIFactory.createToast('Ошибка: ' + err.message, 'error'); } finally { icon.style.pointerEvents = ''; } }); } function injectIcon(lessonEl) { var id = extractLessonId(lessonEl); if (!id) return; if (lessonEl.querySelector('.' + CSS_PREFIX + 'bookmark-icon')) return; var link = lessonEl.querySelector('a[href*="lesson/view"]'); if (!link) return; var icon = UIFactory.createBookmarkIcon(DataLayer.has(id) ? 'filled' : 'outline'); icon.setAttribute('data-lesson-id', id); icon.classList.add(CSS_PREFIX + 'bookmark-icon--inline'); _attachClickHandler(icon, lessonEl); link.classList.add(CSS_PREFIX + 'bookmark-icon-target'); link.appendChild(icon); } function scanAndInject() { document.querySelectorAll('li[data-lesson-id]').forEach(injectIcon); document.querySelectorAll('.lesson-list a[href*="lesson/view"]').forEach(function (a) { var p = a.closest('li, .lesson-item, .list-group-item'); if (p && !p.querySelector('.' + CSS_PREFIX + 'bookmark-icon')) injectIcon(p); }); var lh = document.querySelector('.lesson-header-block'); if (lh && !lh.querySelector('.' + CSS_PREFIX + 'bookmark-icon')) injectLessonPageIcon(lh); var cb = document.querySelector('.chatium_body'); if (cb && !cb.querySelector('.' + CSS_PREFIX + 'bookmark-icon')) { var te = cb.querySelector('.lesson-title') || cb.querySelector('h1, h2, h3'); if (te) injectLessonPageIcon(te); } // Страница урока: ищем заголовок в разных контейнерах var titleEl = document.querySelector('.lesson-title-value, .lesson-view__title, h1.lesson__title, .lesson__title'); if (titleEl && !titleEl.querySelector('.' + CSS_PREFIX + 'bookmark-icon')) { injectLessonPageIcon(titleEl); } // Если всё ещё нет иконки на странице урока — добавляем в body или form var path = window.location.pathname; if ((path.indexOf('/teach/control/lesson/view') !== -1 || path.indexOf('/pl/teach/control/lesson/view') !== -1) && !document.querySelector('.' + CSS_PREFIX + 'bookmark-icon')) { var fallback = document.querySelector('.lesson-content, .lesson-body, #lesson-content, form') || document.body; if (fallback && !fallback.querySelector('.' + CSS_PREFIX + 'bookmark-icon')) { injectLessonPageIcon(fallback); } } } function injectLessonPageIcon(headerBlock) { var id = extractLessonId(headerBlock); if (!id) return; var icon = UIFactory.createBookmarkIcon(DataLayer.has(id) ? 'filled' : 'outline'); icon.setAttribute('data-lesson-id', id); icon.classList.add(CSS_PREFIX + 'bookmark-icon--inline'); _attachClickHandler(icon, headerBlock); headerBlock.appendChild(icon); } var _observer = null; function _setupObserver() { if (_observer) { _observer.disconnect(); _observer = null; } var target = document.querySelector('form') || document.body; _observer = new MutationObserver(function () { scanAndInject(); }); _observer.observe(target, { childList: true, subtree: true }); } function init() { if (_init) { scanAndInject(); _setupObserver(); return; } _init = true; DataLayer.getAll().then(function () { scanAndInject(); _setupObserver(); }).catch(function (err) { console.error('[GCBM:BookmarkIcon]', err); scanAndInject(); _setupObserver(); }); } return { init: init, scanAndInject: scanAndInject, _showCommentDialog: _showCommentDialog }; })(); // =================================================================== // SIDEBAR // =================================================================== var Sidebar = (function () { var CSS_PREFIX = 'gcbm-'; var _init = false; function init() { if (_init) return; _init = true; _addSidebarItem(); DataLayer.getAll().then(function () { _updateCount(); EventBus.on('bookmark:added', _updateCount); EventBus.on('bookmark:removed', _updateCount); }); } function _addSidebarItem(attempt) { attempt = attempt || 0; var menu = document.querySelector('.gc-account-user-menu'); if (!menu) { if (attempt < 20) setTimeout(function () { _addSidebarItem(attempt + 1); }, 500); return; } if (menu.querySelector('.menu-item-bookmarks')) return; var li = document.createElement('li'); li.className = 'menu-item menu-item-bookmarks ' + CSS_PREFIX + 'sidebar-item'; var a = document.createElement('a'); a.href = 'javascript:void(0)'; a.title = 'Закладки'; a.className = 'with-label'; a.innerHTML = '0Закладки'; a.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); _toggleSubmenu(); }); li.appendChild(a); menu.appendChild(li); _updateCount(); } function _toggleSubmenu() { var sb = document.querySelector('.gc-account-user-submenu-bar'); if (!sb) return; var open = sb.classList.contains(CSS_PREFIX + 'sidebar-submenu--open'); if (open) { sb.classList.remove(CSS_PREFIX + 'sidebar-submenu--open'); sb.innerHTML = ''; var fade = document.querySelector('.gc-fade-wrapper'); if (fade) fade.classList.remove(CSS_PREFIX + 'fade--visible'); return; } _renderSubmenu(sb); sb.classList.add(CSS_PREFIX + 'sidebar-submenu--open'); var fade = document.querySelector('.gc-fade-wrapper'); if (fade) { fade.classList.add(CSS_PREFIX + 'fade--visible'); fade.addEventListener('click', function cf() { fade.classList.remove(CSS_PREFIX + 'fade--visible'); sb.classList.remove(CSS_PREFIX + 'sidebar-submenu--open'); sb.innerHTML = ''; fade.removeEventListener('click', cf); }); } } function _renderSubmenu(container) { var bookmarks = (DataLayer.getCached() || []).filter(function (b) { return !b.hidden; }); var recent = bookmarks.slice().sort(function (a, b) { return new Date(b.date) - new Date(a.date); }); if (MAX_VISIBLE_BOOKMARKS > 0) recent = recent.slice(0, MAX_VISIBLE_BOOKMARKS); var hd = document.createElement('div'), h3 = document.createElement('h3'); h3.textContent = 'Закладки'; hd.appendChild(h3); var list = document.createElement('div'); list.className = CSS_PREFIX + 'sidebar-submenu__list'; if (recent.length === 0) { var emp = document.createElement('div'); emp.className = CSS_PREFIX + 'sidebar-submenu__empty'; emp.textContent = 'Нет закладок'; list.appendChild(emp); } else { recent.forEach(function (bm) { var item = document.createElement('div'); item.className = CSS_PREFIX + 'sidebar-submenu__item'; var tr = document.createElement('div'); tr.className = CSS_PREFIX + 'sidebar-submenu__row'; var left = document.createElement('div'); var link = document.createElement('a'); link.className = 'subitem-link ' + CSS_PREFIX + 'sidebar-submenu__link'; link.href = '/teach/control/lesson/view/id/' + bm.id; link.textContent = bm.meta.lessonTitle || 'Урок ' + bm.id; left.appendChild(link); if (bm.meta.moduleTitle) { var mod = bm.meta.trainingId ? document.createElement('a') : document.createElement('div'); mod.className = CSS_PREFIX + 'sidebar-submenu__module'; if (bm.meta.trainingId) { mod.href = '/teach/control/stream/view/id/' + bm.meta.trainingId; } mod.textContent = bm.meta.moduleTitle; left.appendChild(mod); } if (bm.comment) { var cl = document.createElement('div'); cl.className = CSS_PREFIX + 'sidebar-submenu__comment'; cl.textContent = bm.comment.length > 150 ? bm.comment.substring(0, 150) + '…' : bm.comment; left.appendChild(cl); } var ac = document.createElement('div'); ac.className = CSS_PREFIX + 'sidebar-submenu__actions'; var eb = document.createElement('button'); eb.innerHTML = ''; eb.title = 'Редактировать'; eb.className = CSS_PREFIX + 'sidebar-submenu__edit'; eb.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); BookmarkIcon._showCommentDialog(null, bm.id, DataLayer.find(bm.id)); }); var db = document.createElement('button'); db.innerHTML = ''; db.title = 'Удалить'; db.className = CSS_PREFIX + 'sidebar-submenu__delete'; db.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); DataLayer.remove(bm.id).then(function () { _updateCount(); _renderSubmenu(container); }); }); ac.appendChild(eb); ac.appendChild(db); tr.appendChild(left); tr.appendChild(ac); item.appendChild(tr); list.appendChild(item); }); } { var ad = document.createElement('div'); ad.className = CSS_PREFIX + 'sidebar-submenu__all'; var al = document.createElement('a'); al.className = 'subitem-link ' + CSS_PREFIX + 'sidebar-submenu__all-link'; al.href = '/_bookmarks'; al.textContent = 'Все закладки (' + (DataLayer.getCached() || []).length + ') →'; ad.appendChild(al); list.appendChild(ad); } container.innerHTML = ''; container.appendChild(hd); container.appendChild(list); } function _updateCount() { var ce = document.querySelector('.' + CSS_PREFIX + 'sidebar-count'); if (!ce) return; var c = (DataLayer.getCached() || []).filter(function (b) { return !b.hidden; }).length; ce.textContent = c; if (c > 0) ce.classList.add(CSS_PREFIX + 'sidebar-count--visible'); else ce.classList.remove(CSS_PREFIX + 'sidebar-count--visible'); } return { init: init }; })(); // =================================================================== // BOOKMARK WIDGET // =================================================================== var BookmarkWidget = (function () { var CSS_PREFIX = 'gcbm-'; var _init = false, _container = null; function init() { if (_init) return; _container = document.getElementById('bookmark-widget'); if (!_container) return; _init = true; DataLayer.getAll().then(function (bms) { _render(bms); EventBus.on('bookmark:added', function () { _render(DataLayer.getCached() || []); }); EventBus.on('bookmark:removed', function () { _render(DataLayer.getCached() || []); }); EventBus.on('bookmarks:orderChanged', function () { _render(DataLayer.getCached() || []); }); }).catch(function (err) { console.error('[GCBM:BookmarkWidget]', err); _render(DataLayer.getCached() || []); }); } function _render(bookmarks) { if (!_container) return; _container.innerHTML = ''; var filteredB = bookmarks.filter(function (b) { return !b.hidden; }); var sorted = filteredB.slice().sort(function (a, b) { return new Date(b.date) - new Date(a.date); }); var visible = MAX_VISIBLE_BOOKMARKS > 0 ? sorted.slice(0, MAX_VISIBLE_BOOKMARKS) : sorted; var total = filteredB.length; var header = document.createElement('div'); header.className = CSS_PREFIX + 'widget__header'; var title = document.createElement('h3'); title.className = CSS_PREFIX + 'widget__title'; title.textContent = 'Мои закладки'; header.appendChild(title); var count = document.createElement('span'); count.className = CSS_PREFIX + 'widget__count'; count.textContent = String(total); header.appendChild(count); _container.appendChild(header); var body = document.createElement('div'); body.className = CSS_PREFIX + 'widget__body'; if (visible.length === 0) { var emp = document.createElement('div'); emp.className = CSS_PREFIX + 'widget__empty'; emp.textContent = 'У вас пока нет закладок'; body.appendChild(emp); } else { visible.forEach(function (bm) { var item = document.createElement('div'); item.className = CSS_PREFIX + 'widget__item'; var link = document.createElement('a'); link.className = CSS_PREFIX + 'widget__item-link'; link.href = '/teach/control/lesson/view/id/' + bm.id; link.textContent = bm.meta.lessonTitle || ('Урок ' + bm.id); var row = document.createElement('div'); row.className = CSS_PREFIX + 'widget__item-row'; var info = document.createElement('div'); info.className = CSS_PREFIX + 'widget__item-info'; info.appendChild(link); var mn = bm.meta.trainingId && bm.meta.moduleTitle ? document.createElement('a') : document.createElement('div'); mn.className = CSS_PREFIX + 'widget__item-module'; if (bm.meta.trainingId && bm.meta.moduleTitle) { mn.href = '/teach/control/stream/view/id/' + bm.meta.trainingId; } mn.textContent = bm.meta.moduleTitle || ''; info.appendChild(mn); if (bm.comment) { var cl = document.createElement('div'); cl.className = CSS_PREFIX + 'widget__item-comment'; cl.textContent = bm.comment.length > 150 ? bm.comment.substring(0, 150) + '…' : bm.comment; info.appendChild(cl); } var ac = document.createElement('div'); ac.className = CSS_PREFIX + 'widget__item-actions'; var eb = document.createElement('button'); eb.innerHTML = ''; eb.title = 'Редактировать'; eb.className = CSS_PREFIX + 'widget__item-edit'; eb.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); BookmarkIcon._showCommentDialog(null, bm.id, DataLayer.find(bm.id)); }); var db = document.createElement('button'); db.innerHTML = ''; db.title = 'Удалить'; db.className = CSS_PREFIX + 'widget__item-delete'; db.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); DataLayer.remove(bm.id); }); ac.appendChild(eb); ac.appendChild(db); row.appendChild(info); row.appendChild(ac); item.appendChild(row); body.appendChild(item); }); } _container.appendChild(body); if (B_TYPE === 'max') { var footer = document.createElement('div'); footer.className = CSS_PREFIX + 'widget__footer'; var al = document.createElement('a'); al.className = CSS_PREFIX + 'widget__all-link'; al.href = '/_bookmarks'; al.textContent = 'Все закладки (' + total + ') →'; footer.appendChild(al); _container.appendChild(footer); } } return { init: init }; })(); // =================================================================== // BOOKMARKS PAGE (страница /_bookmarks) // =================================================================== var BookmarksPage = (function () { var PREFIX = 'gcbm-page-'; var VIEW_MODE_KEY = 'gcbm_page_viewMode'; var CONTAINER_ID = 'bookmarks-page'; var _container = null; var _state = { sortField: null, sortDirection: 'asc', draggedIndex: null, initialRenderDone: false, searchQuery: '', expandedRowId: null, viewMode: (function () { try { return localStorage.getItem(VIEW_MODE_KEY) || 'cards'; } catch (e) { return 'cards'; } })(), prevCount: 0 }; var _searchDebounceTimer = null; function getBookmarks() { return DataLayer.getCached() || []; } // SVG ICONS function svgSearch() { return ''; } function svgClose() { return ''; } function svgFilter() { return ''; } function svgChevronDown() { return ''; } function svgArrowUp() { return ''; } function svgArrowDown() { return ''; } function svgDragHandle() { return ''; } function svgBookmark() { return ''; } function svgCalendar() { return ''; } function svgEdit() { return ''; } function svgTrash() { return ''; } function svgShield() { return ''; } function svgRefresh() { return ''; } function svgCheck() { return ''; } function svgSwap() { return ''; } function svgInfo() { return ''; } // FORMAT function formatDate(iso) { try { if (!iso) return '—'; iso = iso.trim(); if (!iso) return '—'; if (iso.indexOf('+') > 0) iso = iso.replace(/[+]\d{2}:\d{2}$/, 'Z'); var d = new Date(iso); if (isNaN(d.getTime())) { var m = iso.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/); if (m) d = new Date(+m[1], +m[2]-1, +m[3], +m[4], +m[5]); } if (isNaN(d.getTime())) return '—'; return String(d.getDate()).padStart(2,'0')+'.'+String(d.getMonth()+1).padStart(2,'0')+'.'+d.getFullYear()+' '+String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0'); } catch(e) { return '—'; } } function formatDateShort(iso) { try { if (!iso) return '—'; iso = iso.trim(); if (!iso) return '—'; if (iso.indexOf('+') > 0) iso = iso.replace(/[+]\d{2}:\d{2}$/, 'Z'); var d = new Date(iso); if (isNaN(d.getTime())) { var m = iso.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/); if (m) d = new Date(+m[1], +m[2]-1, +m[3], +m[4], +m[5]); } if (isNaN(d.getTime())) return '—'; var months = ['янв','фев','мар','апр','мая','июн','июл','авг','сен','окт','ноя','дек']; return d.getDate()+' '+months[d.getMonth()]+' '+d.getFullYear(); } catch(e) { return '—'; } } function escapeHtml(s) { if (!s) return ''; return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,'''); } // TOAST function showToast(msg, type) { type = type || 'info'; var c = document.querySelector('.' + PREFIX + 'toast-container'); if (!c) { c = document.createElement('div'); c.className = PREFIX + 'toast-container'; document.body.appendChild(c); } var t = document.createElement('div'); t.className = PREFIX + 'toast ' + PREFIX + 'toast-' + type; t.textContent = msg; c.appendChild(t); requestAnimationFrame(function () { t.classList.add(PREFIX + 'toast--visible'); }); setTimeout(function () { t.classList.add(PREFIX + 'toast--hiding'); setTimeout(function () { if (t.parentNode) t.parentNode.removeChild(t); }, 350); }, 3000); } // DELETE CONFIRM function showDeleteConfirm(bookmarkId) { var bm = DataLayer.find(bookmarkId); var lt = (bm && bm.meta && bm.meta.lessonTitle) ? bm.meta.lessonTitle : ('Урок ' + bookmarkId); var ov = document.createElement('div'); ov.className = PREFIX + 'modal-overlay'; ov.innerHTML = '

Удалить закладку?

Вы уверены, что хотите удалить закладку ' + escapeHtml(lt) + '?

'; document.body.appendChild(ov); function close() { if (ov.parentNode) document.body.removeChild(ov); document.removeEventListener('keydown', escHandler); } ov.querySelector('#' + PREFIX + 'del-cancel').addEventListener('click', close); ov.querySelector('#' + PREFIX + 'del-confirm').addEventListener('click', function () { close(); _removeBookmark(bookmarkId); }); ov.addEventListener('click', function (e) { if (e.target === ov) close(); }); function escHandler(e) { if (e.key === 'Escape') close(); } document.addEventListener('keydown', escHandler); } // OPERATIONS (via DataLayer) async function _removeBookmark(bookmarkId) { try { await DataLayer.remove(bookmarkId); showToast('Закладка удалена', 'info'); } catch (e) { showToast('Ошибка удаления', 'error'); } _state.expandedRowId = null; render(); } async function _updateComment(bookmarkId, comment) { try { await DataLayer.updateComment(bookmarkId, comment); showToast('Комментарий сохранён', 'success'); } catch (e) { showToast('Ошибка сохранения', 'error'); } render(); } async function _savePositions() { var ordered = getBookmarks().map(function (b) { return b.id; }); await DataLayer.updatePositions(ordered); render(); } // SORTING function sortBookmarks(field) { _state.sortField = field; _state.expandedRowId = null; var bms = getBookmarks(); bms.sort(function (a, b) { var va, vb; switch (field) { case 'position': va=a.position; vb=b.position; break; case 'lessonTitle': va=(a.meta&&a.meta.lessonTitle||'').toLowerCase(); vb=(b.meta&&b.meta.lessonTitle||'').toLowerCase(); break; case 'trainingTitle': va=(a.meta&&(a.meta.trainingTitle||a.meta.moduleTitle||'')).toLowerCase(); vb=(b.meta&&(b.meta.trainingTitle||b.meta.moduleTitle||'')).toLowerCase(); break; case 'date': va=new Date(a.date).getTime(); vb=new Date(b.date).getTime(); break; case 'comment': va=(a.comment||'').toLowerCase(); vb=(b.comment||'').toLowerCase(); break; default: va=a.position; vb=b.position; } if (vavb) return _state.sortDirection==='asc'?1:-1; return 0; }); render(); } function setSortDirection(dir) { _state.sortDirection = dir; if (_state.sortField === null) _state.sortField = 'date'; sortBookmarks(_state.sortField); } function clearSort() { _state.sortField = null; _state.expandedRowId = null; getBookmarks().sort(function (a, b) { return a.position - b.position; }); render(); } function setViewMode(mode) { _state.viewMode = mode; _state.expandedRowId = null; try { localStorage.setItem(VIEW_MODE_KEY, mode); } catch (e) {} render(); } // SEARCH function getFilteredBookmarks() { var q = (_state.searchQuery || '').trim().toLowerCase(); var source = getBookmarks(); var filtered = !q ? source.slice() : source.filter(function (b) { var h = [(b.meta&&b.meta.lessonTitle)||'', (b.meta&&b.meta.moduleTitle)||'', (b.meta&&b.meta.trainingTitle)||'', b.comment||''].join(' ').toLowerCase(); return h.indexOf(q) !== -1; }); if (_state.sortField !== null) { var field = _state.sortField, dir = _state.sortDirection; filtered.sort(function (a, b) { var va, vb; switch (field) { case 'position': va=a.position; vb=b.position; break; case 'lessonTitle': va=(a.meta&&a.meta.lessonTitle||'').toLowerCase(); vb=(b.meta&&b.meta.lessonTitle||'').toLowerCase(); break; case 'trainingTitle': va=(a.meta&&(a.meta.trainingTitle||a.meta.moduleTitle||'')).toLowerCase(); vb=(b.meta&&(b.meta.trainingTitle||b.meta.moduleTitle||'')).toLowerCase(); break; case 'date': va=new Date(a.date).getTime(); vb=new Date(b.date).getTime(); break; case 'comment': va=(a.comment||'').toLowerCase(); vb=(b.comment||'').toLowerCase(); break; default: va=a.position; vb=b.position; } if (vavb) return dir==='asc'?1:-1; return 0; }); } return filtered; } function onSearchInput(value) { _state.searchQuery = value; _state.expandedRowId = null; if (_searchDebounceTimer) clearTimeout(_searchDebounceTimer); _searchDebounceTimer = setTimeout(function () { render(); }, 250); } // INLINE EDIT function startEditComment(bookmarkId) { var bm = DataLayer.find(bookmarkId); if (!bm) return; var card = document.querySelector('.' + PREFIX + 'card[data-bookmark-id="' + bookmarkId + '"]'); if (!card) return; var desc = card.querySelector('.' + PREFIX + 'card-desc'); if (!desc) return; var cc = bm.comment || ''; desc.innerHTML = '
'; var ta = desc.querySelector('textarea'); ta.focus(); desc.querySelector('.' + PREFIX + 'comment-editor-btn--cancel').addEventListener('click', function (e) { e.stopPropagation(); render(); }); desc.querySelector('.' + PREFIX + 'comment-editor-btn--save').addEventListener('click', function (e) { e.stopPropagation(); _updateComment(bookmarkId, ta.value.trim()); }); } function startEditTableComment(bookmarkId) { var td = document.querySelector('.' + PREFIX + 'table tr[data-bookmark-id="' + bookmarkId + '"] .' + PREFIX + 'table-col-comment'); if (!td) return; var bm = DataLayer.find(bookmarkId); if (!bm) return; var cc = bm.comment || ''; td.innerHTML = '
'; var ta = td.querySelector('textarea'); ta.focus(); td.querySelector('.' + PREFIX + 'comment-editor-btn--cancel').addEventListener('click', function (e) { e.stopPropagation(); render(); }); td.querySelector('.' + PREFIX + 'comment-editor-btn--save').addEventListener('click', function (e) { e.stopPropagation(); _updateComment(bookmarkId, ta.value.trim()); }); } // RENDER function render() { if (!_container) return; var wasInitial = !_state.initialRenderDone; _state.initialRenderDone = true; var searchFocused = false, sStart = 0, sEnd = 0; var ae = document.activeElement; if (ae && ae.classList.contains(PREFIX + 'search-input')) { searchFocused = true; sStart = ae.selectionStart || 0; sEnd = ae.selectionEnd || 0; } _container.innerHTML = ''; buildHeader(); var bms = getBookmarks(), total = bms.length; if (total === 0) { var emp = document.createElement('div'); emp.className = PREFIX + 'empty'; emp.innerHTML = '🔖

У вас пока нет закладок

Нажимайте на иконку закладки на страницах уроков, чтобы добавлять их сюда

'; _container.appendChild(emp); return; } var layout = document.createElement('div'); layout.className = PREFIX + 'layout'; var main = document.createElement('div'); main.className = PREFIX + 'main-content'; main.appendChild(buildToolbar()); var filtered = getFilteredBookmarks(); if (filtered.length === 0 && _state.searchQuery) { var nr = document.createElement('div'); nr.className = PREFIX + 'empty'; nr.innerHTML = '🔍

Ничего не найдено

Попробуйте изменить поисковый запрос

'; main.appendChild(nr); layout.appendChild(main); _container.appendChild(layout); restoreSearchFocus(searchFocused, sStart, sEnd); return; } var sl = document.createElement('div'); sl.className = PREFIX + 'section-label'; sl.innerHTML = 'Режим ' + (_state.viewMode === 'table' ? 'ТАБЛИЦА' : 'КАРТОЧКИ') + ' (' + (_state.viewMode === 'table' ? 'Table' : 'Grid') + ')'; main.appendChild(sl); var grid; if (_state.viewMode === 'table') { main.appendChild(buildTable(filtered)); } else { grid = document.createElement('div'); grid.className = PREFIX + 'grid'; filtered.forEach(function (b) { grid.appendChild(buildCard(b)); }); main.appendChild(grid); } layout.appendChild(main); _container.appendChild(layout); attachHandlers(); setupDragAndDrop(); restoreSearchFocus(searchFocused, sStart, sEnd); if (wasInitial && _state.viewMode !== 'table' && grid && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) { var cards = grid.querySelectorAll('.' + PREFIX + 'card:not(.' + PREFIX + 'card--drop-zone)'); cards.forEach(function (c, i) { c.classList.add(PREFIX + 'card--animate-in'); c.style.animationDelay = (i * 0.06) + 's'; }); } } function restoreSearchFocus(f, start, end) { if (!f) return; setTimeout(function () { var inp = _container.querySelector('.' + PREFIX + 'search-input'); if (inp) { inp.focus(); inp.setSelectionRange(start, end); } }, 0); } // HEADER function buildHeader() { var h = document.createElement('div'); h.className = PREFIX + 'header'; var left = document.createElement('div'); left.className = PREFIX + 'header-left'; var title = document.createElement('h1'); title.className = PREFIX + 'header-title'; title.textContent = 'Мои закладки'; left.appendChild(title); var count = document.createElement('span'); count.className = PREFIX + 'header-count'; count.textContent = String(getBookmarks().length); if (_state.prevCount !== getBookmarks().length) { count.classList.add(PREFIX + 'header-count--animate'); setTimeout(function () { count.classList.remove(PREFIX + 'header-count--animate'); }, 400); } _state.prevCount = getBookmarks().length; left.appendChild(count); h.appendChild(left); var sw = document.createElement('div'); sw.className = PREFIX + 'search'; var si = document.createElement('span'); si.className = PREFIX + 'search-icon'; si.innerHTML = svgSearch(); var inp = document.createElement('input'); inp.type = 'text'; inp.className = PREFIX + 'search-input'; inp.placeholder = 'Поиск по закладкам...'; inp.value = _state.searchQuery || ''; inp.addEventListener('input', function () { onSearchInput(inp.value); }); var cb = document.createElement('button'); cb.className = PREFIX + 'search-clear'; cb.type = 'button'; cb.title = 'Очистить'; cb.innerHTML = svgClose(); if (!_state.searchQuery) cb.style.display = 'none'; cb.addEventListener('click', function () { inp.value = ''; onSearchInput(''); }); inp.addEventListener('input', function () { cb.style.display = inp.value ? '' : 'none'; }); sw.appendChild(si); sw.appendChild(inp); sw.appendChild(cb); h.appendChild(sw); _container.appendChild(h); } // TOOLBAR function buildToolbar() { var bar = document.createElement('div'); bar.className = PREFIX + 'toolbar'; var left = document.createElement('div'); left.className = PREFIX + 'toolbar-left'; var vt = document.createElement('div'); vt.className = PREFIX + 'view-toggle'; var cb1 = document.createElement('button'); cb1.className = PREFIX + 'view-toggle-btn'; if (_state.viewMode === 'cards') cb1.classList.add(PREFIX + 'view-toggle-btn--active'); cb1.textContent = 'Карточки'; cb1.addEventListener('click', function () { setViewMode('cards'); }); var tb1 = document.createElement('button'); tb1.className = PREFIX + 'view-toggle-btn'; if (_state.viewMode === 'table') tb1.classList.add(PREFIX + 'view-toggle-btn--active'); tb1.textContent = 'Таблица'; tb1.addEventListener('click', function () { setViewMode('table'); }); vt.appendChild(cb1); vt.appendChild(tb1); left.appendChild(vt); var cl = document.createElement('span'); cl.className = PREFIX + 'toolbar-right-label'; var n = getBookmarks().length; cl.textContent = 'Найдено: ' + n + ' заклад'; var lo = String(n).slice(-1); if (n%100>=11&&n%100<=19) cl.textContent += 'ок'; else if (lo==='1') cl.textContent += 'ка'; else if (lo==='2'||lo==='3'||lo==='4') cl.textContent += 'ки'; else cl.textContent += 'ок'; left.appendChild(cl); var rb = document.createElement('button'); rb.className = PREFIX + 'refresh-btn'; rb.innerHTML = svgRefresh(); rb.title = 'Обновить'; rb.addEventListener('click', function () { DataLayer.getAll().then(function () { render(); }); }); left.appendChild(rb); bar.appendChild(left); var right = document.createElement('div'); right.className = PREFIX + 'toolbar-right'; var fb = document.createElement('button'); fb.className = PREFIX + 'filter-btn'; fb.innerHTML = svgFilter() + ' ' + (_state.sortField === null ? 'Ручная сортировка' : {date:'Дата добавления',lessonTitle:'Название урока',trainingTitle:'Тренинг / Модуль',comment:'Комментарий'}[_state.sortField]||'Дата добавления') + ' ' + svgChevronDown(); fb.addEventListener('click', function () { showSortPopup(); }); right.appendChild(fb); var ab = document.createElement('button'); ab.className = PREFIX + 'sort-dir-btn'; if (_state.sortDirection==='asc'&&_state.sortField!==null) ab.classList.add(PREFIX+'sort-dir-btn--active'); ab.innerHTML = svgArrowUp(); ab.title = 'По возрастанию'; ab.addEventListener('click', function () { if (_state.sortField===null)_state.sortField='date'; setSortDirection('asc'); }); right.appendChild(ab); var db = document.createElement('button'); db.className = PREFIX + 'sort-dir-btn'; if (_state.sortDirection==='desc'&&_state.sortField!==null) db.classList.add(PREFIX+'sort-dir-btn--active'); db.innerHTML = svgArrowDown(); db.title = 'По убыванию'; db.addEventListener('click', function () { if (_state.sortField===null)_state.sortField='date'; setSortDirection('desc'); }); right.appendChild(db); bar.appendChild(right); return bar; } // SORT POPUP function showSortPopup() { var ex = document.querySelector('.' + PREFIX + 'sort-overlay'); if (ex) ex.remove(); var fb = document.querySelector('.' + PREFIX + 'filter-btn'); if (!fb) return; var overlay = document.createElement('div'); overlay.className = PREFIX + 'sort-overlay'; var fields = [{field:'date',label:'Дата добавления',group:'Основные поля'},{field:'lessonTitle',label:'Название урока',group:'Основные поля'},{field:'trainingTitle',label:'Тренинг',group:'Основные поля'},{field:'comment',label:'Комментарий',group:'Основные поля'},{field:'position',label:'Порядок добавления',group:'Другие поля'}]; var groups = {}; fields.forEach(function(f){if(!groups[f.group])groups[f.group]=[];groups[f.group].push(f);}); var sh = ''; Object.keys(groups).forEach(function(gn){sh+='
'+gn+'
'; groups[gn].forEach(function(f){var ia=_state.sortField===f.field;sh+='';});sh+='
';}); overlay.innerHTML = '
Сортировка
'+sh+'
'; document.body.appendChild(overlay); var popup = overlay.querySelector('.' + PREFIX + 'sort-popup'); var br = fb.getBoundingClientRect(), gap = 4; var top = br.bottom + window.scrollY + gap, left = br.left + window.scrollX; if (left + 300 > window.innerWidth + window.scrollX) left = window.innerWidth + window.scrollX - 308; if (left < window.scrollX) left = window.scrollX + 8; popup.style.top = top + 'px'; popup.style.left = left + 'px'; function close() { if (overlay.parentNode) overlay.remove(); } overlay.addEventListener('click', close); function esc(e) { if (e.key === 'Escape') { close(); document.removeEventListener('keydown', esc); } } document.addEventListener('keydown', esc); overlay.querySelector('#' + PREFIX + 'sort-popup-close').addEventListener('click', close); overlay.querySelector('.' + PREFIX + 'sort-popup-reset').addEventListener('click', function () { close(); clearSort(); }); overlay.querySelectorAll('.' + PREFIX + 'sort-radio').forEach(function (l) { l.addEventListener('click', function () { var r = l.querySelector('input[type="radio"]'); close(); sortBookmarks(r.value); }); }); } // CARD function buildCard(bm) { var hc = bm.comment && bm.comment.trim().length > 0; var card = document.createElement('div'); card.className = PREFIX + 'card'; card.setAttribute('data-bookmark-id', bm.id); var ln = (bm.meta && bm.meta.lessonTitle) ? bm.meta.lessonTitle : 'Урок ' + bm.id; var mh = ''; if (bm.meta && bm.meta.moduleTitle) { var tl = bm.meta.trainingTitle ? 'Тренинг: ' + escapeHtml(bm.meta.trainingTitle) + '
' : ''; mh = '
' + tl + 'Модуль: ' + escapeHtml(bm.meta.moduleTitle) + '
'; } var cd = hc ? '
' + escapeHtml(bm.comment) + '
' : '
Нет комментария. Нажмите, чтобы добавить.
'; card.innerHTML = '' + svgDragHandle() + '' + mh + cd + ''; return card; } // TABLE function buildTable(filtered) { var w = document.createElement('div'); w.className = PREFIX + 'table-wrap'; var t = document.createElement('table'); t.className = PREFIX + 'table'; var as = _state.sortField; var h = document.createElement('thead'); h.innerHTML = 'Урок '+svgChevronDown()+'Тренинг / Модуль '+svgChevronDown()+'Дата добавления '+svgChevronDown()+'Комментарий '+svgChevronDown()+'Действия'; h.querySelectorAll('.'+PREFIX+'table-sort-icon').forEach(function(ic){ic.addEventListener('click',function(ev){ev.stopPropagation();var f=ic.getAttribute('data-sort');if(_state.sortField===f)_state.sortDirection=_state.sortDirection==='asc'?'desc':'asc';else{_state.sortField=f;_state.sortDirection='asc';}sortBookmarks(f);});});t.appendChild(h); var b = document.createElement('tbody'); filtered.forEach(function(bm){ var ie = _state.expandedRowId === bm.id, hc = bm.comment && bm.comment.trim().length > 0; var tr = document.createElement('tr'); tr.setAttribute('data-bookmark-id', bm.id); if (ie) tr.classList.add(PREFIX+'table-row--active'); var ln = (bm.meta&&bm.meta.lessonTitle)?bm.meta.lessonTitle:'Урок '+bm.id; var ch = hc?''+escapeHtml(bm.comment)+'':'Нет комментария'; var mh = ''; if (bm.meta&&bm.meta.moduleTitle) mh = escapeHtml(bm.meta.trainingTitle||'')+'
'+escapeHtml(bm.meta.moduleTitle); else mh = ''; var di = (_state.sortField===null)?'
'+svgDragHandle()+'
':'
'+svgDragHandle()+'
'; tr.innerHTML = ''+di+''+escapeHtml(ln)+''+mh+''+formatDateShort(bm.date)+'
'+formatDate(bm.date).split(' ')[1]+''+ch+'
'; tr.addEventListener('click',function(e){if(e.target.closest('button')||e.target.closest('a'))return;_state.expandedRowId=_state.expandedRowId===bm.id?null:bm.id;render();});tr.style.cursor='pointer';b.appendChild(tr); if(ie){var etr=document.createElement('tr');etr.className=PREFIX+'table-row--expanded';etr.style.cursor='pointer';etr.addEventListener('click',function(e){if(e.target.closest('button')||e.target.closest('a'))return;_state.expandedRowId=null;render();});var etd=document.createElement('td');etd.colSpan=6;etd.innerHTML='
'+svgInfo()+'
'+escapeHtml(ln)+'
'+escapeHtml((bm.meta&&bm.meta.trainingTitle)||'')+(bm.meta&&bm.meta.moduleTitle?' / '+escapeHtml(bm.meta.moduleTitle):'')+'
'+(hc?'
Комментарий:
'+escapeHtml(bm.comment)+'
':'
Нет комментария. Нажмите «Редактировать», чтобы добавить.
')+'
';etr.appendChild(etd);b.appendChild(etr);} }); t.appendChild(b); w.appendChild(t); return w; } // DRAG-AND-DROP function setupDragAndDrop() { if (_state.sortField !== null) return; var grid = document.querySelector('.' + PREFIX + 'grid'); if (!grid) return; var cards = grid.querySelectorAll('.' + PREFIX + 'card:not(.' + PREFIX + 'card--drop-zone)'); cards.forEach(function (card) { card.setAttribute('draggable', 'true'); card.addEventListener('dragstart', function (e) { _state.draggedIndex = Array.from(grid.querySelectorAll('.' + PREFIX + 'card:not(.' + PREFIX + 'card--drop-zone)')).indexOf(card); card.classList.add(PREFIX + 'card--dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', ''); }); card.addEventListener('dragend', function () { card.classList.remove(PREFIX + 'card--dragging'); grid.querySelectorAll('.' + PREFIX + 'card--drag-over').forEach(function (c) { c.classList.remove(PREFIX + 'card--drag-over'); }); _state.draggedIndex = null; }); card.addEventListener('dragover', function (e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if (_state.draggedIndex === null) return; var ci = Array.from(grid.querySelectorAll('.' + PREFIX + 'card:not(.' + PREFIX + 'card--drop-zone)')).indexOf(card); if (ci !== _state.draggedIndex) { grid.querySelectorAll('.' + PREFIX + 'card--drag-over').forEach(function (c) { if (c !== card) c.classList.remove(PREFIX + 'card--drag-over'); }); card.classList.add(PREFIX + 'card--drag-over'); } }); card.addEventListener('dragleave', function (e) { if (card.contains(e.relatedTarget)) return; card.classList.remove(PREFIX + 'card--drag-over'); }); card.addEventListener('drop', function (e) { e.preventDefault(); card.classList.remove(PREFIX + 'card--drag-over'); if (_state.draggedIndex === null) return; var tid = card.getAttribute('data-bookmark-id'); var bms = getBookmarks(); var ti = bms.findIndex(function (b) { return b.id === tid; }); if (_state.draggedIndex === ti) return; var dragged = bms.splice(_state.draggedIndex, 1)[0]; var at = _state.draggedIndex < ti ? ti - 1 : ti; bms.splice(at, 0, dragged); bms.forEach(function (b, i) { b.position = i; }); _state.draggedIndex = null; DataLayer.save(getBookmarks()).then(function () { render(); }); }); }); } // HANDLERS function attachHandlers() { _container.querySelectorAll('.' + PREFIX + 'card-actions [data-action="edit"]').forEach(function (btn) { btn.addEventListener('click', function (e) { e.stopPropagation(); startEditComment(btn.getAttribute('data-id')); }); }); _container.querySelectorAll('.' + PREFIX + 'card-actions [data-action="delete"]').forEach(function (btn) { btn.addEventListener('click', function (e) { e.stopPropagation(); showDeleteConfirm(btn.getAttribute('data-id')); }); }); _container.querySelectorAll('.' + PREFIX + 'card-desc').forEach(function (el) { el.addEventListener('click', function (e) { var card = el.closest('.' + PREFIX + 'card'); if (!card) return; startEditComment(card.getAttribute('data-bookmark-id')); }); }); _container.querySelectorAll('.' + PREFIX + 'table-actions [data-action="edit"]').forEach(function (btn) { btn.addEventListener('click', function (e) { e.stopPropagation(); startEditTableComment(btn.getAttribute('data-id')); }); }); _container.querySelectorAll('.' + PREFIX + 'table-actions [data-action="delete"]').forEach(function (btn) { btn.addEventListener('click', function (e) { e.stopPropagation(); showDeleteConfirm(btn.getAttribute('data-id')); }); }); _container.querySelectorAll('.' + PREFIX + 'table-comment').forEach(function (el) { el.addEventListener('click', function (e) { e.stopPropagation(); var tr = el.closest('tr[data-bookmark-id]'); if (!tr) return; startEditTableComment(tr.getAttribute('data-bookmark-id')); }); }); } // INIT function init() { _container = document.getElementById(CONTAINER_ID); if (!_container) { _container = document.createElement('div'); _container.id = CONTAINER_ID; document.body.appendChild(_container); } var cached = DataLayer.getCached(); if (cached && cached.length > 0) render(); DataLayer.getAll().then(function () { render(); }); document.addEventListener('visibilitychange', function () { if (!document.hidden) { DataLayer.getAll().then(function () { render(); }); } }); } return { init: init }; })(); // =================================================================== // ENTRY POINT // =================================================================== function init() { var path = window.location.pathname; glog('init: путь =', path, '| лимит закладок (max) =', MAX_BOOKMARKS, '| видимых в виджете =', MAX_VISIBLE_BOOKMARKS); if (path.indexOf('/_bookmarks') !== -1) { glog('init: это страница закладок'); BookmarksPage.init(); return; } BookmarkWidget.init(); Sidebar.init(); var isAdminLessonList = path.indexOf('/teach/control/stream/view') !== -1; var isUserLessonList = path.indexOf('/teach/control/lesson/view') !== -1 || path.indexOf('/pl/teach/control/lesson/view') !== -1; var hasLessonItems = document.querySelectorAll('li[data-lesson-id]').length > 0; var hasLessonLinks = document.querySelectorAll('a[href*="lesson/view"]').length > 0; if (isAdminLessonList || isUserLessonList || hasLessonItems || hasLessonLinks) { BookmarkIcon.init(); } } window.GetCourseBookmarks = { init: init, _config: Config, _eventBus: EventBus, _dataLayer: DataLayer, _uiFactory: UIFactory, _bookmarkIcon: BookmarkIcon }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();