drag-and-drop
drag-and-drop copied to clipboard
Feature request: Add list item cloning
Really like this library and the data-first approach. However I would like to request a new feature; cloning list items from one list to another. My usecase is a drag-and-drop page builder where I want to be able to drag items (layouts/fields) from a sidebar into a canvas, but then keeping the original list intact, and also have sorting disabled in that list. So basically just a drag+clone item to other list. A bit like for example SortableJS does it: https://sortablejs.github.io/Sortable/#cloning.
Seems like a good plugin right @sashamilenkovic? Perhaps we could even write a first-party one. Thanks for the suggestion @joeke.
@joeke Hey there! Cloning is definitely something we want to have implemented (probably as a plugin as opposed to a core feature). The tricky part about this is, because moving these elements around are dependent on the changing of the values of a given list, it makes dealing with duplicate values a bit difficult. I wrote a component here with a custom plugin to enable cloning behavior: https://github.com/formkit/drag-and-drop/blob/main/docs/pages/playground.vue. It is written in Vue but you of course can do this in any other framework. You can check out a link for a "demo" here: https://drag-and-drop.formkit.com/playground. I'll leave this open until we get a first-party plugin (which will be a bit better than the example I wrote up there).
@sashamilenkovic @justin-schroeder Thanks for the quick responses and work on this guys, much appreciated! It looks like that would fit my use case .. I'll play around with it by integrating it into my app, and will report back with my findings. Thanks again!
here i have a code using react js, it works well:
import React, { useState } from 'react';
import { useDragAndDrop } from "@formkit/drag-and-drop/react";
import {
parents,
parentValues,
dragValues,
setParentValues,
} from "@formkit/drag-and-drop";
export function TodoList() {
// Functions and plugins
const sourceTransfer = (state, data) => {
const draggedValues = dragValues(state);
const lastParentValues = parentValues(
state.lastParent.el,
state.lastParent.data
).filter((x) => !draggedValues.includes(x));
setParentValues(state.lastParent.el, state.lastParent.data, lastParentValues);
};
const findDuplicates = (values) => {
const uniqueElements = new Set();
const duplicates = [];
values.forEach((item) => {
if (uniqueElements.has(item)) {
duplicates.push(item);
} else {
uniqueElements.add(item);
}
});
return duplicates;
};
const targetTransfer = (state, data) => {
const draggedValues = dragValues(state);
const targetParentValues = parentValues(
data.targetData.parent.el,
data.targetData.parent.data
);
const reset =
state.initialParent.el === data.targetData.parent.el &&
data.targetData.parent.data.config.sortable === false;
let targetIndex;
if ("node" in data.targetData) {
if (reset) {
targetIndex = state.initialIndex;
} else if (data.targetData.parent.data.config.sortable === false) {
targetIndex = data.targetData.parent.data.enabledNodes.length;
} else {
targetIndex = data.targetData.node.data.index;
}
targetParentValues.splice(targetIndex, 0, ...draggedValues);
} else {
targetIndex = reset
? state.initialIndex
: data.targetData.parent.data.enabledNodes.length;
targetParentValues.splice(targetIndex, 0, ...draggedValues);
}
const duplicates = findDuplicates(targetParentValues);
for (const duplicate of duplicates) {
if (!("key" in duplicate) || typeof duplicate !== "object") continue;
const index = targetParentValues.indexOf(duplicate);
const newKey = `${duplicate.key}-${Math.random()
.toString(36)
.substring(2, 15)}`;
targetParentValues[index] = {
...targetParentValues[index],
key: newKey,
};
}
setParentValues(
data.targetData.parent.el,
data.targetData.parent.data,
targetParentValues
);
};
const targetClone = (parent) => {
const parentData = parents.get(parent);
if (!parentData) return;
return {
setup() {
parentData.config.performTransfer = targetTransfer;
},
};
};
const sourceClone = (parent) => {
const parentData = parents.get(parent);
if (!parentData) return;
return {
setup() {
parentData.config.performTransfer = sourceTransfer;
},
};
};
// Initial todos and done values
const [initialTodos] = useState([
{
label: "Schedule perm",
key: "schedule-perm",
},
{
label: "Rewind VHS tapes",
key: "rewind-vhs",
},
{
label: "Make change for the arcade",
key: "make-change",
},
{
label: "Get disposable camera developed",
key: "disposable-camera",
},
{
label: "Learn C++",
key: "learn-cpp",
},
{
label: "Return Nintendo Power Glove",
key: "return-power-glove",
},
]);
const [todoList, todos] = useDragAndDrop(initialTodos, {
group: "todoList",
sortable: false,
plugins: [sourceClone],
});
const [doneValues] = useState([
{
label: "Pickup new mix-tape from Beth",
key: "mix-tape",
},
]);
const [doneList, dones] = useDragAndDrop(doneValues, {
group: "todoList",
plugins: [targetClone],
});
return (
<div>
<h2>Cloning example</h2>
<div className="group bg-slate-200 dark:bg-slate-800">
<div className="kanban-board p-px grid grid-cols-2 gap-px">
<div className="kanban-column">
<h2 className="kanban-title">ToDos</h2>
<ul ref={todoList} className="kanban-list">
{todos.map(todo => (
<li
key={todo.key}
className="kanban-item flex items-center"
>
{todo.label}
</li>
))}
</ul>
</div>
<div className="kanban-column">
<h2 className="kanban-title">Complete</h2>
<ul ref={doneList} className="kanban-list">
{dones.map(done => (
<li
key={done.key}
className="kanban-item kanban-complete flex items-center"
>
<span>{done.label}</span>
</li>
))}
</ul>
</div>
<pre style={{ fontSize: '10px', color: 'white' }}>
{JSON.stringify(todos, null, 2)}
</pre>
<pre style={{ fontSize: '10px', color: 'white' }}>
{JSON.stringify(dones, null, 2)}
</pre>
</div>
</div>
</div>
);
};
This library is amazing! Using the code provided above (thanks David!) for React, I noticed that with multiple target groups that this will clone a copy into each target while dragging, and even multiple times if you go back and forth between all the groups.
What would be the best way to prevent doing the cloning behavior until I've let go of my mouse click?