使用SortableJS与dcraw时列表项闪烁/被移除的解决咨询
问题修复方案(保留dcraw与SortableJS)
问题根源
- 预览加载时通过
innerHTML替换整个<li>内部DOM,破坏了Sortable已绑定的元素与事件监听,导致拖动时DOM异常 - 重复初始化Sortable实例,多个实例作用于同一元素引发冲突
- 删除文件时修改
filesField.files会触发change事件,意外重建整个列表导致拖动元素丢失
修复后代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <title>Bootstrap-Flask Demo Application</title> <style> .handle { cursor: -webkit-grabbing; cursor: move; } </style> </head> <body> <input class="form-control" id="files" multiple="" name="files" required="" type="file"> <div id="files-selected" class="mb-3"> <ul class="list-group very-first-parent nested-sortable h-200"> </ul> </div> <script src="https://cdn.jsdelivr.net/npm/dcraw"></script> <script src="https://sortablejs.github.io/Sortable/Sortable.js"></script> <script> var filesField; var filesSelected; var sortableInstances = []; // 存储Sortable实例,用于销毁旧实例 document.addEventListener('DOMContentLoaded', function () { filesField = document.getElementById("files"); filesSelected = document.getElementById("files-selected"); filesField.addEventListener('change', function (e) { // 销毁所有旧Sortable实例,避免冲突 sortableInstances.forEach(instance => instance.destroy()); sortableInstances = []; var files = e.target.files; var filenames = Array.from(files).map(file => file.name); filesSelected.querySelector('.very-first-parent').innerHTML = ''; filenames.forEach((filename, index) => { var file = files[index]; var li = document.createElement('li'); li.className = 'list-group-item'; // 单独拆分预览容器,避免替换整个li的innerHTML li.innerHTML = ` <div class="d-flex align-items-center"> <i class="handle bi-arrows-move"></i> <button type="button" class="btn btn-danger me-3 ms-3" onclick="dismiss(this)"> <span aria-hidden="true">×</span> </button> <span class="file-name">${filename}</span> <div class="preview-container ms-auto"> <div class="spinner spinner-border" role="status"><span class="visually-hidden">Loading...</span></div> </div> </div> <ul class="list-group nested-sortable"></ul> `; // 仅更新预览容器内容,不破坏Sortable绑定的DOM showPreview(file, li.querySelector('.preview-container')); filesSelected.querySelector('.very-first-parent').appendChild(li); }); makeNestedSortable(); }); }); function isImage(file) { const imageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp']; return imageTypes.includes(file.type); } function isRawImage(file) { const rawImageExtensions = ['.nef', '.cr2', '.tiff']; return rawImageExtensions.some(ext => file.name.toLowerCase().endsWith(ext)); } function isVideo(file) { const videoTypes = ['video/mp4', 'video/quicktime']; return videoTypes.includes(file.type); } function showPreview(file, container) { if (isImage(file)) { const reader = new FileReader(); reader.onload = function (e) { container.innerHTML = `<img class="preview" src="${e.target.result}" height="70" style="max-height: 70px;">`; }; reader.readAsDataURL(file); } else if (isRawImage(file)) { const reader = new FileReader(); reader.onload = function (e) { var buf = new Uint8Array(e.currentTarget.result); var thumbnail = dcraw(buf, { extractThumbnail: true }); var blob = new Blob([thumbnail], { type: "image/jpeg" }); var imageUrl = window.URL || window.webkitURL; var url = imageUrl.createObjectURL(blob); container.innerHTML = `<img class="preview" src="${url}" height="70" style="max-height: 70px;">`; }; reader.readAsArrayBuffer(file); } else if (isVideo(file)) { container.innerHTML = `<video src="${URL.createObjectURL(file)}" controls height="70" style="max-height: 70px;"></video>`; } else { container.innerHTML = `<span class="preview">Preview not available</span>`; } } function dismiss(target) { var li = target.closest('li'); var fileToDelete = li.querySelector('span.file-name').textContent; var dt = new DataTransfer(); Array.from(filesField.files).forEach((file) => { if (file.name !== fileToDelete) dt.items.add(file); }); // 临时移除change事件监听,避免触发列表重建 const changeHandler = filesField.onchange; filesField.onchange = null; filesField.files = dt.files; filesField.onchange = changeHandler; // 销毁当前li内部的Sortable实例并移除 const nestedUl = li.querySelector('.nested-sortable'); const instanceIndex = sortableInstances.findIndex(inst => inst.el === nestedUl); if (instanceIndex !== -1) { sortableInstances[instanceIndex].destroy(); sortableInstances.splice(instanceIndex, 1); } li.remove(); } function makeNestedSortable() { nestedSortables = [].slice.call(document.querySelectorAll('.nested-sortable')); for (var i = 0; i < nestedSortables.length; i++) { const instance = new Sortable(nestedSortables[i], { group: 'nested', animation: 150, fallbackOnBody: true, swapThreshold: 0.5, handle: '.handle' // 指定仅手柄可拖动,避免误操作 }); sortableInstances.push(instance); } } </script> </body> </html>
修复说明
- 保护Sortable绑定的DOM:将预览区域拆分为独立容器,仅更新该容器内容,不再替换整个
<li>的innerHTML,保留Sortable的事件与元素绑定 - 管理Sortable实例生命周期:用数组存储所有实例,每次重建列表前销毁旧实例,删除单个元素时也销毁对应子实例,避免重复绑定冲突
- 阻止删除时的意外列表重建:临时移除
filesField的change事件监听,修改文件列表后再恢复,防止触发列表清空逻辑 - 指定拖动手柄:配置
handle: '.handle',仅点击手柄图标时可触发拖动,减少误操作
内容的提问来源于stack exchange,提问作者Mohammad Qodrati
相关产品推荐
相关产品推荐

