@mirasen/chessboard 1.2.2 → 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,25 @@
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
+
13
+ ## 1.2.3
14
+
15
+ ### Patch Changes
16
+
17
+ - d8df60e: Fix stale lifted-piece drag cleanup when pointer capture is lost.
18
+
19
+ Fast drag gestures released outside the board could leave an active lifted-piece drag session alive if the browser delivered `lostpointercapture` instead of a normal `pointerup`. The input adapter now handles `lostpointercapture`, clears stale pointer tracking, and routes the event through the interaction controller so active drag sessions are cancelled cleanly.
20
+
21
+ This prevents dragged pieces from remaining visually attached to the pointer after an outside-board release.
22
+
3
23
  ## 1.2.2
4
24
 
5
25
  ### Patch Changes
package/README.md CHANGED
@@ -319,16 +319,35 @@ board = createBoard({
319
319
 
320
320
  Configured built-in extensions can be provided either as declarative `{ builtin, options }` objects or through the built-in extension factory map. If you use custom piece assets, pass the same `pieceUrls` map to renderer and promotion when you want the board and promotion UI to use the same piece set.
321
321
 
322
- ### Default visual identity
322
+ ### Default visual identity and attribution
323
323
 
324
324
  The default first-party extension baseline includes a subtle Mirasen board watermark.
325
325
 
326
326
  If you want full control over the visual baseline, use an explicit extension list and omit the `watermark` extension.
327
327
 
328
+ Attribution is not required by the MIT license, but it is appreciated. If Mirasen Chessboard helps your project, please consider keeping the default watermark or linking to `@mirasen/chessboard`.
329
+
328
330
  ## Documentation
329
331
 
330
332
  See [Documentation](./documentation.md) for the documentation hub.
331
333
 
334
+ ## Versioning and breaking changes
335
+
336
+ `@mirasen/chessboard` follows semantic versioning for public API changes.
337
+
338
+ Breaking public API changes are released as major versions.
339
+
340
+ We do not add backward-compatibility aliases or runtime compatibility bridges by default. When a public contract changes, the new API is kept explicit and clean, and the required migration is documented in the release notes / changelog.
341
+
342
+ Migration notes for breaking changes are included directly in the corresponding changelog entry, so users can see:
343
+
344
+ - what changed
345
+ - why it changed
346
+ - what old usage looked like
347
+ - what new usage should look like
348
+
349
+ See the [CHANGELOG](./CHANGELOG.md) for release history and migration notes.
350
+
332
351
  ## Architecture
333
352
 
334
353
  `@mirasen/chessboard` is built around explicit boundaries:
@@ -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();
@@ -68,7 +68,7 @@ function createLegalMovesInstance(options, config) {
68
68
  'stroke-opacity': internalState.config.captureTarget.color.opacity.toString(),
69
69
  'stroke-width': (geometry.squareSize * internalState.config.captureTarget.strokeWidthRatio).toString(),
70
70
  fill: 'none'
71
- }; // TODO: Remove, this is just for visual test
71
+ };
72
72
  for (const [square, destination] of interaction.activeDestinations) {
73
73
  const rect = geometry.getSquareRect(square);
74
74
  const circleX = rect.x + geometry.squareSize / 2;
@@ -1,23 +1,27 @@
1
1
  import type { Square } from '../../../state/board/types/internal.js';
2
2
  import type { ScenePoint } from '../basic/transient-visuals.js';
3
+ declare const ALL_SCENE_POINTER_EVENT_TYPES_ARRAY: readonly ["pointercancel", "pointerdown", "pointerenter", "pointerleave", "pointermove", "pointerout", "pointerover", "pointerrawupdate", "pointerup", "contextmenu", "lostpointercapture"];
4
+ export type ScenePointerEventType = (typeof ALL_SCENE_POINTER_EVENT_TYPES_ARRAY)[number];
5
+ export declare const ALL_SCENE_POINTER_EVENT_TYPES: ReadonlySet<string>;
3
6
  export interface ScenePointerEvent {
4
- readonly type: 'pointercancel' | 'pointerdown' | 'pointerenter' | 'pointerleave' | 'pointermove' | 'pointerout' | 'pointerover' | 'pointerrawupdate' | 'pointerup' | 'contextmenu';
7
+ readonly type: ScenePointerEventType;
5
8
  readonly point: ScenePoint;
6
9
  readonly clampedPoint: ScenePoint;
7
10
  readonly boardClampedPoint: ScenePoint | null;
8
11
  readonly targetSquare: Square | null;
9
12
  }
10
- export declare const ALL_SCENE_POINTER_EVENT_TYPES: ReadonlySet<string>;
11
13
  export type SceneEvent = ScenePointerEvent;
12
14
  export type RuntimeInteractionActionStartLiftedDrag = {
13
15
  type: 'startLiftedDrag';
14
16
  source: Square;
15
17
  target: Square;
18
+ startButton: number;
16
19
  };
17
20
  export type RuntimeInteractionActionStartReleaseTargetingDrag = {
18
21
  type: 'startReleaseTargetingDrag';
19
22
  source: Square;
20
23
  target: Square;
24
+ startButton: number;
21
25
  };
22
26
  export type RuntimeInteractionActionCompleteCoreDrag = {
23
27
  type: 'completeCoreDragTo';
@@ -38,3 +42,4 @@ export type RuntimeInteractionActionCancelInteraction = {
38
42
  type: 'cancelInteraction';
39
43
  };
40
44
  export type RuntimeInteractionAction = RuntimeInteractionActionStartLiftedDrag | RuntimeInteractionActionStartReleaseTargetingDrag | RuntimeInteractionActionCompleteCoreDrag | RuntimeInteractionActionCompleteExtensionDrag | RuntimeInteractionActionUpdateDragSessionCurrentTarget | RuntimeInteractionActionCancelActiveInteraction | RuntimeInteractionActionCancelInteraction;
45
+ export {};
@@ -1,4 +1,4 @@
1
- export const ALL_SCENE_POINTER_EVENT_TYPES = new Set([
1
+ const ALL_SCENE_POINTER_EVENT_TYPES_ARRAY = [
2
2
  'pointercancel',
3
3
  'pointerdown',
4
4
  'pointerenter',
@@ -8,5 +8,7 @@ export const ALL_SCENE_POINTER_EVENT_TYPES = new Set([
8
8
  'pointerover',
9
9
  'pointerrawupdate',
10
10
  'pointerup',
11
- 'contextmenu'
12
- ]);
11
+ 'contextmenu',
12
+ 'lostpointercapture'
13
+ ];
14
+ export const ALL_SCENE_POINTER_EVENT_TYPES = new Set(ALL_SCENE_POINTER_EVENT_TYPES_ARRAY);
@@ -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);
@@ -195,11 +195,6 @@ export function createRuntime(options) {
195
195
  internalState = createRuntimeInternal(optionsInternal);
196
196
  // Initial creation, so we mark board position as mutated to trigger mutation pipeline
197
197
  internalState.mutation.addMutation('state.board.setPosition', true);
198
- // We will have two different interfaces here
199
- // One is returned, one is for controller - with different methods
200
- // At the moment our assumption that controller will use Runtime but not opposite direction
201
- // So we can construct these two objects separately so they would not be huge!
202
- // TODO: const inputControllerSurface = createRuntimeInputControllerSurface(internalState);
203
198
  // Initial run to process initial mutations and set up previousContext for the next runs
204
199
  internalStatus = 'unmounted';
205
200
  internalState.mutation.run(internalState);
@@ -6,4 +6,5 @@ export declare function releaseCapture(state: InputAdapterInternal): void;
6
6
  export declare function adapterOnPointerDown(state: InputAdapterInternal, e: PointerEvent): void;
7
7
  export declare function adapterOnPointerUp(state: InputAdapterInternal, e: PointerEvent): void;
8
8
  export declare function adapterOnPointerCancel(state: InputAdapterInternal, e: PointerEvent): void;
9
+ export declare function adapterOnLostPointerCapture(state: InputAdapterInternal, e: PointerEvent): void;
9
10
  export declare function pointerEventDestroy(state: InputAdapterInternal): void;
@@ -24,6 +24,9 @@ function pointerEventAdapterHandler(state, e) {
24
24
  case 'pointercancel':
25
25
  adapterOnPointerCancel(state, e);
26
26
  break;
27
+ case 'lostpointercapture':
28
+ adapterOnLostPointerCapture(state, e);
29
+ break;
27
30
  }
28
31
  }
29
32
  /** Release capture for the currently tracked pointer and clear tracking state. */
@@ -56,6 +59,12 @@ export function adapterOnPointerCancel(state, e) {
56
59
  return;
57
60
  releaseCapture(state);
58
61
  }
62
+ export function adapterOnLostPointerCapture(state, e) {
63
+ assert(e.type === 'lostpointercapture', `Expected lostpointercapture event, got ${e.type}`);
64
+ if (e.pointerId !== state.activePointerId)
65
+ return;
66
+ releaseCapture(state);
67
+ }
59
68
  export function pointerEventDestroy(state) {
60
69
  if (state.activePointerId !== null) {
61
70
  releaseCapture(state);
@@ -2,5 +2,6 @@ export const NEED_EVENT_TYPES = new Set([
2
2
  'pointerdown',
3
3
  'pointermove',
4
4
  'pointerup',
5
- 'pointercancel'
5
+ 'pointercancel',
6
+ 'lostpointercapture'
6
7
  ]);
@@ -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 */) {
@@ -1,4 +1,4 @@
1
- import { determineActionPointerCancel, determineActionPointerDown, determineActionPointerMove, determineActionPointerUp } from './pointer.js';
1
+ import { determineActionLostPointerCapture, determineActionPointerCancel, determineActionPointerDown, determineActionPointerMove, determineActionPointerUp } from './pointer.js';
2
2
  export function determineRuntimeInteractionAction(state, context) {
3
3
  switch (context.rawEvent.type) {
4
4
  case 'pointerdown':
@@ -9,6 +9,8 @@ export function determineRuntimeInteractionAction(state, context) {
9
9
  return determineActionPointerUp(state, context);
10
10
  case 'pointercancel':
11
11
  return determineActionPointerCancel(state, context);
12
+ case 'lostpointercapture':
13
+ return determineActionLostPointerCapture(state, context);
12
14
  }
13
15
  return null;
14
16
  }
@@ -4,3 +4,4 @@ export declare function determineActionPointerDown(state: InteractionControllerI
4
4
  export declare function determineActionPointerMove(state: InteractionControllerInternal, context: InteractionControllerOnEventContext): RuntimeInteractionAction | null;
5
5
  export declare function determineActionPointerUp(state: InteractionControllerInternal, context: InteractionControllerOnEventContext): RuntimeInteractionAction | null;
6
6
  export declare function determineActionPointerCancel(state: InteractionControllerInternal, context: InteractionControllerOnEventContext): RuntimeInteractionAction | null;
7
+ export declare function determineActionLostPointerCapture(state: InteractionControllerInternal, context: InteractionControllerOnEventContext): RuntimeInteractionAction | null;
@@ -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,60 +75,61 @@ 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
  };
131
118
  }
119
+ export function determineActionLostPointerCapture(state, context) {
120
+ assert(context.rawEvent.type === 'lostpointercapture', 'determineActionLostPointerCapture should only be called for lostpointercapture events');
121
+ const interaction = state.surface.getInteractionStateSnapshot();
122
+ const dragSession = interaction.dragSession;
123
+ if (!dragSession) {
124
+ return null;
125
+ }
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' };
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.2",
4
+ "version": "1.2.4",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Mirasen",
@@ -16,8 +16,13 @@
16
16
  "typescript",
17
17
  "framework-agnostic",
18
18
  "svg",
19
- "extensions",
20
- "chessboard-platform"
19
+ "drag-and-drop",
20
+ "click-to-move",
21
+ "animation",
22
+ "annotations",
23
+ "chessjs",
24
+ "legal-moves",
25
+ "touch"
21
26
  ],
22
27
  "homepage": "https://mirasen.io/chessboard",
23
28
  "repository": {
@@ -1 +0,0 @@
1
- export {};
@@ -1,2 +0,0 @@
1
- export {};
2
- // TODO: Mark dirty layers based on upstream changes.