554 lines
26 KiB
HTML
554 lines
26 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}Файлы — Morozovka Portal{% endblock %}
|
||
{% block main_class %}container{% endblock %}
|
||
|
||
{% block content %}
|
||
|
||
{# ═══════════════════════════════════════════════════════════════════ #}
|
||
{# ROOT: пространства + файлы без пространства #}
|
||
{# ═══════════════════════════════════════════════════════════════════ #}
|
||
{% if view == 'root' %}
|
||
|
||
<div class="files-hero">
|
||
<div class="files-hero-left">
|
||
<h1>📁 Файлохранилище</h1>
|
||
<p class="subtitle">Пространства, папки, файлы с версиями и правами доступа</p>
|
||
</div>
|
||
<div class="files-hero-actions">
|
||
{% if can_create_space %}
|
||
<a href="/files/space/new" class="btn btn-primary">+ Пространство</a>
|
||
{% endif %}
|
||
<a href="/files/search" class="btn btn-default">🔍 Поиск</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="files-section">
|
||
<div class="files-section-head">
|
||
<h2>🗂 Пространства <span class="count">{{ spaces|length }}</span></h2>
|
||
</div>
|
||
{% if spaces %}
|
||
<div class="space-grid-v2">
|
||
{% for item in spaces %}
|
||
{% set sp = item.space %}
|
||
<a href="/files/space/{{ sp.id }}" class="space-card-v2">
|
||
<div class="space-card-v2-head">
|
||
<div class="space-card-v2-icon">{% if sp.project_id %}📦{% else %}📂{% endif %}</div>
|
||
<span class="space-card-v2-vis {{ sp.visibility }}">
|
||
{% if sp.visibility == 'public' %}Публичное
|
||
{% elif sp.visibility == 'internal' %}Внутреннее
|
||
{% else %}Приватное{% endif %}
|
||
</span>
|
||
</div>
|
||
<div class="space-card-v2-title">{{ sp.name }}</div>
|
||
{% if sp.description %}
|
||
<div class="space-card-v2-desc">{{ sp.description }}</div>
|
||
{% else %}
|
||
<div class="space-card-v2-desc" style="font-style:italic;opacity:.5;">Без описания</div>
|
||
{% endif %}
|
||
<div class="space-card-v2-meta">
|
||
<span>📄 {{ item.file_count }} файлов</span>
|
||
{% if sp.project_id %}<span>🔗 Проект</span>{% endif %}
|
||
</div>
|
||
</a>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
<div class="files-empty">
|
||
<div class="icon">🗂</div>
|
||
<h3>Пространств пока нет</h3>
|
||
<p>{% if can_create_space %}Создайте первое — например, «Документы команды» или «Материалы проекта».{% else %}Попросите lead/admin создать пространство.{% endif %}</p>
|
||
{% if can_create_space %}<a href="/files/space/new" class="btn btn-primary">Создать пространство</a>{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="files-section">
|
||
<div class="files-section-head">
|
||
<h2>📎 Без пространства <span class="count">{{ loose_files|length }}</span></h2>
|
||
</div>
|
||
<p style="color:var(--text-muted);font-size:13px;margin:0 0 14px;">
|
||
Старые файлы и вложения постов. Можно переместить в пространство кнопкой ↔.
|
||
</p>
|
||
|
||
<div class="upload-zone"
|
||
id="upload-zone-loose"
|
||
data-space-id=""
|
||
data-folder-id=""
|
||
role="button"
|
||
tabindex="0"
|
||
aria-label="Загрузить файлы без пространства">
|
||
<input type="file" id="upload-input-loose" multiple>
|
||
<div class="upload-zone-icon">☁️</div>
|
||
<div class="upload-zone-title">Перетащите файлы сюда или кликните</div>
|
||
<div class="upload-zone-hint">Можно выбрать несколько файлов · суммарно до 100 МБ</div>
|
||
</div>
|
||
|
||
{% if loose_files %}
|
||
<div class="file-grid-v2">
|
||
{% for f, uploader in loose_files %}
|
||
<div class="file-tile-v2">
|
||
<div class="file-tile-v2-preview">
|
||
{% if f.original_name|is_image %}
|
||
<a href="/files/preview/{{ f.id }}" target="_blank">
|
||
<img src="/files/preview/{{ f.id }}" alt="{{ f.original_name }}" loading="lazy">
|
||
</a>
|
||
{% else %}
|
||
<div class="file-tile-v2-icon-big">{{ f.original_name|file_icon }}</div>
|
||
{% endif %}
|
||
</div>
|
||
<div class="file-tile-v2-body">
|
||
<div class="file-tile-v2-name" title="{{ f.original_name }}">{{ f.original_name }}</div>
|
||
<div class="file-tile-v2-meta">
|
||
<span>{{ f.size_bytes|format_size }}</span>
|
||
<span>·</span>
|
||
<span>{{ uploader }}</span>
|
||
<span>·</span>
|
||
<span>{{ f.uploaded_at|msk_strftime('%d.%m.%Y') }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="file-tile-v2-actions">
|
||
<a href="/files/download/{{ f.id }}" title="Скачать">⬇</a>
|
||
<a href="/files/{{ f.id }}/move" title="Переместить">↔</a>
|
||
{% if session.get('role') == 'admin' or f.uploaded_by == session.get('user_id') %}
|
||
<form action="/files/{{ f.id }}/delete" method="POST" style="display:inline;" onsubmit="return confirm('Удалить файл?');">
|
||
<input type="hidden" name="next" value="/files">
|
||
<button type="submit" class="danger" title="Удалить">✕</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
<div class="files-empty">
|
||
<div class="icon">📎</div>
|
||
<h3>Файлов без пространства нет</h3>
|
||
<p>Перетащите файл в зону выше или создайте пространство.</p>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{% endif %}
|
||
|
||
{# ═══════════════════════════════════════════════════════════════════ #}
|
||
{# SPACE / FOLDER #}
|
||
{# ═══════════════════════════════════════════════════════════════════ #}
|
||
{% if view in ('space', 'folder') %}
|
||
|
||
<div class="breadcrumbs-bar">
|
||
{% for cr in breadcrumbs %}
|
||
{% if not loop.last %}
|
||
<a href="{{ cr.url }}">{{ cr.name }}</a>
|
||
<span class="sep">/</span>
|
||
{% else %}
|
||
<span class="current">{{ cr.name }}</span>
|
||
{% endif %}
|
||
{% endfor %}
|
||
</div>
|
||
|
||
<div class="files-hero">
|
||
<div class="files-hero-left">
|
||
<h1>{% if view == 'space' %}📂 {{ space.name }}{% else %}📁 {{ folder.name }}{% endif %}</h1>
|
||
<p class="subtitle">
|
||
{% if view == 'space' and space.description %}{{ space.description }}
|
||
{% elif view == 'space' %}Пространство · {{ space.visibility }}
|
||
{% else %}Папка в «{{ space.name }}»{% endif %}
|
||
</p>
|
||
</div>
|
||
<div class="files-hero-actions">
|
||
{% if perms.can_upload %}
|
||
<button type="button" class="btn btn-default" onclick="document.getElementById('new-folder-modal').classList.add('open')">+ Папка</button>
|
||
{% endif %}
|
||
{% if view == 'space' and perms.can_manage %}
|
||
<a href="/files/space/{{ space.id }}/acl" class="btn btn-default">🔐 Права</a>
|
||
<a href="/files/space/{{ space.id }}/edit" class="btn btn-default">⚙ Настройки</a>
|
||
{% endif %}
|
||
{% if view == 'folder' and perms.can_manage %}
|
||
<a href="/files/folder/{{ folder.id }}/acl" class="btn btn-default">🔐 Права папки</a>
|
||
{% endif %}
|
||
<a href="/files/search{% if view == 'space' %}?space_id={{ space.id }}{% endif %}" class="btn btn-default">🔍 Поиск</a>
|
||
{% if view == 'space' and (perms.is_owner or session.get('role') == 'admin') %}
|
||
<form action="/files/space/{{ space.id }}/delete" method="POST" style="display:inline;"
|
||
onsubmit="return confirm('Удалить пространство «{{ space.name }}» со всем содержимым? Это необратимо.');">
|
||
<button type="submit" class="btn btn-danger">🗑 Удалить</button>
|
||
</form>
|
||
{% endif %}
|
||
{% if view == 'folder' and perms.can_delete %}
|
||
<form action="/files/folder/{{ folder.id }}/delete" method="POST" style="display:inline;"
|
||
onsubmit="return confirm('Удалить папку «{{ folder.name }}» со всем содержимым?');">
|
||
<button type="submit" class="btn btn-danger">🗑 Удалить</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
{% if perms.can_upload %}
|
||
<div class="upload-zone"
|
||
id="upload-zone-current"
|
||
data-space-id="{{ space.id }}"
|
||
data-folder-id="{% if view == 'folder' %}{{ folder.id }}{% endif %}"
|
||
role="button"
|
||
tabindex="0"
|
||
aria-label="Загрузить файлы в {% if view == 'folder' %}{{ folder.name }}{% else %}{{ space.name }}{% endif %}">
|
||
<input type="file" id="upload-input-current" multiple>
|
||
<div class="upload-zone-icon">☁️</div>
|
||
<div class="upload-zone-title">Перетащите файлы сюда или кликните</div>
|
||
<div class="upload-zone-hint">
|
||
Загрузится в {% if view == 'folder' %}«{{ folder.name }}»{% else %}корень пространства{% endif %} · суммарно до 100 МБ
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if folders %}
|
||
<div class="files-section">
|
||
<div class="files-section-head">
|
||
<h2>📁 Папки <span class="count">{{ folders|length }}</span></h2>
|
||
</div>
|
||
<div class="folder-grid-v2">
|
||
{% for fd in folders %}
|
||
<div class="folder-tile-v2-wrap">
|
||
<a href="/files/folder/{{ fd.id }}" class="folder-tile-v2">
|
||
<span class="folder-tile-v2-icon">📁</span>
|
||
<div class="folder-tile-v2-info">
|
||
<div class="folder-tile-v2-name">{{ fd.name }}</div>
|
||
<div class="folder-tile-v2-meta">{{ fd.created_at|msk_strftime('%d.%m.%Y') }}</div>
|
||
</div>
|
||
</a>
|
||
{% if perms.can_delete or perms.can_manage %}
|
||
<form action="/files/folder/{{ fd.id }}/delete" method="POST" class="folder-delete-form"
|
||
onsubmit="return confirm('Удалить папку «{{ fd.name }}» со всем содержимым?');">
|
||
<button type="submit" class="folder-delete-btn" title="Удалить папку">✕</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="files-section">
|
||
<div class="files-section-head">
|
||
<h2>📄 Файлы <span class="count">{{ files|length }}</span></h2>
|
||
</div>
|
||
{% if files %}
|
||
<div class="file-grid-v2">
|
||
{% for f, uploader in files %}
|
||
<div class="file-tile-v2">
|
||
<div class="file-tile-v2-preview">
|
||
{% if f.original_name|is_image %}
|
||
<a href="/files/preview/{{ f.id }}" target="_blank">
|
||
<img src="/files/preview/{{ f.id }}" alt="{{ f.original_name }}" loading="lazy">
|
||
</a>
|
||
{% else %}
|
||
<div class="file-tile-v2-icon-big">{{ f.original_name|file_icon }}</div>
|
||
{% endif %}
|
||
<span class="file-tile-v2-badge">v{{ f.version }}</span>
|
||
</div>
|
||
<div class="file-tile-v2-body">
|
||
<div class="file-tile-v2-name" title="{{ f.original_name }}">{{ f.original_name }}</div>
|
||
{% if f.description %}
|
||
<div class="file-tile-v2-desc">{{ f.description }}</div>
|
||
{% endif %}
|
||
<div class="file-tile-v2-meta">
|
||
<span>{{ f.size_bytes|format_size }}</span>
|
||
<span>·</span>
|
||
<span>{{ uploader }}</span>
|
||
<span>·</span>
|
||
<span>{{ f.uploaded_at|msk_strftime('%d.%m.%Y') }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="file-tile-v2-actions">
|
||
<a href="/files/download/{{ f.id }}" title="Скачать">⬇</a>
|
||
<a href="/files/{{ f.id }}/history" title="История версий">🕘</a>
|
||
<a href="/files/{{ f.id }}/move" title="Переместить">↔</a>
|
||
<a href="/files/{{ f.id }}/edit" title="Редактировать">✎</a>
|
||
{% if perms.can_delete or session.get('role') == 'admin' %}
|
||
<form action="/files/{{ f.id }}/delete" method="POST" style="display:inline;" onsubmit="return confirm('Удалить файл и все версии?');">
|
||
<input type="hidden" name="next" value="{% if view == 'folder' %}/files/folder/{{ folder.id }}{% else %}/files/space/{{ space.id }}{% endif %}">
|
||
<button type="submit" class="danger" title="Удалить">✕</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
<div class="files-empty">
|
||
<div class="icon">📄</div>
|
||
<h3>{% if folders %}В этой папке файлов нет{% else %}Пусто{% endif %}</h3>
|
||
<p>{% if perms.can_upload %}Перетащите файл в зону выше или кликните по ней.{% else %}Файлы появятся, когда их загрузят.{% endif %}</p>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{% if perms.can_upload %}
|
||
<div class="modal-overlay" id="new-folder-modal">
|
||
<div class="modal">
|
||
<h2>📁 Новая папка</h2>
|
||
<form action="/files/folder/new" method="POST">
|
||
<input type="hidden" name="space_id" value="{{ space.id }}">
|
||
<input type="hidden" name="parent_id" value="{% if view == 'folder' %}{{ folder.id }}{% endif %}">
|
||
<div class="form-group">
|
||
<label>Название *</label>
|
||
<input type="text" name="name" required maxlength="200" autofocus>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Описание</label>
|
||
<input type="text" name="description" maxlength="300">
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn btn-secondary" onclick="document.getElementById('new-folder-modal').classList.remove('open')">Отмена</button>
|
||
<button type="submit" class="btn btn-primary">Создать</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
<style>
|
||
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.7); z-index: 1000; display: none; align-items: center; justify-content: center; padding: 20px; }
|
||
.modal-overlay.open { display: flex; }
|
||
.modal { background: var(--bg-elev); border: 1px solid var(--border); border-radius: 10px; width: 100%; max-width: 450px; padding: 24px; }
|
||
.modal h2 { margin: 0 0 18px; color: var(--accent); font-size: 18px; }
|
||
.modal .form-group { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
|
||
.modal label { color: var(--text-muted); font-size: 13px; font-weight: 600; }
|
||
.modal input { padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; color: var(--text); font-family: inherit; }
|
||
.modal input:focus { outline: none; border-color: var(--accent); }
|
||
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
|
||
</style>
|
||
{% endif %}
|
||
|
||
{% endif %}
|
||
|
||
{# ═══════════════════════════════════════════════════════════════════ #}
|
||
{# SEARCH #}
|
||
{# ═══════════════════════════════════════════════════════════════════ #}
|
||
{% if view == 'search' %}
|
||
|
||
<div class="files-hero">
|
||
<div class="files-hero-left">
|
||
<h1>🔍 Поиск файлов</h1>
|
||
<p class="subtitle">{% if space_scope %}в пространстве «{{ space_scope.name }}»{% else %}по всем пространствам{% endif %}</p>
|
||
</div>
|
||
<div class="files-hero-actions">
|
||
<a href="/files" class="btn btn-default">← Все файлы</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="filters" style="margin-bottom:20px;">
|
||
<form method="GET" action="/files/search" style="display:flex;gap:8px;flex:1;flex-wrap:wrap;">
|
||
{% if space_scope %}<input type="hidden" name="space_id" value="{{ space_scope.id }}">{% endif %}
|
||
<input type="text" name="q" value="{{ q }}" placeholder="Имя, описание или теги..." class="search-input" style="flex:1;min-width:220px;" autofocus>
|
||
<button type="submit" class="btn btn-primary">Найти</button>
|
||
{% if q %}<a href="/files/search{% if space_scope %}?space_id={{ space_scope.id }}{% endif %}" class="filter-chip filter-chip-clear">✕ Сбросить</a>{% endif %}
|
||
</form>
|
||
</div>
|
||
|
||
{% if q %}
|
||
<div class="muted" style="font-size:13px;margin-bottom:14px;">Найдено: <strong style="color:var(--text);">{{ results|length }}</strong></div>
|
||
{% endif %}
|
||
|
||
{% if results %}
|
||
<div class="file-grid-v2">
|
||
{% for f, uploader in results %}
|
||
<div class="file-tile-v2">
|
||
<div class="file-tile-v2-preview">
|
||
{% if f.original_name|is_image %}
|
||
<a href="/files/preview/{{ f.id }}" target="_blank">
|
||
<img src="/files/preview/{{ f.id }}" alt="{{ f.original_name }}" loading="lazy">
|
||
</a>
|
||
{% else %}
|
||
<div class="file-tile-v2-icon-big">{{ f.original_name|file_icon }}</div>
|
||
{% endif %}
|
||
{% if f.version and f.version > 1 %}<span class="file-tile-v2-badge">v{{ f.version }}</span>{% endif %}
|
||
</div>
|
||
<div class="file-tile-v2-body">
|
||
<div class="file-tile-v2-name" title="{{ f.original_name }}">{{ f.original_name }}</div>
|
||
{% if f.tags %}
|
||
<div class="file-tile-v2-desc" style="color:#58a6ff;">#{{ f.tags }}</div>
|
||
{% elif f.description %}
|
||
<div class="file-tile-v2-desc">{{ f.description }}</div>
|
||
{% endif %}
|
||
<div class="file-tile-v2-meta">
|
||
<span>{{ f.size_bytes|format_size }}</span>
|
||
<span>·</span>
|
||
<span>{{ uploader }}</span>
|
||
<span>·</span>
|
||
<span>{{ f.uploaded_at|msk_strftime('%d.%m.%Y') }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="file-tile-v2-actions">
|
||
<a href="/files/download/{{ f.id }}" title="Скачать">⬇</a>
|
||
{% if f.space_id %}
|
||
<a href="/files/{{ f.id }}/history" title="История">🕘</a>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% elif q %}
|
||
<div class="files-empty">
|
||
<div class="icon">🔍</div>
|
||
<h3>Ничего не найдено</h3>
|
||
<p>Попробуйте другой запрос или <a href="/files">вернитесь к списку</a>.</p>
|
||
</div>
|
||
{% else %}
|
||
<div class="files-empty">
|
||
<div class="icon">🔍</div>
|
||
<h3>Введите запрос</h3>
|
||
<p>Поиск по имени файла, описанию и тегам.</p>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% endif %}
|
||
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
(function() {
|
||
'use strict';
|
||
const MAX_TOTAL_BYTES = 100 * 1024 * 1024; // 100 МБ суммарно
|
||
|
||
function formatBytes(n) {
|
||
if (n < 1024) return n + ' Б';
|
||
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' КБ';
|
||
if (n < 1024 * 1024 * 1024) return (n / 1024 / 1024).toFixed(1) + ' МБ';
|
||
return (n / 1024 / 1024 / 1024).toFixed(2) + ' ГБ';
|
||
}
|
||
|
||
function uploadFiles(files, spaceId, folderId, zone) {
|
||
const arr = Array.from(files || []);
|
||
if (arr.length === 0) return;
|
||
|
||
// ── Проверка суммарного размера ──────────────────────────────
|
||
const totalBytes = arr.reduce(function(s, f) { return s + (f.size || 0); }, 0);
|
||
if (totalBytes > MAX_TOTAL_BYTES) {
|
||
alert(
|
||
'Суммарный размер выбранных файлов (' + formatBytes(totalBytes) + ') ' +
|
||
'превышает лимит 100 МБ.\n\n' +
|
||
'Выбрано файлов: ' + arr.length + '\n' +
|
||
'Уберите часть файлов или загрузите их по очереди.'
|
||
);
|
||
return;
|
||
}
|
||
|
||
// ── UI: блокируем зону и показываем прогресс ─────────────────
|
||
const titleEl = zone.querySelector('.upload-zone-title');
|
||
const hintEl = zone.querySelector('.upload-zone-hint');
|
||
const originalTitle = titleEl ? titleEl.textContent : '';
|
||
const originalHint = hintEl ? hintEl.textContent : '';
|
||
zone.classList.add('uploading');
|
||
zone.style.pointerEvents = 'none';
|
||
|
||
let done = 0;
|
||
let failed = 0;
|
||
|
||
function updateProgress() {
|
||
if (titleEl) {
|
||
titleEl.textContent = 'Загрузка ' + done + ' из ' + arr.length + '…';
|
||
}
|
||
if (hintEl) {
|
||
hintEl.textContent = 'Суммарно: ' + formatBytes(totalBytes);
|
||
}
|
||
}
|
||
updateProgress();
|
||
|
||
// ── Загружаем по одному (каждый POST ≤ 100 МБ гарантирован) ──
|
||
arr.forEach(function(file) {
|
||
const fd = new FormData();
|
||
fd.append('file', file);
|
||
if (spaceId) fd.append('space_id', spaceId);
|
||
if (folderId) fd.append('folder_id', folderId);
|
||
|
||
fetch('/files/upload', {
|
||
method: 'POST',
|
||
body: fd,
|
||
credentials: 'same-origin'
|
||
}).then(function(r) {
|
||
if (!r.ok) failed++;
|
||
}).catch(function() {
|
||
failed++;
|
||
}).finally(function() {
|
||
done++;
|
||
updateProgress();
|
||
if (done === arr.length) {
|
||
zone.classList.remove('uploading');
|
||
zone.style.pointerEvents = '';
|
||
if (failed > 0) {
|
||
alert(
|
||
'Загружено: ' + (arr.length - failed) + ' из ' + arr.length + '.\n' +
|
||
'Не удалось загрузить: ' + failed + '.'
|
||
);
|
||
}
|
||
if (titleEl) titleEl.textContent = originalTitle;
|
||
if (hintEl) hintEl.textContent = originalHint;
|
||
if (failed === 0) {
|
||
location.reload();
|
||
} else {
|
||
// даём возможность увидеть ошибку
|
||
setTimeout(function() { location.reload(); }, 1500);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// ── Обработчик для каждой upload-зоны ─────────────────────────────
|
||
document.querySelectorAll('.upload-zone').forEach(function(zone) {
|
||
const input = zone.querySelector('input[type="file"]');
|
||
if (!input) return;
|
||
const spaceId = zone.dataset.spaceId || '';
|
||
const folderId = zone.dataset.folderId || '';
|
||
|
||
// Клик по зоне → открыть диалог
|
||
zone.addEventListener('click', function(e) {
|
||
if (zone.classList.contains('uploading')) return;
|
||
// если клик пришёлся на input (что маловероятно — display:none) — не мешаем
|
||
if (e.target === input) return;
|
||
input.click();
|
||
});
|
||
|
||
// Клавиатура: Enter / Space
|
||
zone.addEventListener('keydown', function(e) {
|
||
if (zone.classList.contains('uploading')) return;
|
||
if (e.key === 'Enter' || e.key === ' ') {
|
||
e.preventDefault();
|
||
input.click();
|
||
}
|
||
});
|
||
|
||
// Выбор через диалог
|
||
input.addEventListener('change', function() {
|
||
if (input.files && input.files.length > 0) {
|
||
uploadFiles(input.files, spaceId, folderId, zone);
|
||
input.value = '';
|
||
}
|
||
});
|
||
|
||
// Drag & drop
|
||
['dragenter', 'dragover'].forEach(function(evt) {
|
||
zone.addEventListener(evt, function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
if (zone.classList.contains('uploading')) return;
|
||
zone.classList.add('dragover');
|
||
});
|
||
});
|
||
['dragleave', 'drop'].forEach(function(evt) {
|
||
zone.addEventListener(evt, function(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
zone.classList.remove('dragover');
|
||
});
|
||
});
|
||
zone.addEventListener('drop', function(e) {
|
||
if (zone.classList.contains('uploading')) return;
|
||
const files = e.dataTransfer && e.dataTransfer.files;
|
||
if (files && files.length > 0) {
|
||
uploadFiles(files, spaceId, folderId, zone);
|
||
}
|
||
});
|
||
});
|
||
})();
|
||
</script>
|
||
{% endblock %}
|