10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Svelte でドラッグ可能なカードの作成

Last updated at Posted at 2023-02-08

はじめに

Svelte の勉強として REPL でドラッグ可能なカードを作成してみました。最初はシンプルなものだったのですが、使い勝手を上げたいといろいろいじってみたところ、いろんな機能を試すことができました。
例:

  • Context を使って領域毎に z-index(重なり順) を管理
  • $ によるリアクティブな位置修正
  • <slot> に対するスタイルの適用

実装ファイル一覧

ファイル名 説明
App.svelte アプリ本体。
Card.svelte 単純なカードコンポーネント。
DraggableArea.svelte ドラッグ領域を表すコンポーネント。子要素の重なり順を管理。
Draggable.svelte 子要素をドラッグ可能にするコンポーネント。

使い方

こんな感じを想定しています。

サンプル
<!-- 領域その1 -->
<DraggableArea>
  <Draggable><Card color="red"/></Draggable>
  <Draggable><Card color="green"/></Draggable>
  <Draggable><Card color="blue"/></Draggable>
</DraggableArea>
<!-- 領域その2 -->
<DraggableArea>
  <Draggable>あいうえお</Draggable>
  <Draggable>かきくけこ</Draggable>
  <Draggable>さしすせそ</Draggable>
</DraggableArea>

実装

Card.svelte

Card.svelte
<script>
  export let color = "";
</script>

<div class="card" style:background-color={color} />

<style>
 .card {
    width: 100px;
    height: 50px;
    border-radius: 0.5rem;
    border-style: solid;
    border-width: 2px;
    border-color:rgba(0,0,0,0.1);
    box-sizing: border-box;
  }
</style>

DraggableArea.svelte

DraggableArea.svelte
<script>
  import {setContext} from 'svelte'
  
  let area;
  let items = [];
  setContext("area", {
    add: (item) => {
      items.push(item);
    },
    toFront: (item) => {
      items = items.filter(c => c !== item);
      Array.prototype.push.apply(items, [item]);
      items.forEach((item, i) => {
        item.style.zIndex = i + 1;
      });
    },
    remove: (item) => {
      items = items.filter(c => c !== item);
    },
    getWidth: () => {
      return area.clientWidth;
    },
    getHeight: () => {
      return area.clientHeight;
    }
  });
</script>

<div bind:this={area}>
  <slot/>
</div>

<style>
  div {
    position: relative;
  }
</style>

ポイント

  • items は重なり順を管理するための変数で、<DraggableArea> 毎に用意されます。
  • setContext(..) を使って子要素に、add、toFront、remove、getWidth、getHeight、関数を提供することで、子要素インスタンスから重なり具合を調整できるようになっています。
  • area 変数を <div> にバインドすることで area.clientWidth という形で動的に横幅を取得できます。

Draggable.svelte

Draggable.svelte
<script>
  import {onMount, getContext} from 'svelte';
  export let x = 0;
  export let y = 0;
  
  let areaContext= getContext("area");
  let item;
  const position = { x: parseInt(x), y: parseInt(y) }; // 要素の位置
  const diff = {x: 0, y: 0}; // ドラッグ開始時の要素の位置とマウスの位置の差
  let isCatched = false;
  
  function handleMousemove(event) {
    if (isCatched) {
      position.x = parseFloat(event.clientX) - diff.x;
      position.y = parseFloat(event.clientY) - diff.y;
    }
  }

  // position が変更されたら領域内に収まるように調整する
  $: {
    position; // これを記載しないと position が変更されても stickInArea() は実行されない。(センスが悪い...)
    stickInArea()
  };
  
  function stickInArea() {
    if (!item) {
      return;
    }
    // エリアをはみ出さないように調整
    if (position.x < 0) {
      position.x = 0;
    } else if (position.x + item.clientWidth > areaContext.getWidth()) {
      position.x = areaContext.getWidth() - item.clientWidth;
    }
    if (position.y < 0) {
      position.y = 0;
    } else if (position.y + item.clientHeight > areaContext.getHeight()) {
      position.y = areaContext.getHeight() - item.clientHeight;
    }
  }
  
  function handleCatch(event) { 
    isCatched = true;
    diff.x = parseFloat(event.clientX) - parseFloat(position.x);
    diff.y = parseFloat(event.clientY) - parseFloat(position.y);
    areaContext.toFront(item);
  }

  function handleRelease(event) {
    isCatched = false;
  }

  onMount(() => {
    areaContext.add(item);
    return () => {
      areaContext.remove(item);
    };
  })
</script>

<svelte:window on:mousemove={handleMousemove} on:mouseup={handleRelease} on:resize={stickInArea}></svelte:window>

<div class="item" class:isCatched bind:this={item}
     style:top={position.y + "px"}
     style:left={position.x + "px"}
     on:mousedown|stopPropagation={handleCatch}
     >
  <slot/>
</div>

<style>
  .item {
    position: absolute;
    transition: box-shadow 0.3s, transform 0.3s;
    cursor: grab;
    user-select: none;
  }
  .item.isCatched > :global(*) {
    box-shadow: 0 0.25rem 1rem 0 rgba(0,0,0,0.5);
  }
  .item.isCatched {
    transform: scale(1.1);
    cursor: grabbing;
  }
</style>

ポイント

  • position が変更されたら stickInArea() が実行され、<DraggableArea> 内に表示されるように調整されます。
    $: を使うことでリアクティブに実行されます。(React でいうところの useState useEffect みたいなやつです)
  • ドラッグ中の子要素(<slot/>)のスタイルは .item.isCatched > :global(*) のように :global を使って指定できます。
  • <svelte:window> を使うことで、ウィンドウのイベントリスナを設定できます。

App.svelte

App.svelte
<script>
  import DraggableArea from './DraggableArea.svelte';
  import Draggable from './Draggable.svelte';
  import Card from './Card.svelte';  
</script>
<div class="main">
  <DraggableArea>
    <Draggable x=50 y=50><Card color="rgba(255,0,0,0.9)"/></Draggable>
    <Draggable x=80 y=80><Card color="rgba(0,255,0,0.9)"/></Draggable>
    <Draggable x=110 y=110><Card color="rgba(0,0,255,0.9)"/></Draggable>
  </DraggableArea>
  <DraggableArea>
    <Draggable x=50 y=50>あいうえお</Draggable>
    <Draggable x=80 y=80>かきくけこ</Draggable>
    <Draggable x=110 y=110>さしすせそ</Draggable>
  </DraggableArea>
</div>

<style>
  .main {
    display:flex;
    flex-direction: row;
    height: 100%;
  }
  .main > :global(*) {
    width: 100%;
    background-color:rgba(100,100,120,0.2);
  }
  .main > :global(*:nth-child(odd)) {
    background-color:rgba(100,100,120,0.8);
  }
</style>

動作確認

こちら で動作確認できます。
こんな感じで表示されるはずです。
image.png

まとめ

Svelte の REPL を使って簡単にドラッグ可能なカードを作成することができました。また、DraggableArea を導入することでドラッグ可能な範囲を制限することも実現できました。

REPL を使うと簡単に動作確認できるし、GitHub アカウントと連携すれば保存することもできるのでお勧めです。簡単にいろいろ試すことができておもしろいです。:smiley:

10
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?