feat(my-materials): папки в виде рейла слева + drag-and-drop перемещение карточек
This commit is contained in:
+99
-32
@@ -17,16 +17,29 @@
|
||||
.mm-toolbar { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-bottom: 14px; }
|
||||
.mm-search { flex: 1; min-width: 180px; padding: 8px 12px; border: 1px solid var(--border); border-radius: 9px; font: inherit; background: var(--surface); color: var(--text); }
|
||||
.mm-kind { padding: 8px 10px; border: 1px solid var(--border); border-radius: 9px; font: inherit; background: var(--surface); color: var(--text); }
|
||||
.mm-cols { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 18px; }
|
||||
.mm-chip { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border: 1px solid var(--border); border-radius: 99px; background: var(--surface); cursor: pointer; font-size: .8rem; font-weight: 600; color: var(--text-2); }
|
||||
.mm-chip:hover { border-color: var(--violet); color: var(--violet); }
|
||||
.mm-chip.active { background: var(--violet); border-color: var(--violet); color: #fff; }
|
||||
.mm-chip-count { font-size: .7rem; opacity: .7; }
|
||||
.mm-chip-edit { display: inline-flex; opacity: .65; margin-left: 2px; }
|
||||
.mm-chip-edit svg { width: 12px; height: 12px; }
|
||||
.mm-chip-edit:hover { opacity: 1; }
|
||||
.mm-chip-add { border-style: dashed; }
|
||||
/* Двухколоночная раскладка: рейл папок слева + контент справа */
|
||||
.mm-body { display: flex; gap: 20px; align-items: flex-start; }
|
||||
.mm-rail { width: 212px; flex-shrink: 0; position: sticky; top: 14px; display: flex; flex-direction: column; gap: 8px; }
|
||||
.mm-rail-title { font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--text-3); padding: 0 10px; }
|
||||
.mm-rail-list { display: flex; flex-direction: column; gap: 3px; }
|
||||
.mm-rail-item { display: flex; align-items: center; gap: 9px; padding: 8px 10px; border-radius: 9px; cursor: pointer; font-size: .84rem; font-weight: 600; color: var(--text-2); border: 1px solid transparent; transition: background .12s, color .12s, border-color .12s; }
|
||||
.mm-rail-item:hover { background: rgba(155,93,229,0.07); color: var(--violet); }
|
||||
.mm-rail-item.active { background: var(--violet); color: #fff; }
|
||||
.mm-rail-item.drop-hover { border-color: var(--violet); border-style: dashed; background: rgba(155,93,229,0.14); color: var(--violet); }
|
||||
.mm-rail-item svg { width: 15px; height: 15px; flex-shrink: 0; }
|
||||
.mm-rail-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.mm-rail-count { font-size: .72rem; opacity: .65; }
|
||||
.mm-rail-item.active .mm-rail-count { opacity: .85; }
|
||||
.mm-rail-edit { display: inline-flex; opacity: 0; transition: opacity .12s; }
|
||||
.mm-rail-item:hover .mm-rail-edit { opacity: .55; }
|
||||
.mm-rail-edit:hover { opacity: 1 !important; }
|
||||
.mm-rail-edit svg { width: 13px; height: 13px; }
|
||||
.mm-rail-add { display: flex; align-items: center; justify-content: center; gap: 6px; padding: 8px 10px; border: 1px dashed var(--border); border-radius: 9px; background: transparent; cursor: pointer; font-size: .8rem; font-weight: 600; color: var(--text-2); transition: border-color .12s, color .12s; }
|
||||
.mm-rail-add:hover { border-color: var(--violet); color: var(--violet); }
|
||||
.mm-rail-add svg { width: 14px; height: 14px; }
|
||||
.mm-content { flex: 1; min-width: 0; }
|
||||
.mm-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; }
|
||||
.mm-card.mm-dragging { opacity: .45; }
|
||||
.mm-card { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; overflow: hidden; display: flex; flex-direction: column; position: relative; transition: border-color .14s, box-shadow .14s, transform .14s; }
|
||||
.mm-card:hover { border-color: rgba(155,93,229,0.3); box-shadow: 0 8px 24px rgba(15,23,42,0.08); transform: translateY(-2px); }
|
||||
.mm-card-media { background: #f1f5f9; aspect-ratio: 16/10; display: flex; align-items: center; justify-content: center; overflow: hidden; }
|
||||
@@ -57,6 +70,15 @@
|
||||
.mm-viewer-note { white-space: pre-wrap; word-break: break-word; line-height: 1.6; font-size: 0.9rem; color: var(--text-2); }
|
||||
.mm-empty { padding: 60px 20px; text-align: center; color: var(--text-3); }
|
||||
.mm-empty svg { width: 38px; height: 38px; opacity: 0.4; margin-bottom: 12px; }
|
||||
@media (max-width: 768px) {
|
||||
.mm-body { flex-direction: column; }
|
||||
.mm-rail { width: auto; position: static; flex-direction: row; overflow-x: auto; gap: 6px; padding-bottom: 4px; }
|
||||
.mm-rail-title { display: none; }
|
||||
.mm-rail-list { flex-direction: row; gap: 6px; }
|
||||
.mm-rail-item { flex: 0 0 auto; }
|
||||
.mm-rail-label { flex: 0 1 auto; max-width: 120px; }
|
||||
.mm-rail-add { flex: 0 0 auto; white-space: nowrap; }
|
||||
}
|
||||
@media (max-width: 640px) { .mm-grid { grid-template-columns: 1fr 1fr; } .mm-main { padding: 18px 14px; } }
|
||||
</style>
|
||||
</head>
|
||||
@@ -71,18 +93,26 @@
|
||||
<button class="mm-btn" onclick="openDrawModal({})"><i data-lucide="pen-tool"></i> Рисунок</button>
|
||||
</div>
|
||||
<div class="mm-sub">Сохранённые с уроков: страницы доски, заметки и вложения. Хранятся у вас и не пропадают, даже если урок удалят.</div>
|
||||
<div class="mm-toolbar">
|
||||
<input class="mm-search" id="mm-search" placeholder="Поиск по материалам…" oninput="onSearch(this.value)" />
|
||||
<select class="mm-kind" id="mm-kind" onchange="onKind(this.value)">
|
||||
<option value="all">Все типы</option>
|
||||
<option value="board">Доска</option>
|
||||
<option value="image">Изображения</option>
|
||||
<option value="note">Заметки</option>
|
||||
<option value="link">Ссылки</option>
|
||||
</select>
|
||||
<div class="mm-body">
|
||||
<aside class="mm-rail">
|
||||
<div class="mm-rail-title">Папки</div>
|
||||
<div class="mm-rail-list" id="mm-cols"></div>
|
||||
<button class="mm-rail-add" onclick="createCollection()"><i data-lucide="folder-plus"></i> Папка</button>
|
||||
</aside>
|
||||
<div class="mm-content">
|
||||
<div class="mm-toolbar">
|
||||
<input class="mm-search" id="mm-search" placeholder="Поиск по материалам…" oninput="onSearch(this.value)" />
|
||||
<select class="mm-kind" id="mm-kind" onchange="onKind(this.value)">
|
||||
<option value="all">Все типы</option>
|
||||
<option value="board">Доска</option>
|
||||
<option value="image">Изображения</option>
|
||||
<option value="note">Заметки</option>
|
||||
<option value="link">Ссылки</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mm-grid" id="mm-grid"><div class="mm-empty">Загрузка…</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mm-cols" id="mm-cols"></div>
|
||||
<div class="mm-grid" id="mm-grid"><div class="mm-empty">Загрузка…</div></div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -166,8 +196,8 @@
|
||||
const mv = moveSelect(m);
|
||||
|
||||
if (m.kind === 'board' || m.kind === 'image') {
|
||||
return `<div class="mm-card">
|
||||
<a class="mm-card-media" href="${esc(m.url)}" onclick="openViewer(${m.id});return false;"><img src="${esc(m.url)}" alt="" loading="lazy"/></a>
|
||||
return `<div class="mm-card" draggable="true" ondragstart="mmDragStart(event,${m.id})" ondragend="mmDragEnd(event)">
|
||||
<a class="mm-card-media" href="${esc(m.url)}" onclick="openViewer(${m.id});return false;"><img src="${esc(m.url)}" alt="" loading="lazy" draggable="false"/></a>
|
||||
<div class="mm-card-body">
|
||||
${chip}
|
||||
<div class="mm-card-title">${esc(m.title || kind)}</div>
|
||||
@@ -183,7 +213,7 @@
|
||||
}
|
||||
|
||||
if (m.kind === 'link') {
|
||||
return `<div class="mm-card">
|
||||
return `<div class="mm-card" draggable="true" ondragstart="mmDragStart(event,${m.id})" ondragend="mmDragEnd(event)">
|
||||
<a class="mm-card-link" href="${esc(m.url)}" target="_blank" rel="noopener" title="${esc(m.url)}">
|
||||
<span class="mm-card-link-ic"><i data-lucide="link"></i></span>
|
||||
<span class="mm-card-link-meta">
|
||||
@@ -205,7 +235,7 @@
|
||||
}
|
||||
|
||||
// note
|
||||
return `<div class="mm-card">
|
||||
return `<div class="mm-card" draggable="true" ondragstart="mmDragStart(event,${m.id})" ondragend="mmDragEnd(event)">
|
||||
<div class="mm-card-note">${mathHtml(m.body || '')}</div>
|
||||
<div class="mm-card-body">
|
||||
${chip}
|
||||
@@ -216,22 +246,59 @@
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* ── Collections bar ── */
|
||||
function chip(key, label, count, editId) {
|
||||
/* ── Folder rail (вертикальный список папок слева) ── */
|
||||
function railItem(key, label, count, editId, droppable) {
|
||||
const active = _filter.col === key ? ' active' : '';
|
||||
const ed = editId ? `<span class="mm-chip-edit" onclick="event.stopPropagation();editCollection(${editId})" title="Изменить папку">${PENCIL}</span>` : '';
|
||||
return `<button class="mm-chip${active}" onclick="setCol('${key}')">${esc(label)} <span class="mm-chip-count">${count}</span>${ed}</button>`;
|
||||
const ed = editId
|
||||
? `<span class="mm-rail-edit" onclick="event.stopPropagation();editCollection(${editId})" title="Изменить папку">${PENCIL}</span>`
|
||||
: '';
|
||||
const ic = key === 'all' ? 'inbox' : (key === 'none' ? 'folder-minus' : 'folder');
|
||||
const drop = droppable
|
||||
? ` ondragover="mmDragOver(event,this)" ondragleave="mmDragLeave(this)" ondrop="mmDrop(event,'${key}')"`
|
||||
: '';
|
||||
return `<div class="mm-rail-item${active}" onclick="setCol('${key}')"${drop}>
|
||||
<i data-lucide="${ic}"></i>
|
||||
<span class="mm-rail-label">${esc(label)}</span>
|
||||
<span class="mm-rail-count">${count}</span>${ed}
|
||||
</div>`;
|
||||
}
|
||||
function renderCols() {
|
||||
const bar = document.getElementById('mm-cols');
|
||||
const noneCount = _mats.filter(m => !m.collection_id).length;
|
||||
let html = chip('all', 'Все', _mats.length);
|
||||
_cols.forEach(c => { html += chip(String(c.id), c.name, c.count, c.id); });
|
||||
if (noneCount) html += chip('none', 'Без папки', noneCount);
|
||||
html += `<button class="mm-chip mm-chip-add" onclick="createCollection()">+ папка</button>`;
|
||||
let html = railItem('all', 'Все', _mats.length, null, false);
|
||||
_cols.forEach(c => { html += railItem(String(c.id), c.name, c.count, c.id, true); });
|
||||
html += railItem('none', 'Без папки', noneCount, null, true);
|
||||
bar.innerHTML = html;
|
||||
}
|
||||
|
||||
/* ── Drag-and-drop: перетащить карточку на папку, чтобы переместить ── */
|
||||
let _dragId = null;
|
||||
function mmDragStart(e, id) {
|
||||
_dragId = id;
|
||||
try { e.dataTransfer.setData('text/plain', String(id)); e.dataTransfer.effectAllowed = 'move'; } catch (_) {}
|
||||
if (e.currentTarget) e.currentTarget.classList.add('mm-dragging');
|
||||
}
|
||||
function mmDragEnd(e) {
|
||||
_dragId = null;
|
||||
if (e.currentTarget) e.currentTarget.classList.remove('mm-dragging');
|
||||
document.querySelectorAll('.mm-rail-item.drop-hover').forEach(el => el.classList.remove('drop-hover'));
|
||||
}
|
||||
function mmDragOver(e, el) { e.preventDefault(); try { e.dataTransfer.dropEffect = 'move'; } catch (_) {} el.classList.add('drop-hover'); }
|
||||
function mmDragLeave(el) { el.classList.remove('drop-hover'); }
|
||||
function mmDrop(e, key) {
|
||||
e.preventDefault();
|
||||
if (e.currentTarget) e.currentTarget.classList.remove('drop-hover');
|
||||
let id = _dragId;
|
||||
if (id == null && e.dataTransfer) { const d = e.dataTransfer.getData('text/plain'); id = d ? Number(d) : null; }
|
||||
if (id == null) return;
|
||||
const cid = (key === 'none' || key === 'all') ? null : key;
|
||||
const mt = _mats.find(x => x.id === id);
|
||||
if (mt && String(mt.collection_id || '') === String(cid || '')) return; // уже в этой папке
|
||||
moveMaterial(id, cid);
|
||||
}
|
||||
window.mmDragStart = mmDragStart; window.mmDragEnd = mmDragEnd;
|
||||
window.mmDragOver = mmDragOver; window.mmDragLeave = mmDragLeave; window.mmDrop = mmDrop;
|
||||
|
||||
function filtered() {
|
||||
return _mats.filter(m => {
|
||||
if (_filter.col === 'none' && m.collection_id) return false;
|
||||
|
||||
Reference in New Issue
Block a user