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:
parent
b8b1339309
commit
119cfd29f5
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user