javascripthtmlsortablejsdcraw

Flickering/being removed when using sortablejs with dcraw


I try to list files selected using multiple file input using the code below (also available at jsbin). When trying to move li items to sort and nest them, they start flickering and sometimes are dropped from the list and completely removed from DOM. Can it be solved while keep using dcraw and SortableJS? It seems to work

<!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;
        document.addEventListener('DOMContentLoaded', function () {
            filesField = document.getElementById("files");
            filesSelected = document.getElementById("files-selected");

            filesField.addEventListener('change', function (e) {
                var files = e.target.files;

                var reader = [];
                var filenames = Array.from(files).map(file => file.name);

                filesSelected.querySelector('.very-first-parent').innerHTML = '';
                filenames.forEach((filename, index) => {
                    // alert('file selected');
                    var file = files[index];
                    var li = document.createElement('li');
                    li.className = 'list-group-item';
                    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>
                        <!--spinner--><div class="spinner spinner-border ms-auto" role="status"><span class="visually-hidden">Loading...</span></div><!--/spinner-->
                    </div>
                    <ul class="list-group nested-sortable"></ul>
                    `;
                    showPreview(file, li);
                    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 replaceSpinner(string_old, string_new) {
            return string_old.replace(/<!--spinner-->.*<!--\/spinner-->/gi, string_new)
        }

        function showPreview(file, li) {
            if (isImage(file)) {
                const reader = new FileReader();
                reader.onload = function (e) {
                    li.innerHTML = replaceSpinner(li.innerHTML, `<img class="preview ms-auto" src="${e.target.result}" height="70" style="max-height: 70px;">`);
                };
                reader.readAsDataURL(file);
            } else if (isRawImage(file)) {
                // You can use a library like raw.js to decode raw images and show a preview
                const reader = new FileReader();
                reader.onload = (function (o) {
                    return function (e) {
                        // Get the image file as a buffer
                        var buf = new Uint8Array(e.currentTarget.result);

                        // Get the RAW metadata
                        var metadata = dcraw(buf, { verbose: true, identify: true }).split('\n').filter(String);

                        // Extract the thumbnail
                        var thumbnail = dcraw(buf, { extractThumbnail: true });

                        // Create thumbnail
                        var blob = new Blob([thumbnail], { type: "image/jpeg" });
                        var urlCreator = window.URL || window.webkitURL;
                        var imageUrl = urlCreator.createObjectURL(blob);
                        li.innerHTML = replaceSpinner(li.innerHTML, `<img class="preview ms-auto" src="${imageUrl}" height="70" style="max-height: 70px;">`);
                    };
                })(file);
                reader.readAsArrayBuffer(file);
            } else if (isVideo(file)) {
                li.innerHTML = replaceSpinner(li.innerHTML, `<video src="${URL.createObjectURL(file)}" class="ms-auto" controls height="70" style="max-height: 70px;"></video>`);
            } else {
                li.innerHTML = replaceSpinner(li.innerHTML, `<span class="preview ms-auto">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, i) => {
                if (file.name !== fileToDelete)
                    dt.items.add(file)

                filesField.files = dt.files // this will trigger a change event
            });

            li.remove();
        }

        function makeNestedSortable() {
            nestedSortables = [].slice.call(document.querySelectorAll('.nested-sortable'));
            console.log(nestedSortables.length);

            for (var i = 0; i < nestedSortables.length; i++) {
                new Sortable(nestedSortables[i], {
                    group: 'nested',
                    animation: 150,
                    fallbackOnBody: true,
                    // invertSwap: true,
                    swapThreshold: 0.5
                });
            }

        }
    </script>
</body>

</html>

Solution

  • Solution

    It took a while but after removing alot i found what was causing the problem.

    The actual problem wasnt the ul inside of the sortable. what the actual problem was is the replace of the spinner.

    <!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;
        document.addEventListener('DOMContentLoaded', function () {
            filesField = document.getElementById("files");
            filesSelected = document.getElementById("files-selected");
    
            filesField.addEventListener('change', function (e) {
                var files = e.target.files;
    
                var reader = [];
                var filenames = Array.from(files).map(file => file.name);
    
                filesSelected.querySelector('.very-first-parent').innerHTML = '';
                filenames.forEach((filename, index) => {
                    // alert('file selected');
                    var file = files[index];
                    var li = document.createElement('li');
                    li.className = 'list-group-item';
                    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="spinner spinner-border ms-auto" role="status"><span class="visually-hidden">Loading...</span></div>
                        </div>
                        <ul style="height: 100px;" class="list-group nested-sortable nested-2"></ul>
                        `;
                    showPreview(file, li);
                    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 replaceSpinner(element_old, string_new) {
          const parser = new DOMParser();
          string_new = parser.parseFromString(string_new, 'text/html').body.firstChild;
          let elementChanged = element_old.getElementsByClassName('spinner')[0];
          return elementChanged.replaceWith(string_new).innerHTML;
        }
    
        function showPreview(file, li) {
            if (isImage(file)) {
                const reader = new FileReader();
                reader.onload = function (e) {
                    li.innerHTML = replaceSpinner(li, `<img class="preview ms-auto" src="${e.target.result}" height="70" style="max-height: 70px;">`);
                };
                reader.readAsDataURL(file);
            } else if (isRawImage(file)) {
                // You can use a library like raw.js to decode raw images and show a preview
                const reader = new FileReader();
                reader.onload = (function (o) {
                    return function (e) {
                        // Get the image file as a buffer
                        var buf = new Uint8Array(e.currentTarget.result);
    
                        // Get the RAW metadata
                        var metadata = dcraw(buf, { verbose: true, identify: true }).split('\n').filter(String);
    
                        // Extract the thumbnail
                        var thumbnail = dcraw(buf, { extractThumbnail: true });
    
                        // Create thumbnail
                        var blob = new Blob([thumbnail], { type: "image/jpeg" });
                        var urlCreator = window.URL || window.webkitURL;
                        var imageUrl = urlCreator.createObjectURL(blob);
                        li.innerHTML = replaceSpinner(li, `<img class="preview ms-auto" src="${imageUrl}" height="70" style="max-height: 70px;">`);
                    };
                })(file);
                reader.readAsArrayBuffer(file);
            } else if (isVideo(file)) {
                li.innerHTML = replaceSpinner(li, `<video src="${URL.createObjectURL(file)}" class="ms-auto" controls height="70" style="max-height: 70px;"></video>`);
            } else {
                li.innerHTML = replaceSpinner(li, `<span class="preview ms-auto">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, i) => {
                if (file.name !== fileToDelete)
                    dt.items.add(file)
    
                filesField.files = dt.files // this will trigger a change event
            });
    
            li.remove(); 
        } 
    
        function makeNestedSortable() {
          nestedSortables = [].slice.call(document.querySelectorAll('.nested-sortable'));
          console.log(nestedSortables.length);
        
          for (var i = 0; i < nestedSortables.length; i++) {
            new Sortable(nestedSortables[i], {
              group:'nested' ,
              animation: 150,
              fallbackOnBody: true,
              invertSwap: true,
              swapThreshold: 0.5
            });
          }
        
        }
    </script>
      </body>
    </html>

    I know the snipper is realy long this is because it took a while editing and at first i was editing your test example. so i dont know what i did change and what i didnt. what was important is the reload spinner function:

        function replaceSpinner(element_old, string_new) {
          const parser = new DOMParser();
          string_new = parser.parseFromString(string_new, 'text/html').body.firstChild;
          let elementChanged = element_old.getElementsByClassName('spinner')[0];
          return elementChanged.replaceWith(string_new).innerHTML;
        }
    

    and how this is called/defined: li.innerHTML = replaceSpinner(li, '<img class="preview ms-auto" src="${imageUrl}" height="70" style="max-height: 70px;">');/<div class="spinner spinner-border ms-auto" role="status"><span class="visually-hidden">Loading...</span></div>. without the spinnner comments arround it. LOOKOUT: tjis example has quotes arround the img tag it should have been back-ticks but that ruins SO formatting.

    Simple solution

    You seem to have made a little mistake in your code that creates this problem your current code is:

    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>
          <!--spinner--><div class="spinner spinner-border ms-auto" role="status">
             <span class="visually-hidden">Loading...</span>
          </div><!--/spinner-->
       </div>
       <ul class="list-group nested-sortable"></ul> <-----small mistake i believe
    `;
    

    what the mistake is that the added image or somethinge else also has an list insifr himself where it can be added on. So what happens is that the li tries to change place within the ul but also tries to place the li in the ul that is inside the li. this might be confusing but if you remove this line the flickering wil be helped or have the ul that is inside the li be added to another group that isnt 'nested' so it they are also lists but the parents cant be added to it.

    if you want them to be able to be inside each other

    You should consider removing and saving the ul inside the li while dragging and add it again when when its dropped.