From 8ed2b1c84118059c748aeb187c27cd787e07e184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 15 Sep 2021 13:49:47 +0200 Subject: [PATCH 1/6] Use the same `dragSessionTracker` machine in the `ResizableBox` as in the `CanvasContainer` --- src/ResizableBox.tsx | 238 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 196 insertions(+), 42 deletions(-) diff --git a/src/ResizableBox.tsx b/src/ResizableBox.tsx index c5b38b0c..ee986490 100644 --- a/src/ResizableBox.tsx +++ b/src/ResizableBox.tsx @@ -1,53 +1,210 @@ import { Box, BoxProps } from '@chakra-ui/react'; import { useMachine } from '@xstate/react'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { createModel } from 'xstate/lib/model'; +import { + actions, + assign, + ContextFrom, + sendParent, + SpecialTargets, +} from 'xstate'; import { Point } from './pathUtils'; -const dragDropModel = createModel( +interface DragSession { + pointerId: number; + point: Point; +} + +interface PointDelta { + x: number; + y: number; +} + +const dragSessionModel = createModel( + { + session: null as DragSession | null, + ref: null as React.MutableRefObject | null, + }, + { + events: { + DRAG_SESSION_STARTED: ({ pointerId, point }: DragSession) => ({ + pointerId, + point, + }), + DRAG_SESSION_STOPPED: () => ({}), + DRAG_POINT_MOVED: ({ point }: Pick) => ({ point }), + }, + }, +); + +const dragSessionTracker = dragSessionModel.createMachine( + { + preserveActionOrder: true, + initial: 'idle', + states: { + idle: { + invoke: { + id: 'dragSessionStartedListener', + src: + ({ ref }) => + (sendBack) => { + const node = ref!.current!; + const listener = (ev: PointerEvent) => { + const isMouseLeftButton = ev.button === 0; + if (isMouseLeftButton) { + sendBack( + dragSessionModel.events.DRAG_SESSION_STARTED({ + pointerId: ev.pointerId, + point: { + x: ev.pageX, + y: ev.pageY, + }, + }), + ); + } + }; + node.addEventListener('pointerdown', listener); + return () => node.removeEventListener('pointerdown', listener); + }, + }, + on: { + DRAG_SESSION_STARTED: { + target: 'active', + actions: actions.forwardTo(SpecialTargets.Parent), + }, + }, + }, + active: { + entry: ['capturePointer', 'setSessionData'], + exit: ['releasePointer', 'clearSessionData'], + invoke: { + id: 'dragSessionListeners', + src: + ({ ref, session }) => + (sendBack) => { + const node = ref!.current!; + + const moveListener = (ev: PointerEvent) => { + if (ev.pointerId !== session!.pointerId) { + return; + } + sendBack( + dragSessionModel.events.DRAG_POINT_MOVED({ + point: { x: ev.pageX, y: ev.pageY }, + }), + ); + }; + const stopListener = (ev: PointerEvent) => { + if (ev.pointerId !== session!.pointerId) { + return; + } + sendBack(dragSessionModel.events.DRAG_SESSION_STOPPED()); + }; + node.addEventListener('pointermove', moveListener); + node.addEventListener('pointerup', stopListener); + node.addEventListener('pointercancel', stopListener); + + return () => { + node.removeEventListener('pointermove', moveListener); + node.removeEventListener('pointerup', stopListener); + node.removeEventListener('pointercancel', stopListener); + }; + }, + }, + on: { + DRAG_POINT_MOVED: { + actions: ['sendPointDelta', 'updatePoint'], + }, + DRAG_SESSION_STOPPED: { + target: 'idle', + actions: actions.forwardTo(SpecialTargets.Parent), + }, + }, + }, + }, + }, { - prevWidth: 0, + actions: { + capturePointer: ({ ref }, ev: any) => + ref!.current!.setPointerCapture(ev!.pointerId), + releasePointer: ({ ref, session }) => + ref!.current!.releasePointerCapture(session!.pointerId), + setSessionData: assign({ + session: (ctx, ev: any) => ({ + pointerId: ev.pointerId, + point: ev.point, + }), + }), + clearSessionData: assign({ + session: null, + }) as any, + updatePoint: assign({ + session: (ctx, ev: any) => ({ + ...ctx.session!, + point: ev.point, + }), + }), + sendPointDelta: sendParent( + ( + ctx: ContextFrom, + ev: ReturnType, + ) => ({ + type: 'POINTER_MOVED_BY', + delta: { + x: ctx.session!.point.x - ev.point.x, + y: ctx.session!.point.y - ev.point.y, + }, + }), + ) as any, + }, + }, +); + +const resizableModel = createModel( + { + ref: null as React.MutableRefObject | null, widthDelta: 0, - dragPoint: { x: 0, y: 0 }, - point: { x: 0, y: 0 }, }, { events: { - 'DRAG.START': (point: Point) => ({ point }), - 'DRAG.MOVE': (point: Point) => ({ point }), - 'DRAG.END': () => ({}), + DRAG_SESSION_STARTED: ({ point }: { point: Point }) => ({ + point, + }), + DRAG_SESSION_STOPPED: () => ({}), + POINTER_MOVED_BY: ({ delta }: { delta: PointDelta }) => ({ + delta, + }), }, }, ); -const dragDropMachine = dragDropModel.createMachine({ +const resizableMachine = resizableModel.createMachine({ + invoke: { + id: 'dragSessionTracker', + src: (ctx) => + dragSessionTracker.withContext({ + ...dragSessionModel.initialContext, + ref: ctx.ref, + }), + }, initial: 'idle', states: { idle: { on: { - 'DRAG.START': { - target: 'dragging', - actions: dragDropModel.assign({ point: (_, e) => e.point }), - }, + DRAG_SESSION_STARTED: 'active', }, }, - dragging: { + active: { on: { - 'DRAG.MOVE': { - actions: dragDropModel.assign({ - dragPoint: (_, e) => e.point, + POINTER_MOVED_BY: { + actions: assign({ widthDelta: (ctx, e) => { - return Math.max(0, ctx.prevWidth + (ctx.point.x - e.point.x)); + return Math.max(0, ctx.widthDelta + e.delta.x); }, }), }, - 'DRAG.END': { - target: 'idle', - actions: dragDropModel.assign({ - point: (ctx) => ctx.dragPoint, - prevWidth: (ctx) => ctx.widthDelta, - }), - }, + DRAG_SESSION_STOPPED: 'idle', }, }, }, @@ -56,7 +213,19 @@ const dragDropMachine = dragDropModel.createMachine({ const ResizeHandle: React.FC<{ onChange: (width: number) => void; }> = ({ onChange }) => { - const [state, send] = useMachine(dragDropMachine); + const ref = useRef(null!); + + const [state, send] = useMachine( + resizableMachine.withConfig( + { + actions: {}, + }, + { + ...resizableModel.initialContext, + ref, + }, + ), + ); useEffect(() => { onChange(state.context.widthDelta); @@ -64,6 +233,7 @@ const ResizeHandle: React.FC<{ return ( { - e.stopPropagation(); - e.currentTarget.setPointerCapture(e.pointerId); - send( - dragDropModel.events['DRAG.START']({ x: e.clientX, y: e.clientY }), - ); - }} - onPointerMove={(e) => { - send(dragDropModel.events['DRAG.MOVE']({ x: e.clientX, y: e.clientY })); - }} - onPointerUp={() => { - send(dragDropModel.events['DRAG.END']()); - }} - onPointerCancel={() => { - send(dragDropModel.events['DRAG.END']()); - }} > ); }; From 0c6b19f662b862ddc605277e951ca8943b7f7260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 15 Sep 2021 13:57:03 +0200 Subject: [PATCH 2/6] Deduplicate `dragSessionTracker` --- src/CanvasContainer.tsx | 163 ++-------------------------------- src/ResizableBox.tsx | 178 +++----------------------------------- src/dragSessionTracker.ts | 159 ++++++++++++++++++++++++++++++++++ src/pathUtils.ts | 7 +- src/types.ts | 5 ++ 5 files changed, 184 insertions(+), 328 deletions(-) create mode 100644 src/dragSessionTracker.ts diff --git a/src/CanvasContainer.tsx b/src/CanvasContainer.tsx index 95d32cd7..dcc55cbb 100644 --- a/src/CanvasContainer.tsx +++ b/src/CanvasContainer.tsx @@ -2,169 +2,18 @@ import React, { CSSProperties, useEffect, useRef } from 'react'; import { canvasModel, ZoomFactor } from './canvasMachine'; import { useCanvas } from './CanvasContext'; import { useMachine } from '@xstate/react'; -import { - assign, - actions, - sendParent, - ContextFrom, - SpecialTargets, -} from 'xstate'; +import { actions } from 'xstate'; import { createModel } from 'xstate/lib/model'; import { Point } from './pathUtils'; import { isAcceptingSpaceNatively, isWithPlatformMetaKey } from './utils'; import { useEmbed } from './embedContext'; +import { + dragSessionModel, + dragSessionTracker, + PointDelta, +} from './dragSessionTracker'; import { AnyState } from './types'; -interface DragSession { - pointerId: number; - point: Point; -} - -interface PointDelta { - x: number; - y: number; -} - -const dragSessionModel = createModel( - { - session: null as DragSession | null, - ref: null as React.MutableRefObject | null, - }, - { - events: { - DRAG_SESSION_STARTED: ({ pointerId, point }: DragSession) => ({ - pointerId, - point, - }), - DRAG_SESSION_STOPPED: () => ({}), - DRAG_POINT_MOVED: ({ point }: Pick) => ({ point }), - }, - }, -); - -const dragSessionTracker = dragSessionModel.createMachine( - { - preserveActionOrder: true, - initial: 'idle', - states: { - idle: { - invoke: { - id: 'dragSessionStartedListener', - src: - ({ ref }) => - (sendBack) => { - const node = ref!.current!; - const listener = (ev: PointerEvent) => { - const isMouseLeftButton = ev.button === 0; - if (isMouseLeftButton) { - sendBack( - dragSessionModel.events.DRAG_SESSION_STARTED({ - pointerId: ev.pointerId, - point: { - x: ev.pageX, - y: ev.pageY, - }, - }), - ); - } - }; - node.addEventListener('pointerdown', listener); - return () => node.removeEventListener('pointerdown', listener); - }, - }, - on: { - DRAG_SESSION_STARTED: { - target: 'active', - actions: actions.forwardTo(SpecialTargets.Parent), - }, - }, - }, - active: { - entry: ['capturePointer', 'setSessionData'], - exit: ['releasePointer', 'clearSessionData'], - invoke: { - id: 'dragSessionListeners', - src: - ({ ref, session }) => - (sendBack) => { - const node = ref!.current!; - - const moveListener = (ev: PointerEvent) => { - if (ev.pointerId !== session!.pointerId) { - return; - } - sendBack( - dragSessionModel.events.DRAG_POINT_MOVED({ - point: { x: ev.pageX, y: ev.pageY }, - }), - ); - }; - const stopListener = (ev: PointerEvent) => { - if (ev.pointerId !== session!.pointerId) { - return; - } - sendBack(dragSessionModel.events.DRAG_SESSION_STOPPED()); - }; - node.addEventListener('pointermove', moveListener); - node.addEventListener('pointerup', stopListener); - node.addEventListener('pointercancel', stopListener); - - return () => { - node.removeEventListener('pointermove', moveListener); - node.removeEventListener('pointerup', stopListener); - node.removeEventListener('pointercancel', stopListener); - }; - }, - }, - on: { - DRAG_POINT_MOVED: { - actions: ['sendPointDelta', 'updatePoint'], - }, - DRAG_SESSION_STOPPED: { - target: 'idle', - actions: actions.forwardTo(SpecialTargets.Parent), - }, - }, - }, - }, - }, - { - actions: { - capturePointer: ({ ref }, ev: any) => - ref!.current!.setPointerCapture(ev!.pointerId), - releasePointer: ({ ref, session }) => - ref!.current!.releasePointerCapture(session!.pointerId), - setSessionData: assign({ - session: (ctx, ev: any) => ({ - pointerId: ev.pointerId, - point: ev.point, - }), - }), - clearSessionData: assign({ - session: null, - }) as any, - updatePoint: assign({ - session: (ctx, ev: any) => ({ - ...ctx.session!, - point: ev.point, - }), - }), - sendPointDelta: sendParent( - ( - ctx: ContextFrom, - ev: ReturnType, - ) => ({ - type: 'POINTER_MOVED_BY', - delta: { - x: ctx.session!.point.x - ev.point.x, - y: ctx.session!.point.y - ev.point.y, - }, - }), - ) as any, - }, - }, -); - const dragModel = createModel( { ref: null as React.MutableRefObject | null, diff --git a/src/ResizableBox.tsx b/src/ResizableBox.tsx index ee986490..c473670c 100644 --- a/src/ResizableBox.tsx +++ b/src/ResizableBox.tsx @@ -2,164 +2,13 @@ import { Box, BoxProps } from '@chakra-ui/react'; import { useMachine } from '@xstate/react'; import { useEffect, useRef, useState } from 'react'; import { createModel } from 'xstate/lib/model'; +import { assign } from 'xstate'; import { - actions, - assign, - ContextFrom, - sendParent, - SpecialTargets, -} from 'xstate'; -import { Point } from './pathUtils'; - -interface DragSession { - pointerId: number; - point: Point; -} - -interface PointDelta { - x: number; - y: number; -} - -const dragSessionModel = createModel( - { - session: null as DragSession | null, - ref: null as React.MutableRefObject | null, - }, - { - events: { - DRAG_SESSION_STARTED: ({ pointerId, point }: DragSession) => ({ - pointerId, - point, - }), - DRAG_SESSION_STOPPED: () => ({}), - DRAG_POINT_MOVED: ({ point }: Pick) => ({ point }), - }, - }, -); - -const dragSessionTracker = dragSessionModel.createMachine( - { - preserveActionOrder: true, - initial: 'idle', - states: { - idle: { - invoke: { - id: 'dragSessionStartedListener', - src: - ({ ref }) => - (sendBack) => { - const node = ref!.current!; - const listener = (ev: PointerEvent) => { - const isMouseLeftButton = ev.button === 0; - if (isMouseLeftButton) { - sendBack( - dragSessionModel.events.DRAG_SESSION_STARTED({ - pointerId: ev.pointerId, - point: { - x: ev.pageX, - y: ev.pageY, - }, - }), - ); - } - }; - node.addEventListener('pointerdown', listener); - return () => node.removeEventListener('pointerdown', listener); - }, - }, - on: { - DRAG_SESSION_STARTED: { - target: 'active', - actions: actions.forwardTo(SpecialTargets.Parent), - }, - }, - }, - active: { - entry: ['capturePointer', 'setSessionData'], - exit: ['releasePointer', 'clearSessionData'], - invoke: { - id: 'dragSessionListeners', - src: - ({ ref, session }) => - (sendBack) => { - const node = ref!.current!; - - const moveListener = (ev: PointerEvent) => { - if (ev.pointerId !== session!.pointerId) { - return; - } - sendBack( - dragSessionModel.events.DRAG_POINT_MOVED({ - point: { x: ev.pageX, y: ev.pageY }, - }), - ); - }; - const stopListener = (ev: PointerEvent) => { - if (ev.pointerId !== session!.pointerId) { - return; - } - sendBack(dragSessionModel.events.DRAG_SESSION_STOPPED()); - }; - node.addEventListener('pointermove', moveListener); - node.addEventListener('pointerup', stopListener); - node.addEventListener('pointercancel', stopListener); - - return () => { - node.removeEventListener('pointermove', moveListener); - node.removeEventListener('pointerup', stopListener); - node.removeEventListener('pointercancel', stopListener); - }; - }, - }, - on: { - DRAG_POINT_MOVED: { - actions: ['sendPointDelta', 'updatePoint'], - }, - DRAG_SESSION_STOPPED: { - target: 'idle', - actions: actions.forwardTo(SpecialTargets.Parent), - }, - }, - }, - }, - }, - { - actions: { - capturePointer: ({ ref }, ev: any) => - ref!.current!.setPointerCapture(ev!.pointerId), - releasePointer: ({ ref, session }) => - ref!.current!.releasePointerCapture(session!.pointerId), - setSessionData: assign({ - session: (ctx, ev: any) => ({ - pointerId: ev.pointerId, - point: ev.point, - }), - }), - clearSessionData: assign({ - session: null, - }) as any, - updatePoint: assign({ - session: (ctx, ev: any) => ({ - ...ctx.session!, - point: ev.point, - }), - }), - sendPointDelta: sendParent( - ( - ctx: ContextFrom, - ev: ReturnType, - ) => ({ - type: 'POINTER_MOVED_BY', - delta: { - x: ctx.session!.point.x - ev.point.x, - y: ctx.session!.point.y - ev.point.y, - }, - }), - ) as any, - }, - }, -); + dragSessionModel, + dragSessionTracker, + PointDelta, +} from './dragSessionTracker'; +import { Point } from './types'; const resizableModel = createModel( { @@ -215,16 +64,11 @@ const ResizeHandle: React.FC<{ }> = ({ onChange }) => { const ref = useRef(null!); - const [state, send] = useMachine( - resizableMachine.withConfig( - { - actions: {}, - }, - { - ...resizableModel.initialContext, - ref, - }, - ), + const [state] = useMachine( + resizableMachine.withContext({ + ...resizableModel.initialContext, + ref, + }), ); useEffect(() => { diff --git a/src/dragSessionTracker.ts b/src/dragSessionTracker.ts new file mode 100644 index 00000000..63841aec --- /dev/null +++ b/src/dragSessionTracker.ts @@ -0,0 +1,159 @@ +import { + assign, + actions, + sendParent, + ContextFrom, + SpecialTargets, +} from 'xstate'; +import { createModel } from 'xstate/lib/model'; +import { Point } from './types'; + +interface DragSession { + pointerId: number; + point: Point; +} + +export interface PointDelta { + x: number; + y: number; +} + +export const dragSessionModel = createModel( + { + session: null as DragSession | null, + ref: null as React.MutableRefObject | null, + }, + { + events: { + DRAG_SESSION_STARTED: ({ pointerId, point }: DragSession) => ({ + pointerId, + point, + }), + DRAG_SESSION_STOPPED: () => ({}), + DRAG_POINT_MOVED: ({ point }: Pick) => ({ point }), + }, + }, +); + +export const dragSessionTracker = dragSessionModel.createMachine( + { + preserveActionOrder: true, + initial: 'idle', + states: { + idle: { + invoke: { + id: 'dragSessionStartedListener', + src: + ({ ref }) => + (sendBack) => { + const node = ref!.current!; + const listener = (ev: PointerEvent) => { + const isMouseLeftButton = ev.button === 0; + if (isMouseLeftButton) { + sendBack( + dragSessionModel.events.DRAG_SESSION_STARTED({ + pointerId: ev.pointerId, + point: { + x: ev.pageX, + y: ev.pageY, + }, + }), + ); + } + }; + node.addEventListener('pointerdown', listener); + return () => node.removeEventListener('pointerdown', listener); + }, + }, + on: { + DRAG_SESSION_STARTED: { + target: 'active', + actions: actions.forwardTo(SpecialTargets.Parent), + }, + }, + }, + active: { + entry: ['capturePointer', 'setSessionData'], + exit: ['releasePointer', 'clearSessionData'], + invoke: { + id: 'dragSessionListeners', + src: + ({ ref, session }) => + (sendBack) => { + const node = ref!.current!; + + const moveListener = (ev: PointerEvent) => { + if (ev.pointerId !== session!.pointerId) { + return; + } + sendBack( + dragSessionModel.events.DRAG_POINT_MOVED({ + point: { x: ev.pageX, y: ev.pageY }, + }), + ); + }; + const stopListener = (ev: PointerEvent) => { + if (ev.pointerId !== session!.pointerId) { + return; + } + sendBack(dragSessionModel.events.DRAG_SESSION_STOPPED()); + }; + node.addEventListener('pointermove', moveListener); + node.addEventListener('pointerup', stopListener); + node.addEventListener('pointercancel', stopListener); + + return () => { + node.removeEventListener('pointermove', moveListener); + node.removeEventListener('pointerup', stopListener); + node.removeEventListener('pointercancel', stopListener); + }; + }, + }, + on: { + DRAG_POINT_MOVED: { + actions: ['sendPointDelta', 'updatePoint'], + }, + DRAG_SESSION_STOPPED: { + target: 'idle', + actions: actions.forwardTo(SpecialTargets.Parent), + }, + }, + }, + }, + }, + { + actions: { + capturePointer: ({ ref }, ev: any) => + ref!.current!.setPointerCapture(ev!.pointerId), + releasePointer: ({ ref, session }) => + ref!.current!.releasePointerCapture(session!.pointerId), + setSessionData: assign({ + session: (ctx, ev: any) => ({ + pointerId: ev.pointerId, + point: ev.point, + }), + }), + clearSessionData: assign({ + session: null, + }) as any, + updatePoint: assign({ + session: (ctx, ev: any) => ({ + ...ctx.session!, + point: ev.point, + }), + }), + sendPointDelta: sendParent( + ( + ctx: ContextFrom, + ev: ReturnType, + ) => ({ + type: 'POINTER_MOVED_BY', + delta: { + x: ctx.session!.point.x - ev.point.x, + y: ctx.session!.point.y - ev.point.y, + }, + }), + ) as any, + }, + }, +); diff --git a/src/pathUtils.ts b/src/pathUtils.ts index a418a7ec..f05390aa 100644 --- a/src/pathUtils.ts +++ b/src/pathUtils.ts @@ -1,7 +1,6 @@ -export interface Point { - x: number; - y: number; -} +import { Point } from './types'; + +export type { Point }; enum Sides { Top = 'top', diff --git a/src/types.ts b/src/types.ts index 651a7ddb..f83440f6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -78,3 +78,8 @@ export interface ParsedEmbed { export type EmbedContext = | { isEmbedded: false } | ({ isEmbedded: true; originalUrl: string } & ParsedEmbed); + +export interface Point { + x: number; + y: number; +} From 3b5e1c87e5697fb58bdc8854aeac93432f8521aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 15 Sep 2021 14:25:12 +0200 Subject: [PATCH 3/6] Allow for panning with wheel being pressed --- src/CanvasContainer.tsx | 60 +++++++++++++++++++++++++++++++++++---- src/dragSessionTracker.ts | 16 +++++++++-- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/CanvasContainer.tsx b/src/CanvasContainer.tsx index dcc55cbb..90b9e8e5 100644 --- a/src/CanvasContainer.tsx +++ b/src/CanvasContainer.tsx @@ -10,6 +10,7 @@ import { useEmbed } from './embedContext'; import { dragSessionModel, dragSessionTracker, + DragSession, PointDelta, } from './dragSessionTracker'; import { AnyState } from './types'; @@ -22,7 +23,9 @@ const dragModel = createModel( events: { LOCK: () => ({}), RELEASE: () => ({}), - ENABLE_PANNING: () => ({}), + ENABLE_PANNING: (sessionSeed: DragSession | null = null) => ({ + sessionSeed, + }), DISABLE_PANNING: () => ({}), ENABLE_PAN_MODE: () => ({}), DISABLE_PAN_MODE: () => ({}), @@ -33,6 +36,8 @@ const dragModel = createModel( POINTER_MOVED_BY: ({ delta }: { delta: PointDelta }) => ({ delta, }), + WHEEL_PRESSED: (data: DragSession) => ({}), + WHEEL_RELEASED: () => ({}), }, }, ); @@ -62,11 +67,17 @@ const dragMachine = dragModel.createMachine( initial: 'released', states: { released: { - invoke: { - src: 'invokeDetectLock', - }, + invoke: [ + { + src: 'invokeDetectLock', + }, + { + src: 'wheelPressListener', + }, + ], on: { LOCK: 'locked', + WHEEL_PRESSED: 'wheelPressed', }, }, locked: { @@ -81,6 +92,16 @@ const dragMachine = dragModel.createMachine( src: 'invokeDetectRelease', }, }, + wheelPressed: { + invoke: { + src: 'wheelReleaseListener', + }, + entry: actions.raise(((ctx: any, ev: any) => + dragModel.events.ENABLE_PANNING(ev.data)) as any) as any, + exit: actions.raise( + dragModel.events.DISABLE_PANNING(), + ) as any, + }, }, on: { ENABLE_PAN_MODE: 'pan', @@ -108,10 +129,11 @@ const dragMachine = dragModel.createMachine( exit: 'enableTextSelection', invoke: { id: 'dragSessionTracker', - src: (ctx) => + src: (ctx, ev) => dragSessionTracker.withContext({ ...dragSessionModel.initialContext, ref: ctx.ref, + session: (ev as any).sessionSeed, }), }, on: { @@ -178,6 +200,34 @@ const dragMachine = dragModel.createMachine( }, }, services: { + wheelPressListener: (ctx) => (sendBack) => { + const node = ctx.ref!.current!; + const listener = (ev: PointerEvent) => { + if (ev.button === 1) { + sendBack( + dragModel.events.WHEEL_PRESSED({ + pointerId: ev.pointerId, + point: { + x: ev.pageX, + y: ev.pageY, + }, + }), + ); + } + }; + node.addEventListener('pointerdown', listener); + return () => node.removeEventListener('pointerdown', listener); + }, + wheelReleaseListener: (ctx) => (sendBack) => { + const node = ctx.ref!.current!; + const listener = (ev: PointerEvent) => { + if (ev.button === 1) { + sendBack(dragModel.events.WHEEL_RELEASED()); + } + }; + node.addEventListener('pointerup', listener); + return () => node.removeEventListener('pointerup', listener); + }, invokeDetectLock: () => (sendBack) => { function keydownListener(e: KeyboardEvent) { const target = e.target as HTMLElement; diff --git a/src/dragSessionTracker.ts b/src/dragSessionTracker.ts index 63841aec..1d694939 100644 --- a/src/dragSessionTracker.ts +++ b/src/dragSessionTracker.ts @@ -8,7 +8,7 @@ import { import { createModel } from 'xstate/lib/model'; import { Point } from './types'; -interface DragSession { +export interface DragSession { pointerId: number; point: Point; } @@ -38,8 +38,20 @@ export const dragSessionModel = createModel( export const dragSessionTracker = dragSessionModel.createMachine( { preserveActionOrder: true, - initial: 'idle', + initial: 'check_session_data', states: { + check_session_data: { + always: [ + { + cond: (ctx) => !!ctx.session, + target: 'active', + actions: sendParent((ctx) => + dragSessionModel.events.DRAG_SESSION_STARTED(ctx.session!), + ), + }, + 'idle', + ], + }, idle: { invoke: { id: 'dragSessionStartedListener', From d9d776cd3bea498dd6b0c4a92acd2adf84f8ce4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Tue, 19 Oct 2021 14:01:32 +0200 Subject: [PATCH 4/6] Tweak some types --- src/CanvasContainer.tsx | 24 ++++++++++-------------- src/ResizableBox.tsx | 4 ++-- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/CanvasContainer.tsx b/src/CanvasContainer.tsx index 1a349ab3..0289f5e2 100644 --- a/src/CanvasContainer.tsx +++ b/src/CanvasContainer.tsx @@ -81,12 +81,8 @@ const dragMachine = dragModel.createMachine( }, }, locked: { - entry: actions.raise( - dragModel.events.ENABLE_PANNING(), - ) as any, - exit: actions.raise( - dragModel.events.DISABLE_PANNING(), - ) as any, + entry: actions.raise(dragModel.events.ENABLE_PANNING()), + exit: actions.raise(dragModel.events.DISABLE_PANNING()), on: { RELEASE: 'released' }, invoke: { src: 'invokeDetectRelease', @@ -96,11 +92,9 @@ const dragMachine = dragModel.createMachine( invoke: { src: 'wheelReleaseListener', }, - entry: actions.raise(((ctx: any, ev: any) => - dragModel.events.ENABLE_PANNING(ev.data)) as any) as any, - exit: actions.raise( - dragModel.events.DISABLE_PANNING(), - ) as any, + entry: actions.raise(((_ctx: any, ev: any) => + dragModel.events.ENABLE_PANNING(ev.data)) as any), + exit: actions.raise(dragModel.events.DISABLE_PANNING()), }, }, on: { @@ -108,8 +102,8 @@ const dragMachine = dragModel.createMachine( }, }, pan: { - entry: actions.raise(dragModel.events.ENABLE_PANNING()) as any, - exit: actions.raise(dragModel.events.DISABLE_PANNING()) as any, + entry: actions.raise(dragModel.events.ENABLE_PANNING()), + exit: actions.raise(dragModel.events.DISABLE_PANNING()), on: { DISABLE_PAN_MODE: 'lockable', }, @@ -133,7 +127,9 @@ const dragMachine = dragModel.createMachine( dragSessionTracker.withContext({ ...dragSessionModel.initialContext, ref: ctx.ref, - session: (ev as any).sessionSeed, + session: ( + ev as Extract + ).sessionSeed, }), }, on: { diff --git a/src/ResizableBox.tsx b/src/ResizableBox.tsx index c473670c..05ff81ee 100644 --- a/src/ResizableBox.tsx +++ b/src/ResizableBox.tsx @@ -62,7 +62,7 @@ const resizableMachine = resizableModel.createMachine({ const ResizeHandle: React.FC<{ onChange: (width: number) => void; }> = ({ onChange }) => { - const ref = useRef(null!); + const ref = useRef(null!); const [state] = useMachine( resizableMachine.withContext({ @@ -77,7 +77,7 @@ const ResizeHandle: React.FC<{ return ( Date: Tue, 19 Oct 2021 15:01:59 +0200 Subject: [PATCH 5/6] Fixed wheel panning --- src/CanvasContainer.tsx | 34 +++++++++++++++++----------------- src/dragSessionTracker.ts | 16 ++++++++++------ 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/CanvasContainer.tsx b/src/CanvasContainer.tsx index 0289f5e2..4563c258 100644 --- a/src/CanvasContainer.tsx +++ b/src/CanvasContainer.tsx @@ -36,7 +36,7 @@ const dragModel = createModel( POINTER_MOVED_BY: ({ delta }: { delta: PointDelta }) => ({ delta, }), - WHEEL_PRESSED: (data: DragSession) => ({}), + WHEEL_PRESSED: (data: DragSession) => ({ data }), WHEEL_RELEASED: () => ({}), }, }, @@ -89,12 +89,12 @@ const dragMachine = dragModel.createMachine( }, }, wheelPressed: { - invoke: { - src: 'wheelReleaseListener', - }, entry: actions.raise(((_ctx: any, ev: any) => dragModel.events.ENABLE_PANNING(ev.data)) as any), exit: actions.raise(dragModel.events.DISABLE_PANNING()), + on: { + DRAG_SESSION_STOPPED: 'released', + }, }, }, on: { @@ -127,9 +127,19 @@ const dragMachine = dragModel.createMachine( dragSessionTracker.withContext({ ...dragSessionModel.initialContext, ref: ctx.ref, - session: ( - ev as Extract - ).sessionSeed, + session: + // this is just defensive programming + // this really should receive ENABLE_PANNING at all times as this is the event that is making this state to be entered + // however, raised events are not given to invoke creators so we have to fallback handling WHEEL_PRESSED event + // in reality, because of this issue, ENABLE_PANNING that we can receive here won't ever hold any `sessionSeed` (as that is only coming from the wheel-oriented interaction) + ev.type === 'ENABLE_PANNING' + ? ev.sessionSeed + : ( + ev as Extract< + typeof ev, + { type: 'WHEEL_PRESSED' } + > + ).data, }), }, on: { @@ -214,16 +224,6 @@ const dragMachine = dragModel.createMachine( node.addEventListener('pointerdown', listener); return () => node.removeEventListener('pointerdown', listener); }, - wheelReleaseListener: (ctx) => (sendBack) => { - const node = ctx.ref!.current!; - const listener = (ev: PointerEvent) => { - if (ev.button === 1) { - sendBack(dragModel.events.WHEEL_RELEASED()); - } - }; - node.addEventListener('pointerup', listener); - return () => node.removeEventListener('pointerup', listener); - }, invokeDetectLock: () => (sendBack) => { function keydownListener(e: KeyboardEvent) { const target = e.target as HTMLElement; diff --git a/src/dragSessionTracker.ts b/src/dragSessionTracker.ts index 1d694939..aec021b2 100644 --- a/src/dragSessionTracker.ts +++ b/src/dragSessionTracker.ts @@ -135,15 +135,19 @@ export const dragSessionTracker = dragSessionModel.createMachine( }, { actions: { - capturePointer: ({ ref }, ev: any) => - ref!.current!.setPointerCapture(ev!.pointerId), + capturePointer: ({ ref, session }, ev: any) => + ref!.current!.setPointerCapture(ev!.pointerId || session?.pointerId), releasePointer: ({ ref, session }) => ref!.current!.releasePointerCapture(session!.pointerId), setSessionData: assign({ - session: (ctx, ev: any) => ({ - pointerId: ev.pointerId, - point: ev.point, - }), + session: (ctx, ev: any) => { + if (ev.pointerId && ev.point) + return { + pointerId: ev.pointerId, + point: ev.point, + }; + return ctx.session; + }, }), clearSessionData: assign({ session: null, From 0f8e205fd2652df6d07dff53eea18588d7b3e2d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Tue, 19 Oct 2021 15:04:10 +0200 Subject: [PATCH 6/6] Add changeset --- .changeset/beige-points-smash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/beige-points-smash.md diff --git a/.changeset/beige-points-smash.md b/.changeset/beige-points-smash.md new file mode 100644 index 00000000..36b4e5d5 --- /dev/null +++ b/.changeset/beige-points-smash.md @@ -0,0 +1,5 @@ +--- +'xstate-viz-app': minor +--- + +A possibility to start panning the canvas by pressing the middle button of a mouse has been added.