Skip to content

Instantly share code, notes, and snippets.

@zaydek
Last active July 13, 2022 01:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zaydek/f047a7904d776a3233d09c70c52c1c0d to your computer and use it in GitHub Desktop.
Save zaydek/f047a7904d776a3233d09c70c52c1c0d to your computer and use it in GitHub Desktop.
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