diff --git a/packages/pointer/dev/index.tsx b/packages/pointer/dev/index.tsx index d315c6586..1ac0e7b67 100644 --- a/packages/pointer/dev/index.tsx +++ b/packages/pointer/dev/index.tsx @@ -1,4 +1,4 @@ -import { createPointerListeners, pointerHover, createPointerList } from "../src/index.js"; +import { createPointerListeners, pointerHover, createPointerList } from "../src/index-ols.js"; import { Component, createSignal, For } from "solid-js"; pointerHover; diff --git a/packages/pointer/package.json b/packages/pointer/package.json index 37fcc824f..728aec7d8 100644 --- a/packages/pointer/package.json +++ b/packages/pointer/package.json @@ -13,10 +13,7 @@ "name": "pointer", "stage": 2, "list": [ - "createPointerListeners", - "createPerPointerListeners", - "createPointerPosition", - "createPointerList" + "createPointers" ], "category": "Inputs" }, @@ -57,9 +54,7 @@ "test:ssr": "pnpm run vitest --mode ssr" }, "dependencies": { - "@solid-primitives/event-listener": "workspace:^", - "@solid-primitives/rootless": "workspace:^", - "@solid-primitives/utils": "workspace:^" + "@solid-primitives/event-listener": "workspace:^" }, "peerDependencies": { "solid-js": "^1.6.12" diff --git a/packages/pointer/src/helpers.ts b/packages/pointer/src/helpers.ts deleted file mode 100644 index 44969127c..000000000 --- a/packages/pointer/src/helpers.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { pick } from "@solid-primitives/utils/immutable"; -import { Position } from "@solid-primitives/utils"; -import { - AnyOnEventName, - ParsedEventHandlers, - PointerState, - PointerStateWithActive, - ReverseOnEventName, -} from "./types.js"; - -/** - * A non-reactive helper function. It turns a position relative to the screen/window, to be relative to an element. - * @param poz object containing `x` & `y` - * @param el element to calculate the position of - * @returns the `poz` with `x` and `y` changed, and `isInside` added - */ -export const getPositionToElement = ( - poz: T, - el: Element, -): T & { isInside: boolean } => { - const { top, left, width, height } = el.getBoundingClientRect(), - x = poz.x - left, - y = poz.y - top; - return { - ...poz, - x, - y, - isInside: x >= 0 && y >= 0 && x <= width && y <= height, - }; -}; - -const parseOnEventName = (name: T) => - name.substring(2).toLowerCase() as ReverseOnEventName; -export const parseHandlersMap = >( - handlers: H, -): ParsedEventHandlers => { - const result = {} as any; - Object.entries(handlers).forEach(([name, fn]) => (result[parseOnEventName(name)] = fn)); - return result; -}; - -const pointerStateKeys: (keyof PointerState)[] = [ - "x", - "y", - "pointerId", - "pressure", - "tiltX", - "tiltY", - "width", - "height", - "twist", - "pointerType", -]; -export const toState = (e: PointerEvent): PointerState => - pick(e, ...pointerStateKeys) as PointerState; -export const toStateActive = (e: PointerEvent, isActive: boolean) => ({ - ...toState(e), - isActive, -}); - -export const DEFAULT_STATE: PointerStateWithActive = { - x: 0, - y: 0, - pointerId: 0, - pressure: 0, - tiltX: 0, - tiltY: 0, - width: 0, - height: 0, - twist: 0, - pointerType: null, - isActive: false, -}; diff --git a/packages/pointer/src/index.ts b/packages/pointer/src/index.ts index a0f4e5c04..66d8cf363 100644 --- a/packages/pointer/src/index.ts +++ b/packages/pointer/src/index.ts @@ -1,386 +1,48 @@ -import { createEventListener } from "@solid-primitives/event-listener"; -import { remove, split } from "@solid-primitives/utils/immutable"; -import { createSubRoot } from "@solid-primitives/rootless"; -import { Directive, entries, Many, MaybeAccessor } from "@solid-primitives/utils"; -import { Accessor, createSignal, getOwner, DEV } from "solid-js"; +import { Accessor, createSignal } from "solid-js"; import { isServer } from "solid-js/web"; -import { DEFAULT_STATE, parseHandlersMap, toState, toStateActive } from "./helpers.js"; -import { - 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> & { - target?: MaybeAccessor; - 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, fn: Handler) => - createEventListener(target, type, guardCB(fn) as any, { passive }); +import { createEventListener } from "@solid-primitives/event-listener"; - entries(nativeHandlers).forEach( - ([name, fn]) => fn && addEventListener(`pointer${name}` as keyof HTMLElementEventMap, fn), - ); - if (onGotCapture) addEventListener("gotpointercapture", onGotCapture); - if (onLostCapture) addEventListener("lostpointercapture", onLostCapture); +function upsert_pointer(pointers: PointerEvent[], e: PointerEvent): PointerEvent[] { + let i = 0 + for (;i < pointers.length && pointers[i]!.pointerId !== e.pointerId; i++) {} + return pointers.toSpliced(i, 1, e) } -/** - * 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; - 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, 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); +function remove_pointer(pointers: PointerEvent[], e: PointerEvent): PointerEvent[] { + for (let i = 0; i < pointers.length; i++) { + if (pointers[i]!.pointerId === e.pointerId) { + return pointers.toSpliced(i, 1) + } } + return pointers } -/** - * 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; - pointerTypes?: PointerType[]; - value?: PointerStateWithActive; - } = {}, -): Accessor { - 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; -} +export type ListenerTarget = SVGSVGElement | HTMLElement | Window | Document -/** - * 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[]> - * ``` - * @example - * ```tsx - * const points = createPointerList(); - * -* - {poz =>
{poz()}
} -
- ``` - */ -export function createPointerList( - config: { - target?: MaybeAccessor; - pointerTypes?: PointerType[]; - } = {}, -): Accessor[]> { +export function createPointers( + target: ListenerTarget | Accessor = () => document, +): Accessor { if (isServer) { - return () => []; + 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; + const [pointers, setPointers] = createSignal([]) + + createEventListener(target, "pointermove", + e => setPointers(p => upsert_pointer(p, e)), + ) + createEventListener(target, "pointerdown", + e => setPointers(p => upsert_pointer(p, e)), + ) + createEventListener(target, "pointerup", + e => setPointers(p => e.pointerType === "touch" ? remove_pointer(p, e) : upsert_pointer(p, e)), + ) + createEventListener(target, "pointerleave", + e => setPointers(p => remove_pointer(p, e)), + ) + createEventListener(target, "pointercancel", + e => setPointers(p => remove_pointer(p, e)), + ) + + return pointers } - -// -// DIRECTIVES: -// - -/** - * A directive that will fire a callback once the pointer position change. - */ -export const pointerPosition: Directive = (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 = (el, props) => { - const { pointerTypes, handler } = (() => { - const v = props(); - return typeof v === "function" ? { handler: v, pointerTypes: undefined } : v; - })(); - const pointers = new Set(); - 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); - }, - }); -}; diff --git a/packages/pointer/src/types.ts b/packages/pointer/src/types.ts deleted file mode 100644 index b1ebf8584..000000000 --- a/packages/pointer/src/types.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { JSX } from "solid-js"; - -export type PointerType = "mouse" | "touch" | "pen"; -export type EventTarget = Window | Document | HTMLElement; - -export type PointerEventNames = - | "over" - | "enter" - | "down" - | "move" - | "up" - | "cancel" - | "out" - | "leave" - | "gotCapture" - | "lostCapture"; - -export type OnEventName = `on${Lowercase}` | `on${Capitalize}`; - -export type OnEventRecord = Record, V>; -export type Handler = (e: PointerEvent) => void; - -export type AnyOnEventName = `on${string}`; -export type ReverseOnEventName = T extends string - ? Lowercase - : never; -export type ParsedEventHandlers void>> = { - [K in ReverseOnEventName]: H[`on${K}`]; -}; - -export type PointerState = { - pressure: number; - pointerId: number; - tiltX: number; - tiltY: number; - width: number; - height: number; - twist: number; - pointerType: PointerType | null; - x: number; - y: number; -}; - -export type PointerStateWithActive = PointerState & { isActive: boolean }; - -export type PointerListItem = PointerState & { isDown: boolean }; - -declare module "solid-js" { - namespace JSX { - interface Directives { - pointerHover: PointerHoverDirectiveProps; - } - } -} -export type E = JSX.Element; - -export type PointerPositionDirectiveHandler = (state: PointerStateWithActive, el: Element) => void; -export type PointerPositionDirectiveProps = - | PointerPositionDirectiveHandler - | { - pointerTypes?: PointerType[]; - handler: PointerPositionDirectiveHandler; - }; - -export type PointerHoverDirectiveHandler = (hovering: boolean, el: Element) => void; -export type PointerHoverDirectiveProps = - | PointerHoverDirectiveHandler - | { pointerTypes?: PointerType[]; handler: PointerHoverDirectiveHandler }; diff --git a/packages/pointer/test/index.test.ts b/packages/pointer/test/index.test.ts index c2aa33197..16a1175b5 100644 --- a/packages/pointer/test/index.test.ts +++ b/packages/pointer/test/index.test.ts @@ -1,90 +1,117 @@ -import { describe, it, test, expect } from "vitest"; -import { createRoot, createSignal } from "solid-js"; -import { createPointerListeners } from "../src/index.js"; - -describe("createPointerListeners", () => { - const move_event = new Event("pointermove"), - enter_event = new Event("pointerenter"), - up_event = new Event("pointerup"), - ref = document.createElement("div"); - - it("listens to pointer events", () => { - createRoot(dispose => { - const captured_events = { - move: undefined as undefined | PointerEvent, - enter: undefined as undefined | PointerEvent, - up: undefined as undefined | PointerEvent, - }; - - createPointerListeners({ - target: ref, - onMove: e => (captured_events.move = e), - onEnter: e => (captured_events.enter = e), - onUp: e => (captured_events.up = e), - }); - - ref.dispatchEvent(move_event); - expect(captured_events.move).toBe(move_event); - expect(captured_events.enter).toBe(undefined); - expect(captured_events.up).toBe(undefined); - - ref.dispatchEvent(enter_event); - expect(captured_events.enter).toBe(enter_event); - - ref.dispatchEvent(up_event); - expect(captured_events.up).toBe(up_event); - - dispose(); - }); +import { describe, it, expect, afterAll } from "vitest"; +import { createRoot } from "solid-js"; +import { createPointers } from "../src/index.js"; + +class PointerEvent extends Event { + constructor(type: string, init: PointerEventInit) { + super(type, init); + Object.assign(this, init) + } +} + +global.PointerEvent = PointerEvent as any; + +describe("createPointers", () => { + const move_event_mouse = new PointerEvent("pointermove", { pointerId: 1, pointerType: "mouse" }); + const down_event_mouse = new PointerEvent("pointerdown", { pointerId: 1, pointerType: "mouse" }); + const up_event_mouse = new PointerEvent("pointerup", { pointerId: 1, pointerType: "mouse" }); + const leave_event_mouse = new PointerEvent("pointerleave", { pointerId: 1, pointerType: "mouse" }); + const cancel_event_mouse = new PointerEvent("pointercancel", { pointerId: 1, pointerType: "mouse" }); + + const move_event_touch = new PointerEvent("pointermove", { pointerId: 2, pointerType: "touch" }); + const down_event_touch = new PointerEvent("pointerdown", { pointerId: 2, pointerType: "touch" }); + const up_event_touch = new PointerEvent("pointerup", { pointerId: 2, pointerType: "touch" }); + const cancel_event_touch = new PointerEvent("pointercancel", { pointerId: 2, pointerType: "touch" }); + + const ref = document.createElement("div"); + + document.body.appendChild(ref) + afterAll(() => { + document.body.removeChild(ref); }); - test("listeners are removed on dispose", () => - createRoot(dispose => { - let captured_events = 0; + it("listens to mouse events", () => { + const [pointers, dispose] = createRoot(dispose => [createPointers(ref), dispose]); + expect(pointers()).toEqual([]); - createPointerListeners({ - target: ref, - onMove: _ => captured_events++, - }); + ref.dispatchEvent(down_event_mouse); + expect(pointers()).toEqual([down_event_mouse]); - ref.dispatchEvent(move_event); - expect(captured_events).toBe(1); + ref.dispatchEvent(move_event_mouse); + expect(pointers()).toEqual([move_event_mouse]); - dispose(); + ref.dispatchEvent(up_event_mouse); + expect(pointers()).toEqual([up_event_mouse]); - ref.dispatchEvent(move_event); - expect(captured_events).toBe(1); - })); + ref.dispatchEvent(leave_event_mouse); + expect(pointers()).toEqual([]); - test("reactive target", () => { - let captured_events = 0; - const [target, setTarget] = createSignal(ref); + ref.dispatchEvent(down_event_mouse); + expect(pointers()).toEqual([down_event_mouse]); - const dispose = createRoot(dispose => { - createPointerListeners({ - target, - onMove: _ => captured_events++, - }); + ref.dispatchEvent(cancel_event_mouse); + expect(pointers()).toEqual([]); - ref.dispatchEvent(move_event); - expect(captured_events).toBe(0); + dispose(); + + ref.dispatchEvent(down_event_mouse); + expect(pointers()).toEqual([]); + }); - return dispose; - }); + it("listens to touch events", () => { + const [pointers, dispose] = createRoot(dispose => [createPointers(ref), dispose]); + expect(pointers()).toEqual([]); - ref.dispatchEvent(move_event); - expect(captured_events).toBe(1); + ref.dispatchEvent(down_event_touch); + expect(pointers()).toEqual([down_event_touch]); - setTarget(); + ref.dispatchEvent(move_event_touch); + expect(pointers()).toEqual([move_event_touch]); - ref.dispatchEvent(move_event); - expect(captured_events).toBe(1); + ref.dispatchEvent(up_event_touch); + expect(pointers()).toEqual([]); - setTarget(ref); + ref.dispatchEvent(down_event_touch); + expect(pointers()).toEqual([down_event_touch]); - ref.dispatchEvent(move_event); - expect(captured_events).toBe(2); + ref.dispatchEvent(cancel_event_touch); + expect(pointers()).toEqual([]); dispose(); + + ref.dispatchEvent(down_event_touch); + expect(pointers()).toEqual([]); + }) + + it("keeps track of multiple pointers", () => { + const [pointers, dispose] = createRoot(dispose => [createPointers(ref), dispose]); + expect(pointers()).toEqual([]); + + ref.dispatchEvent(down_event_mouse); + expect(pointers()).toEqual([down_event_mouse]); + + ref.dispatchEvent(down_event_touch); + expect(pointers()).toEqual([down_event_mouse, down_event_touch]); + + ref.dispatchEvent(move_event_mouse); + expect(pointers()).toEqual([move_event_mouse, down_event_touch]); + + ref.dispatchEvent(move_event_touch); + expect(pointers()).toEqual([move_event_mouse, move_event_touch]); + + ref.dispatchEvent(up_event_mouse); + expect(pointers()).toEqual([up_event_mouse, move_event_touch]); + + ref.dispatchEvent(up_event_touch); + expect(pointers()).toEqual([up_event_mouse]); + + ref.dispatchEvent(leave_event_mouse); + expect(pointers()).toEqual([]); + + dispose(); + + ref.dispatchEvent(down_event_mouse); + ref.dispatchEvent(down_event_touch); + expect(pointers()).toEqual([]); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 00b14cb42..d025bc0c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -665,12 +665,6 @@ importers: '@solid-primitives/event-listener': specifier: workspace:^ version: link:../event-listener - '@solid-primitives/rootless': - specifier: workspace:^ - version: link:../rootless - '@solid-primitives/utils': - specifier: workspace:^ - version: link:../utils devDependencies: solid-js: specifier: ^1.8.7