コードを試す
興味のあるコードを以下にコピぺして試すことができる
SoildJS Playground
split panel(画面分割)
ドラッグで画面分割サイズを変えることのできるコンポーネント
custom drag & drop(ドラッグ&ドロップ)
自前のロジックを使った拡張性の高いドラッグ&ドロップコンポーネント
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(可変フローティングウィンドウ)
コンポーネント側
// コンポーネント側
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を利用しています
このままコピぺすると見た目がややおかしくなるかもしれない
// 呼び出し側
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解説:
// 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]