PDF pages: visual rotate/delete/reorder with action buttons
- Delete: click pages (red highlight), counter, Удалить выбранные (N) button - Rotate: click pages (visual rotation preview 90/180/270), Повернуть button - Reorder: drag-and-drop thumbnails, visual number update, Применить button - CSS: rotation transforms, delete/drag states, action bar with buttons - Direct execute functions (bypass main execute flow)
This commit is contained in:
parent
6bf2a0b6a1
commit
816753c925
161
public/pdf.html
161
public/pdf.html
@ -203,6 +203,21 @@
|
||||
.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; }
|
||||
.page-thumb.rotate-90 img { transform: rotate(90deg); }
|
||||
.page-thumb.rotate-180 img { transform: rotate(180deg); }
|
||||
.page-thumb.rotate-270 img { transform: rotate(270deg); }
|
||||
.page-thumb.dragging { opacity: 0.4; border-color: var(--accent); }
|
||||
.page-thumb.drag-over { border-color: var(--accent); transform: scale(1.05); }
|
||||
.page-thumb.marked-delete { border-color: #ef4444; opacity: 0.6; }
|
||||
.page-thumb.marked-delete .page-thumb-check { display: flex; background: #ef4444; border-color: #ef4444; }
|
||||
.page-thumb.marked-delete .page-thumb-num { background: rgba(239,68,68,.8); }
|
||||
.grid-action-bar { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
|
||||
.grid-action-btn { padding: 8px 16px; border-radius: 8px; font-size: 13px; font-weight: 600; border: none; cursor: pointer; font-family: 'Manrope', sans-serif; transition: all .15s; }
|
||||
.grid-action-btn.danger { background: rgba(239,68,68,.1); color: #ef4444; border: 1px solid rgba(239,68,68,.3); }
|
||||
.grid-action-btn.danger:hover { background: rgba(239,68,68,.2); }
|
||||
.grid-action-btn.primary { background: var(--accent); color: #fff; }
|
||||
.grid-action-btn.primary:hover { background: var(--accent-dim); }
|
||||
.grid-action-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.page-grid-hint { font-size:11px; color:var(--text-muted); margin-top:6px; font-family:'JetBrains Mono',monospace; }
|
||||
|
||||
.file-card-remove:hover {
|
||||
@ -727,27 +742,36 @@
|
||||
<button class="angle-btn" data-angle="180">180°</button>
|
||||
<button class="angle-btn" data-angle="270">270° (−90°)</button>
|
||||
</div>
|
||||
<span class="option-label" style="margin-top:8px;">Страницы (нажмите для выбора)</span>
|
||||
<span class="option-label" style="margin-top:8px;">Нажмите на страницы для поворота</span>
|
||||
<div class="page-grid" id="rotatePageGrid"></div>
|
||||
<div class="page-grid-hint" id="rotateHint">Загрузите файл для просмотра страниц</div>
|
||||
<div class="grid-action-bar">
|
||||
<button class="grid-action-btn primary" id="btnRotateSelected" disabled onclick="executeRotatePages()">Повернуть выбранные (<span id="rotateCount">0</span>)</button>
|
||||
<span class="page-grid-hint" id="rotateHint"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete sub-panel -->
|
||||
<div id="subpanel-delete" class="sub-panel wa-card">
|
||||
<div class="option-group">
|
||||
<span class="option-label">Выберите страницы для удаления</span>
|
||||
<span class="option-label">Нажмите на страницы для удаления</span>
|
||||
<div class="page-grid" id="deletePageGrid"></div>
|
||||
<div class="page-grid-hint" id="deleteHint">Загрузите файл для просмотра страниц</div>
|
||||
<div class="grid-action-bar">
|
||||
<button class="grid-action-btn danger" id="btnDeleteSelected" disabled onclick="executeDeletePages()">Удалить выбранные (<span id="deleteCount">0</span>)</button>
|
||||
<span class="page-grid-hint" id="deleteHint"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reorder sub-panel -->
|
||||
<div id="subpanel-reorder" class="sub-panel wa-card">
|
||||
<div class="option-group">
|
||||
<span class="option-label">Перетаскивайте страницы для изменения порядка</span>
|
||||
<span class="option-label">Перетаскивайте страницы мышкой для нового порядка</span>
|
||||
<div class="page-grid" id="reorderPageGrid"></div>
|
||||
<div class="page-grid-hint" id="reorderHint">Загрузите файл для просмотра страниц</div>
|
||||
<div class="grid-action-bar">
|
||||
<button class="grid-action-btn primary" id="btnReorderApply" onclick="executeReorderPages()">Применить порядок</button>
|
||||
<span class="page-grid-hint" id="reorderHint"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1676,27 +1700,88 @@
|
||||
if (!grid) return;
|
||||
if (!fileId || pages < 1) {
|
||||
grid.innerHTML = '';
|
||||
if (hint) hint.textContent = 'Выберите файл';
|
||||
if (hint) hint.textContent = '';
|
||||
return;
|
||||
}
|
||||
if (hint) hint.textContent = mode === 'delete' ? 'Нажмите на страницы для выбора' : mode === 'rotate' ? 'Нажмите для выбора страниц' : 'Перетаскивайте для изменения порядка';
|
||||
if (hint) hint.textContent = '';
|
||||
|
||||
grid.innerHTML = '';
|
||||
for (let i = 1; i <= pages; i++) {
|
||||
for (let i = 1; i <= Math.min(pages, 100); i++) {
|
||||
const thumb = document.createElement('div');
|
||||
thumb.className = 'page-thumb';
|
||||
thumb.dataset.page = i;
|
||||
thumb.innerHTML = `<img src="/pdf/preview/${fileId}/${i}" alt="Page ${i}" loading="lazy"><div class="page-thumb-num">${i}</div><div class="page-thumb-check"><svg fill="none" stroke="#fff" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg></div>`;
|
||||
thumb.innerHTML = '<img src="/pdf/preview/' + fileId + '/' + i + '" alt="' + i + '" loading="lazy"><div class="page-thumb-num">' + i + '</div><div class="page-thumb-check"><svg fill="none" stroke="#fff" viewBox="0 0 24 24" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg></div>';
|
||||
|
||||
if (mode === 'delete' || mode === 'rotate') {
|
||||
if (mode === 'delete') {
|
||||
thumb.addEventListener('click', () => {
|
||||
thumb.classList.toggle('selected');
|
||||
thumb.classList.toggle('marked-delete');
|
||||
updateDeleteCount();
|
||||
});
|
||||
} else if (mode === 'rotate') {
|
||||
thumb.addEventListener('click', () => {
|
||||
thumb.classList.toggle('selected');
|
||||
// Visual rotation preview
|
||||
if (thumb.classList.contains('selected')) {
|
||||
const angle = state.rotateAngle || 90;
|
||||
thumb.classList.add('rotate-' + angle);
|
||||
} else {
|
||||
thumb.classList.remove('rotate-90', 'rotate-180', 'rotate-270');
|
||||
}
|
||||
updateRotateCount();
|
||||
});
|
||||
} else if (mode === 'reorder') {
|
||||
// Drag and drop
|
||||
thumb.draggable = true;
|
||||
thumb.addEventListener('dragstart', e => {
|
||||
e.dataTransfer.setData('text/plain', i);
|
||||
thumb.classList.add('dragging');
|
||||
});
|
||||
thumb.addEventListener('dragend', () => thumb.classList.remove('dragging'));
|
||||
thumb.addEventListener('dragover', e => { e.preventDefault(); thumb.classList.add('drag-over'); });
|
||||
thumb.addEventListener('dragleave', () => thumb.classList.remove('drag-over'));
|
||||
thumb.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
thumb.classList.remove('drag-over');
|
||||
const fromPage = e.dataTransfer.getData('text/plain');
|
||||
const fromEl = grid.querySelector('[data-page="' + fromPage + '"]');
|
||||
if (fromEl && fromEl !== thumb) {
|
||||
// Swap positions in DOM
|
||||
const parent = thumb.parentNode;
|
||||
const thumbNext = thumb.nextSibling;
|
||||
if (fromEl.nextSibling === thumb) {
|
||||
parent.insertBefore(thumb, fromEl);
|
||||
} else {
|
||||
parent.insertBefore(fromEl, thumb);
|
||||
parent.insertBefore(thumb, thumbNext);
|
||||
}
|
||||
// Update visual numbers
|
||||
Array.from(grid.children).forEach((el, idx) => {
|
||||
el.querySelector('.page-thumb-num').textContent = idx + 1;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
grid.appendChild(thumb);
|
||||
}
|
||||
}
|
||||
|
||||
function updateDeleteCount() {
|
||||
const count = getSelectedPages('deletePageGrid').length;
|
||||
const el = document.getElementById('deleteCount');
|
||||
const btn = document.getElementById('btnDeleteSelected');
|
||||
if (el) el.textContent = count;
|
||||
if (btn) btn.disabled = count === 0;
|
||||
}
|
||||
|
||||
function updateRotateCount() {
|
||||
const count = getSelectedPages('rotatePageGrid').length;
|
||||
const el = document.getElementById('rotateCount');
|
||||
const btn = document.getElementById('btnRotateSelected');
|
||||
if (el) el.textContent = count;
|
||||
if (btn) btn.disabled = count === 0;
|
||||
}
|
||||
|
||||
function getSelectedPages(gridId) {
|
||||
const grid = document.getElementById(gridId);
|
||||
if (!grid) return [];
|
||||
@ -1705,6 +1790,60 @@
|
||||
|
||||
|
||||
|
||||
|
||||
// Direct execute for page operations (from grid action buttons)
|
||||
window.executeDeletePages = async function() {
|
||||
const pages = getSelectedPages('deletePageGrid');
|
||||
if (!pages.length) { showError('Выберите страницы'); return; }
|
||||
const fileId = state.selectedFileId;
|
||||
if (!fileId) { showError('Выберите файл'); return; }
|
||||
clearError();
|
||||
try {
|
||||
const res = await fetch('/pdf/delete', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ fileId, pages: pages.join(',') }) });
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
showResult(data);
|
||||
} catch(e) { showError(e.message); }
|
||||
};
|
||||
|
||||
window.executeRotatePages = async function() {
|
||||
const pages = getSelectedPages('rotatePageGrid');
|
||||
const fileId = state.selectedFileId;
|
||||
if (!fileId) { showError('Выберите файл'); return; }
|
||||
clearError();
|
||||
try {
|
||||
const res = await fetch('/pdf/rotate', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ fileId, pages: pages.length ? pages.join(',') : 'all', angle: state.rotateAngle || 90 }) });
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
showResult(data);
|
||||
} catch(e) { showError(e.message); }
|
||||
};
|
||||
|
||||
window.executeReorderPages = async function() {
|
||||
const grid = document.getElementById('reorderPageGrid');
|
||||
if (!grid) return;
|
||||
const order = Array.from(grid.querySelectorAll('.page-thumb')).map(t => parseInt(t.dataset.page));
|
||||
const fileId = state.selectedFileId;
|
||||
if (!fileId) { showError('Выберите файл'); return; }
|
||||
clearError();
|
||||
try {
|
||||
const res = await fetch('/pdf/reorder', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ fileId, order }) });
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
showResult(data);
|
||||
} catch(e) { showError(e.message); }
|
||||
};
|
||||
|
||||
function showResult(data) {
|
||||
if (data.downloadUrl) {
|
||||
document.getElementById('downloadLink').href = data.downloadUrl;
|
||||
document.getElementById('downloadLink').style.display = '';
|
||||
if (data.size) document.getElementById('resultSize').textContent = formatSize(data.size);
|
||||
stepConfig.style.display = 'none';
|
||||
stepResult.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
</script>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user