Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/pointer-velocity-momentum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solid-primitives/pointer": minor
---

Add `createPointerVelocity` for kinematic pointer velocity tracking (vx, vy, speed in px/s) with automatic idle momentum decay.
388 changes: 2 additions & 386 deletions packages/pointer/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,386 +1,2 @@
import { createEventListener } from "@solid-primitives/event-listener";
import { remove, split } from "@solid-primitives/utils/immutable";
import { createSubRoot } from "@solid-primitives/rootless";
import { type Directive, entries, type Many, type MaybeAccessor } from "@solid-primitives/utils";
import { type Accessor, createSignal, getOwner, DEV } from "solid-js";
import { isServer } from "solid-js/web";
import { DEFAULT_STATE, parseHandlersMap, toState, toStateActive } from "./helpers.js";
import type {
Handler,
OnEventRecord,
PointerEventNames,
PointerHoverDirectiveProps,
PointerListItem,
PointerPositionDirectiveProps,
PointerStateWithActive,
PointerType,
} from "./types.js";

export { getPositionToElement } from "./helpers.js";
export type {
PointerHoverDirectiveHandler,
PointerHoverDirectiveProps,
PointerListItem,
PointerPositionDirectiveHandler,
PointerPositionDirectiveProps,
PointerState,
PointerStateWithActive,
PointerType,
} from "./types.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: Partial<OnEventRecord<PointerEventNames, Handler>> & {
target?: MaybeAccessor<EventTarget | undefined>;
pointerTypes?: PointerType[];
passive?: boolean;
},
): void {
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: Handler) => (event: PointerEvent) =>
(!pointerTypes || pointerTypes.includes(event.pointerType as PointerType)) && handler(event);
const addEventListener = (type: Many<keyof HTMLElementEventMap>, fn: Handler) =>
createEventListener(target, type, guardCB(fn) as any, { passive });

entries(nativeHandlers).forEach(
([name, fn]) => fn && addEventListener(`pointer${name}` as keyof HTMLElementEventMap, 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: {
target?: MaybeAccessor<EventTarget>;
pointerTypes?: PointerType[];
passive?: boolean;
} & Partial<
OnEventRecord<
"enter",
(
event: PointerEvent,
handlers: Readonly<
OnEventRecord<"down" | "move" | "up" | "leave" | "cancel", (handler: Handler) => void>
>,
) => void
> &
OnEventRecord<
"down",
(
event: PointerEvent,
onMove: (handler: Handler) => void,
onUp: (handler: Handler) => void,
) => void
>
>,
) {
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: Many<string>, fn: Handler, pointerId?: number): void =>
createEventListener(
target,
type,
((e: PointerEvent) =>
(!pointerTypes || pointerTypes.includes(e.pointerType as PointerType)) &&
(!pointerId || e.pointerId === pointerId) &&
fn(e)) as any,
{ passive },
);

if (onEnter) {
const handleEnter = (e: PointerEvent) => {
createSubRoot(dispose => {
const { pointerId } = e;
let init = true;
let onLeave: Handler | undefined;

addListener(
"pointerleave",
e => {
onLeave?.(e);
dispose();
},
pointerId,
);

onEnter(
e,
new Proxy(
{},
{
get: (_, key: string) => {
const type = "pointer" + key.substring(2).toLowerCase();
return (fn: Handler) => {
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);
};
},
},
) as any,
);
init = false;
}, owner);
};
addListener("pointerenter", handleEnter);
}

if (onDown) {
const handleDown = (e: PointerEvent) => {
createSubRoot(dispose => {
const { pointerId } = e;
let init = true;
let onUp: Handler | undefined;

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: {
target?: MaybeAccessor<EventTarget>;
pointerTypes?: PointerType[];
value?: PointerStateWithActive;
} = {},
): Accessor<PointerStateWithActive> {
if (isServer) {
return () => DEFAULT_STATE;
}

const [state, setState] = createSignal(config.value ?? DEFAULT_STATE);
let pointer: null | number = null;
const handler = (e: PointerEvent, 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: {
target?: MaybeAccessor<EventTarget>;
pointerTypes?: PointerType[];
} = {},
): Accessor<Accessor<PointerListItem>[]> {
if (isServer) {
return () => [];
}

const [pointers, setPointers] = createSignal<Accessor<PointerListItem>[]>([]);
createPerPointerListeners({
...config,
onEnter(e, { onMove, onDown, onUp, onLeave }) {
const [pointer, setPointer] = createSignal<PointerListItem>({
...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: Directive<PointerPositionDirectiveProps> = (el, props) => {
const { pointerTypes, handler } = (() => {
const v = props();
return typeof v === "function" ? { handler: v, pointerTypes: undefined } : v;
})();
const runHandler = (e: PointerEvent, active = true) => handler(toStateActive(e, active), el);
let pointer: null | number = 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: Directive<PointerHoverDirectiveProps> = (el, props) => {
const { pointerTypes, handler } = (() => {
const v = props();
return typeof v === "function" ? { handler: v, pointerTypes: undefined } : v;
})();
const pointers = new Set<number>();
createPointerListeners({
target: el as HTMLElement,
pointerTypes: pointerTypes,
onEnter: e => {
pointers.add(e.pointerId);
handler(true, el);
},
onLeave: e => {
pointers.delete(e.pointerId);
if (pointers.size === 0) handler(false, el);
},
});
};
export * from "./index.js";
export * from "./velocity.js";
Loading
Loading