@mirasen/chessboard 1.2.3 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @mirasen/chessboard
2
2
 
3
+ ## 1.2.4
4
+
5
+ ### Patch Changes
6
+
7
+ - eccb340: Fix fast drag-and-drop reliability in Chrome and other browsers that may dispatch `lostpointercapture` before or instead of `pointerup`.
8
+
9
+ Active drag gestures now remember the pointer button that started them. When that button is released during `lostpointercapture`, the board resolves the gesture through the same terminal release path as `pointerup` instead of cancelling it prematurely.
10
+
11
+ This fixes fast drops that could previously return a piece to its source square even when the target was valid, while preserving proper cleanup for fast releases outside the board and for extension-owned drag gestures such as annotations.
12
+
3
13
  ## 1.2.3
4
14
 
5
15
  ### Patch Changes
@@ -41,7 +41,8 @@ export function handleAnnotationsEvent(state, context) {
41
41
  type: ANNOTATIONS_DRAW_DRAG_TYPE,
42
42
  sourceSquare: targetSquare,
43
43
  sourcePieceCode: null,
44
- targetSquare: targetSquare
44
+ targetSquare: targetSquare,
45
+ startButton: rawEvent.button
45
46
  });
46
47
  if (success) {
47
48
  rawEvent.preventDefault();
@@ -68,7 +69,8 @@ export function handleAnnotationsEvent(state, context) {
68
69
  type: ANNOTATIONS_IDLE_CLEAR_DRAG_TYPE,
69
70
  sourceSquare: targetSquare,
70
71
  sourcePieceCode: null,
71
- targetSquare: targetSquare
72
+ targetSquare: targetSquare,
73
+ startButton: rawEvent.button
72
74
  });
