0.0.1 • Published 2 months ago

svelte-pragmatic-list v0.0.1

Weekly downloads
-
License
-
Repository
-
Last release
2 months ago

Svelte-pragmatic-list

Opinionated simple and headless DND list reordering action for Svelte. The goal here is to provide a simple and easy way to build a DND list reordering in Svelte. This a single action to put on the the list container to allow every child to be draggable and reorderable.

Svelte-pragmatic-list uses pragmatic-dnd as DND engine which is known to be fast and lightweight.

It makes no assumptions about the structure of the list items neither the style of the list. You can use any shape of list items and style them as you wish.

Features

  • Simple and easy to use
  • No assumptions about the list items
  • No assumptions about the style of the list
  • Nested list / board
  • Horizontal or vertical list
  • Drag handle
  • Auto scroll on window / list edge
  • Drag indicator
  • Custom drag preview
  • Cross window DND
  • Move / Copy / (Swap) modes
  • Svelte version agnostic

Installation

npm install svelte-pragmatic-list
pnpm add svelte-pragmatic-list
bun add svelte-pragmatic-list

Usage

The most basic example is the following. Just declare the list of items and use the dnd action on the container of the list. The action needs pass the items and declare the onChange callback to update them. Every child of the container will be draggable and reorderable by default.

<script>
  import { dnd } from 'svelte-pragmatic-list';
  let items = $state(['item 1', 'item 2', 'item 3', 'item 4', 'item 5']);
  // Or without the state rune if you are in Svelte 4
  // let items = ['item 1', 'item 2', 'item 3', 'item 4', 'item 5'];
</script>

<div
  use:dnd={{
    items,
    onChange: (newItems) => (items = newItems)
  }}
