@bokeh/bokehjs
Version:
Interactive, novel data visualization
533 lines • 17.8 kB
JavaScript
import { MouseButton, offset_bbox } from "./dom";
import { assert, unreachable } from "./util/assert";
export class UIGestures {
hit_area;
handlers;
static __name__ = "UIGestures";
must_be_target;
constructor(hit_area, handlers, options = {}) {
this.hit_area = hit_area;
this.handlers = handlers;
this.must_be_target = options.must_be_target ?? false;
this._pointer_over = this._pointer_over.bind(this);
this._pointer_out = this._pointer_out.bind(this);
this._pointer_down = this._pointer_down.bind(this);
this._pointer_move = this._pointer_move.bind(this);
this._pointer_up = this._pointer_up.bind(this);
this._pointer_cancel = this._pointer_cancel.bind(this);
}
connect_signals() {
this.hit_area.addEventListener("pointerover", this._pointer_over);
this.hit_area.addEventListener("pointerout", this._pointer_out);
this.hit_area.addEventListener("pointerdown", this._pointer_down);
this.hit_area.addEventListener("pointermove", this._pointer_move);
this.hit_area.addEventListener("pointerup", this._pointer_up);
this.hit_area.addEventListener("pointercancel", this._pointer_cancel);
}
disconnect_signals() {
this.hit_area.removeEventListener("pointerover", this._pointer_over);
this.hit_area.removeEventListener("pointerout", this._pointer_out);
this.hit_area.removeEventListener("pointerdown", this._pointer_down);
this.hit_area.removeEventListener("pointermove", this._pointer_move);
this.hit_area.removeEventListener("pointerup", this._pointer_up);
this.hit_area.removeEventListener("pointercancel", this._pointer_cancel);
}
remove() {
this.disconnect_signals();
}
_self_is_target(event) {
return event.composedPath()[0] == this.hit_area;
}
_is_event_target(event) {
return !this.must_be_target || this._self_is_target(event);
}
phase = "idle";
pointers = new Map();
press_timer = null;
tap_timestamp = -Infinity;
last_scale = null;
last_rotation = null;
reset() {
this._cancel_timeout();
this._user_select(true);
this.phase = "idle";
this.pointers.clear();
this.press_timer = null;
this.tap_timestamp = -Infinity;
this.last_scale = null;
this.last_rotation = null;
}
_user_select(allow) {
this.hit_area.style.userSelect = allow ? "" : "none";
}
static move_threshold = 5; /*px*/
static press_threshold = 300; /*ms*/
static doubletap_threshold = 300; /*ms*/
static pinch_threshold = 0; /*unit less*/
static rotate_threshold = 0; /*rad*/
get _is_multi_gesture() {
return this.pointers.size >= 2;
}
_within_threshold(ptr) {
const { dx, dy } = this._movement(ptr);
return dx ** 2 + dy ** 2 <= UIGestures.move_threshold ** 2;
}
get _any_movement() {
return [...this.pointers.values()].some((ptr) => !this._within_threshold(ptr));
}
_start_timeout() {
assert(this.press_timer == null);
this.press_timer = setTimeout(() => this._pointer_timeout(), UIGestures.press_threshold);
}
_cancel_timeout() {
const { press_timer } = this;
if (press_timer != null) {
clearTimeout(press_timer);
this.press_timer = null;
}
}
_pointer_timeout() {
assert(this.phase == "started");
assert(!this._is_multi_gesture);
this.phase = "pressing";
this.press_timer = null;
const [pointer] = this.pointers.values();
this.on_press(pointer.init);
}
_pointer_over(event) {
if (!this._is_event_target(event)) {
return;
}
if (event.isPrimary) {
this.on_enter(event);
}
}
_pointer_out(event) {
if (!this._is_event_target(event)) {
return;
}
if (event.isPrimary) {
this.on_leave(event);
}
}
_pointer_down(event) {
if (!this._is_event_target(event)) {
return;
}
if (this._is_multi_gesture) {
return;
}
if (this.pointers.has(event.pointerId)) {
return;
}
if (event.isPrimary && event.pointerType == "mouse" && event.buttons != MouseButton.Left) {
return;
}
if (!this.hit_area.isConnected) {
return;
}
this.pointers.set(event.pointerId, { init: event, last: event });
this.hit_area.setPointerCapture(event.pointerId);
this._user_select(false);
switch (this.phase) {
case "idle": {
this.phase = "started";
this._start_timeout();
break;
}
case "started": {
this._cancel_timeout();
break;
}
case "pressing":
case "panning":
case "pinching":
case "rotating":
case "transitional":
break;
}
}
_pointer_move(event) {
if (!this._is_event_target(event)) {
return;
}
if (event.isPrimary) {
this.on_move(event);
}
const pointer = this.pointers.get(event.pointerId);
if (pointer == null) {
return;
}
pointer.last = event;
switch (this.phase) {
case "idle": {
this.reset();
unreachable();
}
case "started":
case "transitional": {
if (!this._any_movement) {
return;
}
this._cancel_timeout();
if (!this._is_multi_gesture) {
this.phase = "panning";
const [ptr] = this.pointers.values();
const { dx, dy } = this._movement(ptr);
this.on_pan_start(ptr.init, 0, 0);
this.on_pan(ptr.last, dx, dy);
}
else {
const [ptr0, ptr1] = this.pointers.values();
const scale = this._scale(ptr0, ptr1);
const rotation = this._rotation(ptr0, ptr1);
if (Math.abs(scale - 1) > UIGestures.pinch_threshold) {
this.phase = "pinching";
this.on_pinch_start(ptr0.init, ptr1.init, 1);
this.on_pinch(ptr0.last, ptr1.last, scale);
this.last_scale = scale;
}
else if (Math.abs(rotation) > UIGestures.rotate_threshold) {
this.phase = "rotating";
this.on_rotate_start(ptr0.init, ptr1.init, 0);
this.on_rotate(ptr1.last, ptr1.last, rotation);
this.last_rotation = rotation;
}
}
break;
}
case "pressing": {
break;
}
case "panning": {
const [ptr] = this.pointers.values();
const { dx, dy } = this._movement(ptr);
this.on_pan(event, dx, dy);
break;
}
case "pinching": {
const [ptr0, ptr1] = this.pointers.values();
const scale = this._scale(ptr0, ptr1);
if (scale != this.last_scale) {
this.on_pinch(ptr0.last, ptr1.last, scale);
this.last_scale = scale;
}
break;
}
case "rotating": {
const [ptr0, ptr1] = this.pointers.values();
const rotation = this._rotation(ptr0, ptr1);
if (rotation != this.last_rotation) {
this.on_rotate(ptr0.last, ptr1.last, rotation);
this.last_rotation = rotation;
}
break;
}
}
}
_pointer_up(event) {
if (!this._is_event_target(event)) {
return;
}
const pointer = this.pointers.get(event.pointerId);
if (pointer == null) {
return;
}
pointer.last = event;
this._cancel_timeout();
switch (this.phase) {
case "idle": {
this.reset();
unreachable();
}
case "started": {
const [ptr] = this.pointers.values();
const { tap_timestamp } = this;
if (ptr.last.timeStamp - tap_timestamp < UIGestures.doubletap_threshold) {
this.tap_timestamp = -Infinity;
this.on_doubletap(ptr.last);
}
else {
this.tap_timestamp = ptr.last.timeStamp;
this.on_tap(ptr.last);
}
this.phase = "idle";
break;
}
case "transitional": {
this.phase = "idle";
break;
}
case "pressing": {
const [ptr] = this.pointers.values();
this.on_pressup(ptr.last);
this.phase = "idle";
break;
}
case "panning": {
const [ptr] = this.pointers.values();
const { dx, dy } = this._movement(ptr);
this.on_pan_end(event, dx, dy);
this.phase = "idle";
break;
}
case "pinching": {
const [ptr0, ptr1] = this.pointers.values();
const scale = this._scale(ptr0, ptr1);
this.on_pinch_end(ptr0.last, ptr1.last, scale);
this.phase = "transitional";
this.last_scale = null;
break;
}
case "rotating": {
const [ptr0, ptr1] = this.pointers.values();
const rotation = this._rotation(ptr0, ptr1);
this.on_rotate_end(ptr0.last, ptr1.last, rotation);
this.phase = "transitional";
this.last_rotation = null;
break;
}
}
this.pointers.delete(event.pointerId);
if (this.phase == "transitional") {
const [ptr] = this.pointers.values();
ptr.init = ptr.last;
}
if (this.pointers.size == 0) {
this._user_select(true);
}
}
_pointer_cancel(event) {
if (!this.pointers.has(event.pointerId)) {
return;
}
this._cancel_timeout();
switch (this.phase) {
case "idle": {
this.reset();
unreachable();
}
case "started":
case "pressing":
case "transitional": {
this.phase = "idle";
break;
}
case "panning": {
const [ptr] = this.pointers.values();
const { dx, dy } = this._movement(ptr);
this.on_pan_end(event, dx, dy);
this.phase = "idle";
break;
}
case "pinching": {
const [ptr0, ptr1] = this.pointers.values();
const scale = this._scale(ptr0, ptr1);
this.on_pinch_end(ptr0.last, ptr1.last, scale);
this.phase = "transitional";
this.last_scale = null;
break;
}
case "rotating": {
const [ptr0, ptr1] = this.pointers.values();
const rotation = this._rotation(ptr0, ptr1);
this.on_rotate_end(ptr0.last, ptr1.last, rotation);
this.phase = "transitional";
this.last_rotation = null;
break;
}
}
this.pointers.delete(event.pointerId);
if (this.phase == "transitional") {
const [ptr] = this.pointers.values();
ptr.init = ptr.last;
}
if (this.pointers.size == 0) {
this._user_select(true);
}
}
on_tap(ev) {
const { on_tap } = this.handlers;
if (on_tap != null) {
on_tap(this._tap_event("tap", ev));
}
}
on_doubletap(ev) {
const { on_doubletap } = this.handlers;
if (on_doubletap != null) {
on_doubletap(this._tap_event("double_tap", ev));
}
}
on_press(ev) {
const { on_press } = this.handlers;
if (on_press != null) {
on_press(this._tap_event("press", ev));
}
}
on_pressup(ev) {
const { on_pressup } = this.handlers;
if (on_pressup != null) {
on_pressup(this._tap_event("press_up", ev));
}
}
on_enter(ev) {
const { on_enter } = this.handlers;
if (on_enter != null) {
on_enter(this._move_event("enter", ev));
}
}
on_move(ev) {
const { on_move } = this.handlers;
if (on_move != null) {
on_move(this._move_event("move", ev));
}
}
on_leave(ev) {
const { on_leave } = this.handlers;
if (on_leave != null) {
on_leave(this._move_event("leave", ev));
}
}
on_pan_start(ev, dx, dy) {
const { on_pan_start } = this.handlers;
if (on_pan_start != null) {
on_pan_start(this._pan_event("pan_start", ev, dx, dy));
}
}
on_pan(ev, dx, dy) {
const { on_pan } = this.handlers;
if (on_pan != null) {
on_pan(this._pan_event("pan", ev, dx, dy));
}
}
on_pan_end(ev, dx, dy) {
const { on_pan_end } = this.handlers;
if (on_pan_end != null) {
on_pan_end(this._pan_event("pan_end", ev, dx, dy));
}
}
on_pinch_start(ev0, ev1, scale) {
const { on_pinch_start } = this.handlers;
if (on_pinch_start != null) {
on_pinch_start(this._pinch_event("pinch_start", ev0, ev1, scale));
}
}
on_pinch(ev0, ev1, scale) {
const { on_pinch } = this.handlers;
if (on_pinch != null) {
on_pinch(this._pinch_event("pinch", ev0, ev1, scale));
}
}
on_pinch_end(ev0, ev1, scale) {
const { on_pinch_end } = this.handlers;
if (on_pinch_end != null) {
on_pinch_end(this._pinch_event("pinch_end", ev0, ev1, scale));
}
}
on_rotate_start(ev0, ev1, rotation) {
const { on_rotate_start } = this.handlers;
if (on_rotate_start != null) {
on_rotate_start(this._rotate_event("rotate_start", ev0, ev1, rotation));
}
}
on_rotate(ev0, ev1, rotation) {
const { on_rotate } = this.handlers;
if (on_rotate != null) {
on_rotate(this._rotate_event("rotate", ev0, ev1, rotation));
}
}
on_rotate_end(ev0, ev1, rotation) {
const { on_rotate_end } = this.handlers;
if (on_rotate_end != null) {
on_rotate_end(this._rotate_event("rotate_end", ev0, ev1, rotation));
}
}
_get_sxy(event) {
const { pageX, pageY } = event;
const { left, top } = offset_bbox(this.hit_area);
return {
sx: pageX - left,
sy: pageY - top,
};
}
_get_modifiers(event) {
return {
shift: event.shiftKey,
ctrl: event.ctrlKey,
alt: event.altKey,
};
}
_tap_event(type, event) {
return {
type,
...this._get_sxy(event),
modifiers: this._get_modifiers(event),
native: event,
};
}
_move_event(type, event) {
return {
type,
...this._get_sxy(event),
modifiers: this._get_modifiers(event),
native: event,
};
}
_pan_event(type, event, dx, dy) {
return {
type,
...this._get_sxy(event),
dx,
dy,
modifiers: this._get_modifiers(event),
native: event,
};
}
_pinch_event(type, event0, event1, scale) {
const { sx: sx0, sy: sy0 } = this._get_sxy(event0);
const { sx: sx1, sy: sy1 } = this._get_sxy(event1);
return {
type,
sx: (sx0 + sx1) / 2,
sy: (sy0 + sy1) / 2,
scale,
modifiers: this._get_modifiers(event0),
native: event0,
};
}
_rotate_event(type, event0, event1, rotation) {
const { sx: sx0, sy: sy0 } = this._get_sxy(event0);
const { sx: sx1, sy: sy1 } = this._get_sxy(event1);
return {
type,
sx: (sx0 + sx1) / 2,
sy: (sy0 + sy1) / 2,
rotation,
modifiers: this._get_modifiers(event0),
native: event0,
};
}
_movement(ptr) {
return {
dx: ptr.last.x - ptr.init.x,
dy: ptr.last.y - ptr.init.y,
};
}
_distance(ev0, ev1) {
const x = ev1.x - ev0.x;
const y = ev1.y - ev0.y;
return Math.sqrt(x ** 2 + y ** 2);
}
_angle(ev0, ev1) {
const x = ev1.x - ev0.x;
const y = ev1.y - ev0.y;
return Math.atan2(y, x) * 180 / Math.PI;
}
_scale(ptr0, ptr1) {
return this._distance(ptr0.last, ptr1.last) / this._distance(ptr0.init, ptr1.init);
}
_rotation(ptr0, ptr1) {
return this._angle(ptr1.last, ptr0.last) + this._angle(ptr1.init, ptr0.init);
}
}
//# sourceMappingURL=ui_gestures.js.map