@thi.ng/rstream-gestures
Advanced tools
Comparing version 5.0.36 to 5.0.37
# Change Log | ||
- **Last updated**: 2023-12-09T19:12:03Z | ||
- **Last updated**: 2023-12-11T10:07:09Z | ||
- **Generator**: [thi.ng/monopub](https://thi.ng/monopub) | ||
@@ -5,0 +5,0 @@ |
@@ -8,215 +8,180 @@ import { isBoolean } from "@thi.ng/checks/is-boolean"; | ||
import { map } from "@thi.ng/transducers/map"; | ||
const START_EVENTS = new Set([ | ||
"mousedown", | ||
"touchmove", | ||
"touchstart", | ||
"mousemove", | ||
const START_EVENTS = /* @__PURE__ */ new Set([ | ||
"mousedown", | ||
"touchmove", | ||
"touchstart", | ||
"mousemove" | ||
]); | ||
const END_EVENTS = new Set(["mouseup", "touchend", "touchcancel"]); | ||
const END_EVENTS = /* @__PURE__ */ new Set(["mouseup", "touchend", "touchcancel"]); | ||
const BASE_EVENTS = ["mousemove", "mousedown", "touchstart", "wheel"]; | ||
const EVENT_GESTURETYPES = { | ||
touchstart: "start", | ||
touchmove: "drag", | ||
touchend: "end", | ||
touchcancel: "end", | ||
mousedown: "start", | ||
mouseup: "end", | ||
wheel: "zoom", | ||
touchstart: "start", | ||
touchmove: "drag", | ||
touchend: "end", | ||
touchcancel: "end", | ||
mousedown: "start", | ||
mouseup: "end", | ||
wheel: "zoom" | ||
}; | ||
/** | ||
* Attaches mouse & touch event listeners to given DOM element and returns a | ||
* stream of {@link GestureEvent}s and their {@link GestureInfo} details. | ||
* | ||
* In multi-touch environments, a `GestureEvent` can contain multiple such | ||
* `GestureInfo` objects (one per active touch). In general, the `click` and | ||
* `delta` values are only present if the abstracted event `type == "drag"`. | ||
* Both (and `pos` too) are 2-element arrays of `[x,y]` coordinates. | ||
* | ||
* The `zoom` value is always present, but is only updated with wheel events. | ||
* The value will be constrained to `minZoom` ... `maxZoom` interval (provided | ||
* via options object). | ||
* | ||
* Note: If using `preventDefault` and attaching the event stream to | ||
* `document.body`, the following event listener options SHOULD be used: | ||
* | ||
* @example | ||
* ```ts | ||
* eventOpts: { passive: false } | ||
* ``` | ||
* | ||
* https://www.chromestatus.com/features/5093566007214080 | ||
* | ||
* @param el - | ||
* @param opts - | ||
*/ | ||
export const gestureStream = (el, _opts) => { | ||
const opts = { | ||
zoom: 1, | ||
absZoom: true, | ||
minZoom: 0.25, | ||
maxZoom: 4, | ||
smooth: 1, | ||
eventOpts: { capture: true }, | ||
preventDefault: true, | ||
preventScrollOnZoom: true, | ||
preventContextMenu: true, | ||
local: true, | ||
scale: false, | ||
..._opts, | ||
}; | ||
opts.id = opts.id || `gestures-${__nextID()}`; | ||
const active = []; | ||
let zoom = clamp(isNumber(opts.zoom) ? opts.zoom : opts.zoom.deref() || 1, opts.minZoom, opts.maxZoom); | ||
let zoomDelta = 0; | ||
let numTouches = 0; | ||
let lastPos = [0, 0]; | ||
let tempStreams; | ||
const isBody = el === document.body; | ||
const tempEvents = [ | ||
"touchend", | ||
"touchcancel", | ||
"touchmove", | ||
"mouseup", | ||
]; | ||
!isBody && tempEvents.push("mousemove"); | ||
opts.preventContextMenu && | ||
el.addEventListener("contextmenu", (e) => e.preventDefault()); | ||
const gestureStart = (etype, events, bounds, isTouch) => { | ||
const isStart = etype === "mousedown" || etype === "touchstart"; | ||
for (let t of events) { | ||
const id = t.identifier || 0; | ||
const pos = getPos(t, bounds, opts.local, opts.scale); | ||
let touch = active.find((t) => t.id === id); | ||
if (!touch && isStart) { | ||
touch = { id, start: pos }; | ||
active.push(touch); | ||
numTouches++; | ||
} | ||
if (touch) { | ||
touch.pos = pos; | ||
touch.delta = [ | ||
pos[0] - touch.start[0], | ||
pos[1] - touch.start[1], | ||
]; | ||
if (isTouch) { | ||
touch.force = t.force; | ||
} | ||
} | ||
const gestureStream = (el, _opts) => { | ||
const opts = { | ||
zoom: 1, | ||
absZoom: true, | ||
minZoom: 0.25, | ||
maxZoom: 4, | ||
smooth: 1, | ||
eventOpts: { capture: true }, | ||
preventDefault: true, | ||
preventScrollOnZoom: true, | ||
preventContextMenu: true, | ||
local: true, | ||
scale: false, | ||
..._opts | ||
}; | ||
opts.id = opts.id || `gestures-${__nextID()}`; | ||
const active = []; | ||
let zoom = clamp( | ||
isNumber(opts.zoom) ? opts.zoom : opts.zoom.deref() || 1, | ||
opts.minZoom, | ||
opts.maxZoom | ||
); | ||
let zoomDelta = 0; | ||
let numTouches = 0; | ||
let lastPos = [0, 0]; | ||
let tempStreams; | ||
const isBody = el === document.body; | ||
const tempEvents = [ | ||
"touchend", | ||
"touchcancel", | ||
"touchmove", | ||
"mouseup" | ||
]; | ||
!isBody && tempEvents.push("mousemove"); | ||
opts.preventContextMenu && el.addEventListener("contextmenu", (e) => e.preventDefault()); | ||
const gestureStart = (etype, events, bounds, isTouch) => { | ||
const isStart = etype === "mousedown" || etype === "touchstart"; | ||
for (let t of events) { | ||
const id = t.identifier || 0; | ||
const pos = getPos(t, bounds, opts.local, opts.scale); | ||
let touch = active.find((t2) => t2.id === id); | ||
if (!touch && isStart) { | ||
touch = { id, start: pos }; | ||
active.push(touch); | ||
numTouches++; | ||
} | ||
if (touch) { | ||
touch.pos = pos; | ||
touch.delta = [ | ||
pos[0] - touch.start[0], | ||
pos[1] - touch.start[1] | ||
]; | ||
if (isTouch) { | ||
touch.force = t.force; | ||
} | ||
if (isStart && !tempStreams) { | ||
tempStreams = tempEvents.map((id) => eventSource(document.body, id, opts, "-temp")); | ||
stream.addAll(tempStreams); | ||
!isBody && stream.removeID("mousemove"); | ||
} | ||
} | ||
if (isStart && !tempStreams) { | ||
tempStreams = tempEvents.map( | ||
(id) => eventSource(document.body, id, opts, "-temp") | ||
); | ||
stream.addAll(tempStreams); | ||
!isBody && stream.removeID("mousemove"); | ||
} | ||
}; | ||
const gestureEnd = (events) => { | ||
for (let t of events) { | ||
const id = t.identifier || 0; | ||
const idx = active.findIndex((t2) => t2.id === id); | ||
if (idx !== -1) { | ||
active.splice(idx, 1); | ||
numTouches--; | ||
} | ||
} | ||
if (numTouches === 0) { | ||
stream.removeAll(tempStreams); | ||
!isBody && stream.add(eventSource(el, "mousemove", opts)); | ||
tempStreams = void 0; | ||
} | ||
}; | ||
const updateZoom = (e) => { | ||
const zdelta = opts.smooth * ("wheelDeltaY" in e ? -e.wheelDeltaY / 120 : e.deltaY / 40); | ||
zoom = opts.absZoom ? clamp(zoom + zdelta, opts.minZoom, opts.maxZoom) : zdelta; | ||
zoomDelta = zdelta; | ||
}; | ||
const stream = merge({ | ||
id: opts.id, | ||
src: BASE_EVENTS.map((id) => eventSource(el, id, opts)), | ||
xform: map((e) => { | ||
const etype = e.type; | ||
if (etype === "$zoom") { | ||
zoomDelta = e.value - zoom; | ||
if (opts.absZoom) { | ||
zoom = clamp(zoom + zoomDelta, opts.minZoom, opts.maxZoom); | ||
} else { | ||
zoom = zoomDelta; | ||
} | ||
}; | ||
const gestureEnd = (events) => { | ||
for (let t of events) { | ||
const id = t.identifier || 0; | ||
const idx = active.findIndex((t) => t.id === id); | ||
if (idx !== -1) { | ||
active.splice(idx, 1); | ||
numTouches--; | ||
} | ||
} | ||
if (numTouches === 0) { | ||
stream.removeAll(tempStreams); | ||
!isBody && stream.add(eventSource(el, "mousemove", opts)); | ||
tempStreams = undefined; | ||
} | ||
}; | ||
const updateZoom = (e) => { | ||
const zdelta = opts.smooth * | ||
("wheelDeltaY" in e | ||
? -e.wheelDeltaY / 120 | ||
: e.deltaY / 40); | ||
zoom = opts.absZoom | ||
? clamp(zoom + zdelta, opts.minZoom, opts.maxZoom) | ||
: zdelta; | ||
zoomDelta = zdelta; | ||
}; | ||
const stream = merge({ | ||
id: opts.id, | ||
src: BASE_EVENTS.map((id) => eventSource(el, id, opts)), | ||
xform: map((e) => { | ||
const etype = e.type; | ||
if (etype === "$zoom") { | ||
zoomDelta = e.value - zoom; | ||
if (opts.absZoom) { | ||
zoom = clamp(zoom + zoomDelta, opts.minZoom, opts.maxZoom); | ||
} | ||
else { | ||
zoom = zoomDelta; | ||
} | ||
return { | ||
pos: lastPos.slice(), | ||
buttons: 0, | ||
type: "zoom", | ||
active, | ||
zoom, | ||
zoomDelta, | ||
isTouch: false, | ||
}; | ||
} | ||
const type = classifyEventType(etype, !!tempStreams); | ||
let isTouch = !!e.touches; | ||
let events = isTouch | ||
? Array.from(e.changedTouches) | ||
: [e]; | ||
const bounds = el.getBoundingClientRect(); | ||
if (START_EVENTS.has(etype)) { | ||
gestureStart(etype, events, bounds, isTouch); | ||
} | ||
else if (END_EVENTS.has(etype)) { | ||
gestureEnd(events); | ||
} | ||
else if (type === "zoom") { | ||
updateZoom(e); | ||
} | ||
lastPos = getPos(events[0], bounds, opts.local, opts.scale); | ||
opts.preventDefault && e.preventDefault(); | ||
return { | ||
event: e, | ||
pos: lastPos, | ||
buttons: isTouch ? active.length : e.buttons, | ||
type, | ||
active, | ||
zoom, | ||
zoomDelta, | ||
isTouch, | ||
}; | ||
}), | ||
}); | ||
// attach zoom reset | ||
if (!isNumber(opts.zoom)) { | ||
stream.add(opts.zoom.map((x) => ({ type: "$zoom", value: x }))); | ||
} | ||
return stream; | ||
return { | ||
pos: lastPos.slice(), | ||
buttons: 0, | ||
type: "zoom", | ||
active, | ||
zoom, | ||
zoomDelta, | ||
isTouch: false | ||
}; | ||
} | ||
const type = classifyEventType(etype, !!tempStreams); | ||
let isTouch = !!e.touches; | ||
let events = isTouch ? Array.from(e.changedTouches) : [e]; | ||
const bounds = el.getBoundingClientRect(); | ||
if (START_EVENTS.has(etype)) { | ||
gestureStart(etype, events, bounds, isTouch); | ||
} else if (END_EVENTS.has(etype)) { | ||
gestureEnd(events); | ||
} else if (type === "zoom") { | ||
updateZoom(e); | ||
} | ||
lastPos = getPos(events[0], bounds, opts.local, opts.scale); | ||
opts.preventDefault && e.preventDefault(); | ||
return { | ||
event: e, | ||
pos: lastPos, | ||
buttons: isTouch ? active.length : e.buttons, | ||
type, | ||
active, | ||
zoom, | ||
zoomDelta, | ||
isTouch | ||
}; | ||
}) | ||
}); | ||
if (!isNumber(opts.zoom)) { | ||
stream.add(opts.zoom.map((x) => ({ type: "$zoom", value: x }))); | ||
} | ||
return stream; | ||
}; | ||
const eventSource = (el, type, opts, suffix = "") => { | ||
let eventOpts = opts.eventOpts; | ||
if (type === "wheel" && opts.preventScrollOnZoom) { | ||
eventOpts = isBoolean(eventOpts) | ||
? { capture: eventOpts, passive: false } | ||
: { ...eventOpts, passive: false }; | ||
} | ||
return fromDOMEvent(el, type, eventOpts, { id: type + suffix }); | ||
let eventOpts = opts.eventOpts; | ||
if (type === "wheel" && opts.preventScrollOnZoom) { | ||
eventOpts = isBoolean(eventOpts) ? { capture: eventOpts, passive: false } : { ...eventOpts, passive: false }; | ||
} | ||
return fromDOMEvent(el, type, eventOpts, { id: type + suffix }); | ||
}; | ||
const classifyEventType = (etype, isActive) => etype === "mousemove" | ||
? isActive | ||
? "drag" | ||
: "move" | ||
: EVENT_GESTURETYPES[etype]; | ||
const classifyEventType = (etype, isActive) => etype === "mousemove" ? isActive ? "drag" : "move" : EVENT_GESTURETYPES[etype]; | ||
const getPos = (e, bounds, isLocal, doScale) => { | ||
let x = e.clientX; | ||
let y = e.clientY; | ||
if (isLocal) { | ||
x -= bounds.left; | ||
y -= bounds.top; | ||
} | ||
if (doScale) { | ||
const dpr = window.devicePixelRatio || 1; | ||
x *= dpr; | ||
y *= dpr; | ||
} | ||
return [x | 0, y | 0]; | ||
let x = e.clientX; | ||
let y = e.clientY; | ||
if (isLocal) { | ||
x -= bounds.left; | ||
y -= bounds.top; | ||
} | ||
if (doScale) { | ||
const dpr = window.devicePixelRatio || 1; | ||
x *= dpr; | ||
y *= dpr; | ||
} | ||
return [x | 0, y | 0]; | ||
}; | ||
export { | ||
gestureStream | ||
}; |
{ | ||
"name": "@thi.ng/rstream-gestures", | ||
"version": "5.0.36", | ||
"version": "5.0.37", | ||
"description": "Unified mouse, mouse wheel & multi-touch event stream abstraction", | ||
@@ -31,3 +31,5 @@ "type": "module", | ||
"scripts": { | ||
"build": "yarn clean && tsc --declaration", | ||
"build": "yarn build:esbuild && yarn build:decl", | ||
"build:decl": "tsc --declaration --emitDeclarationOnly", | ||
"build:esbuild": "esbuild --format=esm --platform=neutral --target=es2022 --tsconfig=tsconfig.json --outdir=. src/**/*.ts", | ||
"clean": "rimraf --glob '*.js' '*.d.ts' '*.map' doc", | ||
@@ -41,10 +43,11 @@ "doc": "typedoc --excludePrivate --excludeInternal --out doc src/index.ts", | ||
"dependencies": { | ||
"@thi.ng/api": "^8.9.11", | ||
"@thi.ng/checks": "^3.4.11", | ||
"@thi.ng/math": "^5.7.6", | ||
"@thi.ng/rstream": "^8.2.13", | ||
"@thi.ng/transducers": "^8.8.14" | ||
"@thi.ng/api": "^8.9.12", | ||
"@thi.ng/checks": "^3.4.12", | ||
"@thi.ng/math": "^5.7.7", | ||
"@thi.ng/rstream": "^8.2.14", | ||
"@thi.ng/transducers": "^8.8.15" | ||
}, | ||
"devDependencies": { | ||
"@microsoft/api-extractor": "^7.38.3", | ||
"esbuild": "^0.19.8", | ||
"rimraf": "^5.0.5", | ||
@@ -98,3 +101,3 @@ "tools": "^0.0.1", | ||
}, | ||
"gitHead": "25f2ac8ff795a432a930119661b364d4d93b59a0\n" | ||
"gitHead": "5e7bafedfc3d53bc131469a28de31dd8e5b4a3ff\n" | ||
} |
48675
6
355
Updated@thi.ng/api@^8.9.12
Updated@thi.ng/checks@^3.4.12
Updated@thi.ng/math@^5.7.7
Updated@thi.ng/rstream@^8.2.14
Updated@thi.ng/transducers@^8.8.15