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
相关产品推荐
相关产品推荐