73
75
  if (success) {
74
76
  rawEvent.preventDefault();
@@ -15,11 +15,13 @@ export type RuntimeInteractionActionStartLiftedDrag = {
15
15
  type: 'startLiftedDrag';
16
16
  source: Square;
17
17
  target: Square;
18
+ startButton: number;
18
19
  };
19
20
  export type RuntimeInteractionActionStartReleaseTargetingDrag = {
20
21
  type: 'startReleaseTargetingDrag';
21
22
  source: Square;
22
23
  target: Square;
24
+ startButton: number;
23
25
  };
24
26
  export type RuntimeInteractionActionCompleteCoreDrag = {
25
27
  type: 'completeCoreDragTo';
@@ -4,6 +4,7 @@ export interface ExtensionDragSessionBase {
4
4
  sourceSquare: Square | null;
5
5
  sourcePieceCode: PieceCode | null;
6
6
  targetSquare: Square | null;
7
+ startButton: number;
7
8
  }
8
9
  export interface ExtensionDragSessionLiftedPiece extends ExtensionDragSessionBase {
9
10
  type: 'lifted-piece-drag';
@@ -18,7 +18,7 @@ export function createRuntimeInteractionSurface(state) {
18
18
  const internalState = state();
19
19
  return internalState.state.board.getPieceCodeAt(square);
20
20
  },
21
- startLiftedDrag(source, target) {
21
+ startLiftedDrag(source, target, startButton) {
22
22
  const internalState = state();
23
23
  const interactionMutationSession = internalState.mutation.getSession();
24
24
  const interaction = internalState.state.interaction;
@@ -34,7 +34,8 @@ export function createRuntimeInteractionSurface(state) {
34
34
  type: 'lifted-piece-drag',
35
35
  sourceSquare: interactionSource.square,
36
36
  sourcePieceCode: interactionSource.pieceCode,
37
- targetSquare: target
37
+ targetSquare: target,
38
+ startButton
38
39
  };
39
40
  interaction.setDragSession(dragSession, interactionMutationSession);
40
41
  runtimeRunMutationPipeline(internalState);
@@ -66,7 +67,7 @@ export function createRuntimeInteractionSurface(state) {
66
67
  mutationSession.addMutation('runtime.interaction.completeExtensionDragTo', true, dragSession);
67
68
  runtimeRunMutationPipeline(internalState);
68
69
  },
69
- startReleaseTargetingDrag(source, target) {
70
+ startReleaseTargetingDrag(source, target, startButton) {
70
71
  const internalState = state();
71
72
  const mutationSession = internalState.mutation.getSession();
72
73
  const interaction = internalState.state.interaction;
@@ -77,7 +78,8 @@ export function createRuntimeInteractionSurface(state) {
77
78
  type: 'release-targeting',
78
79
  sourceSquare: source,
79
80
  sourcePieceCode: interaction.selected.pieceCode,
80
- targetSquare: target
81
+ targetSquare: target,
82
+ startButton
81
83
  };
82
84
  interaction.setDragSession(dragSession, mutationSession);
83
85
  runtimeRunMutationPipeline(internalState);
@@ -22,10 +22,10 @@ export function createInteractionController(options) {
22
22
  if (action) {
23
23
  switch (action.type) {
24
24
  case 'startLiftedDrag':
25
- internalState.surface.startLiftedDrag(action.source, action.target);
25
+ internalState.surface.startLiftedDrag(action.source, action.target, action.startButton);
26
26
  break;
27
27
  case 'startReleaseTargetingDrag':
28
- internalState.surface.startReleaseTargetingDrag(action.source, action.target);
28
+ internalState.surface.startReleaseTargetingDrag(action.source, action.target, action.startButton);
29
29
  break;
30
30
  case 'completeCoreDragTo':
31
31
  internalState.surface.completeCoreDragTo(action.target);
@@ -1,3 +1,4 @@
1
1
  import type { Square } from '../../../state/board/types/internal.js';
2
2
  import type { InteractionStateSnapshot } from '../../../state/interaction/types/main.js';
3
+ export declare function buttonToButtonsMask(button: number): number | null;
3
4
  export declare function canMoveTo(snapshot: InteractionStateSnapshot, target: Square): boolean;
@@ -1,3 +1,19 @@
1
+ export function buttonToButtonsMask(button) {
2
+ switch (button) {
3
+ case 0:
4
+ return 1;
5
+ case 1:
6
+ return 4;
7
+ case 2:
8
+ return 2;
9
+ case 3:
10
+ return 8;
11
+ case 4:
12
+ return 16;
13
+ default:
14
+ return null;
15
+ }
16
+ }
1
17
  export function canMoveTo(snapshot, target) {
2
18
  const { movability } = snapshot;
3
19
  if (movability.mode === 0 /* MovabilityModeCode.Disabled */) {
@@ -2,7 +2,7 @@ import assert from '@ktarmyshov/assert';
2
2
  import { isEmptyPieceCode, isNonEmptyPieceCode } from '../../../state/board/check.js';
3
3
  import { fromPieceCode } from '../../../state/board/piece.js';
4
4
  import { isDragSessionCoreOwned } from '../../../state/interaction/helpers.js';
5
- import { canMoveTo } from './helpers.js';
5
+ import { buttonToButtonsMask, canMoveTo } from './helpers.js';
6
6
  export function determineActionPointerDown(state, context) {
7
7
  assert(context.rawEvent.type === 'pointerdown', 'determineActionPointerDown should only be called for pointerdown events');
8
8
  const rawEvent = context.rawEvent;
@@ -31,7 +31,8 @@ export function determineActionPointerDown(state, context) {
31
31
  return {
32
32
  type: 'startReleaseTargetingDrag',
33
33
  source: interaction.selected.square,
34
- target: sceneEvent.targetSquare
34
+ target: sceneEvent.targetSquare,
35
+ startButton: rawEvent.button
35
36
  };
36
37
  }
37
38
  const isLegalMoveTarget = interaction.movability.mode === 1 /* MovabilityModeCode.Free */ ||
@@ -42,7 +43,8 @@ export function determineActionPointerDown(state, context) {
42
43
  return {
43
44
  type: 'startReleaseTargetingDrag',
44
45
  source: interaction.selected.square,
45
- target: sceneEvent.targetSquare
46
+ target: sceneEvent.targetSquare,
47
+ startButton: rawEvent.button
46
48
  };
47
49
  }
48
50
  }
@@ -55,7 +57,8 @@ export function determineActionPointerDown(state, context) {
55
57
  return {
56
58
  type: 'startLiftedDrag',
57
59
  source: sceneEvent.targetSquare,
58
- target: sceneEvent.targetSquare
60
+ target: sceneEvent.targetSquare,
61
+ startButton: rawEvent.button
59
62
  };
60
63
  }
61
64
  }
@@ -72,59 +75,43 @@ export function determineActionPointerMove(state, context) {
72
75
  }
73
76
  return null;
74
77
  }
75
- export function determineActionPointerUp(state, context) {
76
- assert(context.rawEvent.type === 'pointerup', 'determineActionPointerUp should only be called for pointerup events');
78
+ function determineActionTerminalRelease(state, context) {
77
79
  const interaction = state.surface.getInteractionStateSnapshot();
78
80
  const dragSession = interaction.dragSession;
79
81
  if (!dragSession) {
80
- // No active drag session, so nothing to do on pointer up
81
82
  return null;
82
83
  }
83
84
  const sceneEvent = context.sceneEvent;
84
- assert(sceneEvent, 'Scene event should be present for pointer events');
85
+ assert(sceneEvent, 'Scene event should be present for terminal release');
85
86
  if (!isDragSessionCoreOwned(dragSession)) {
86
- // For extension-owned drag sessions, we simply end the session without attempting to make a move,
87
- // since the runtime doesn't have enough information about the semantics of the drag session.
88
87
  return {
89
88
  type: 'completeExtensionDrag',
90
89
  target: sceneEvent.targetSquare
91
90
  };
92
91
  }
93
- // Check if the square is the same as the source square of the drag session.
94
- // If it is, then we can end the drag session without making a move.
95
92
  if (sceneEvent.targetSquare === dragSession.sourceSquare) {
96
- return {
97
- type: 'cancelActiveInteraction'
98
- };
93
+ return { type: 'cancelActiveInteraction' };
99
94
  }
100
- // Check if the target square is a valid destination for the selected piece.
101
95
  if (sceneEvent.targetSquare !== null && canMoveTo(interaction, sceneEvent.targetSquare)) {
102
- return {
103
- type: 'completeCoreDragTo',
104
- target: sceneEvent.targetSquare
105
- };
96
+ return { type: 'completeCoreDragTo', target: sceneEvent.targetSquare };
106
97
  }
107
- // Invalid target: piece returns to source for lifted drag (selection preserved),
108
- // or selection is cleared for release targeting.
109
98
  if (dragSession.type === 'lifted-piece-drag') {
110
- return {
111
- type: 'cancelActiveInteraction'
112
- };
99
+ return { type: 'cancelActiveInteraction' };
113
100
  }
114
101
  else {
115
- return {
116
- type: 'cancelInteraction'
117
- };
102
+ return { type: 'cancelInteraction' };
118
103
  }
119
104
  }
105
+ export function determineActionPointerUp(state, context) {
106
+ assert(context.rawEvent.type === 'pointerup', 'determineActionPointerUp should only be called for pointerup events');
107
+ return determineActionTerminalRelease(state, context);
108
+ }
120
109
  export function determineActionPointerCancel(state, context) {
121
110
  assert(context.rawEvent.type === 'pointercancel', 'determineActionPointerCancel should only be called for pointercancel events');
122
111
  const interaction = state.surface.getInteractionStateSnapshot();
123
112
  if (!interaction.dragSession) {
124
- // No active drag session, so nothing to do on pointer cancel
125
113
  return null;
126
114
  }
127
- // Cancel the active interaction on pointer cancel
128
115
  return {
129
116
  type: 'cancelActiveInteraction'
130
117
  };
@@ -132,12 +119,17 @@ export function determineActionPointerCancel(state, context) {
132
119
  export function determineActionLostPointerCapture(state, context) {
133
120
  assert(context.rawEvent.type === 'lostpointercapture', 'determineActionLostPointerCapture should only be called for lostpointercapture events');
134
121
  const interaction = state.surface.getInteractionStateSnapshot();
135
- if (!interaction.dragSession) {
136
- // No active drag session, so nothing to do on lost pointer capture
122
+ const dragSession = interaction.dragSession;
123
+ if (!dragSession) {
137
124
  return null;
138
125
  }
139
- // Cancel the active interaction on lost pointer capture
140
- return {
141
- type: 'cancelActiveInteraction'
142
- };
126
+ const rawEvent = context.rawEvent;
127
+ const mask = buttonToButtonsMask(dragSession.startButton);
128
+ if (mask === null) {
129
+ return { type: 'cancelActiveInteraction' };
130
+ }
131
+ if ((rawEvent.buttons & mask) === 0) {
132
+ return determineActionTerminalRelease(state, context);
133
+ }
134
+ return { type: 'cancelActiveInteraction' };
143
135
  }
@@ -5,8 +5,8 @@ import type { InteractionStateSnapshot } from '../../../state/interaction/types/
5
5
  export interface RuntimeInteractionSurface {
6
6
  getInteractionStateSnapshot(): InteractionStateSnapshot;
7
7
  getPieceCodeAt(square: Square): PieceCode;
8
- startLiftedDrag(source: Square, target: Square): void;
9
- startReleaseTargetingDrag(source: Square, target: Square): void;
8
+ startLiftedDrag(source: Square, target: Square, startButton: number): void;
9
+ startReleaseTargetingDrag(source: Square, target: Square, startButton: number): void;
10
10
  completeCoreDragTo(target: Square): void;
11
11
  completeExtensionDrag(target: Square | null): void;
12
12
  updateDragSessionCurrentTarget(target: Square | null): void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mirasen/chessboard",
3
3
  "description": "Framework-agnostic TypeScript chessboard platform with real chess interaction built in.",
4
- "version": "1.2.3",
4
+ "version": "1.2.4",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Mirasen",