Списки представляют собой гибкие контейнеры с flex-wrap: Wrap, поэтому элементы размещаются в строках и могут переноситься на следующую строку.
Элементы можно перетаскивать между списками, а также в другие элементы, которые действуют как родительские (они содержат собственный вложенный список). В целом это работает нормально, но есть одна UX-проблема, которую я не могу понять, как правильно решить.
Когда я хватаю самый правый элемент строки и пытаюсь поместить его в родительский элемент, расположенный в следующей строке, макет меняется во время перетаскивания. Как только перетаскиваемый элемент покидает исходное положение, flexbox пересчитывает макет, и целевой родительский элемент может переместиться в другую строку. Визуально я стремлюсь к одной позиции, но во время перетаскивания элемент, который я хочу поместить, смещается под курсором, из-за чего очень сложно попасть в предполагаемый родительский элемент.
Чтобы уменьшить скачки макета, я попробовал использовать невидимую прокладку, которая удерживает исходный контейнер от разрушения во время перетаскивания элемента. Прокладка вставляется только тогда, когда перетаскивание покидает исходную иерархию (а не при перемещении внутри одной и той же родительской/дочерней структуры). В некоторых случаях это помогает, но все равно не полностью предотвращает смещение целевого родителя при использовании обернутых строк.
Код: Выделить всё
const containers = document.querySelectorAll(".list-group");
let spacer = null;
let originContainer = null;
let originNextSibling = null;
containers.forEach((container) => {
new Sortable(container, {
group: "nested",
direction: "horizontal",
animation: 150,
swapThreshold: 0.6,
invertSwap: true,
emptyInsertThreshold: 10,
fallbackOnBody: true,
onStart(evt) {
originContainer = evt.from;
originNextSibling = evt.item.nextSibling;
const item = evt.item;
const rect = item.getBoundingClientRect();
const style = getComputedStyle(item);
spacer = document.createElement("div");
spacer.className = "layout-preserver";
spacer.style.width = rect.width + "px";
spacer.style.height = rect.height + "px";
spacer.style.margin = style.margin;
spacer.style.display = style.display;
document.body.style.cursor = "grabbing";
},
onChange(evt) {
if (!spacer) return;
const toContainer = evt.to;
const isHierarchyMove =
originContainer === toContainer ||
originContainer.contains(toContainer) ||
toContainer.contains(originContainer);
if (isHierarchyMove) {
if (spacer.parentNode) {
spacer.remove();
}
} else {
if (!spacer.parentNode && originContainer) {
originContainer.insertBefore(spacer, originNextSibling);
}
}
},
onEnd() {
if (spacer && spacer.parentNode) {
spacer.remove();
}
spacer = null;
originContainer = null;
originNextSibling = null;
document.body.style.cursor = "default";
}
});
});Код: Выделить всё
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
padding: 20px;
background-color: #f3f3f3;
}
h1 {
text-align: center;
margin-bottom: 30px;
}
/* ------------------------------
LIST CONTAINERS
------------------------------ */
.list-group {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
align-content: flex-start;
gap: 10px;
padding: 15px;
min-height: 80px;
background-color: #e0e0e0;
border-radius: 8px;
border: 1px dashed #aaa;
}
/* Nested container */
.nested-list {
margin-top: 10px;
min-height: 20px;
padding: 10px;
background-color: rgba(255, 255, 255, 0.6);
border: 1px dotted #999;
}
/* ------------------------------
ITEMS
------------------------------ */
.list-item {
position: relative;
display: flex;
flex-direction: column;
padding: 15px;
min-width: 50px;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 4px;
cursor: grab;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}
/* Nested items */
.list-item.nested {
background-color: #d1e7dd;
border-color: #a3cfbb;
}
/* Parent items */
.parent-item {
background-color: #fff3cd;
border-color: #ffecb5;
}
/* ------------------------------
SORTABLE HELPERS
------------------------------ */
/* Ghost (placeholder from SortableJS) */
.sortable-ghost {
visibility: hidden;
}
/* Dragging element */
.sortable-drag {
cursor: grabbing;
opacity: 0.9;
}
/* Layout spacer (our placeholder) */
.layout-preserver {
visibility: hidden;
pointer-events: none;
flex: 0 0 auto;
box-sizing: border-box;
}Код: Выделить всё
Horizontal Nested Sortable
Nested Sortable
Item element 1
Item element 2
[b]Item 3 (Parent)[/b]
Sub-item 3.1
Sub-item 3.2
Sub-item 3.3
Sub-item 3.4
Item element 4
[b]Item 5 (Parent)[/b]
Item element 6
Как можно предотвратить такое смещение макета при использовании SortableJS с горизонтальными списками и гибкой оберткой?
Есть ли лучший подход, чем использование разделителя, чтобы сохранить визуальную стабильность целевых элементов во время перетаскивания?
Подробнее здесь: https://stackoverflow.com/questions/798 ... -item-of-a
Мобильная версия