@solid-primitives/pointer
Version:
A collection of primitives, giving you a nicer API to handle pointer events in a reactive context.
277 lines (276 loc) • 10.7 kB
JavaScript
import { createEventListener } from "@solid-primitives/event-listener";
import { remove, split } from "@solid-primitives/utils/immutable";
import { createSubRoot } from "@solid-primitives/rootless";
import { entries } from "@solid-primitives/utils";
import { createSignal, getOwner, DEV } from "solid-js";
import { isServer } from "solid-js/web";
import { DEFAULT_STATE, parseHandlersMap, toState, toStateActive } from "./helpers.js";
export { getPositionToElement } from "./helpers.js";
/**
* Setups event listeners for pointer events, that will get automatically removed on cleanup.
* @param config event handlers, target, and chosen pointer types
* - `target` - specify the target to attach the listeners to. Will default to `document.body`
* - `pointerTypes` - specify array of pointer types you want to listen to. By default listens to `["mouse", "touch", "pen"]`
* - `passive` - Add passive option to event listeners. Defaults to `true`.
* - your event handlers: e.g. `onenter`, `onLeave`, `onMove`, ...
* @returns function stopping currently attached listener **!deprecated!**
*
* @example
* createPointerListeners({
* // pass a function if the element is yet to mount
* target: () => el,
* pointerTypes: ["touch"],
* onEnter: e => console.log("enter", e.x, e.y),
* onmove: e => console.log({ x: e.x, y: e.y }),
* onup: e => console.log("pointer up", e.x, e.y),
* onLostCapture: e => console.log("lost")
* });
*/
export function createPointerListeners(config) {
if (isServer) {
return;
}
const [{ target = document.body, pointerTypes, passive = true }, handlers] = split(config, "target", "pointerTypes", "passive");
const [{ gotcapture: onGotCapture, lostcapture: onLostCapture }, nativeHandlers] = split(parseHandlersMap(handlers), "gotcapture", "lostcapture");
const guardCB = (handler) => (event) => (!pointerTypes || pointerTypes.includes(event.pointerType)) && handler(event);
const addEventListener = (type, fn) => createEventListener(target, type, guardCB(fn), { passive });
entries(nativeHandlers).forEach(([name, fn]) => fn && addEventListener(`pointer${name}`, fn));
if (onGotCapture)
addEventListener("gotpointercapture", onGotCapture);
if (onLostCapture)
addEventListener("lostpointercapture", onLostCapture);
}
/**
* Setup pointer event listeners, while following the pointers individually, from when they appear, until they're gone.
* @param config primitive configuration:
* - `target` - specify the target to attach the listeners to. Will default to `document.body`
* - `pointerTypes` - specify array of pointer types you want to listen to. By default listens to `["mouse", "touch", "pen"]`
* - `passive` - Add passive option to event listeners. Defaults to `true`.
* - `onDown` - Start following a pointer from when it's down.
* - `onEnter` - Start following a pointer from when it enters the screen.
* @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/pointer#createPerPointerListeners
* @example
* createPerPointerListeners({
* target: el,
* pointerTypes: ['touch', 'pen'],
* onDown({ x, y, pointerId }, onMove, onUp) {
* console.log(x, y, pointerId);
* onMove(e => {...});
* onUp(e => {...});
* }
* })
*/
export function createPerPointerListeners(config) {
if (isServer) {
return;
}
const [{ target = document.body, pointerTypes, passive = true }, handlers] = split(config, "pointerTypes", "target", "passive");
const { down: onDown, enter: onEnter } = parseHandlersMap(handlers);
const owner = getOwner();
const onlyInitMessage = "All listeners need to be added synchronously in the initial event.";
const addListener = (type, fn, pointerId) => createEventListener(target, type, ((e) => (!pointerTypes || pointerTypes.includes(e.pointerType)) &&
(!pointerId || e.pointerId === pointerId) &&
fn(e)), { passive });
if (onEnter) {
const handleEnter = (e) => {
createSubRoot(dispose => {
const { pointerId } = e;
let init = true;
let onLeave;
addListener("pointerleave", e => {
onLeave?.(e);
dispose();
}, pointerId);
onEnter(e, new Proxy({}, {
get: (_, key) => {
const type = "pointer" + key.substring(2).toLowerCase();
return (fn) => {
if (!init) {
// eslint-disable-next-line no-console
if (!isServer && DEV)
console.warn(onlyInitMessage);
return;
}
if (type === "pointerleave")
onLeave = fn;
else
addListener(type, fn, pointerId);
};
},
}));
init = false;
}, owner);
};
addListener("pointerenter", handleEnter);
}
if (onDown) {
const handleDown = (e) => {
createSubRoot(dispose => {
const { pointerId } = e;
let init = true;
let onUp;
addListener(["pointerup", "pointercancel"], e => {
onUp?.(e);
dispose();
}, pointerId);
onDown(e,
// onMove()
fn => {
if (init)
addListener("pointermove", fn, pointerId);
// eslint-disable-next-line no-console
else if (!isServer && DEV)
console.warn(onlyInitMessage);
},
// onUp()
fn => {
if (init)
onUp = fn;
// eslint-disable-next-line no-console
else if (!isServer && DEV)
console.warn(onlyInitMessage);
});
init = false;
}, owner);
};
addListener("pointerdown", handleDown);
}
}
/**
* Returns a signal with autoupdating Pointer position.
* @param config primitive config:
* - `target` - specify the target to attach the listeners to. Will default to `document.body`
* - `pointerTypes` - specify array of pointer types you want to listen to. By default listens to `["mouse", "touch", "pen"]`
* - `value` - set the initial value of the returned signal *(before the first event)*
* @returns position signal
*
* @example
* const position = createPointerPosition({
* target: document.querySelector('my-el'),
* pointerTypes: ["touch"]
* });
*/
export function createPointerPosition(config = {}) {
if (isServer) {
return () => DEFAULT_STATE;
}
const [state, setState] = createSignal(config.value ?? DEFAULT_STATE);
let pointer = null;
const handler = (e, active = true) => setState(toStateActive(e, active));
createPointerListeners({
...config,
onEnter: e => {
if (pointer === null) {
pointer = e.pointerId;
handler(e);
}
},
onMove: e => {
if (e.pointerId === pointer)
handler(e);
},
onLeave: e => {
if (e.pointerId === pointer) {
pointer = null;
handler(e, false);
}
},
});
return state;
}
/**
* Provides a signal of current pointers on screen.
* @param config primitive config:
* - `target` - specify the target to attach the listeners to. Will default to `document.body`
* - `pointerTypes` - specify array of pointer types you want to listen to. By default listens to `["mouse", "touch", "pen"]`
* @returns list of pointers on the screen
* ```
* Accessor<Accessor<PointerListItem>[]>
* ```
* @example
* ```tsx
* const points = createPointerList();
*
* <For each={points()}>
{poz => <div>{poz()}</div>}
</For>
```
*/
export function createPointerList(config = {}) {
if (isServer) {
return () => [];
}
const [pointers, setPointers] = createSignal([]);
createPerPointerListeners({
...config,
onEnter(e, { onMove, onDown, onUp, onLeave }) {
const [pointer, setPointer] = createSignal({
...toState(e),
isDown: false,
});
setPointers(p => [...p, pointer]);
onMove(e => setPointer(p => ({ ...toState(e), isDown: p.isDown })));
onDown(e => setPointer({ ...toState(e), isDown: true }));
onUp(e => setPointer({ ...toState(e), isDown: false }));
onLeave(() => setPointers(p => remove(p, pointer)));
},
});
return pointers;
}
//
// DIRECTIVES:
//
/**
* A directive that will fire a callback once the pointer position change.
*/
export const pointerPosition = (el, props) => {
const { pointerTypes, handler } = (() => {
const v = props();
return typeof v === "function" ? { handler: v, pointerTypes: undefined } : v;
})();
const runHandler = (e, active = true) => handler(toStateActive(e, active), el);
let pointer = null;
createPointerListeners({
target: el,
pointerTypes,
onEnter: e => {
if (pointer === null) {
pointer = e.pointerId;
runHandler(e);
}
},
onMove: e => {
if (e.pointerId === pointer)
runHandler(e);
},
onLeave: e => {
if (e.pointerId === pointer) {
pointer = null;
runHandler(e, false);
}
},
});
};
/**
* A directive for checking if the element is being hovered by at least one pointer.
*/
export const pointerHover = (el, props) => {
const { pointerTypes, handler } = (() => {
const v = props();
return typeof v === "function" ? { handler: v, pointerTypes: undefined } : v;
})();
const pointers = new Set();
createPointerListeners({
target: el,
pointerTypes: pointerTypes,
onEnter: e => {
pointers.add(e.pointerId);
handler(true, el);
},
onLeave: e => {
pointers.delete(e.pointerId);
if (pointers.size === 0)
handler(false, el);
},
});
};