cm-chessboard 8.13.0 → 8.14.1

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
@@ -214,6 +214,11 @@ The event has the following **`event.type`**:
214
214
  recapture premoves. Return `true` to commit it (e.g. castling by clicking the rook). When you return `false` here the
215
215
  clicked own piece simply becomes the new selection, so if you show illegal-move feedback, skip it when `event.squareTo`
216
216
  still holds one of the mover's own pieces.
217
+ A move made by click always plays its own move animation. Set `event.animate = false` before returning to skip it for this move only
218
+ (e.g. a premove made by click); this is read once, right after validation, so there is nothing to reset afterward.
219
+ The opt-out only applies to click moves. A drag move never animates its completion, so `event.animate = true` has no effect there.
220
+ Extensions receive a clone of the event and therefore opt out by setting `chessboard.state.moveInputAnimate = false`
221
+ in their `moveInput` extension point handler instead.
217
222
  - **`INPUT_EVENT_TYPE.moveInputCanceled`**: The user canceled the move with clicking again on the start square, clicking
218
223
  outside the board or right click.
219
224
  - **`INPUT_EVENT_TYPE.moveInputFinished`**: Fired after the move was made, also when canceled.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cm-chessboard",
3
- "version": "8.13.0",
3
+ "version": "8.14.1",
4
4
  "description": "A JavaScript chessboard which is lightweight, ES6 module based, responsive, SVG rendered and without dependencies.",
5
5
  "keywords": [
6
6
  "chess",
@@ -34,6 +34,9 @@ export const PROMOTION_DIALOG_RESULT_TYPE = {
34
34
  canceled: "canceled"
35
35
  }
36
36
 
37
+ // Keys the open dialog handles itself, see handleKeyDown
38
+ const HANDLED_KEYS = ["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight", "Enter", " ", "Escape", "Tab"]
39
+
37
40
  export class PromotionDialog extends Extension {
38
41
 
39
42
  /** @constructor */
@@ -259,8 +262,12 @@ export class PromotionDialog extends Extension {
259
262
  this.promotionDialogOnClickPiece.bind(this))
260
263
  this.contextMenuListener = this.contextMenu.bind(this)
261
264
  this.chessboard.view.svg.addEventListener("contextmenu", this.contextMenuListener)
262
- // Add keyboard listener
263
- document.addEventListener("keydown", this.handleKeyDown)
265
+ // Keyboard listener in the capture phase: while the dialog is open its keys
266
+ // must not reach anything else. Applications commonly bind the arrow keys on
267
+ // document for history navigation (chess-console does), and those listeners
268
+ // are registered before the dialog opens, so in the bubble phase they would
269
+ // run first and change the position under the open dialog.
270
+ document.addEventListener("keydown", this.handleKeyDown, true)
264
271
  } else if (displayState === DISPLAY_STATE.hidden) {
265
272
  if (this.clickDelegate) {
266
273
  this.clickDelegate.remove()
@@ -271,7 +278,7 @@ export class PromotionDialog extends Extension {
271
278
  this.contextMenuListener = null
272
279
  }
273
280
  // Remove keyboard listener
274
- document.removeEventListener("keydown", this.handleKeyDown)
281
+ document.removeEventListener("keydown", this.handleKeyDown, true)
275
282
  // Restore focus (only if the dialog was actually shown before)
276
283
  if (prevState === DISPLAY_STATE.shown &&
277
284
  this.previouslyFocusedElement && this.previouslyFocusedElement.focus) {
@@ -292,6 +299,12 @@ export class PromotionDialog extends Extension {
292
299
  if (this.state.displayState !== DISPLAY_STATE.shown) {
293
300
  return
294
301
  }
302
+ // Keys the dialog owns are consumed here, they must not reach the page below.
303
+ // Without this the arrow keys would also drive an application's own history
304
+ // navigation and change the position while the dialog is waiting for an answer.
305
+ if (HANDLED_KEYS.includes(event.key)) {
306
+ event.stopPropagation()
307
+ }
295
308
  switch (event.key) {
296
309
  case "ArrowDown":
297
310
  event.preventDefault()
@@ -395,7 +408,7 @@ export class PromotionDialog extends Extension {
395
408
  clearTimeout(this.announceTimeoutId)
396
409
  this.announceTimeoutId = null
397
410
  }
398
- document.removeEventListener("keydown", this.handleKeyDown)
411
+ document.removeEventListener("keydown", this.handleKeyDown, true)
399
412
  if (this.liveRegion && this.liveRegion.parentNode) {
400
413
  this.liveRegion.parentNode.removeChild(this.liveRegion)
401
414
  this.liveRegion = null
@@ -14,6 +14,9 @@ export class ChessboardState {
14
14
  this.inputBlackEnabled = false
15
15
  this.squareSelectEnabled = false
16
16
  this.moveInputCallback = null
17
+ // set per move from `event.animate` in the validateMoveInput callback; `false` skips
18
+ // the completion animation of a click move (see ChessboardView/VisualMoveInput)
19
+ this.moveInputAnimate = undefined
17
20
  this.extensionPoints = {}
18
21
  this.moveInputProcess = Promise.resolve()
19
22
  }
@@ -421,6 +421,11 @@ export class ChessboardView {
421
421
  if (this.chessboard.state.moveInputCallback) {
422
422
  data.moveInputCallbackResult = this.chessboard.state.moveInputCallback(data)
423
423
  }
424
+ // A validator can set `event.animate = false` to skip this move's own completion animation
425
+ // (e.g. a premove made by click, which would otherwise always animate).
426
+ // Assigned before the extension points on purpose: extensions receive a clone of `data`,
427
+ // so they opt out by setting `state.moveInputAnimate` directly, which must not be clobbered here.
428
+ this.chessboard.state.moveInputAnimate = data.animate
424
429
  this.chessboard.state.invokeExtensionPoints(EXTENSION_POINT.moveInput, data)
425
430
  return data.moveInputCallbackResult
426
431
  }
@@ -178,7 +178,10 @@ export class VisualMoveInput {
178
178
  const validated = params.validated !== undefined ?
179
179
  params.validated : this.validateMoveInputCallback(this.fromSquare, this.toSquare)
180
180
  if (this.toSquare && validated) {
181
- this.chessboard.movePiece(this.fromSquare, this.toSquare, prevState === MOVE_INPUT_STATE.clickTo).then(() => {
181
+ // A click move always completes with its own animation, unless the
182
+ // validator opted out via `event.animate = false` (see ChessboardView).
183
+ const animate = prevState === MOVE_INPUT_STATE.clickTo && this.chessboard.state.moveInputAnimate !== false
184
+ this.chessboard.movePiece(this.fromSquare, this.toSquare, animate).then(() => {
182
185
  if (prevState === MOVE_INPUT_STATE.clickTo) {
183
186
  this.view.setPieceVisibility(this.toSquare, true)
184
187
  }
@@ -124,6 +124,58 @@ describe("TestVisualMoveInput", () => {
124
124
  board.destroy()
125
125
  })
126
126
 
127
+ // A click move always completes with its own animation (see the test above),
128
+ // which is unwanted for premoves made by click. `event.animate = false` in the
129
+ // validator lets a consumer opt out per-move instead of having to toggle
130
+ // `chessboard.props.style.animationDuration` around the move.
131
+ it("should skip a click move's own animation when the validator sets event.animate = false", async () => {
132
+ const board = newBoard()
133
+ let capturedAnimated = null
134
+ const originalMovePiece = board.movePiece.bind(board)
135
+ board.movePiece = (from, to, animated) => {
136
+ capturedAnimated = animated
137
+ return originalMovePiece(from, to, animated)
138
+ }
139
+ board.enableMoveInput((e) => {
140
+ if (e.type === INPUT_EVENT_TYPE.validateMoveInput) {
141
+ e.animate = false
142
+ return true
143
+ }
144
+ return true
145
+ }, COLOR.white)
146
+ const vmi = board.view.visualMoveInput
147
+
148
+ vmi.onPointerDown(mouseDown("e2"))
149
+ vmi.onPointerUp(mouseUp("e2"))
150
+ vmi.onPointerDown(mouseDown("e4"))
151
+ await tick()
152
+
153
+ assert.equal(capturedAnimated, false)
154
+ assert.equal(board.getPiece("e4"), "wp")
155
+ board.destroy()
156
+ })
157
+
158
+ it("should still animate a click move by default (event.animate untouched)", async () => {
159
+ const board = newBoard()
160
+ let capturedAnimated = null
161
+ const originalMovePiece = board.movePiece.bind(board)
162
+ board.movePiece = (from, to, animated) => {
163
+ capturedAnimated = animated
164
+ return originalMovePiece(from, to, animated)
165
+ }
166
+ board.enableMoveInput(() => true, COLOR.white)
167
+ const vmi = board.view.visualMoveInput
168
+
169
+ vmi.onPointerDown(mouseDown("e2"))
170
+ vmi.onPointerUp(mouseUp("e2"))
171
+ vmi.onPointerDown(mouseDown("e4"))
172
+ await tick()
173
+
174
+ assert.equal(capturedAnimated, true)
175
+ assert.equal(board.getPiece("e4"), "wp");
176
+ board.destroy()
177
+ })
178
+
127
179
  it("should complete a drag move to a new square", async () => {
128
180
  const board = newBoard()
129
181
  board.enableMoveInput(() => true, COLOR.white)