You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用SortableJS与dcraw时列表项闪烁/被移除的解决咨询

问题修复方案(保留dcraw与SortableJS)

问题根源

  1. 预览加载时通过innerHTML替换整个<li>内部DOM,破坏了Sortable已绑定的元素与事件监听,导致拖动时DOM异常
  2. 重复初始化Sortable实例,多个实例作用于同一元素引发冲突
  3. 删除文件时修改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">&times;</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>

修复说明

  1. 保护Sortable绑定的DOM:将预览区域拆分为独立容器,仅更新该容器内容,不再替换整个<li>的innerHTML,保留Sortable的事件与元素绑定
  2. 管理Sortable实例生命周期:用数组存储所有实例,每次重建列表前销毁旧实例,删除单个元素时也销毁对应子实例,避免重复绑定冲突
  3. 阻止删除时的意外列表重建:临时移除filesField的change事件监听,修改文件列表后再恢复,防止触发列表清空逻辑
  4. 指定拖动手柄:配置handle: '.handle',仅点击手柄图标时可触发拖动,减少误操作

内容的提问来源于stack exchange,提问作者Mohammad Qodrati

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 05:07:21