PDF merge: drag-and-drop file sorting for merge order

- Sortable file list with drag handles in merge panel
- Visual order numbers, file info (pages, size)
- Drag to reorder files before merging
- mergeOrder state synced with file list changes
- Execute uses mergeOrder for merge API call
This commit is contained in:
treamz 2026-03-22 10:28:33 +03:00
parent b8b1339309
commit 119cfd29f5

View File

@ -203,6 +203,17 @@
.page-thumb-check { position:absolute; top:4px; right:4px; width:18px; height:18px; border-radius:50%; border:2px solid rgba(255,255,255,.5); background:rgba(0,0,0,.3); display:none; align-items:center; justify-content:center; }
.page-thumb.selected .page-thumb-check { display:flex; background:var(--accent); border-color:var(--accent); }
.page-thumb-check svg { width:12px; height:12px; }
/* Merge sortable list */
.merge-sort-list { display:flex; flex-direction:column; gap:6px; }
.merge-sort-item { display:flex; align-items:center; gap:10px; padding:10px 12px; background:var(--surface-700); border:1px solid var(--surface-600); border-radius:8px; cursor:grab; transition:all .15s; user-select:none; }
.merge-sort-item:active { cursor:grabbing; }
.merge-sort-item.dragging { opacity:0.4; }
.merge-sort-item.drag-over { border-color:var(--accent); background:var(--accent-bg); }
.merge-sort-item .merge-num { width:24px; height:24px; border-radius:6px; background:var(--accent); color:#fff; display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:700; font-family:'JetBrains Mono',monospace; flex-shrink:0; }
.merge-sort-item .merge-name { font-size:13px; font-weight:500; color:var(--text-primary); flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.merge-sort-item .merge-meta { font-size:11px; color:var(--text-muted); font-family:'JetBrains Mono',monospace; flex-shrink:0; }
.merge-sort-item .merge-grip { color:var(--text-muted); flex-shrink:0; }
.page-thumb.rotate-90 img { transform: rotate(90deg); }
.page-thumb.rotate-180 img { transform: rotate(180deg); }
.page-thumb.rotate-270 img { transform: rotate(270deg); }
@ -700,8 +711,9 @@
<div id="panel-merge" class="op-panel active" style="margin-bottom:24px;">
<div class="wa-card">
<div class="option-group">
<span class="option-label">Порядок файлов</span>
<div class="hint-note">Файлы будут объединены в порядке списка выше. Нужно минимум 2 файла.</div>
<span class="option-label">Перетащите файлы для изменения порядка объединения</span>
<div id="mergeFileList" class="merge-sort-list"></div>
<div class="hint-note" id="mergeHint">Нужно минимум 2 файла</div>
</div>
</div>
</div>
@ -1145,6 +1157,7 @@
});
renderSingleFileSelectors();
renderMergeList();
}
// Render file selector for single-file operations
@ -1520,7 +1533,7 @@
switch (op) {
case 'merge':
url = '/pdf/merge';
body = JSON.stringify({ fileIds: state.files.map(f => f.id) });
body = JSON.stringify({ fileIds: state.mergeOrder || state.files.map(f => f.id) });
break;
case 'split':
url = '/pdf/split';
@ -1952,6 +1965,62 @@
}, 300);
}
// ─── Merge sortable file list ─────────────────────────────────────────────
function renderMergeList() {
const list = document.getElementById('mergeFileList');
const hint = document.getElementById('mergeHint');
if (!list) return;
list.innerHTML = '';
if (state.files.length < 2) {
if (hint) hint.textContent = 'Нужно минимум 2 файла';
return;
}
if (hint) hint.textContent = state.files.length + ' файлов — перетащите для изменения порядка';
// Use mergeOrder or default
if (!state.mergeOrder || state.mergeOrder.length !== state.files.length) {
state.mergeOrder = state.files.map(f => f.id);
}
state.mergeOrder.forEach((fileId, idx) => {
const f = state.files.find(x => x.id === fileId);
if (!f) return;
const item = document.createElement('div');
item.className = 'merge-sort-item';
item.dataset.id = f.id;
item.draggable = true;
item.innerHTML = '<div class="merge-grip"><svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/></svg></div>'
+ '<div class="merge-num">' + (idx + 1) + '</div>'
+ '<div class="merge-name">' + escHtml(f.name) + '</div>'
+ '<div class="merge-meta">' + f.pages + ' стр. · ' + formatSize(f.size) + '</div>';
item.addEventListener('dragstart', e => {
e.dataTransfer.setData('text/plain', f.id);
item.classList.add('dragging');
});
item.addEventListener('dragend', () => item.classList.remove('dragging'));
item.addEventListener('dragover', e => { e.preventDefault(); item.classList.add('drag-over'); });
item.addEventListener('dragleave', () => item.classList.remove('drag-over'));
item.addEventListener('drop', e => {
e.preventDefault();
item.classList.remove('drag-over');
const fromId = e.dataTransfer.getData('text/plain');
if (fromId === f.id) return;
// Reorder mergeOrder array
const fromIdx = state.mergeOrder.indexOf(fromId);
const toIdx = state.mergeOrder.indexOf(f.id);
if (fromIdx < 0 || toIdx < 0) return;
state.mergeOrder.splice(fromIdx, 1);
state.mergeOrder.splice(toIdx, 0, fromId);
renderMergeList();
});
list.appendChild(item);
});
}
})();
</script>