>
  {#each items as item}
    <div>
      {item}
    </div>
  {/each}
</div>

Using a drag handle

If you want to use a drag handle, just add a data-dnd-handle attribute to the element you want to use as a handle inside the draggable child.

  • the handle must be a direct child of the draggable element.
<script>
  import { dnd } from 'svelte-pragmatic-list';
  let items = $state(['item 1', 'item 2', 'item 3', 'item 4', 'item 5']);
</script>

<div
  use:dnd={{
    items,
    onChange: (newItems) => (items = newItems)
  }}
>
  {#each items as item}
    <div>
      <div data-dnd-handle>:::</div>
      {item}
    </div>
  {/each}
</div>

Displaying a drag indicator

If you want to display a drag indicator, just add a data-dnd-indicator attribute to the element you want to use as an indicator. The indicator will be displayed at the place between the two items where the dragged item will be dropped. Its position will be automatically updated when the dragged item is moved.

  • The indicator must be a direct child of the container.
  • The indicator must have an absolute position and the container must have any position. (svelte-pragmatic-list will add these styles for you)
  • You should add the hidden attribute to it in order to hide it when the list is mounted.
<script>
  import { dnd } from 'svelte-pragmatic-list';
  let items = $state(['item 1', 'item 2', 'item 3', 'item 4', 'item 5']);
</script>

<div
  use:dnd={{
    items,
    onChange: (newItems) => (items = newItems)
  }}
>
  {#each items as item}
    <div>
      {item}
    </div>
  {/each}
  <div data-dnd-indicator hidden style="height: 2px; background-color: red; width: 100%;"></div>
</div>

Nested list / board

You can also use svelte-pragmatic-list to create a nested list or a board. To do so just nest the dnd list inside your markup and use the type and accept options of the action.

For example, you can create a board with column and cards. The columns will accept ["column"] and have a type of column and the cards will accept ["card"] and have a type of card. This will prevent a card for being dropped in a column and a column to be dropped in a card.

<script>
  import { dnd } from 'svelte-pragmatic-list';
  let board = $state([
    {
      title: 'Column 1',
      cards: ['Card 1', 'Card 2', 'Card 3']
    },
    {
      title: 'Column 2',
      cards: ['Card 4', 'Card 5', 'Card 6']
    },
    {
      title: 'Column 3',
      cards: ['Card 7', 'Card 8', 'Card 9']
    }
  ]);
</script>

<div
  use:dnd={{
    getItems: () => board,
    onChange: (newBoard) => (board = newBoard),
    type: 'column',
    accept: ['column']
  }}
>
  {#each board as column}
    <div>
      <h2>{column.title}</h2>
      <div
        use:dnd={{
          getItems: () => column.cards,
          onChange: (newCards) => (column.cards = newCards),
          type: 'card',
          accept: ['card']
        }}
      >
        {#each column.cards as card}
          <div>
            {card}
          </div>
        {/each}
      </div>
    </div>
  {/each}
</div>

Customizing the drag preview

You can customize the drag preview with the preview options.

  • it is possible to transform and translate the default preview element with the y and x scale and rotate options.
  • it is also possible to create a custom preview element by using an element with the data-dnd-preview attribute. You can get the data of the item being dragged with getPreviewData function.
  • Use the hidden attribute to hide the default preview element.
  • The preview element must be a direct child of the container.
<script>
  import { dnd } from 'svelte-pragmatic-list';
  let items = $state(['item 1', 'item 2', 'item 3', 'item 4', 'item 5']);
  let previewData = $state(null);
</script>

<div
  use:dnd={{
    items,
    onChange: (newItems) => (items = newItems),
    preview: {
      y: 10,
      x: 10,
      scale: 1.1,
      rotate: 5,
      getPreviewData: (item) => (previewData = item)
    }
  }}
>
  {#each items as item}
    <div>
      {item}
    </div>
  {/each}

  <div data-dnd-preview hidden>
    {previewData}
  </div>
</div>

Drag modes and dynamic drag mode

You can use the mode option to set the drag mode. The available drag modes are move, copy or swap. The default mode is move.

You can also use a dynamic drag mode by using a function that returns the mode. This function will receive the item being dragged, the DOM element, and the isSameList boolean as arguments. It can be useful to change the mode based on the item being dragged or the target list or based on a key being pressed.

<script>
  import { dnd } from 'svelte-pragmatic-list';
  let items = $state(['item 1', 'item 2', 'item 3', 'item 4', 'item 5']);
</script>

<div
  use:dnd={{
    items,
    onChange: (newItems) => (items = newItems),
    mode: 'copy'
    // Or with a dynamic mode
    // mode: (item, element, isSameList) => {
    //   return isSameList ? 'move' : 'copy';
    // }
  }}
>
  {#each items as item}
    <div>
      {item}
    </div>
  {/each}
</div>

Cross window DND

You can use svelte-pragmatic-list to create a DND between two tabs or from a parent to an iframe. To do so, set externalDrag to the list that can be dragged outside of the window and externalDrop to the list that can receive the dragged item. It is possible to use all drag modes. Svelte-pragmatic-list will take care of the communication between the windows using localStorage. For example in case a swap between the two windows both state will be updated.

<!-- routes/+page.svelte -->
<script>
  import { dnd } from 'svelte-pragmatic-list';
  let draggableItems = $state(['item 1', 'item 2', 'item 3', 'item 4', 'item 5']);
  let droppableItems = $state(['item 6', 'item 7', 'item 8', 'item 9', 'item 10']);
</script>

<div
  use:dnd={{
    items: draggableItems,
    onChange: (newItems) => (draggableItems = newItems),
    externalDrag: true
  }}
>
  {#each draggableItems as item}
    <div>
      {item}
    </div>
  {/each}
</div>

<div
  use:dnd={{
    items: droppableItems,
    onChange: (newItems) => (droppableItems = newItems),
    externalDrop: true
  }}
>
  {#each droppableItems as item}
    <div>
      {item}
    </div>
  {/each}
</div>

<!-- The second list of the iframe will be able to receive the elements of the first list.  -->
<!-- The second list of the parent window will be able to receive the elements of the first list from the iframe  -->
<iframe src="/" />