Open7
Svelte5のSortableJSライクなDnDコンポーネントサンプル
グループ間移動&ドラッグハンドル
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>
クローン&複数選択
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>
スワップ&ゴースト設定
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>
移動・ソート制限&確定時間設定
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>
データ変化時に追加処理
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>
親からデータ編集
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>
グループ間順序同期
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>