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

Sortable列表中li元素恢复不可排序类后仍可拖拽的问题排查

问题描述

我实现了一个可排序的图片列表,仅允许已上传图片的li元素排序,图片上传框(带有non-sortable-item类)始终不可排序。大部分场景下功能正常,但在删除并重新添加图片后出现异常:

复现步骤

  • 上传第一张图片,第二张上传框显示
  • 上传第二张图片,第三张上传框显示
  • 上传第三张图片,第四张上传框显示
  • 点击红色X删除第一张图片,图片消失
  • 重新上传第三张图片

此时,带有non-sortable-item类的上传框可被拖拽排序,与初始状态不符。我尝试通过以下代码刷新Sortable配置,但无效果:

$('#imageListItem_'+imageIndex).addClass('non-sortable-item');
$("#sortableList").sortable("option", "items", "li:not(.non-sortable-item)");

最小可复现代码

HTML

<ul id="sortableList" class="sortable-list">        

  <li class="non-sortable-item" id="imageListItem_1">
    <div id="imageItem_1" class="upload-image-item">

      <span onclick="deleteUploadedImage(1);" style="display:none;" id="deleteUploadImageSymbol_1" class="delete-upload-image-symbol"> X</span>
      <span id="enableFileUpload_1" class="upload-image-svg-wrapper">
        <label for="imageUploadID_1" class="custom-file-upload">
          <div class="upload-image-svg">+</div>
          <div class="upload-image-text">Choose image</div>
        </label>
        <input onchange="uploadNewImage(this, event)" imageIndex="1" id="imageUploadID_1" type="file" accept="image/*"/>
        <img id="img-upload_1" style="display:none;">
      </span>
    </div>
  </li>

  <li class="non-sortable-item" id="imageListItem_2" style="display:none;">
    <div id="imageItem_2" class="upload-image-item">

      <span onclick="deleteUploadedImage(2);" style="display:none;" id="deleteUploadImageSymbol_2" class="delete-upload-image-symbol"> X</span>
      <span id="enableFileUpload_2" class="upload-image-svg-wrapper">
        <label for="imageUploadID_2" class="custom-file-upload">
          <div class="upload-image-svg">+</div>
          <div class="upload-image-text">Choose image</div>
        </label>
        <input onchange="uploadNewImage(this, event)" imageIndex="2" id="imageUploadID_2" type="file" accept="image/*"/>
        <img id="img-upload_2" style="display:none;">
      </span>
    </div>
  </li>

  <li class="non-sortable-item" id="imageListItem_3" style="display:none;">
    <div id="imageItem_3" class="upload-image-item">

      <span onclick="deleteUploadedImage(3);" style="display:none;" id="deleteUploadImageSymbol_3" class="delete-upload-image-symbol"> X</span>
      <span id="enableFileUpload_3" class="upload-image-svg-wrapper">
        <label for="imageUploadID_3" class="custom-file-upload">
          <div class="upload-image-svg">+</div>
          <div class="upload-image-text">Choose image</div>
        </label>
        <input onchange="uploadNewImage(this, event)" imageIndex="3" id="imageUploadID_3" type="file" accept="image/*"/>
        <img id="img-upload_3" style="display:none;">
      </span>
    </div>
  </li>

  <li class="non-sortable-item" id="imageListItem_4" style="display:none;">
    <div id="imageItem_4" class="upload-image-item">

      <span onclick="deleteUploadedImage(4);" style="display:none;" id="deleteUploadImageSymbol_4" class="delete-upload-image-symbol"> X</span>
      <span id="enableFileUpload_4" class="upload-image-svg-wrapper">
        <label for="imageUploadID_4" class="custom-file-upload">
          <div class="upload-image-svg">+</div>
          <div class="upload-image-text">Choose image</div>
        </label>
        <input onchange="uploadNewImage(this, event)" imageIndex="4" id="imageUploadID_4" type="file" accept="image/*"/>
        <img id="img-upload_4" style="display:none;">
      </span>
    </div>
  </li>


</ul>

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.3/jquery.ui.touch-punch.min.js"></script>

CSS

.sortable-list{
    list-style-type: none;
    padding: 0;
    margin: 0;
    display: flex;
    justify-content: left;
    flex-flow: row wrap;
    gap: 0.5rem;
}
.upload-image-item{
    height: clamp(125px, 15vw, 200px);
    width: clamp(125px, 15vw, 200px);
    border: thin solid rgb(180 180 180);
    border-radius: 0.5rem;
    position: relative;
    color: rgb(80 80 80);
    background-repeat: no-repeat;
    background-size: contain;
    background-position: center;
    display: flex;
    justify-content: center;
    align-items: center;
}
.delete-upload-image-symbol{
    position: absolute;
    top: -10px;
    right: -10px;
    display: block;
    width: 30px;
    height: 30px;
    color: rgb(190, 30, 30);
    cursor: pointer;
}
.upload-image-svg-wrapper{
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    height: clamp(125px, 15vw, 200px);
    font-size: 13px;    
}
.custom-file-upload{
    cursor: pointer;
}
.upload-image-svg{
    display: block;
    width: clamp(30px, 5vw, 42px);
    height: clamp(30px, 5vw, 42px);
    margin: auto;
}
.featured-upload-image-text{
    position: absolute;
    width: 100%;
    font-size: 0.85rem;
    text-align: center;
    background-color: rgba(0, 100, 173, 0.9);
    border-radius: 0.4rem 0.4rem 0 0;
    color: #FFFFFF;
    align-self: flex-start;
}
input[type="file"] {
    display: none;
}

