[solidjs]solid一本で作る実用的なコンポーネントサンプル(code example)

# コードを試す
興味のあるコードを以下にコピぺして試すことができる
[SoildJS　Playground](https://playground.solidjs.com/)

# split panel（画面分割）
ドラッグで画面分割サイズを変えることのできるコンポーネント

https://qiita.com/mitsuki_march/items/e7e9fd6b675a27e75c41

# custom drag & drop（ドラッグ＆ドロップ）
自前のロジックを使った拡張性の高いドラッグ＆ドロップコンポーネント

```tsx
import { createSignal } from "solid-js";

const Draggable = () => {
  const [mouseCoordinates, setMouseCoordinates] = createSignal({
    x: 0,
    y: 0,
  })
  const [dragElem, setDragElem] = createSignal<{
    zIndex: number | "auto",
    position: "static" | "relative" | "absolute" | "sticky" | "fixed",
    hidden: boolean,
  }>({
    zIndex: "auto",
    position: "static",
    hidden: false,
  })

  let dragElemRef: HTMLDivElement | undefined;
  let dropElemRef: HTMLDivElement | undefined;

  let startDragHandler = (e: MouseEvent) => {
    let belowElem: Element | null

    onmousemove = (e: MouseEvent) => {
      e.preventDefault()
      setDragElem({
        zIndex: 1000,
        position: "absolute",
        hidden: true,
      })
      belowElem = document.elementFromPoint(mouseCoordinates().x, mouseCoordinates().y)
      setDragElem({
        zIndex: 1000,
        position: "absolute",
        hidden: false,
      })
      setMouseCoordinates({
        x: e.clientX,
        y: e.clientY,
      })
      if (belowElem) {
        if (belowElem === dropElemRef) {
          console.log("drag enter");
        }
      }
    }

    onmouseup = (e: MouseEvent) => {
      setDragElem({
        zIndex: "auto",
        position: "static",
        hidden: false,
      })
      if (belowElem) {
        if (belowElem === dropElemRef) {
          console.log("drop in");
        }
      }
      onmousemove = () => null
      onmouseup = () => null
    }
  }

  return (
    <>
      <div
        ref={dragElemRef}
        style={{
          "z-index": `${dragElem().zIndex}`,
          "position": `${dragElem().position}`,
          "left": `${dragElemRef ? mouseCoordinates().x - dragElemRef.offsetWidth/2 : ""}px`,
          "top": `${dragElemRef ? mouseCoordinates().y - dragElemRef.offsetHeight/2 : ""}px`
        }}
        onMouseDown={startDragHandler}
        hidden={dragElem().hidden}
      >
        Drag me
      </div>
      <div
        ref={dropElemRef}
        style={{
          "height": "300px",
          "background-color": "rgba(120, 230, 60, 0.2)",
        }}
      >
        drop here
      </div>
    </>
  )
}

export default Draggable

```

# risizeable floating window(可変フローティングウィンドウ)

コンポーネント側
```tsx
// コンポーネント側
import { createSignal } from "solid-js";

import type { ParentComponent, JSXElement } from "solid-js";

const FloatingWindow: ParentComponent<{
  controllerWrapperClass?: string,
  floatingControlClass?: string,
  floatingControlContent: JSXElement,
  floatingContent?: JSXElement,
  cancelControlContent: JSXElement,
  cancelControlClass?: string,
  contentsWrapperClass?: string,
  wrapperClass?: string,
  defaultWindowSize: {
    width: number | "",
    height: number | "",
  }
}> = (props) => {
  const [mouseCoordinates, setMouseCoordinates] = createSignal({
    x: 0,
    y: 0,
  })
  const [floatingElem, setFloatingElem] = createSignal<{
    zIndex: number | "auto" | "",
    position: "static" | "relative" | "absolute" | "sticky" | "fixed",
    isFloating: boolean,
  }>({
    zIndex: "auto",
    position: "static",
    isFloating: false,
  })
  const [windowSize, setWindowSize] = createSignal({
    width: props.defaultWindowSize.width,
    height: props.defaultWindowSize.height,
  })

  let floatingElemRef: HTMLDivElement | undefined;
  let wrapperElemRef: HTMLDivElement | undefined;
  let contentsWrapperElemRef: HTMLDivElement | undefined;
  let resizeElemRef: HTMLDivElement | undefined;;

  let startDragHandler = (e: MouseEvent) => {
    onmousemove = (e: MouseEvent) => {
      e.preventDefault()

      setFloatingElem({
        zIndex: 1000,
        position: "fixed",
        isFloating: true,
      })
      setMouseCoordinates({
        x: e.clientX,
        y: e.clientY,
      })
    }

    onmouseup = () => {
      setFloatingElem({
        zIndex: 1000,
        position: "absolute",
        isFloating: true,
      })
      onmousemove = () => null
      onmouseup = () => null
    }
  }

  const startResize = (e: MouseEvent) => {
    console.log("wrapper offset height", wrapperElemRef?.offsetHeight)
    console.log("mouse client y height", e.clientY)

    onmousemove = (e: MouseEvent) => {
      e.preventDefault()
      if (wrapperElemRef && contentsWrapperElemRef && floatingElemRef && resizeElemRef) {
        console.log(e.clientY);
        setWindowSize({
          width: e.clientX - (wrapperElemRef.offsetLeft - resizeElemRef.offsetWidth),
          height: e.clientY - (wrapperElemRef.offsetTop + floatingElemRef.offsetHeight),
        })
      }
    }
    onmouseup = () => {
      console.log("stop resize");
      onmousemove = () => null
      onmouseup = () => null
    }
  }

  const cancelFloating = () => {
    setFloatingElem({
      zIndex: "auto",
      position: "static",
      isFloating: false,
    })
    if (floatingElemRef) {
      setMouseCoordinates({
        x: 0,
        y: 0,
      })
    }
  }

  return (
    <>
      <div
        ref={wrapperElemRef}
        style={{
          "z-index": floatingElem().zIndex,
          "position": `${floatingElem().position}`,
          "left": `${floatingElemRef ? mouseCoordinates().x - floatingElemRef.offsetWidth/2 : ""}px`,
          "top": `${floatingElemRef ? mouseCoordinates().y - floatingElemRef.offsetHeight/2 : ""}px`,
        }}
        class={props.wrapperClass}
      >
        <div
          style={{
            "width": floatingElem().isFloating ? windowSize().width +"px" : "",
          }}
          class={props.controllerWrapperClass}
        >
          <div
            ref={floatingElemRef}
            style={{
              "cursor": "move",
            }}
            onMouseDown={startDragHandler}
            class={props.floatingControlClass}
          >
            {props.floatingControlContent}
          </div>
          {props.floatingContent}
          <div
            onClick={cancelFloating}
            class={props.cancelControlClass}
          >
            {props.cancelControlContent}
          </div>
        </div>
        <div
          ref={contentsWrapperElemRef}
          style={{
            "position": "relative",
            "width": floatingElem().isFloating ? windowSize().width+"px" : "",
            "height": floatingElem().isFloating ? windowSize().height+"px" : "",  
          }}
          class={props.contentsWrapperClass}
        >
          {props.children}
          {floatingElem().isFloating
            && <div
            ref={resizeElemRef}
            style={{
            "position": "absolute",
            "right": "0",
            "bottom": "0",
            "z-index": floatingElem().zIndex,
            "background-color": "#e6e6fa",
            "cursor": "nwse-resize",
            }}
            onMouseDown={startResize}
          >
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4">
              <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 4.5l15 15m0 0V8.25m0 11.25H8.25" />
            </svg>
          </div>}
        </div>
      </div>
    </>
  )
}

export default FloatingWindow
```

呼び出し側
コンポーネント側にsolidjs単体で作ったが、呼び出し側は便利のためにtailwindcssとheroiconsを利用しています
このままコピぺすると見た目がややおかしくなるかもしれない

```tsx
// 呼び出し側
import { FloatingWindow } from "@/components";

const FloatingPage = () => {
  return (
    <>
      <div
        style={{
          "height": "300px",
          "background-color": "rgba(120, 230, 60, 0.2)",
        }}
      >
        some contents
      </div>
      <FloatingWindow
        defaultWindowSize={{
          height: 200,
          width: 200,
        }}
        wrapperClass=""
        controllerWrapperClass="flex justify-between items-center border-2 rounded-lg mb-1 bg-red-200"
        floatingControlClass="w-fit"
        floatingControlContent={
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
            <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" />
          </svg>
        }
        floatingContent={
          <div>VideoSource</div>
        }
        cancelControlContent={
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
            <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
          </svg>
        }
        contentsWrapperClass="border-2 rounded-lg bg-red-200 h-full"
      >
        <div>
          Floating Contents 02
        </div>
      </FloatingWindow>
      <FloatingWindow
        defaultWindowSize={{
          height: 200,
          width: 200,
        }}
        wrapperClass=""
        controllerWrapperClass="flex justify-between items-center border-2 rounded-lg mb-1 bg-sky-200"
        floatingControlClass="w-fit"
        floatingControlContent={
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
            <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" />
          </svg>
        }
        floatingContent={
          <div>VideoSource</div>
        }
        cancelControlContent={
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
            <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
          </svg>
        }
        contentsWrapperClass="border-2 rounded-lg bg-sky-200 w-full"
      >
        <div>
          Floating Contents 01
        </div>
      </FloatingWindow>
    </>
  )
}

export default FloatingPage
```
全部propsに打ち込んで使っているので、呼び出し側の記述がやや苦しいです。さらにコンポーネントを細かく抽出して、コードの見栄えを良くしたほうがいいですね。
package用に汎用性を持たせていないため、実際に使うときは色々な場所を変更することになると思います。

props解説:
```tsx
// componentに渡すprops
ParentComponent<{
  controllerWarpperClass?: string, // controllerWrapperのclass HTML要素を制御する
  floatingControlClass?: string, // dragして位置を移動させるところのclass
  floatingControlContent: JSXElement, // dragして移動させるところの見た目
  floatingContent?: JSXElement, // dragControlに位置しているが、dragやclickなどの操作に反応しない部分のcontent、説明とかを入れるところ
  cancelControlContent: JSXElement, // clickしたらfloatingしたコンテンツを元の位置に戻す
  cancelControlClass?: string, // cancelControlの見た目
  contentsWrapperClass?: string, // mainコンテンツ（つまりprops.children）を包むwrapperのclass
  wrapperClass?: string, // floatingWindowまるごと包んだ最上層divのclass
  defaultWindowSize: { // floatingした後のdefaultWindowSize
    width: number | "",
    height: number | "",
  }
}>
```

# 随時加筆
* latest -> 20220914-1233JST[make floating window resizeable]
* 20220913-1605JST[add floating window & dnd]
* 20220913[init]