sortable-dnd-uploader
Version:
Drag and Drop media uploader with sorting and titles
134 lines (120 loc) • 4.26 kB
JavaScript
class MediaUploader {
constructor(container, options = {}) {
this.container = container;
this.options = options;
this.dragSrcEl = null;
this.init();
}
init() {
this.container.classList.add('sortable-dnd-uploader');
this.container.setAttribute('draggable', 'false');
this.container.addEventListener('dragstart', this.handleDragStart.bind(this), false);
this.container.addEventListener('dragover', this.handleDragOver.bind(this), false);
this.container.addEventListener('drop', this.handleDrop.bind(this), false);
this.container.addEventListener('dragend', this.handleDragEnd.bind(this), false);
this.container.addEventListener('click', this.handleClick.bind(this), false);
this.container.addEventListener('dragenter', this.handleDragEnter.bind(this), false);
this.container.addEventListener('dragleave', this.handleDragLeave.bind(this), false);
// For file input fallback
this.fileInput = document.createElement('input');
this.fileInput.type = 'file';
this.fileInput.multiple = true;
this.fileInput.style.display = 'none';
this.fileInput.accept = this.options.accept || 'image/*,video/*';
this.fileInput.addEventListener('change', e => {
this.handleFiles(e.target.files);
e.target.value = '';
});
this.container.appendChild(this.fileInput);
// For sorting, we will keep track of blocks inside container
this.refreshBlocks();
}
refreshBlocks() {
this.blocks = Array.from(this.container.querySelectorAll('.sdu-block'));
this.blocks.forEach(block => {
block.setAttribute('draggable', 'true');
});
}
handleDragStart(e) {
if (!e.target.classList.contains('sdu-block')) return;
this.dragSrcEl = e.target;
e.dataTransfer.effectAllowed = 'move';
try {
e.dataTransfer.setData('text/html', e.target.outerHTML);
} catch {}
e.target.classList.add('dragging');
}
handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const target = e.target.closest('.sdu-block');
if (target && target !== this.dragSrcEl) {
const rect = target.getBoundingClientRect();
const next = (e.clientY - rect.top) / (rect.bottom - rect.top) > 0.5;
this.container.insertBefore(this.dragSrcEl, next ? target.nextSibling : target);
this.refreshBlocks();
}
}
handleDrop(e) {
e.preventDefault();
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
this.handleFiles(e.dataTransfer.files);
e.dataTransfer.clearData();
}
}
handleDragEnd(e) {
if (this.dragSrcEl) {
this.dragSrcEl.classList.remove('dragging');
this.dragSrcEl = null;
}
}
handleClick(e) {
const block = e.target.closest('.sdu-block');
if (block) {
this.fileInput.click();
this.currentEditBlock = block;
}
}
handleFiles(files) {
Array.from(files).forEach(file => {
this.addMediaBlock(file);
});
}
addMediaBlock(file) {
const reader = new FileReader();
reader.onload = e => {
const ext = file.name.split('.').pop().toLowerCase();
let mediaHtml = '';
if (['mp4', 'webm', 'ogg'].includes(ext)) {
mediaHtml = `<video controls src="${e.target.result}" class="sdu-media"></video>`;
} else {
mediaHtml = `<img src="${e.target.result}" class="sdu-media" alt="">`;
}
const block = document.createElement('div');
block.className = 'sdu-block';
block.setAttribute('draggable', 'true');
block.innerHTML = `
${mediaHtml}
<input type="text" class="sdu-title" placeholder="Enter title...">
<button class="sdu-delete" title="Delete">×</button>
`;
this.container.appendChild(block);
this.attachBlockEvents(block);
this.refreshBlocks();
};
reader.readAsDataURL(file);
}
attachBlockEvents(block) {
const deleteBtn = block.querySelector('.sdu-delete');
deleteBtn.addEventListener('click', e => {
e.stopPropagation();
block.remove();
this.refreshBlocks();
});
}
}
// Для подключения через <script>
if (typeof window !== 'undefined') {
window.MediaUploader = MediaUploader;
}
export default MediaUploader;