JavaScript

// Init the page
$(document).ready(function() {

    // Make list of images sortable
    $( "#sortableList" ).sortable({
      items: "li:not(.non-sortable-item)"
    });
});

function uploadNewImage(thisObject, e){

    // Find image and see if it is of the correct type
    var imageFile = e.target.files[0];

    // Find this image number, hide upload option and display the image
    var imageNumber = thisObject.getAttribute("imageIndex");    
    var tempBlobURL = URL.createObjectURL(imageFile);
    $('#enableFileUpload_'+imageNumber).css('display', 'none');
    $('#imageItem_'+imageNumber).css("background-image", "url('" + tempBlobURL + "')");
    $('#imageListItem_'+imageNumber).removeClass('non-sortable-item');
  $("#sortableList").sortable("option", "items", "li:not(.non-sortable-item)");


  // Show the delete and change image symbol
  $('#deleteUploadImageSymbol_'+imageNumber).css('display', 'block');

  // Show the next image element, if we have any more to show 
  $('*[id*=imageListItem_]:hidden').each(function() {
    $(this).css('display', 'block');
    return false;
  });

}

function deleteUploadedImage(imageIndex){

    var thisListItem = document.getElementById('imageListItem_'+imageIndex);

    // Find the image filename
    var img = document.getElementById('imageItem_'+imageIndex);
    var styles = img.currentStyle || window.getComputedStyle(img, false);
    var imageURL = styles.backgroundImage.slice(4, -1).replace(/['"]/g, "");

    // Remove image and the buttons
    $('#imageItem_'+imageIndex).css('background-image', 'none');
    $('#deleteUploadImageSymbol_'+imageIndex).css('display', 'none');

    // Hide image wrapper and put it last in the list
    var imageWrapper = $('#imageListItem_'+imageIndex);
    imageWrapper.css('display', 'none');
    $('#sortableList').find('#imageListItem_'+imageIndex).appendTo('#sortableList');

    // Show image uploader
    $('#enableFileUpload_'+imageIndex).css('display', 'flex');

    // Make it not sortable
    $('#imageListItem_'+imageIndex).addClass('non-sortable-item');
    $("#sortableList").sortable("option", "items", "li:not(.non-sortable-item)");

}

问题原因与解决方案

原因分析

jQuery UI Sortable的items选项是一次性初始化绑定的:初始化时它会匹配所有符合选择器的元素,并为这些元素绑定拖拽事件。当你后续动态修改元素类名后,仅仅更新items选项并不会自动移除已绑定的拖拽事件——那些曾经被设为可排序的元素,即使后来添加了non-sortable-item类,之前绑定的拖拽事件依然存在,导致它们依然能被拖拽。

简单说:初始状态下,带non-sortable-item的元素从未被绑定拖拽事件;但当你移除该类让它变成可排序后,Sortable为它绑定了拖拽事件,之后再添加类名,只是更新了选择器规则,却没移除已绑定的事件,所以它还是能被拖拽。

解决方案

方案1:使用cancel选项配合refresh(推荐)

改用cancel选项指定不可拖拽的元素,每次修改类名后调用refresh让Sortable重新识别元素状态:

// 初始化时调整配置
$(document).ready(function() {
    $( "#sortableList" ).sortable({
      items: "li",
      cancel: ".non-sortable-item"
    });
});

// 在uploadNewImage函数中替换原有更新代码
$('#imageListItem_'+imageNumber).removeClass('non-sortable-item');
$("#sortableList").sortable("refresh");

// 在deleteUploadedImage函数中替换原有更新代码
$('#imageListItem_'+imageIndex).addClass('non-sortable-item');
$("#sortableList").sortable("refresh");

这种方式不需要销毁重建组件,性能更优,逻辑也更清晰。

方案2:销毁后重新初始化

如果不想调整初始化配置,也可以在修改类名后先销毁Sortable,再重新初始化:

// 在uploadNewImage和deleteUploadedImage函数中替换原有更新代码
$("#sortableList").sortable("destroy");
$("#sortableList").sortable({
  items: "li:not(.non-sortable-item)"
});

这种方式直接有效,但频繁销毁重建会有一定性能开销。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 06:49:54