Last active
July 13, 2022 01:38
-
-
Save zaydek/f047a7904d776a3233d09c70c52c1c0d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as Solid from "solid-js" | |
import * as u from "./utils" | |
import { css } from "./styled" | |
//////////////////////////////////////////////////////////////////////////////// | |
type DefaultProps = { | |
class?: string, | |
style?: string | Solid.JSX.CSSProperties, | |
children?: Solid.JSXElement, | |
} | |
//////////////////////////////////////////////////////////////////////////////// | |
// Ready flag for effects | |
const [ready, setReady] = Solid.createSignal(false) | |
// Registered props | |
const [flexDirection, setFlexDirection] = Solid.createSignal< | |
| "row" | |
| "row-reverse" | |
| "column" | |
| "column-reverse">("row") | |
const [shrinkSize, setShrinkSize] = Solid.createSignal(0) | |
const [shrinkMinSize, setShrinkMinSize] = Solid.createSignal(0) | |
const [shrinkMaxSize, setShrinkMaxSize] = Solid.createSignal(0) | |
const [shrinkCollapseSize, setShrinkCollapseSize] = Solid.createSignal(0) | |
const [sliderSize, setSliderSize] = Solid.createSignal(0) | |
// The direction of flex-direction | |
const direction = () => { | |
if (flexDirection() === "row" || flexDirection() === "row-reverse") { | |
return "horizontal" | |
} else { | |
return "vertical" | |
} | |
} | |
// The sign of flex-direction | |
const sign = () => { | |
if (flexDirection() === "row" || flexDirection() === "column") { | |
return +1 | |
} else { | |
return -1 | |
} | |
} | |
// The bounding box size of the root element | |
const [rootElementSize, setRootElementSize] = Solid.createSignal(0) | |
// The clientX or clientY offset | |
const [clientOffset, setClientOffset] = Solid.createSignal(0) | |
// The minimum clientX or clientY offset | |
const minClientOffset = () => { | |
if (sign() === 1) { | |
return sign() * (shrinkSize() - shrinkMaxSize()) | |
} else { | |
return sign() * (shrinkSize() - shrinkMinSize()) | |
} | |
} | |
// The maximum clientX or clientY offset | |
const maxClientOffset = () => { | |
if (sign() === 1) { | |
return sign() * (shrinkSize() - shrinkMinSize()) | |
} else { | |
return sign() * (shrinkSize() - shrinkMaxSize()) | |
} | |
} | |
// The clientX or clientY offset that collapses the shrink area | |
const collapseOffset = () => { | |
return sign() * (shrinkSize() - shrinkCollapseSize()) | |
} | |
// The normalized clientX or clientY offset (uses Math.trunc) | |
const normClientOffset = () => { | |
const trunc = Math.trunc(clientOffset()) | |
if (trunc < minClientOffset()) { | |
if (sign() === -1 && trunc <= collapseOffset()) { | |
return sign() * shrinkSize() | |
} | |
return minClientOffset() | |
} else if (trunc > maxClientOffset()) { | |
if (sign() === +1 && trunc >= collapseOffset()) { | |
return sign() * shrinkSize() | |
} | |
return maxClientOffset() | |
} | |
return Math.trunc(clientOffset()) | |
} | |
//////////////////////////////////////////////////////////////////////////////// | |
const Resizer: Solid.Component<DefaultProps & { | |
"flex-direction": | |
| "row" | |
| "row-reverse" | |
| "column" | |
| "column-reverse" | |
}> = props => { | |
const [ref, setRef] = Solid.createSignal<HTMLElement>() | |
// Register props | |
Solid.createEffect(() => { | |
queueMicrotask(() => { | |
setReady(true) | |
}) | |
setFlexDirection(props["flex-direction"]) | |
}) | |
Solid.createEffect(() => { | |
if (!ready()) { return } | |
function handleResize(e?: UIEvent) { | |
if (direction() === "horizontal") { | |
setRootElementSize(ref()!.offsetWidth) | |
} else { | |
setRootElementSize(ref()!.offsetHeight) | |
} | |
} | |
handleResize() | |
window.addEventListener("resize", handleResize, false) | |
Solid.onCleanup(() => { | |
window.removeEventListener("resize", handleResize, false) | |
}) | |
}) | |
return ( | |
<div | |
ref={setRef} | |
class={u.cx( | |
css(` | |
display: flex; | |
flex-direction: ${flexDirection()}; | |
`, { key: "resizer" }), | |
props.class, | |
)} | |
> | |
{props.children} | |
</div> | |
) | |
} | |
const Area: Solid.Component<DefaultProps & ({ | |
size: "grow", | |
} | { | |
size: number, | |
minSize?: number | ((size: number) => number), | |
maxSize?: number | ((size: number) => number), | |
collapseSize?: number | ((size: number) => number), | |
})> = props => { | |
// Register props | |
Solid.createEffect(() => { | |
if (props.size === "grow") { return } | |
const size = props.size | |
const minSize = typeof props.minSize === "function" | |
? props.minSize(props.size) | |
: (props.minSize ?? 0) | |
const maxSize = typeof props.maxSize === "function" | |
? props.maxSize(props.size) | |
: (props.maxSize ?? 0) | |
const collapseSize = typeof props.collapseSize === "function" | |
? props.collapseSize(props.size) | |
: (props.collapseSize ?? 0) | |
setShrinkSize(size) | |
setShrinkMinSize(minSize) | |
setShrinkMaxSize(maxSize) | |
setShrinkCollapseSize(collapseSize) | |
}) | |
const computedSize = () => { | |
if (!ready()) { return 0 } | |
if (props.size === "grow") { | |
return rootElementSize() - sliderSize() / 2 - shrinkSize() + | |
sign() * normClientOffset() | |
} else { | |
return shrinkSize() - sliderSize() / 2 - | |
sign() * normClientOffset() | |
} | |
} | |
return ( | |
<Solid.Show when={computedSize() > 0}> | |
<div | |
class={u.cx( | |
css(` | |
${direction() === "horizontal" ? "width" : "height"}: | |
${computedSize()}px; | |
`, { | |
key: props.size === "grow"? "resizer-grow-area": "resizer-shrink-area", | |
}), | |
props.class, | |
)} | |
> | |
{props.children} | |
</div> | |
</Solid.Show> | |
) | |
} | |
const Slider: Solid.Component<DefaultProps & { | |
size: number, | |
}> = props => { | |
const [ref, setRef] = Solid.createSignal<HTMLElement>() | |
// Register props | |
Solid.createEffect(() => { | |
setSliderSize(props.size) | |
}) | |
Solid.createEffect(() => { | |
if (!ready()) { return } | |
const [pointerDown, setPointerDown] = Solid.createSignal(false) | |
// "pointerdown" | |
function handlePointerDown(e: PointerEvent) { | |
setPointerDown(true) | |
} | |
// "pointermove" | |
let initialClientXOrY = 0 | |
function handlePointerMove(e: PointerEvent) { | |
if (pointerDown()) { | |
if (initialClientXOrY === 0) { | |
if (direction() === "horizontal") { | |
initialClientXOrY = e.clientX - normClientOffset() | |
} else { | |
initialClientXOrY = e.clientY - normClientOffset() | |
} | |
} | |
if (direction() === "horizontal") { | |
setClientOffset(e.clientX - initialClientXOrY) | |
} else { | |
setClientOffset(e.clientY - initialClientXOrY) | |
} | |
} | |
} | |
// "pointerup" | |
function handlePointerUp(e: PointerEvent) { | |
setPointerDown(false) | |
} | |
ref()!.addEventListener ("pointerdown", handlePointerDown, false) | |
document.addEventListener("pointermove", handlePointerMove, false) | |
document.addEventListener("pointerup", handlePointerUp, false) | |
Solid.onCleanup(() => { | |
ref()!.addEventListener ("pointerdown", handlePointerDown, false) | |
document.removeEventListener("pointermove", handlePointerMove, false) | |
document.removeEventListener("pointerup", handlePointerUp, false) | |
}) | |
}) | |
return ( | |
<div | |
ref={setRef} | |
class={u.cx( | |
css(` | |
${direction() === "horizontal" ? "width" : "height"}: | |
${props.size}px; | |
cursor: ${direction() === "horizontal" ? `col-resize` : `row-resize`}; | |
user-select: none; | |
`, { key: "resizer-slider" }), | |
props.class, | |
)} | |
onKeyDown={e => { | |
if ((direction() === "horizontal" && e.code === u.codes.ArrowLeft) || (direction() === "vertical" && e.code === u.codes.ArrowUp)) { | |
if (e.shiftKey) { | |
// Shift key + nudge | |
if (normClientOffset() > 0) { | |
setClientOffset(0) | |
} else if (normClientOffset() <= 0 && normClientOffset() > minClientOffset()) { | |
setClientOffset(minClientOffset()) | |
} else if (sign() === -1 && normClientOffset() <= minClientOffset()) { | |
setClientOffset(sign() * shrinkSize()) | |
} | |
} else { | |
// Nudge | |
setClientOffset(normClientOffset() - 1) | |
} | |
} else if ((direction() === "horizontal" && e.code === u.codes.ArrowRight) || (direction() === "vertical" && e.code === u.codes.ArrowDown)) { | |
if (e.shiftKey) { | |
// Shift key + nudge | |
if (normClientOffset() < minClientOffset()) { | |
setClientOffset(minClientOffset()) | |
} else if (normClientOffset() < 0) { | |
setClientOffset(0) | |
} else if (normClientOffset() < maxClientOffset()) { | |
setClientOffset(maxClientOffset()) | |
} else if (sign() === +1 && normClientOffset() >= maxClientOffset()) { | |
setClientOffset(sign() * shrinkSize()) | |
} | |
} else { | |
// Nudge | |
setClientOffset(normClientOffset() + 1) | |
} | |
} | |
}} | |
tabIndex={(() => 0)()} | |
> | |
{props.children} | |
</div> | |
) | |
} | |
export const App5: Solid.Component = props => { | |
return ( | |
<Resizer | |
class={css(`height: 100vh;`)} | |
flex-direction="row" | |
> | |
<Area | |
class={css(` | |
padding: 16px; | |
background-color: hsl(0deg 0% 100%); | |
`)} | |
size="grow" | |
> | |
Lorem ipsum dolor sit amet, consectetur adipiscing elit.{" "} | |
Ut convallis nunc ut maximus efficitur.{" "} | |
Fusce sed ante mattis, porta ligula sed, mollis sem.{" "} | |
Vivamus consectetur nunc ut ante posuere consequat.{" "} | |
Duis vulputate maximus odio, eget fermentum ipsum auctor sed.{" "} | |
Donec accumsan rutrum purus.{" "} | |
Fusce suscipit commodo interdum.{" "} | |
Cras rutrum, odio ut varius tristique, nulla sapien semper neque, at egestas nunc libero sed tortor.{" "} | |
Sed eu commodo felis. | |
</Area> | |
<Slider | |
class={css(` | |
&:focus { outline: unset; } | |
background-color: hsl(0deg 0% 90%); | |
transition: background-color 100ms cubic-bezier(0, 0.5, 0.5, 1); | |
&:hover, &:focus { | |
background-color: hsl(200deg 100% 50%); | |
} | |
`)} | |
size={5} | |
/> | |
<Area | |
class={css(` | |
padding: 16px; | |
background-color: hsl(0deg 0% 100%); | |
`)} | |
size={320} | |
minSize={size => size - 64} | |
maxSize={size => size + 320} | |
collapseSize={64} | |
> | |
Lorem ipsum dolor sit amet, consectetur adipiscing elit.{" "} | |
Ut convallis nunc ut maximus efficitur.{" "} | |
Fusce sed ante mattis, porta ligula sed, mollis sem.{" "} | |
Vivamus consectetur nunc ut ante posuere consequat.{" "} | |
Duis vulputate maximus odio, eget fermentum ipsum auctor sed.{" "} | |
Donec accumsan rutrum purus.{" "} | |
Fusce suscipit commodo interdum.{" "} | |
Cras rutrum, odio ut varius tristique, nulla sapien semper neque, at egestas nunc libero sed tortor.{" "} | |
Sed eu commodo felis. | |
</Area> | |
</Resizer> | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment