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 +6 -0
- package/package.json +1 -1
- package/src/Chessboard.js +8 -0
- package/src/view/VisualMoveInput.js +38 -1
- package/test/TestVisualMoveInput.js +127 -0
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
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
|
})
|