cm-chessboard 8.12.18 → 8.12.19

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/README.md CHANGED
@@ -247,6 +247,12 @@ chessboard.enableMoveInput((event) => {
247
247
 
248
248
  Disables moves via user input.
249
249
 
250
+ ### cancelMoveInput()
251
+
252
+ Cancels a move input that is currently in progress (a piece picked up or a square selected), without disabling move
253
+ input. Fires a `moveInputCanceled` event with `event.reason === "canceled"`. It is a no-op when no move input is in
254
+ progress. Useful to abort a move from your own UI, e.g. on an "escape" key press or a cancel button.
255
+
250
256
  ### isMoveInputEnabled()
251
257
 
252
258
  Returns `true` if move input is currently enabled for white or black.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cm-chessboard",
3
- "version": "8.12.18",
3
+ "version": "8.12.19",
4
4
  "description": "A JavaScript chessboard which is lightweight, ES6 module based, responsive, SVG rendered and without dependencies.",
5
5
  "keywords": [
6
6
  "chess",
package/src/Chessboard.js CHANGED
@@ -77,6 +77,7 @@ export class Chessboard {
77
77
  async setPiece(square, piece, animated = false) {
78
78
  const positionFrom = this.state.position.clone()
79
79
  this.state.position.setPiece(square, piece)
80
+ this.view.visualMoveInput.positionChanged()
80
81
  this.state.invokeExtensionPoints(EXTENSION_POINT.positionChanged)
81
82
  return this.positionAnimationsQueue.enqueuePositionChange(positionFrom, this.state.position.clone(), animated)
82
83
  }
@@ -84,6 +85,7 @@ export class Chessboard {
84
85
  async movePiece(squareFrom, squareTo, animated = false) {
85
86
  const positionFrom = this.state.position.clone()
86
87
  this.state.position.movePiece(squareFrom, squareTo)
88
+ this.view.visualMoveInput.positionChanged()
87
89
  this.state.invokeExtensionPoints(EXTENSION_POINT.positionChanged)
88
90
  return this.positionAnimationsQueue.enqueuePositionChange(positionFrom, this.state.position.clone(), animated)
89
91
  }
@@ -93,6 +95,7 @@ export class Chessboard {
93
95
  const positionTo = new Position(fen)
94
96
  if (positionFrom.getFen() !== positionTo.getFen()) {
95
97
  this.state.position.setFen(fen)
98
+ this.view.visualMoveInput.positionChanged()
96
99
  this.state.invokeExtensionPoints(EXTENSION_POINT.positionChanged)
97
100
  }
98
101
  return this.positionAnimationsQueue.enqueuePositionChange(positionFrom, this.state.position.clone(), animated)
@@ -131,6 +134,11 @@ export class Chessboard {
131
134
  this.view.disableMoveInput()
132
135
  }
133
136
 
137
+ // Cancel a move input that is currently in progress (leaves move input enabled).
138
+ cancelMoveInput() {
139
+ this.view.visualMoveInput.cancelMoveInput()
140
+ }
141
+
134
142
  isMoveInputEnabled() {
135
143
  return this.state.inputWhiteEnabled || this.state.inputBlackEnabled
136
144
  }
@@ -24,7 +24,9 @@ export const MOVE_CANCELED_REASON = {
24
24
  movedOutOfBoard: "movedOutOfBoard",
25
25
  draggedBack: "draggedBack", // dragged to the start square
26
26
  clickedAnotherPiece: "clickedAnotherPiece", // of the same color
27
- touchCanceled: "touchCanceled"
27
+ touchCanceled: "touchCanceled",
28
+ movedPieceChanged: "movedPieceChanged", // the held piece changed on the board, most likely got captured
29
+ canceled: "canceled" // cancelled programmatically via chessboard.cancelMoveInput()
28
30
  }
29
31
 
30
32
  const DRAG_THRESHOLD = 4
@@ -37,6 +39,7 @@ export class VisualMoveInput {
37
39
  this.moveInputState = null
38
40
  this.fromSquare = null
39
41
  this.toSquare = null
42
+ this.movedPiece = null
40
43
 
41
44
  this.setMoveInputState(MOVE_INPUT_STATE.waitForInputStart)
42
45
  }
@@ -432,6 +435,40 @@ export class VisualMoveInput {
432
435
  this.moveInputCanceledCallback(this.fromSquare, null, MOVE_CANCELED_REASON.secondaryClick)
433
436
  }
434
437
 
438
+ // Cancel a move input that is currently in progress. No-op when idle.
439
+ cancelMoveInput() {
440
+ if (this.moveInputState !== MOVE_INPUT_STATE.waitForInputStart) {
441
+ const moveStartSquare = this.fromSquare
442
+ this.view.redrawPieces()
443
+ this.setMoveInputState(MOVE_INPUT_STATE.reset)
444
+ this.moveInputCanceledCallback(moveStartSquare, null, MOVE_CANCELED_REASON.canceled)
445
+ }
446
+ }
447
+
448
+ // Called after the board position changed (setPosition/movePiece/setPiece).
449
+ // If the piece we are holding changed on its square — most likely captured
450
+ // by an external position update — cancel the in-progress move input.
451
+ positionChanged() {
452
+ // Only cancel while actually holding a piece. Never in moveDone/reset:
453
+ // completing a move itself goes through movePiece() and must not self-cancel.
454
+ const holdingStates = [
455
+ MOVE_INPUT_STATE.pieceClickedThreshold,
456
+ MOVE_INPUT_STATE.clickTo,
457
+ MOVE_INPUT_STATE.secondClickThreshold,
458
+ MOVE_INPUT_STATE.dragTo,
459
+ MOVE_INPUT_STATE.clickDragTo
460
+ ]
461
+ if (this.fromSquare && holdingStates.includes(this.moveInputState) &&
462
+ this.chessboard.getPiece(this.fromSquare) !== this.movedPiece) {
463
+ const moveStartSquare = this.fromSquare
464
+ // drop the stale held piece so the reset branch does not stamp it
465
+ // back onto the square, overwriting the piece just placed there
466
+ this.movedPiece = null
467
+ this.setMoveInputState(MOVE_INPUT_STATE.reset)
468
+ this.moveInputCanceledCallback(moveStartSquare, null, MOVE_CANCELED_REASON.movedPieceChanged)
469
+ }
470
+ }
471
+
435
472
  isDragging() {
436
473
  return this.moveInputState === MOVE_INPUT_STATE.dragTo || this.moveInputState === MOVE_INPUT_STATE.clickDragTo
437
474
  }
@@ -231,4 +231,131 @@ describe("TestVisualMoveInput", () => {
231
231
  board.destroy()
232
232
  })
233
233
 
234
+ // Regression for https://github.com/shaack/cm-chessboard/pull/165
235
+ it("should cancel and keep the new position when the held piece is captured via setPosition", async () => {
236
+ const board = newBoard()
237
+ let canceled = null
238
+ board.enableMoveInput((e) => {
239
+ if (e.type === INPUT_EVENT_TYPE.moveInputCanceled) canceled = {square: e.squareFrom, reason: e.reason}
240
+ return true
241
+ }, COLOR.white)
242
+ const vmi = board.view.visualMoveInput
243
+ vmi.onPointerDown(mouseDown("e2"))
244
+ vmi.onPointerUp(mouseUp("e2"))
245
+ assert.equal(vmi.moveInputState, STATE_CLICK_TO)
246
+
247
+ // the opponent captures the held e2 pawn (e2 now holds a black knight)
248
+ const capturedFen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPnPPP/RNBQKBNR"
249
+ await board.setPosition(capturedFen)
250
+
251
+ assert.equal(canceled && canceled.reason, MOVE_CANCELED_REASON.movedPieceChanged)
252
+ assert.equal(canceled && canceled.square, "e2") // the real start square, not null
253
+ assert.equal(vmi.moveInputState, STATE_WAIT_FOR_INPUT_START)
254
+ // the position must not be corrupted back to the white pawn
255
+ assert.equal("" + board.getPosition(), capturedFen)
256
+ await tick()
257
+ board.destroy()
258
+ })
259
+
260
+ it("should not cancel a normal move completion", async () => {
261
+ const board = newBoard()
262
+ let canceledReason
263
+ let finished = 0
264
+ board.enableMoveInput((e) => {
265
+ if (e.type === INPUT_EVENT_TYPE.moveInputCanceled) canceledReason = e.reason
266
+ if (e.type === INPUT_EVENT_TYPE.moveInputFinished) finished++
267
+ return true
268
+ }, COLOR.white)
269
+ const vmi = board.view.visualMoveInput
270
+ vmi.onPointerDown(mouseDown("e2"))
271
+ vmi.onPointerUp(mouseUp("e2"))
272
+ vmi.onPointerDown(mouseDown("e4")) // completes via moveDone -> movePiece()
273
+ await tick()
274
+ assert.equal(board.getPiece("e4"), "wp")
275
+ assert.equal(canceledReason, undefined) // the guard must prevent a spurious cancel
276
+ assert.equal(finished, 1)
277
+ board.destroy()
278
+ })
279
+
280
+ it("should cancel when the held piece is captured via movePiece or setPiece", async () => {
281
+ // via setPiece
282
+ let board = newBoard()
283
+ let reason
284
+ board.enableMoveInput((e) => {
285
+ if (e.type === INPUT_EVENT_TYPE.moveInputCanceled) reason = e.reason
286
+ return true
287
+ }, COLOR.white)
288
+ let vmi = board.view.visualMoveInput
289
+ vmi.onPointerDown(mouseDown("e2"))
290
+ vmi.onPointerUp(mouseUp("e2"))
291
+ await board.setPiece("e2", "bn")
292
+ assert.equal(reason, MOVE_CANCELED_REASON.movedPieceChanged)
293
+ await tick()
294
+ board.destroy()
295
+
296
+ // via movePiece (a black pawn captures onto e2)
297
+ board = newBoard()
298
+ reason = undefined
299
+ board.enableMoveInput((e) => {
300
+ if (e.type === INPUT_EVENT_TYPE.moveInputCanceled) reason = e.reason
301
+ return true
302
+ }, COLOR.white)
303
+ vmi = board.view.visualMoveInput
304
+ vmi.onPointerDown(mouseDown("e2"))
305
+ vmi.onPointerUp(mouseUp("e2"))
306
+ await board.movePiece("d7", "e2")
307
+ assert.equal(reason, MOVE_CANCELED_REASON.movedPieceChanged)
308
+ await tick()
309
+ board.destroy()
310
+ })
311
+
312
+ it("should cancel an in-progress click selection via cancelMoveInput()", async () => {
313
+ const board = newBoard()
314
+ let canceled = null
315
+ board.enableMoveInput((e) => {
316
+ if (e.type === INPUT_EVENT_TYPE.moveInputCanceled) canceled = {square: e.squareFrom, reason: e.reason}
317
+ return true
318
+ }, COLOR.white)
319
+ const vmi = board.view.visualMoveInput
320
+ vmi.onPointerDown(mouseDown("e2"))
321
+ vmi.onPointerUp(mouseUp("e2"))
322
+ assert.equal(vmi.moveInputState, STATE_CLICK_TO)
323
+
324
+ board.cancelMoveInput()
325
+ assert.equal(canceled && canceled.reason, MOVE_CANCELED_REASON.canceled)
326
+ assert.equal(canceled && canceled.square, "e2")
327
+ assert.equal(vmi.moveInputState, STATE_WAIT_FOR_INPUT_START)
328
+ await tick()
329
+ board.destroy()
330
+ })
331
+
332
+ it("should cancel a drag in progress via cancelMoveInput() without moving the piece", async () => {
333
+ const board = newBoard()
334
+ board.enableMoveInput(() => true, COLOR.white)
335
+ const vmi = board.view.visualMoveInput
336
+ vmi.onPointerDown(mouseDown("e2", 100, 100))
337
+ vmi.onPointerMove(mouseMove("e4", 100, 140)) // -> dragTo
338
+ assert.equal(vmi.moveInputState, STATE_DRAG_TO)
339
+
340
+ board.cancelMoveInput()
341
+ assert.equal(vmi.moveInputState, STATE_WAIT_FOR_INPUT_START)
342
+ assert.equal(board.getPiece("e2"), "wp")
343
+ assert.equal(board.getPiece("e4"), null)
344
+ await tick()
345
+ board.destroy()
346
+ })
347
+
348
+ it("should be a no-op when cancelMoveInput() is called with no move in progress", () => {
349
+ const board = newBoard()
350
+ let canceledCount = 0
351
+ board.enableMoveInput((e) => {
352
+ if (e.type === INPUT_EVENT_TYPE.moveInputCanceled) canceledCount++
353
+ return true
354
+ }, COLOR.white)
355
+ board.cancelMoveInput()
356
+ assert.equal(canceledCount, 0)
357
+ assert.equal(board.view.visualMoveInput.moveInputState, STATE_WAIT_FOR_INPUT_START)
358
+ board.destroy()
359
+ })
360
+
234
361
  })