diff --git a/src/js/logic/bmp-to-pdf-page.ts b/src/js/logic/bmp-to-pdf-page.ts index 97ccd46fa..b46fe5df7 100644 --- a/src/js/logic/bmp-to-pdf-page.ts +++ b/src/js/logic/bmp-to-pdf-page.ts @@ -1,6 +1,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; import { createIcons, icons } from 'lucide'; +import Sortable from 'sortablejs'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; let files: File[] = []; @@ -54,7 +55,8 @@ const updateUI = () => { files.forEach((file, index) => { const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm draggable-file'; + fileDiv.setAttribute('data-index', index.toString()); const infoContainer = document.createElement('div'); infoContainer.className = 'flex items-center gap-2 overflow-hidden'; @@ -69,24 +71,55 @@ const updateUI = () => { infoContainer.append(nameSpan, sizeSpan); + const rightGroup = document.createElement('div'); + rightGroup.className = 'flex items-center gap-1 ml-4'; + + const dragHandle = document.createElement('div'); + dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors'; + dragHandle.innerHTML = ``; + const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.className = 'text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.innerHTML = ''; removeBtn.onclick = () => { files = files.filter((_, i) => i !== index); updateUI(); }; - fileDiv.append(infoContainer, removeBtn); + rightGroup.append(dragHandle, removeBtn); + fileDiv.append(infoContainer, rightGroup); fileDisplayArea.appendChild(fileDiv); }); createIcons({ icons }); + initializeFileListSortable(); } else { fileControls.classList.add('hidden'); processBtn.classList.add('hidden'); } }; +function initializeFileListSortable() { + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea) return; + if ((fileDisplayArea as any)._sortableInstance) { + (fileDisplayArea as any)._sortableInstance.destroy(); + } + (fileDisplayArea as any)._sortableInstance = Sortable.create(fileDisplayArea, { + handle: '.drag-handle', + animation: 150, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + onEnd: function (evt: any) { + if (evt.oldIndex !== evt.newIndex) { + const moved = files.splice(evt.oldIndex, 1)[0]; + files.splice(evt.newIndex, 0, moved); + updateUI(); + } + }, + }); +} + const resetState = () => { files = []; updateUI(); @@ -201,4 +234,7 @@ document.addEventListener('DOMContentLoaded', () => { if (processBtn) { processBtn.addEventListener('click', convert); } + setTimeout(() => { + initializeFileListSortable(); + }, 0); }); diff --git a/src/js/logic/csv-to-pdf-page.ts b/src/js/logic/csv-to-pdf-page.ts index e0ec5560c..5e747f77d 100644 --- a/src/js/logic/csv-to-pdf-page.ts +++ b/src/js/logic/csv-to-pdf-page.ts @@ -5,6 +5,7 @@ import { formatBytes, } from '../utils/helpers.js'; import { state } from '../state.js'; +import Sortable from 'sortablejs'; import { createIcons, icons } from 'lucide'; document.addEventListener('DOMContentLoaded', () => { @@ -33,10 +34,12 @@ document.addEventListener('DOMContentLoaded', () => { if (fileDisplayArea) { fileDisplayArea.innerHTML = ''; + for (let index = 0; index < state.files.length; index++) { const file = state.files[index]; const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm draggable-file'; + fileDiv.setAttribute('data-index', index.toString()); const infoContainer = document.createElement('div'); infoContainer.className = 'flex flex-col overflow-hidden'; @@ -51,19 +54,28 @@ document.addEventListener('DOMContentLoaded', () => { infoContainer.append(nameSpan, metaSpan); + const rightGroup = document.createElement('div'); + rightGroup.className = 'flex items-center gap-1 ml-4'; + + const dragHandle = document.createElement('div'); + dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors'; + dragHandle.innerHTML = ``; + const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.className = 'text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.innerHTML = ''; removeBtn.onclick = () => { state.files = state.files.filter((_, i) => i !== index); updateUI(); }; - fileDiv.append(infoContainer, removeBtn); + rightGroup.append(dragHandle, removeBtn); + fileDiv.append(infoContainer, rightGroup); fileDisplayArea.appendChild(fileDiv); } createIcons({ icons }); + initializeFileListSortable(); } if (fileControls) fileControls.classList.remove('hidden'); convertOptions.classList.remove('hidden'); @@ -228,4 +240,26 @@ document.addEventListener('DOMContentLoaded', () => { } updateUI(); + + function initializeFileListSortable() { + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea) return; + if ((fileDisplayArea as any)._sortableInstance) { + (fileDisplayArea as any)._sortableInstance.destroy(); + } + (fileDisplayArea as any)._sortableInstance = Sortable.create(fileDisplayArea, { + handle: '.drag-handle', + animation: 150, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + onEnd: function (evt: any) { + if (evt.oldIndex !== evt.newIndex) { + const moved = state.files.splice(evt.oldIndex, 1)[0]; + state.files.splice(evt.newIndex, 0, moved); + updateUI(); + } + }, + }); + } }); diff --git a/src/js/logic/excel-to-pdf-page.ts b/src/js/logic/excel-to-pdf-page.ts index 8d4dbb7d7..78b7ab803 100644 --- a/src/js/logic/excel-to-pdf-page.ts +++ b/src/js/logic/excel-to-pdf-page.ts @@ -7,6 +7,7 @@ import { import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js'; +import Sortable from 'sortablejs'; document.addEventListener('DOMContentLoaded', () => { state.files = []; @@ -37,7 +38,8 @@ document.addEventListener('DOMContentLoaded', () => { for (let index = 0; index < state.files.length; index++) { const file = state.files[index]; const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm draggable-file'; + fileDiv.setAttribute('data-index', index.toString()); const infoContainer = document.createElement('div'); infoContainer.className = 'flex flex-col overflow-hidden'; @@ -52,15 +54,23 @@ document.addEventListener('DOMContentLoaded', () => { infoContainer.append(nameSpan, metaSpan); + const rightGroup = document.createElement('div'); + rightGroup.className = 'flex items-center gap-1 ml-4'; + + const dragHandle = document.createElement('div'); + dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors'; + dragHandle.innerHTML = ``; + const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.className = 'text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.innerHTML = ''; removeBtn.onclick = () => { state.files = state.files.filter((_, i) => i !== index); updateUI(); }; - fileDiv.append(infoContainer, removeBtn); + rightGroup.append(dragHandle, removeBtn); + fileDiv.append(infoContainer, rightGroup); fileDisplayArea.appendChild(fileDiv); } @@ -213,4 +223,29 @@ document.addEventListener('DOMContentLoaded', () => { if (processBtn) { processBtn.addEventListener('click', convertToPdf); } + setTimeout(() => { + initializeFileListSortable(); + }, 0); + + function initializeFileListSortable() { + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea) return; + if ((fileDisplayArea as any)._sortableInstance) { + (fileDisplayArea as any)._sortableInstance.destroy(); + } + (fileDisplayArea as any)._sortableInstance = Sortable.create(fileDisplayArea, { + handle: '.drag-handle', + animation: 150, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + onEnd: function (evt: any) { + if (evt.oldIndex !== evt.newIndex) { + const moved = state.files.splice(evt.oldIndex, 1)[0]; + state.files.splice(evt.newIndex, 0, moved); + updateUI(); + } + }, + }); + } }); diff --git a/src/js/logic/heic-to-pdf-page.ts b/src/js/logic/heic-to-pdf-page.ts index 3f2f6c006..361847ef1 100644 --- a/src/js/logic/heic-to-pdf-page.ts +++ b/src/js/logic/heic-to-pdf-page.ts @@ -1,6 +1,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; import { createIcons, icons } from 'lucide'; +import Sortable from 'sortablejs'; import heic2any from 'heic2any'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; @@ -21,7 +22,8 @@ const updateUI = () => { files.forEach((file, index) => { const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm draggable-file'; + fileDiv.setAttribute('data-index', index.toString()); const infoContainer = document.createElement('div'); infoContainer.className = 'flex items-center gap-2 overflow-hidden'; @@ -36,24 +38,55 @@ const updateUI = () => { infoContainer.append(nameSpan, sizeSpan); + const rightGroup = document.createElement('div'); + rightGroup.className = 'flex items-center gap-1 ml-4'; + + const dragHandle = document.createElement('div'); + dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors'; + dragHandle.innerHTML = ``; + const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.className = 'text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.innerHTML = ''; removeBtn.onclick = () => { files = files.filter((_, i) => i !== index); updateUI(); }; - fileDiv.append(infoContainer, removeBtn); + rightGroup.append(dragHandle, removeBtn); + fileDiv.append(infoContainer, rightGroup); fileDisplayArea.appendChild(fileDiv); }); createIcons({ icons }); + initializeFileListSortable(); } else { fileControls.classList.add('hidden'); processBtn.classList.add('hidden'); } }; +function initializeFileListSortable() { + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea) return; + if ((fileDisplayArea as any)._sortableInstance) { + (fileDisplayArea as any)._sortableInstance.destroy(); + } + (fileDisplayArea as any)._sortableInstance = Sortable.create(fileDisplayArea, { + handle: '.drag-handle', + animation: 150, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + onEnd: function (evt: any) { + if (evt.oldIndex !== evt.newIndex) { + const moved = files.splice(evt.oldIndex, 1)[0]; + files.splice(evt.newIndex, 0, moved); + updateUI(); + } + }, + }); +} + const resetState = () => { files = []; updateUI(); @@ -176,4 +209,7 @@ document.addEventListener('DOMContentLoaded', () => { if (processBtn) { processBtn.addEventListener('click', convert); } + setTimeout(() => { + initializeFileListSortable(); + }, 0); }); diff --git a/src/js/logic/image-to-pdf-page.ts b/src/js/logic/image-to-pdf-page.ts index 3051aca98..6bf91c373 100644 --- a/src/js/logic/image-to-pdf-page.ts +++ b/src/js/logic/image-to-pdf-page.ts @@ -3,6 +3,7 @@ import { showAlert, showLoader, hideLoader } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; import { PyMuPDF } from '@bentopdf/pymupdf-wasm'; import { getWasmBaseUrl } from '../config/wasm-cdn-config.js'; +import Sortable from 'sortablejs'; import heic2any from 'heic2any'; const SUPPORTED_FORMATS = '.jpg,.jpeg,.png,.bmp,.gif,.tiff,.tif,.pnm,.pgm,.pbm,.ppm,.pam,.jxr,.jpx,.jp2,.psd,.svg,.heic,.heif,.webp'; @@ -17,6 +18,28 @@ if (document.readyState === 'loading') { initializePage(); } +function initializeFileListSortable() { + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea) return; + if ((fileDisplayArea as any)._sortableInstance) { + (fileDisplayArea as any)._sortableInstance.destroy(); + } + (fileDisplayArea as any)._sortableInstance = Sortable.create(fileDisplayArea, { + handle: '.drag-handle', + animation: 150, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + onEnd: function (evt: any) { + if (evt.oldIndex !== evt.newIndex) { + const moved = files.splice(evt.oldIndex, 1)[0]; + files.splice(evt.newIndex, 0, moved); + updateUI(); + } + }, + }); +} + function initializePage() { createIcons({ icons }); @@ -80,6 +103,10 @@ function initializePage() { document.getElementById('back-to-tools')?.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; }); + + setTimeout(() => { + initializeFileListSortable(); + }, 0); } function handleFileUpload(e: Event) { @@ -132,7 +159,8 @@ function updateUI() { files.forEach((file, index) => { const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm draggable-file'; + fileDiv.setAttribute('data-index', index.toString()); const infoContainer = document.createElement('div'); infoContainer.className = 'flex items-center gap-2 overflow-hidden'; @@ -147,18 +175,27 @@ function updateUI() { infoContainer.append(nameSpan, sizeSpan); + const rightGroup = document.createElement('div'); + rightGroup.className = 'flex items-center gap-1 ml-4'; + + const dragHandle = document.createElement('div'); + dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors'; + dragHandle.innerHTML = ``; + const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.className = 'text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.innerHTML = ''; removeBtn.onclick = () => { files = files.filter((_, i) => i !== index); updateUI(); }; - fileDiv.append(infoContainer, removeBtn); + rightGroup.append(dragHandle, removeBtn); + fileDiv.append(infoContainer, rightGroup); fileDisplayArea.appendChild(fileDiv); }); createIcons({ icons }); + initializeFileListSortable(); } else { fileControls.classList.add('hidden'); optionsDiv.classList.add('hidden'); diff --git a/src/js/logic/jpg-to-pdf-page.ts b/src/js/logic/jpg-to-pdf-page.ts index 40264cd69..80b693e98 100644 --- a/src/js/logic/jpg-to-pdf-page.ts +++ b/src/js/logic/jpg-to-pdf-page.ts @@ -4,6 +4,8 @@ import { downloadFile, formatBytes } from '../utils/helpers.js'; import { PyMuPDF } from '@bentopdf/pymupdf-wasm'; import { getWasmBaseUrl } from '../config/wasm-cdn-config.js'; +import Sortable from 'sortablejs'; + const SUPPORTED_FORMATS = '.jpg,.jpeg,.jp2,.jpx'; const SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/jpg', 'image/jp2']; @@ -16,6 +18,28 @@ if (document.readyState === 'loading') { initializePage(); } +function initializeFileListSortable() { + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea) return; + if ((fileDisplayArea as any)._sortableInstance) { + (fileDisplayArea as any)._sortableInstance.destroy(); + } + (fileDisplayArea as any)._sortableInstance = Sortable.create(fileDisplayArea, { + handle: '.drag-handle', + animation: 150, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + onEnd: function (evt: any) { + if (evt.oldIndex !== evt.newIndex) { + const moved = files.splice(evt.oldIndex, 1)[0]; + files.splice(evt.newIndex, 0, moved); + updateUI(); + } + }, + }); +} + function initializePage() { createIcons({ icons }); @@ -73,6 +97,10 @@ function initializePage() { document.getElementById('back-to-tools')?.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; }); + + setTimeout(() => { + initializeFileListSortable(); + }, 0); } function handleFileUpload(e: Event) { @@ -125,7 +153,8 @@ function updateUI() { files.forEach((file, index) => { const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm draggable-file'; + fileDiv.setAttribute('data-index', index.toString()); const infoContainer = document.createElement('div'); infoContainer.className = 'flex items-center gap-2 overflow-hidden'; @@ -140,18 +169,27 @@ function updateUI() { infoContainer.append(nameSpan, sizeSpan); + const rightGroup = document.createElement('div'); + rightGroup.className = 'flex items-center gap-1 ml-4'; + + const dragHandle = document.createElement('div'); + dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors'; + dragHandle.innerHTML = ``; + const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.className = 'text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.innerHTML = ''; removeBtn.onclick = () => { files = files.filter((_, i) => i !== index); updateUI(); }; - fileDiv.append(infoContainer, removeBtn); + rightGroup.append(dragHandle, removeBtn); + fileDiv.append(infoContainer, rightGroup); fileDisplayArea.appendChild(fileDiv); }); createIcons({ icons }); + initializeFileListSortable(); } else { fileControls.classList.add('hidden'); optionsDiv.classList.add('hidden'); diff --git a/src/js/logic/ods-to-pdf-page.ts b/src/js/logic/ods-to-pdf-page.ts index 24343ece9..18b07abbe 100644 --- a/src/js/logic/ods-to-pdf-page.ts +++ b/src/js/logic/ods-to-pdf-page.ts @@ -6,6 +6,7 @@ import { import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js'; +import Sortable from 'sortablejs'; const ACCEPTED_EXTENSIONS = ['.ods']; const FILETYPE_NAME = 'ODS'; @@ -39,7 +40,8 @@ document.addEventListener('DOMContentLoaded', () => { for (let index = 0; index < state.files.length; index++) { const file = state.files[index]; const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm draggable-file'; + fileDiv.setAttribute('data-index', index.toString()); const infoContainer = document.createElement('div'); infoContainer.className = 'flex flex-col overflow-hidden'; @@ -54,15 +56,23 @@ document.addEventListener('DOMContentLoaded', () => { infoContainer.append(nameSpan, metaSpan); + const rightGroup = document.createElement('div'); + rightGroup.className = 'flex items-center gap-1 ml-4'; + + const dragHandle = document.createElement('div'); + dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors'; + dragHandle.innerHTML = ``; + const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.className = 'text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.innerHTML = ''; removeBtn.onclick = () => { state.files = state.files.filter((_, i) => i !== index); updateUI(); }; - fileDiv.append(infoContainer, removeBtn); + rightGroup.append(dragHandle, removeBtn); + fileDiv.append(infoContainer, rightGroup); fileDisplayArea.appendChild(fileDiv); } @@ -186,4 +196,30 @@ document.addEventListener('DOMContentLoaded', () => { } updateUI(); + + setTimeout(() => { + initializeFileListSortable(); + }, 0); + + function initializeFileListSortable() { + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea) return; + if ((fileDisplayArea as any)._sortableInstance) { + (fileDisplayArea as any)._sortableInstance.destroy(); + } + (fileDisplayArea as any)._sortableInstance = Sortable.create(fileDisplayArea, { + handle: '.drag-handle', + animation: 150, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + onEnd: function (evt: any) { + if (evt.oldIndex !== evt.newIndex) { + const moved = state.files.splice(evt.oldIndex, 1)[0]; + state.files.splice(evt.newIndex, 0, moved); + updateUI(); + } + }, + }); + } }); diff --git a/src/js/logic/odt-to-pdf-page.ts b/src/js/logic/odt-to-pdf-page.ts index 43ff89561..4f4ebb52d 100644 --- a/src/js/logic/odt-to-pdf-page.ts +++ b/src/js/logic/odt-to-pdf-page.ts @@ -6,6 +6,7 @@ import { } from '../utils/helpers.js'; import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; +import Sortable from 'sortablejs'; import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js'; document.addEventListener('DOMContentLoaded', () => { @@ -37,7 +38,8 @@ document.addEventListener('DOMContentLoaded', () => { for (let index = 0; index < state.files.length; index++) { const file = state.files[index]; const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm draggable-file'; + fileDiv.setAttribute('data-index', index.toString()); const infoContainer = document.createElement('div'); infoContainer.className = 'flex flex-col overflow-hidden'; @@ -52,15 +54,23 @@ document.addEventListener('DOMContentLoaded', () => { infoContainer.append(nameSpan, metaSpan); + const rightGroup = document.createElement('div'); + rightGroup.className = 'flex items-center gap-1 ml-4'; + + const dragHandle = document.createElement('div'); + dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors'; + dragHandle.innerHTML = ``; + const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.className = 'text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.innerHTML = ''; removeBtn.onclick = () => { state.files = state.files.filter((_, i) => i !== index); updateUI(); }; - fileDiv.append(infoContainer, removeBtn); + rightGroup.append(dragHandle, removeBtn); + fileDiv.append(infoContainer, rightGroup); fileDisplayArea.appendChild(fileDiv); } @@ -211,5 +221,31 @@ document.addEventListener('DOMContentLoaded', () => { processBtn.addEventListener('click', convertToPdf); } + setTimeout(() => { + initializeFileListSortable(); + }, 0); + + function initializeFileListSortable() { + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea) return; + if ((fileDisplayArea as any)._sortableInstance) { + (fileDisplayArea as any)._sortableInstance.destroy(); + } + (fileDisplayArea as any)._sortableInstance = Sortable.create(fileDisplayArea, { + handle: '.drag-handle', + animation: 150, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + onEnd: function (evt: any) { + if (evt.oldIndex !== evt.newIndex) { + const moved = state.files.splice(evt.oldIndex, 1)[0]; + state.files.splice(evt.newIndex, 0, moved); + updateUI(); + } + }, + }); + } + updateUI(); }); diff --git a/src/js/logic/pages-to-pdf-page.ts b/src/js/logic/pages-to-pdf-page.ts index 506eced32..a03354946 100644 --- a/src/js/logic/pages-to-pdf-page.ts +++ b/src/js/logic/pages-to-pdf-page.ts @@ -6,6 +6,7 @@ import { import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js'; +import Sortable from 'sortablejs'; const ACCEPTED_EXTENSIONS = ['.pages']; const FILETYPE_NAME = 'Pages'; @@ -39,7 +40,8 @@ document.addEventListener('DOMContentLoaded', () => { for (let index = 0; index < state.files.length; index++) { const file = state.files[index]; const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm draggable-file'; + fileDiv.setAttribute('data-index', index.toString()); const infoContainer = document.createElement('div'); infoContainer.className = 'flex flex-col overflow-hidden'; @@ -54,15 +56,23 @@ document.addEventListener('DOMContentLoaded', () => { infoContainer.append(nameSpan, metaSpan); + const rightGroup = document.createElement('div'); + rightGroup.className = 'flex items-center gap-1 ml-4'; + + const dragHandle = document.createElement('div'); + dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors'; + dragHandle.innerHTML = ``; + const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.className = 'text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.innerHTML = ''; removeBtn.onclick = () => { state.files = state.files.filter((_, i) => i !== index); updateUI(); }; - fileDiv.append(infoContainer, removeBtn); + rightGroup.append(dragHandle, removeBtn); + fileDiv.append(infoContainer, rightGroup); fileDisplayArea.appendChild(fileDiv); } @@ -185,4 +195,26 @@ document.addEventListener('DOMContentLoaded', () => { } updateUI(); + + function initializeFileListSortable() { + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea) return; + if ((fileDisplayArea as any)._sortableInstance) { + (fileDisplayArea as any)._sortableInstance.destroy(); + } + (fileDisplayArea as any)._sortableInstance = Sortable.create(fileDisplayArea, { + handle: '.drag-handle', + animation: 150, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + onEnd: function (evt: any) { + if (evt.oldIndex !== evt.newIndex) { + const moved = state.files.splice(evt.oldIndex, 1)[0]; + state.files.splice(evt.newIndex, 0, moved); + updateUI(); + } + }, + }); + } }); diff --git a/src/js/logic/png-to-pdf-page.ts b/src/js/logic/png-to-pdf-page.ts index eb9e34a08..92fb41281 100644 --- a/src/js/logic/png-to-pdf-page.ts +++ b/src/js/logic/png-to-pdf-page.ts @@ -1,6 +1,7 @@ import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; import { downloadFile, readFileAsArrayBuffer, formatBytes } from '../utils/helpers.js'; +import Sortable from 'sortablejs'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; let files: File[] = []; @@ -11,6 +12,28 @@ if (document.readyState === 'loading') { initializePage(); } +function initializeFileListSortable() { + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea) return; + if ((fileDisplayArea as any)._sortableInstance) { + (fileDisplayArea as any)._sortableInstance.destroy(); + } + (fileDisplayArea as any)._sortableInstance = Sortable.create(fileDisplayArea, { + handle: '.drag-handle', + animation: 150, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + onEnd: function (evt: any) { + if (evt.oldIndex !== evt.newIndex) { + const moved = files.splice(evt.oldIndex, 1)[0]; + files.splice(evt.newIndex, 0, moved); + updateUI(); + } + }, + }); +} + function initializePage() { createIcons({ icons }); @@ -69,6 +92,9 @@ function initializePage() { document.getElementById('back-to-tools')?.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; }); + setTimeout(() => { + initializeFileListSortable(); + }, 0); } function handleFileUpload(e: Event) { @@ -113,7 +139,8 @@ function updateUI() { files.forEach((file, index) => { const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm draggable-file'; + fileDiv.setAttribute('data-index', index.toString()); const infoContainer = document.createElement('div'); infoContainer.className = 'flex items-center gap-2 overflow-hidden'; @@ -128,18 +155,27 @@ function updateUI() { infoContainer.append(nameSpan, sizeSpan); + const rightGroup = document.createElement('div'); + rightGroup.className = 'flex items-center gap-1 ml-4'; + + const dragHandle = document.createElement('div'); + dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors'; + dragHandle.innerHTML = ``; + const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.className = 'text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.innerHTML = ''; removeBtn.onclick = () => { files = files.filter((_, i) => i !== index); updateUI(); }; - fileDiv.append(infoContainer, removeBtn); + rightGroup.append(dragHandle, removeBtn); + fileDiv.append(infoContainer, rightGroup); fileDisplayArea.appendChild(fileDiv); }); createIcons({ icons }); + initializeFileListSortable(); } else { fileControls.classList.add('hidden'); optionsDiv.classList.add('hidden'); diff --git a/src/js/logic/powerpoint-to-pdf-page.ts b/src/js/logic/powerpoint-to-pdf-page.ts index 130fac52a..e51bb5b97 100644 --- a/src/js/logic/powerpoint-to-pdf-page.ts +++ b/src/js/logic/powerpoint-to-pdf-page.ts @@ -7,6 +7,7 @@ import { import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js'; +import Sortable from 'sortablejs'; document.addEventListener('DOMContentLoaded', () => { state.files = []; @@ -37,7 +38,8 @@ document.addEventListener('DOMContentLoaded', () => { for (let index = 0; index < state.files.length; index++) { const file = state.files[index]; const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm draggable-file'; + fileDiv.setAttribute('data-index', index.toString()); const infoContainer = document.createElement('div'); infoContainer.className = 'flex flex-col overflow-hidden'; @@ -52,15 +54,23 @@ document.addEventListener('DOMContentLoaded', () => { infoContainer.append(nameSpan, metaSpan); + const rightGroup = document.createElement('div'); + rightGroup.className = 'flex items-center gap-1 ml-4'; + + const dragHandle = document.createElement('div'); + dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors'; + dragHandle.innerHTML = ``; + const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.className = 'text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.innerHTML = ''; removeBtn.onclick = () => { state.files = state.files.filter((_, i) => i !== index); updateUI(); }; - fileDiv.append(infoContainer, removeBtn); + rightGroup.append(dragHandle, removeBtn); + fileDiv.append(infoContainer, rightGroup); fileDisplayArea.appendChild(fileDiv); } @@ -215,4 +225,26 @@ document.addEventListener('DOMContentLoaded', () => { } updateUI(); + + function initializeFileListSortable() { + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea) return; + if ((fileDisplayArea as any)._sortableInstance) { + (fileDisplayArea as any)._sortableInstance.destroy(); + } + (fileDisplayArea as any)._sortableInstance = Sortable.create(fileDisplayArea, { + handle: '.drag-handle', + animation: 150, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + onEnd: function (evt: any) { + if (evt.oldIndex !== evt.newIndex) { + const moved = state.files.splice(evt.oldIndex, 1)[0]; + state.files.splice(evt.newIndex, 0, moved); + updateUI(); + } + }, + }); + } }); diff --git a/src/js/logic/pub-to-pdf-page.ts b/src/js/logic/pub-to-pdf-page.ts index 1c2e68248..4979dd71e 100644 --- a/src/js/logic/pub-to-pdf-page.ts +++ b/src/js/logic/pub-to-pdf-page.ts @@ -2,6 +2,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, formatBytes } from '../utils/helpers.js'; import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; +import Sortable from 'sortablejs'; import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js'; const ACCEPTED_EXTENSIONS = ['.pub']; @@ -35,7 +36,11 @@ document.addEventListener('DOMContentLoaded', () => { for (let index = 0; index < state.files.length; index++) { const file = state.files[index]; const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm draggable-file'; + fileDiv.setAttribute('data-index', String(index)); + const dragHandle = document.createElement('div'); + dragHandle.className = 'drag-handle cursor-grab mr-3 flex-shrink-0'; + dragHandle.innerHTML = ''; const infoContainer = document.createElement('div'); infoContainer.className = 'flex flex-col overflow-hidden'; const nameSpan = document.createElement('div'); @@ -52,10 +57,11 @@ document.addEventListener('DOMContentLoaded', () => { state.files = state.files.filter((_, i) => i !== index); updateUI(); }; - fileDiv.append(infoContainer, removeBtn); + fileDiv.append(dragHandle, infoContainer, removeBtn); fileDisplayArea.appendChild(fileDiv); } createIcons({ icons }); + setTimeout(() => initializeFileListSortable(), 0); } if (fileControls) fileControls.classList.remove('hidden'); convertOptions.classList.remove('hidden'); @@ -66,6 +72,25 @@ document.addEventListener('DOMContentLoaded', () => { } }; + let sortable: any = null; + const initializeFileListSortable = () => { + if (!fileDisplayArea) return; + if (sortable) sortable.destroy(); + sortable = Sortable.create(fileDisplayArea, { + animation: 150, + handle: '.drag-handle', + draggable: '.draggable-file', + onEnd: (evt: any) => { + const oldIndex = evt.oldIndex; + const newIndex = evt.newIndex; + if (oldIndex === undefined || newIndex === undefined) return; + const moved = state.files.splice(oldIndex, 1)[0]; + state.files.splice(newIndex, 0, moved); + updateUI(); + } + }); + }; + const resetState = () => { state.files = []; updateUI(); diff --git a/src/js/logic/svg-to-pdf-page.ts b/src/js/logic/svg-to-pdf-page.ts index e88f7bd4a..f9ff75c24 100644 --- a/src/js/logic/svg-to-pdf-page.ts +++ b/src/js/logic/svg-to-pdf-page.ts @@ -2,6 +2,7 @@ import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; import { downloadFile, readFileAsArrayBuffer, formatBytes } from '../utils/helpers.js'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; +import Sortable from 'sortablejs'; let files: File[] = []; @@ -11,6 +12,28 @@ if (document.readyState === 'loading') { initializePage(); } +function initializeFileListSortable() { + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea) return; + if ((fileDisplayArea as any)._sortableInstance) { + (fileDisplayArea as any)._sortableInstance.destroy(); + } + (fileDisplayArea as any)._sortableInstance = Sortable.create(fileDisplayArea, { + handle: '.drag-handle', + animation: 150, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + onEnd: function (evt: any) { + if (evt.oldIndex !== evt.newIndex) { + const moved = files.splice(evt.oldIndex, 1)[0]; + files.splice(evt.newIndex, 0, moved); + updateUI(); + } + }, + }); +} + function initializePage() { createIcons({ icons }); @@ -69,6 +92,9 @@ function initializePage() { document.getElementById('back-to-tools')?.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; }); + setTimeout(() => { + initializeFileListSortable(); + }, 0); } function handleFileUpload(e: Event) { @@ -113,7 +139,8 @@ function updateUI() { files.forEach((file, index) => { const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm draggable-file'; + fileDiv.setAttribute('data-index', index.toString()); const infoContainer = document.createElement('div'); infoContainer.className = 'flex items-center gap-2 overflow-hidden'; @@ -128,18 +155,27 @@ function updateUI() { infoContainer.append(nameSpan, sizeSpan); + const rightGroup = document.createElement('div'); + rightGroup.className = 'flex items-center gap-1 ml-4'; + + const dragHandle = document.createElement('div'); + dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors'; + dragHandle.innerHTML = ``; + const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.className = 'text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.innerHTML = ''; removeBtn.onclick = () => { files = files.filter((_, i) => i !== index); updateUI(); }; - fileDiv.append(infoContainer, removeBtn); + rightGroup.append(dragHandle, removeBtn); + fileDiv.append(infoContainer, rightGroup); fileDisplayArea.appendChild(fileDiv); }); createIcons({ icons }); + initializeFileListSortable(); } else { fileControls.classList.add('hidden'); optionsDiv.classList.add('hidden'); diff --git a/src/js/logic/tiff-to-pdf-page.ts b/src/js/logic/tiff-to-pdf-page.ts index 436cfcd4b..04d893da5 100644 --- a/src/js/logic/tiff-to-pdf-page.ts +++ b/src/js/logic/tiff-to-pdf-page.ts @@ -1,6 +1,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, formatBytes, readFileAsArrayBuffer } from '../utils/helpers.js'; import { createIcons, icons } from 'lucide'; +import Sortable from 'sortablejs'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { decode } from 'tiff'; @@ -21,7 +22,8 @@ const updateUI = () => { files.forEach((file, index) => { const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm draggable-file'; + fileDiv.setAttribute('data-index', index.toString()); const infoContainer = document.createElement('div'); infoContainer.className = 'flex items-center gap-2 overflow-hidden'; @@ -36,24 +38,55 @@ const updateUI = () => { infoContainer.append(nameSpan, sizeSpan); + const rightGroup = document.createElement('div'); + rightGroup.className = 'flex items-center gap-1 ml-4'; + + const dragHandle = document.createElement('div'); + dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors'; + dragHandle.innerHTML = ``; + const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.className = 'text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.innerHTML = ''; removeBtn.onclick = () => { files = files.filter((_, i) => i !== index); updateUI(); }; - fileDiv.append(infoContainer, removeBtn); + rightGroup.append(dragHandle, removeBtn); + fileDiv.append(infoContainer, rightGroup); fileDisplayArea.appendChild(fileDiv); }); createIcons({ icons }); + initializeFileListSortable(); } else { fileControls.classList.add('hidden'); processBtn.classList.add('hidden'); } }; +function initializeFileListSortable() { + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea) return; + if ((fileDisplayArea as any)._sortableInstance) { + (fileDisplayArea as any)._sortableInstance.destroy(); + } + (fileDisplayArea as any)._sortableInstance = Sortable.create(fileDisplayArea, { + handle: '.drag-handle', + animation: 150, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + onEnd: function (evt: any) { + if (evt.oldIndex !== evt.newIndex) { + const moved = files.splice(evt.oldIndex, 1)[0]; + files.splice(evt.newIndex, 0, moved); + updateUI(); + } + }, + }); +} + const resetState = () => { files = []; updateUI(); @@ -197,4 +230,7 @@ document.addEventListener('DOMContentLoaded', () => { if (processBtn) { processBtn.addEventListener('click', convert); } + setTimeout(() => { + initializeFileListSortable(); + }, 0); }); diff --git a/src/js/logic/vsd-to-pdf-page.ts b/src/js/logic/vsd-to-pdf-page.ts index 01430d650..31c0998ff 100644 --- a/src/js/logic/vsd-to-pdf-page.ts +++ b/src/js/logic/vsd-to-pdf-page.ts @@ -3,6 +3,7 @@ import { downloadFile, formatBytes } from '../utils/helpers.js'; import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js'; +import Sortable from 'sortablejs'; const ACCEPTED_EXTENSIONS = ['.vsd', '.vsdx']; const FILETYPE_NAME = 'VSD'; @@ -35,7 +36,8 @@ document.addEventListener('DOMContentLoaded', () => { for (let index = 0; index < state.files.length; index++) { const file = state.files[index]; const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm draggable-file'; + fileDiv.setAttribute('data-index', index.toString()); const infoContainer = document.createElement('div'); infoContainer.className = 'flex flex-col overflow-hidden'; const nameSpan = document.createElement('div'); @@ -45,14 +47,20 @@ document.addEventListener('DOMContentLoaded', () => { metaSpan.className = 'text-xs text-gray-400'; metaSpan.textContent = formatBytes(file.size); infoContainer.append(nameSpan, metaSpan); + const rightGroup = document.createElement('div'); + rightGroup.className = 'flex items-center gap-1 ml-4'; + const dragHandle = document.createElement('div'); + dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors'; + dragHandle.innerHTML = ``; const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.className = 'text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.innerHTML = ''; removeBtn.onclick = () => { state.files = state.files.filter((_, i) => i !== index); updateUI(); }; - fileDiv.append(infoContainer, removeBtn); + rightGroup.append(dragHandle, removeBtn); + fileDiv.append(infoContainer, rightGroup); fileDisplayArea.appendChild(fileDiv); } createIcons({ icons }); @@ -139,4 +147,26 @@ document.addEventListener('DOMContentLoaded', () => { if (processBtn) processBtn.addEventListener('click', convert); updateUI(); + + function initializeFileListSortable() { + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea) return; + if ((fileDisplayArea as any)._sortableInstance) { + (fileDisplayArea as any)._sortableInstance.destroy(); + } + (fileDisplayArea as any)._sortableInstance = Sortable.create(fileDisplayArea, { + handle: '.drag-handle', + animation: 150, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + onEnd: function (evt: any) { + if (evt.oldIndex !== evt.newIndex) { + const moved = state.files.splice(evt.oldIndex, 1)[0]; + state.files.splice(evt.newIndex, 0, moved); + updateUI(); + } + }, + }); + } }); diff --git a/src/js/logic/webp-to-pdf-page.ts b/src/js/logic/webp-to-pdf-page.ts index 087c8b239..eba365a10 100644 --- a/src/js/logic/webp-to-pdf-page.ts +++ b/src/js/logic/webp-to-pdf-page.ts @@ -1,6 +1,7 @@ import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; import { downloadFile, readFileAsArrayBuffer, formatBytes } from '../utils/helpers.js'; +import Sortable from 'sortablejs'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; let files: File[] = []; @@ -11,6 +12,28 @@ if (document.readyState === 'loading') { initializePage(); } +function initializeFileListSortable() { + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea) return; + if ((fileDisplayArea as any)._sortableInstance) { + (fileDisplayArea as any)._sortableInstance.destroy(); + } + (fileDisplayArea as any)._sortableInstance = Sortable.create(fileDisplayArea, { + handle: '.drag-handle', + animation: 150, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + onEnd: function (evt: any) { + if (evt.oldIndex !== evt.newIndex) { + const moved = files.splice(evt.oldIndex, 1)[0]; + files.splice(evt.newIndex, 0, moved); + updateUI(); + } + }, + }); +} + function initializePage() { createIcons({ icons }); @@ -69,6 +92,9 @@ function initializePage() { document.getElementById('back-to-tools')?.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; }); + setTimeout(() => { + initializeFileListSortable(); + }, 0); } function handleFileUpload(e: Event) { @@ -113,7 +139,8 @@ function updateUI() { files.forEach((file, index) => { const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm draggable-file'; + fileDiv.setAttribute('data-index', index.toString()); const infoContainer = document.createElement('div'); infoContainer.className = 'flex items-center gap-2 overflow-hidden'; @@ -128,18 +155,27 @@ function updateUI() { infoContainer.append(nameSpan, sizeSpan); + const rightGroup = document.createElement('div'); + rightGroup.className = 'flex items-center gap-1 ml-4'; + + const dragHandle = document.createElement('div'); + dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors'; + dragHandle.innerHTML = ``; + const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.className = 'text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.innerHTML = ''; removeBtn.onclick = () => { files = files.filter((_, i) => i !== index); updateUI(); }; - fileDiv.append(infoContainer, removeBtn); + rightGroup.append(dragHandle, removeBtn); + fileDiv.append(infoContainer, rightGroup); fileDisplayArea.appendChild(fileDiv); }); createIcons({ icons }); + initializeFileListSortable(); } else { fileControls.classList.add('hidden'); optionsDiv.classList.add('hidden'); diff --git a/src/js/logic/word-to-pdf-page.ts b/src/js/logic/word-to-pdf-page.ts index e198e3baa..19732f328 100644 --- a/src/js/logic/word-to-pdf-page.ts +++ b/src/js/logic/word-to-pdf-page.ts @@ -7,6 +7,7 @@ import { import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js'; +import Sortable from 'sortablejs'; document.addEventListener('DOMContentLoaded', () => { state.files = []; @@ -37,7 +38,8 @@ document.addEventListener('DOMContentLoaded', () => { for (let index = 0; index < state.files.length; index++) { const file = state.files[index]; const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm draggable-file'; + fileDiv.setAttribute('data-index', index.toString()); const infoContainer = document.createElement('div'); infoContainer.className = 'flex flex-col overflow-hidden'; @@ -52,15 +54,23 @@ document.addEventListener('DOMContentLoaded', () => { infoContainer.append(nameSpan, metaSpan); + const rightGroup = document.createElement('div'); + rightGroup.className = 'flex items-center gap-1 ml-4'; + + const dragHandle = document.createElement('div'); + dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors'; + dragHandle.innerHTML = ``; + const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.className = 'text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.innerHTML = ''; removeBtn.onclick = () => { state.files = state.files.filter((_, i) => i !== index); updateUI(); }; - fileDiv.append(infoContainer, removeBtn); + rightGroup.append(dragHandle, removeBtn); + fileDiv.append(infoContainer, rightGroup); fileDisplayArea.appendChild(fileDiv); } @@ -233,4 +243,26 @@ document.addEventListener('DOMContentLoaded', () => { // Initialize UI state (ensures button is hidden when no files) updateUI(); + + function initializeFileListSortable() { + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea) return; + if ((fileDisplayArea as any)._sortableInstance) { + (fileDisplayArea as any)._sortableInstance.destroy(); + } + (fileDisplayArea as any)._sortableInstance = Sortable.create(fileDisplayArea, { + handle: '.drag-handle', + animation: 150, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + onEnd: function (evt: any) { + if (evt.oldIndex !== evt.newIndex) { + const moved = state.files.splice(evt.oldIndex, 1)[0]; + state.files.splice(evt.newIndex, 0, moved); + updateUI(); + } + }, + }); + } }); diff --git a/src/js/logic/wps-to-pdf-page.ts b/src/js/logic/wps-to-pdf-page.ts index 641bae233..a6d063e2d 100644 --- a/src/js/logic/wps-to-pdf-page.ts +++ b/src/js/logic/wps-to-pdf-page.ts @@ -6,6 +6,7 @@ import { import { state } from '../state.js'; import { createIcons, icons } from 'lucide'; import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js'; +import Sortable from 'sortablejs'; const ACCEPTED_EXTENSIONS = ['.wps']; const FILETYPE_NAME = 'WPS'; @@ -39,7 +40,8 @@ document.addEventListener('DOMContentLoaded', () => { for (let index = 0; index < state.files.length; index++) { const file = state.files[index]; const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm draggable-file'; + fileDiv.setAttribute('data-index', index.toString()); const infoContainer = document.createElement('div'); infoContainer.className = 'flex flex-col overflow-hidden'; @@ -54,15 +56,23 @@ document.addEventListener('DOMContentLoaded', () => { infoContainer.append(nameSpan, metaSpan); + const rightGroup = document.createElement('div'); + rightGroup.className = 'flex items-center gap-1 ml-4'; + + const dragHandle = document.createElement('div'); + dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors'; + dragHandle.innerHTML = ``; + const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.className = 'text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.innerHTML = ''; removeBtn.onclick = () => { state.files = state.files.filter((_, i) => i !== index); updateUI(); }; - fileDiv.append(infoContainer, removeBtn); + rightGroup.append(dragHandle, removeBtn); + fileDiv.append(infoContainer, rightGroup); fileDisplayArea.appendChild(fileDiv); } @@ -185,4 +195,26 @@ document.addEventListener('DOMContentLoaded', () => { } updateUI(); + + function initializeFileListSortable() { + const fileDisplayArea = document.getElementById('file-display-area'); + if (!fileDisplayArea) return; + if ((fileDisplayArea as any)._sortableInstance) { + (fileDisplayArea as any)._sortableInstance.destroy(); + } + (fileDisplayArea as any)._sortableInstance = Sortable.create(fileDisplayArea, { + handle: '.drag-handle', + animation: 150, + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + onEnd: function (evt: any) { + if (evt.oldIndex !== evt.newIndex) { + const moved = state.files.splice(evt.oldIndex, 1)[0]; + state.files.splice(evt.newIndex, 0, moved); + updateUI(); + } + }, + }); + } });