У меня возникли проблемы с сортировкой вложенного дерева слоев JSON с помощью SortableJS в приложении Alpine.js. У меня есть сложная структура JSON, представляющая слои, которые могут быть вложены на несколько уровней. Моя цель — отсортировать эти слои и соответствующим образом обновить структуру JSON.
Вот упрощенная версия моей структуры слоев и соответствующий код:
// Pre-define theme
document.documentElement.setAttribute('data-theme', 'dark');
function App() {
return {
data: {
layerStructure: [
{
"tag": "header",
"type": "box",
"name": "box",
"id": "wnl0mxhml",
"state": {
"collapsed": false,
"visible": true,
"selected": false
},
"props": {
"class": "p4dmvkj5v"
},
"children": [
{
"tag": "hgroup",
"type": "box",
"name": "box",
"id": "xw0wqhizc",
"state": {
"collapsed": false,
"visible": true,
"selected": false
},
"props": {
"class": "w78d2h"
},
"children": [
{
"tag": "h1",
"type": "text",
"name": "text",
"id": "orfik88na",
"state": {
"collapsed": false,
"visible": true,
"selected": false
},
"props": {
"class": "n6zv2tuar"
},
"text": "App name"
},
{
"tag": "h2",
"type": "text",
"name": "text",
"id": "lzsntwjt3",
"state": {
"collapsed": false,
"visible": true,
"selected": false
},
"props": {
"class": "xqkuxhejp"
},
"text": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Commodi accusantium rem sint voluptatum quisquam cum. Nostrum dolorum alias doloribus quod accusantium odit vero dolor excepturi cumque mollitia? Laboriosam, dolore rem!"
}
]
}
]
},
{
"tag": "main",
"type": "box",
"name": "box",
"id": "o03yq4tqx",
"state": {
"collapsed": false,
"visible": true,
"selected": false
},
"props": {
"class": "p4dmvkj5v"
},
"children": [
{
"tag": "figure",
"type": "box",
"name": "box",
"id": "ary2rnlid",
"state": {
"collapsed": false,
"visible": true,
"selected": false
},
"children": [
{
"tag": "img",
"type": "img",
"name": "img",
"id": "o4cwkc0zb",
"state": {
"collapsed": false,
"visible": true,
"selected": false
},
"props": {
"class": "cc7uwye7i",
"src": "imgs/image.webp",
"alt": "Polyrise"
}
},
{
"tag": "figcaption",
"type": "box",
"name": "box",
"id": "t57ciu00f",
"state": {
"collapsed": false,
"visible": true,
"selected": false
},
"children": [
{
"tag": "a",
"type": "text",
"name": "text",
"id": "u50q0cuz9",
"state": {
"collapsed": false,
"visible": true,
"selected": false
},
"props": {
"href": "https://michaelsboost.com/Polyrise/",
"target": "_blank"
},
"text": "michaelsboost.com/Polyrise"
}
],
"text": "Image from"
}
]
}
]
}
]
},
icons: {
move: `<svg class = "w-3" viewBox = "0 0 512 512" style = "color: unset;">
<path
fill = "currentColor"
d = "M278.6 9.4c-12.5-12.5-32.8-12.5-45.3 0l-64 64c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8h32v96H128V192c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9l-64 64c-12.5 12.5-12.5 32.8 0 45.3l64 64c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6V288h96v96H192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l64 64c12.5 12.5 32.8 12.5 45.3 0l64-64c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8H288V288h96v32c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l64-64c12.5-12.5 12.5-32.8 0-45.3l-64-64c-9.2-9.2-22.9-11.9-34.9-6.9s-19.8 16.6-19.8 29.6v32H288V128h32c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-64-64z"/>
</svg>`
},
renderLayer(layer){
let html = `<code
class = "p-0 flex justify-between whitespace-nowrap min-w-min">
<span>
<button
data-move
aria-label = "sort layer"
name = "sort layer"
class = "bg-transparent border-0 p-2 text-xs cursor-grab focus:shadow-none" style = "color: unset;">
<span x-html = "icons.move"></span>
</button>
</span>
<button
aria-label = "toggle selected layer"
name = "toggle selected layer"
class = "bg-transparent border-0 p-2 text-xs text-right capitalize"
style = "color: unset;">
<span x-html = "layer.name"></span>
</button>
</code>`;
if (layer.children) {
html += `<ul
class = "mt-1 mb-1 ml-4"
data-id = "${layer.id}">
<template x-for='layer in layer.children'>
<li class = "list-none select-none" x-html = "renderLayer(layer)" data-id = "${layer.id}"></li>
</template>
</ul>`;
}
return html;
},
init() {
// Initialize SortableJS on the element with ID 'sortableContainer'
const initializeSortable = element => {
new Sortable(element, {
group: 'shared', // Enable moving items between containers
handle: '[data-move]',
animation: 150,
fallbackOnBody: true,
swapThreshold: 0.65,
onEnd: evt => {
// Get the dragged item's ID
const draggableId = evt.item.dataset.id;
const draggableLayer = this.findLayerById(draggableId, this.data.layerStructure);
const dropId = evt.to.dataset.id;
console.info(draggableLayer);
// Get the updated order
const newOrder = Array.from(evt.from.children).map(item => item.dataset.id);
// Update the layer structure based on new order
// this.updateLayerStructure(draggableId, newOrder);
}
})
};
const containerElement = document.getElementById('sortableContainer');
this.$nextTick(() => {
initializeSortable(containerElement);
containerElement.querySelectorAll('ul').forEach(ul => initializeSortable(ul));
});
},
findLayerById(id, layers) {
for (const layer of layers) {
if (layer.id === id) return layer;
if (layer.children) {
const found = this.findLayerById(id, layer.children);
if (found) return found;
}
}
return null;
},
updateLayerStructure(id, newOrder) {
// Find the layer by ID in the structure
const layerToUpdate = this.findLayerById(id, this.data.layerStructure);
if (layerToUpdate) {
// Update the children order based on newOrder
layerToUpdate.children = newOrder.map(itemId => this.findLayerById(itemId, this.data.layerStructure));
}
// Update your output element with the updated layer structure
this.$refs.output.value = JSON.stringify(this.data.layerStructure, null, 2);
this.$refs.output.textContent = JSON.stringify(this.data.layerStructure, null, 2);
}
}
}
<link rel = "stylesheet" href = "https://cdnjs.cloudflare.com/ajax/libs/picocss/2.0.6/pico.min.css"/>
<link rel = "stylesheet" href = "https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css"/>
<script defer src = "https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
<main x-data = "App()" x-init = "init()">
<div
class = "absolute inset-0 p-2 overflow-auto"
>
<div class = "grid grid-cols-2 justify-between h-full gap-4">
<!-- layers -->
<div id = "sortableContainer">
<template x-for = "layer in data.layerStructure">
<ul class = "mt-1 mb-1 ml-4" :data-id = "layer.id">
<li class = "list-none select-none" x-html = "renderLayer(layer)" :data-id = "layer.id"></li>
</ul>
</template>
</div>
<!-- json -->
<textarea x-ref = "output" class = "resize-none" placeholder = "json code here" x-text = "JSON.stringify(data.layerStructure, null, 2)"></textarea>
</div>
</main>
<script src = "https://michaelsboost.com/TailwindCSSMod/tailwind-mod.min.js"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.2/Sortable.min.js"></script>
Я попробовал несколько подходов к решению этой проблемы, в том числе:
Кроме того, я наткнулся на соответствующую публикацию о переполнении стека, в которой пользователь предоставил решение с использованием SortableJS для сериализации вложенных списков в JSON. Вот ссылка на их подход: SortableJS Получить заказ из вложенного списка.
Несмотря на эти попытки, мне не удалось добиться желаемой функциональности прямого обновления определенных узлов с HTML на JSON. Не могли бы вы предложить какие-либо альтернативные подходы или решения, позволяющие правильно работать сортировке вложенных слоев JSON с SortableJS в Alpine.js?
где вы устанавливаете идентификатор данных?
// Get the updated order
const newOrder = Array.from(evt.from.children).map(item => item.dataset.id);
// Update the layer structure based on new order
this.updateLayerStructure(evt.item.dataset.id, newOrder);
Кроме того, похоже, что это возвращает тег шаблона, а не li,
evt.from.children
-- РЕДАКТИРОВАТЬ --
ок, думаю, у меня это работает.
Я уменьшил набор данных, чтобы было легче увидеть изменения.
Я удалил шаблон alpine из функции renderLayer и изменил отображение Layer.name, так как иногда он показывал неверные данные. Для тестирования я изменил Layer.name на Layer.id.
Я изменил инициализациюSortable несколькими способами: теперь она сохраняет сортируемый объект в массиве, чтобы получить порядок сортировки каждого сортируемого объекта для построения выходных данных, а onEnd был изменен для использования новой функции обработки.
findLayerById был изменен, поскольку он постоянно обновлял data.layerStructure при использовании, что приводило к сбою страницы, поскольку он использовался в шаблоне alpine x-for.
updateLayerStructure был удален, так как не удалось обновить data.layerStructure, поскольку он обновил шаблон alpine и испортил отображение.
ProcessSortable3 был создан для генерации вывода (да, третья попытка)
init был обновлен для отображения первого сгенерированного вывода.
У меня есть рабочий пример https://codepen.io/terreporter/pen/WNqrWga
просто нужно иметь способ добавлять новые элементы в список, и я думаю, что это был бы хороший функциональный вложенный редактор перетаскивания.
@MichaelSchwartz внес некоторые изменения :)
Object.entries — интересный подход к работе с массивом. Я не думал об этом. Хорошая работа! Мне нужно, чтобы функция onclick работала, чтобы теперь нацеливаться на слой для управления состоянием, но сортировка - это то, что меня застало. Это экспериментальное перо, над которым я работаю над мелочами в бесплатном конструкторе веб-сайтов с открытым исходным кодом, который я создаю для мобильных устройств. - codepen.io/michaelsboost/pen/mdZeZBV
Выглядит потрясающе, я попробую превратить его в конструктор меню для проекта, над которым я работаю, который также будет с открытым исходным кодом.
Это репозиторий GitHub. Новую версию еще не выпустил. Он еще не готов, но у вас есть ссылка, когда он будет готов github.com/michaelsboost/Polyrise
хорошо, спасибо. Я изменил функцию findByLayer с Object.assign на JSON.parse(JSON.stringify(layer)). поскольку он все еще не создавал реальную копию объекта, а некоторые дочерние свойства копировались по ссылке.
@MichaelSchwartz Мне пришлось внести некоторые изменения в некоторые функции, чтобы обновить данные меню. codepen.io/terreporter/pen/NWZNMYY
По Ли и ул. Я заметил, что сегодня при перетаскивании он даже не нацелен на правильный JSON, и я пока не знаю, почему. Я буду дома через 3,5 часа, чтобы все посмотреть поподробнее.