mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-08 16:57:14 +01:00
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:
1 parent
4edb1718cc
commit
49ab2ef17f
12 files changed
+281
-11
No files matched your search
@@ -0,0 +1,102 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { createMock, optionMock } = vi.hoisted(() => ({
|
||||
createMock: vi.fn(),
|
||||
optionMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('sortablejs', () => ({
|
||||
default: { create: createMock },
|
||||
}));
|
||||
|
||||
import {
|
||||
createDragHandle,
|
||||
enableFileListReorder,
|
||||
moveItem,
|
||||
} from '@/js/utils/sortable-file-list';
|
||||
|
||||
type OnEnd = (evt: { oldIndex?: number; newIndex?: number }) => void;
|
||||
|
||||
function lastOnEnd(): OnEnd {
|
||||
return createMock.mock.calls[createMock.mock.calls.length - 1][1].onEnd;
|
||||
}
|
||||
|
||||
describe('moveItem', () => {
|
||||
it('should move an item forward', () => {
|
||||
expect(moveItem(['a', 'b', 'c', 'd'], 0, 2)).toEqual(['b', 'c', 'a', 'd']);
|
||||
});
|
||||
|
||||
it('should move an item backward', () => {
|
||||
expect(moveItem(['a', 'b', 'c', 'd'], 3, 1)).toEqual(['a', 'd', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('should not mutate the input array', () => {
|
||||
const items = ['a', 'b', 'c'];
|
||||
moveItem(items, 0, 2);
|
||||
expect(items).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('should return an unchanged copy for out-of-range indices', () => {
|
||||
expect(moveItem(['a', 'b'], -1, 1)).toEqual(['a', 'b']);
|
||||
expect(moveItem(['a', 'b'], 0, 5)).toEqual(['a', 'b']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createDragHandle', () => {
|
||||
it('should create an element Sortable can use as a handle', () => {
|
||||
const handle = createDragHandle();
|
||||
expect(handle.classList.contains('drag-handle')).toBe(true);
|
||||
expect(handle.getAttribute('aria-label')).toBe('Drag to reorder');
|
||||
expect(
|
||||
handle.querySelector('[data-lucide="grip-vertical"]')
|
||||
).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('enableFileListReorder', () => {
|
||||
beforeEach(() => {
|
||||
createMock.mockReset();
|
||||
optionMock.mockReset();
|
||||
createMock.mockImplementation(() => ({ option: optionMock }));
|
||||
});
|
||||
|
||||
it('should create Sortable restricted to the drag handle', () => {
|
||||
const container = document.createElement('div');
|
||||
enableFileListReorder(container, vi.fn());
|
||||
|
||||
expect(createMock).toHaveBeenCalledTimes(1);
|
||||
expect(createMock.mock.calls[0][0]).toBe(container);
|
||||
expect(createMock.mock.calls[0][1].handle).toBe('.drag-handle');
|
||||
});
|
||||
|
||||
it('should report moves to the callback', () => {
|
||||
const onReorder = vi.fn();
|
||||
enableFileListReorder(document.createElement('div'), onReorder);
|
||||
|
||||
lastOnEnd()({ oldIndex: 2, newIndex: 0 });
|
||||
expect(onReorder).toHaveBeenCalledWith(2, 0);
|
||||
});
|
||||
|
||||
it('should ignore drops that do not change position', () => {
|
||||
const onReorder = vi.fn();
|
||||
enableFileListReorder(document.createElement('div'), onReorder);
|
||||
|
||||
lastOnEnd()({ oldIndex: 1, newIndex: 1 });
|
||||
lastOnEnd()({ oldIndex: undefined, newIndex: 1 });
|
||||
expect(onReorder).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reuse the Sortable instance and swap the callback on re-render', () => {
|
||||
const container = document.createElement('div');
|
||||
enableFileListReorder(container, vi.fn());
|
||||
const second = vi.fn();
|
||||
enableFileListReorder(container, second);
|
||||
|
||||
expect(createMock).toHaveBeenCalledTimes(1);
|
||||
expect(optionMock).toHaveBeenCalledWith('onEnd', expect.any(Function));
|
||||
|
||||
const swapped: OnEnd = optionMock.mock.calls[0][1];
|
||||
swapped({ oldIndex: 0, newIndex: 1 });
|
||||
expect(second).toHaveBeenCalledWith(0, 1);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user