Open7

Svelte5のSortableJSライクなDnDコンポーネントサンプル

scirexsscirexs

グループ間移動&ドラッグハンドル

Svelte REPL

code
App.svelte
<script module lang="ts">
  import SimpleSortable, { SortableItems } from "./SimpleSortable.svelte";
</script>
<script lang="ts">
  const fruits = new Map([
    ["apple", {img:"🍎", color: "red"}],
    ["banana", {img:"🍌", color: "yellow"}],
    ["orange", {img:"🍊", color: "orange"}],
    ["grape", {img:"🍇", color: "purple"}],
    ["peach", {img:"🍑", color: "hotpink"}],
    ["melon", {img:"🍈", color: "green"}],
  ]);
  const items1 = new SortableItems(["apple","orange","peach"]);
  const items2 = new SortableItems(["banana","grape","melon"]);
  const style = {parent:"container", children:"item"}
</script>

<div class="base">
  <div class="area">
    basket1
    <SimpleSortable items={items1} draggable={false} {style}>
      {#snippet item(name,_,handler)}
        {@render inner(name,handler)}
      {/snippet}
    </SimpleSortable>
  </div>
  <div class="area">
    basket2
    <SimpleSortable items={items2} draggable={false} {style}>
      {#snippet item(name,_,handler)}
        {@render inner(name,handler)}
      {/snippet}
    </SimpleSortable>
  </div>
</div>

{#snippet inner(name: string, onpointerdown)}
  <div class="inner" style={`--color: ${fruits.get(name)?.color};`}>
    <span class="handle" {onpointerdown}>{fruits.get(name)?.img}</span> {name}
  </div>
{/snippet}

<!----- styling ----->

<style>
  .handle {
    cursor: move;
  }
  .inner {
    text-align: center;
    width: 150px;
    height: 30px;
    border: solid;
    border-width: 2px;
    border-color: var(--color);
  }
  :global(.container) {
    margin: 0px;
    padding: 0px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 10px;
    list-style-type: none;
    width: 200px;
    height: 300px;
    cursor: default;
    user-select: none;
  }
  :global(.item) {
    width: fit-contents;
    height: fit-contents;
    border-width: 0px;
  }
  .base {
    display: flex;
    gap: 20px;
  }
  .area {
    width: fit-contents;
    height: fit-contents;
    border: solid;
    border-width: 1px;
    border-color: blue;
  }
</style>
scirexsscirexs

クローン&複数選択

Svelte REPL

code
App.svelte
<script module lang="ts">
  import SimpleSortable, { SortableItems } from "./SimpleSortable.svelte";
</script>
<script lang="ts">
  const fruits = new Map([
    ["apple", {img:"🍎", color: "red"}],
    ["banana", {img:"🍌", color: "yellow"}],
    ["orange", {img:"🍊", color: "orange"}],
    ["grape", {img:"🍇", color: "purple"}],
    ["peach", {img:"🍑", color: "hotpink"}],
    ["melon", {img:"🍈", color: "green"}],
  ]);
  const items1 = new SortableItems(["apple","orange","peach"]);
  const items2 = new SortableItems(["banana","grape","melon"]);
  const style = {parent:"container", children:"item"}
</script>

<div class="base">
  <div class="area">
    basket1
    <SimpleSortable items={items1} mode={"clone"} multi={true} appendable={true} {style}>
      {#snippet item(name,selected)}
        {@render inner(name,selected)}
      {/snippet}
    </SimpleSortable>
  </div>
  <div class="area">
    basket2
    <SimpleSortable items={items2} appendable={true} {style}>
      {#snippet item(name,selected)}
        {@render inner(name,selected)}
      {/snippet}
    </SimpleSortable>
  </div>
</div>

{#snippet inner(name: string, selected: boolean)}
  <div class="inner" style={`--color: ${fruits.get(name)?.color}; --bg: ${selected ? "gray" : "transparent"}`}>
    <span>{fruits.get(name)?.img}</span> {name}
  </div>
{/snippet}

<!----- styling ----->

<style>
  .inner {
    text-align: center;
    width: 150px;
    height: 30px;
    border: solid;
    border-width: 2px;
    border-color: var(--color);
    background-color: var(--bg);
  }
  :global(.container) {
    margin: 0px;
    padding: 0px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 10px;
    list-style-type: none;
    width: 200px;
    height: 300px;
    cursor: default;
  }
  :global(.item) {
    width: fit-contents;
    height: fit-contents;
    border-width: 0px;
  }
  .base {
    display: flex;
    gap: 20px;
  }
  .area {
    width: fit-contents;
    height: fit-contents;
    border: solid;
    border-width: 1px;
    border-color: blue;
  }
</style>
scirexsscirexs

スワップ&ゴースト設定

Svelte REPL

code
App.svelte
<script module lang="ts">
  import SimpleSortable, { SortableItems } from "./SimpleSortable.svelte";
</script>
<script lang="ts">
  const fruits = new Map([
    ["apple", {img:"🍎", color: "red"}],
    ["banana", {img:"🍌", color: "yellow"}],
    ["orange", {img:"🍊", color: "orange"}],
    ["grape", {img:"🍇", color: "purple"}],
    ["peach", {img:"🍑", color: "hotpink"}],
    ["melon", {img:"🍈", color: "green"}],
  ]);
  const items1 = new SortableItems(["apple","orange","peach"]);
  const items2 = new SortableItems(["banana","grape","melon"]);
  const style = {parent:"container", children:"item"}
</script>

<div class="base">
  <div class="area">
    basket1
    <SimpleSortable items={items1} mode={"swap"} {ghost} {style}>
      {#snippet item(name,selected)}
        {@render inner(name,selected)}
      {/snippet}
    </SimpleSortable>
  </div>
  <div class="area">
    basket2
    <SimpleSortable items={items2} mode={"swap"} {ghost} {style}>
      {#snippet item(name,selected)}
        {@render inner(name,selected)}
      {/snippet}
    </SimpleSortable>
  </div>
</div>

{#snippet inner(name: string, selected: boolean)}
  <div class="inner" style={`--color: ${fruits.get(name)?.color}; background-color: ${selected ? "gray" : "transparent"};`}>
    {fruits.get(name)?.img} {name}
  </div>
{/snippet}
{#snippet ghost(name)}
  <div class="ghost">{fruits.get(name)?.img}</div>
{/snippet}

<!----- styling ----->

<style>
  .ghost {
    width: fit-contents;
    height: fit-contents;
    font-size: 60px;
  }
  .inner {
    text-align: center;
    width: 150px;
    height: 30px;
    border: solid;
    border-width: 2px;
    border-color: var(--color);
  }
  :global(.container) {
    margin: 0px;
    padding: 0px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 10px;
    list-style-type: none;
    width: 200px;
    height: 300px;
    cursor: default;
  }
  :global(.item) {
    width: fit-contents;
    height: fit-contents;
    border-width: 0px;
  }
  .base {
    display: flex;
    gap: 20px;
  }
  .area {
    width: fit-contents;
    height: fit-contents;
    border: solid;
    border-width: 1px;
    border-color: blue;
  }
</style>
scirexsscirexs

移動・ソート制限&確定時間設定

Svelte REPL

code
App.svelte
<script module lang="ts">
  import SimpleSortable, { SortableItems } from "./SimpleSortable.svelte";
</script>
<script lang="ts">
  const fruits = new Map([
    ["apple", {img:"🍎", color: "red"}],
    ["banana", {img:"🍌", color: "yellow"}],
    ["orange", {img:"🍊", color: "orange"}],
    ["grape", {img:"🍇", color: "purple"}],
    ["peach", {img:"🍑", color: "hotpink"}],
    ["melon", {img:"🍈", color: "green"}],
  ]);
  const items1 = new SortableItems(["apple","grape","melon"]);
  const items2 = new SortableItems(["banana","peach"]);
  const items3 = new SortableItems(["orange"]);
  const style = {parent:"container", children:"item"}
</script>

<div class="base">
  <div class="area">
    want
    <SimpleSortable items={items1} name="want" accept={["eat"]} appendable={true} {style}>
      {#snippet item(name,selected)}
        {@render inner(name,selected)}
      {/snippet}
    </SimpleSortable>
  </div>
  <div class="area">
    bought
    <SimpleSortable items={items2} name="buy" accept={["want"]} confirm={true} {style}>
      {#snippet item(name,selected)}
        {@render inner(name,selected)}
      {/snippet}
    </SimpleSortable>
  </div>
  <div class="area">
    ate
    <SimpleSortable items={items3} mode={"clone"} name="eat" accept={["buy"]} confirm={true} {style}>
      {#snippet item(name,selected)}
        {@render inner(name,selected)}
      {/snippet}
    </SimpleSortable>
  </div>
</div>

{#snippet inner(name: string, selected: boolean)}
  <div class="inner" style={`--color: ${fruits.get(name)?.color}; background-color: ${selected ? "gray" : "transparent"};`}>
    {fruits.get(name)?.img} {name}
  </div>
{/snippet}

<!----- styling ----->

<style>
  .inner {
    text-align: center;
    width: 150px;
    height: 30px;
    border: solid;
    border-width: 2px;
    border-color: var(--color);
  }
  :global(.container) {
    margin: 0px;
    padding: 0px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 10px;
    list-style-type: none;
    width: 200px;
    height: 300px;
    cursor: default;
  }
  :global(.item) {
    width: fit-contents;
    height: fit-contents;
    border-width: 0px;
  }
  .base {
    display: flex;
    gap: 20px;
  }
  .area {
    width: fit-contents;
    height: fit-contents;
    border: solid;
    border-width: 1px;
    border-color: blue;
  }
</style>
scirexsscirexs

データ変化時に追加処理

Svelte REPL

code
App.svelte
<script module lang="ts">
  import SimpleSortable, { SortableItems } from "./SimpleSortable.svelte";
</script>
<script lang="ts">
  const fruits = new Map([
    ["apple", {img:"🍎", color: "red"}],
    ["banana", {img:"🍌", color: "yellow"}],
    ["orange", {img:"🍊", color: "orange"}],
    ["grape", {img:"🍇", color: "purple"}],
    ["peach", {img:"🍑", color: "hotpink"}],
    ["melon", {img:"🍈", color: "green"}],
  ]);
  const items1 = new SortableItems(["apple","orange","peach"]);
  const items2 = new SortableItems(["banana","grape","melon"]);
  const style = {parent:"container", children:"item"}

  let len = items1.length;
  let event = $state("");
  $effect(() => { items1.updated;
    if (items1.dragging && items1.length === len) {
      event = "SORT";
    } else if (items1.dragging && items1.length < len) {
      event = "REMOVE";
      len = items1.length
    } else if (items1.dragging && items1.length > len) {
      event = "ADD";
      len = items1.length
    }
  });
</script>

<div class="base" style={`--cursor: ${items1.active ? "grabbing" : "grab"};`}>
  <div class="area">
    basket1
    <SimpleSortable items={items1} {style}>
      {#snippet item(name,selected)}
        {@render inner(name,selected)}
      {/snippet}
    </SimpleSortable>
  </div>
  <div class="area">
    basket2
    <SimpleSortable items={items2} {style}>
      {#snippet item(name,selected)}
        {@render inner(name,selected)}
      {/snippet}
    </SimpleSortable>
  </div>
</div>
<p>{items1.active ? "ACTIVATE" : ""}</p>
<p>{items1.dragging ? "DRAGGING" : ""}</p>
<p>{items1.dragging ? event : ""}</p>

{#snippet inner(name: string, selected: boolean)}
  <div class="inner" style={`--color: ${fruits.get(name)?.color}; background-color: ${selected ? "gray" : "transparent"};`}>
    {fruits.get(name)?.img} {name}
  </div>
{/snippet}

<!----- styling ----->

<style>
  .inner {
    text-align: center;
    width: 150px;
    height: 30px;
    border: solid;
    border-width: 2px;
    border-color: var(--color);
  }
  :global(.container) {
    margin: 0px;
    padding: 0px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 10px;
    list-style-type: none;
    width: 200px;
    height: 300px;
    cursor: inherit;
  }
  :global(.item) {
    width: fit-contents;
    height: fit-contents;
    border-width: 0px;
    cursor: inherit;
  }
  .base {
    display: flex;
    gap: 20px;
    cursor: var(--cursor);
  }
  .area {
    width: fit-contents;
    height: fit-contents;
    border: solid;
    border-width: 1px;
    border-color: blue;
    cursor: inherit;
  }
</style>
scirexsscirexs

親からデータ編集

Svelte REPL

code
App.svelte
<script module lang="ts">
  import SimpleSortable, { SortableItems } from "./SimpleSortable.svelte";
</script>
<script lang="ts">
  const fruits = new Map([
    ["apple", {img:"🍎", color: "red"}],
    ["banana", {img:"🍌", color: "yellow"}],
    ["orange", {img:"🍊", color: "orange"}],
    ["grape", {img:"🍇", color: "purple"}],
    ["peach", {img:"🍑", color: "hotpink"}],
    ["melon", {img:"🍈", color: "green"}],
  ]);
  const items1 = new SortableItems(["apple","orange","peach"]);
  const items2 = new SortableItems(["banana","grape","melon"]);
  const style = {parent:"container", children:"item"}

  function onclick() {
    items1.push("apple");
  }
</script>

<button type="button" {onclick}>add apple</button>
<div class="base">
  <div class="area">
    basket1
    <SimpleSortable items={items1} {style}>
      {#snippet item(name,selected)}
        {@render inner(name,selected)}
      {/snippet}
    </SimpleSortable>
  </div>
  <div class="area">
    basket2
    <SimpleSortable items={items2} {style}>
      {#snippet item(name,selected)}
        {@render inner(name,selected)}
      {/snippet}
    </SimpleSortable>
  </div>
</div>

{#snippet inner(name: string, selected: boolean)}
  <div class="inner" style={`--color: ${fruits.get(name)?.color}; background-color: ${selected ? "gray" : "transparent"};`}>
    {fruits.get(name)?.img} {name}
  </div>
{/snippet}

<!----- styling ----->

<style>
  .inner {
    text-align: center;
    width: 150px;
    height: 30px;
    border: solid;
    border-width: 2px;
    border-color: var(--color);
  }
  :global(.container) {
    margin: 0px;
    padding: 0px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 10px;
    list-style-type: none;
    width: 200px;
    height: 300px;
    cursor: inherit;
  }
  :global(.item) {
    width: fit-contents;
    height: fit-contents;
    border-width: 0px;
    cursor: inherit;
  }
  .base {
    display: flex;
    gap: 20px;
    cursor: default;
  }
  .area {
    width: fit-contents;
    height: fit-contents;
    border: solid;
    border-width: 1px;
    border-color: blue;
    cursor: inherit;
  }
</style>
scirexsscirexs

グループ間順序同期

Svelte REPL

code
App.svelte
<script module lang="ts">
  import SimpleSortable, { SortableItems } from "./SimpleSortable.svelte";
</script>
<script lang="ts">
  const fruits = new Map([
    ["apple", {img:"🍎", color: "red"}],
    ["banana", {img:"🍌", color: "yellow"}],
    ["orange", {img:"🍊", color: "orange"}],
    ["grape", {img:"🍇", color: "purple"}],
    ["peach", {img:"🍑", color: "hotpink"}],
    ["melon", {img:"🍈", color: "green"}],
  ]);
  const items = new SortableItems([...fruits.keys()]);
  const style = {parent:"container", children:"item"}
</script>

<div class="base">
  <div class="area">
    image
    <SimpleSortable items={items} accept={[]} {style}>
      {#snippet item(name,selected)}
        <div class="inner" style={`--color: ${fruits.get(name)?.color}; background-color: ${selected ? "gray" : "transparent"};`}>
          {fruits.get(name)?.img}
        </div>
      {/snippet}
    </SimpleSortable>
  </div>
  <div class="area">
    color
    <SimpleSortable items={items} accept={[]} {style}>
      {#snippet item(name,selected)}
        <div class="inner" style={`--color: ${fruits.get(name)?.color}; background-color: ${selected ? "gray" : "transparent"};`}>
          {fruits.get(name)?.color}
        </div>
      {/snippet}
    </SimpleSortable>
  </div>
</div>

<!----- styling ----->

<style>
  .inner {
    text-align: center;
    width: 150px;
    height: 30px;
    border: solid;
    border-width: 2px;
    border-color: var(--color);
  }
  :global(.container) {
    margin: 0px;
    padding: 0px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 10px;
    list-style-type: none;
    width: 200px;
    height: 300px;
    cursor: inherit;
  }
  :global(.item) {
    width: fit-contents;
    height: fit-contents;
    border-width: 0px;
    cursor: inherit;
  }
  .base {
    display: flex;
    gap: 20px;
    cursor: default;
  }
  .area {
    width: fit-contents;
    height: fit-contents;
    border: solid;
    border-width: 1px;
    border-color: blue;
    cursor: inherit;
  }
</style>