feat: drag-and-drop reordering for multi-file to PDF conversions

Tools that combine several files into a single PDF used the upload
order as the page order, with no way to change it short of clearing
and re-adding files. Each file row now has a drag handle, and the
new order is used when the PDF is built.

Applies to the JPG, PNG, WebP, BMP, HEIC, SVG, TIFF, Image and TXT
to PDF tools. The shared logic lives in utils/sortable-file-list.ts
(built on the existing sortablejs dependency) instead of being
duplicated per page.
This commit is contained in:
Atharva Bedekar committed 2026-10-25 15:34:16 +05:30
1 parent 4edb1718cc
commit 49ab2ef17f
12 files changed
+281 -11

No files matched your search

+75
View File
@@ -0,0 +1,75 @@
import Sortable from 'sortablejs';
import { trText } from '../i18n/i18n.js';
type ReorderHandler = (oldIndex: number, newIndex: number) => void;
const sortables = new WeakMap<HTMLElement, Sortable>();
export function moveItem<T>(
items: readonly T[],
oldIndex: number,
newIndex: number
): T[] {
const result = [...items];
if (
oldIndex === newIndex ||
oldIndex < 0 ||
oldIndex >= result.length ||
newIndex < 0 ||
newIndex >= result.length
) {
return result;
}
const [moved] = result.splice(oldIndex, 1);
result.splice(newIndex, 0, moved);
return result;
}
export function createDragHandle(): HTMLElement {
const handle = document.createElement('div');
handle.className =
'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors flex-shrink-0';
const label = trText('Drag to reorder');
handle.title = label;
handle.setAttribute('aria-label', label);
handle.innerHTML = '<i data-lucide="grip-vertical" class="w-4 h-4"></i>';
return handle;
}
// Rows must contain an element created by createDragHandle(). The list is
// re-rendered by the caller, so one Sortable per container is kept and only
// its reorder callback is swapped on subsequent calls.
export function enableFileListReorder(
container: HTMLElement,
onReorder: ReorderHandler
): void {
const onEnd = (evt: Sortable.SortableEvent) => {
const { oldIndex, newIndex } = evt;
if (
oldIndex === undefined ||
newIndex === undefined ||
oldIndex === newIndex
) {
return;
}
onReorder(oldIndex, newIndex);
};
const existing = sortables.get(container);
if (existing) {
existing.option('onEnd', onEnd);
return;
}
sortables.set(
container,
Sortable.create(container, {
handle: '.drag-handle',
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
onEnd,
})
);
}