formkit / drag-and-drop

https://drag-and-drop.formkit.com
MIT License
1.29k stars 21 forks source link

Feature request: Add list item cloning #8

Open joeke opened 6 months ago

joeke commented 6 months ago

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.

justin-schroeder commented 6 months ago

Seems like a good plugin right @sashamilenkovic? Perhaps we could even write a first-party one. Thanks for the suggestion @joeke.

sashamilenkovic commented 6 months ago

@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).

joeke commented 6 months ago

@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!

DavidNavarroSaiz commented 5 months ago

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>
    );
  };