@bedard/hexboard 0.0.8 → 0.0.10

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/package.json CHANGED
@@ -58,5 +58,5 @@
58
58
  "license": "MIT",
59
59
  "type": "module",
60
60
  "types": "dist/index.d.ts",
61
- "version": "0.0.8"
61
+ "version": "0.0.10"
62
62
  }
@@ -172,6 +172,7 @@ import {
172
172
  type Component,
173
173
  computed,
174
174
  h,
175
+ onBeforeMount,
175
176
  onMounted,
176
177
  onUnmounted,
177
178
  shallowRef,
@@ -462,6 +463,12 @@ const promotionPieces = computed(() => {
462
463
  // lifecycle
463
464
  //
464
465
 
466
+ onBeforeMount(() => {
467
+ if (props.autoselect) {
468
+ selectCurrentTargets()
469
+ }
470
+ })
471
+
465
472
  onMounted(() => {
466
473
  if (props.active) {
467
474
  listen()
@@ -486,6 +493,8 @@ watch(
486
493
  val => (val ? listen() : unlisten()),
487
494
  )
488
495
 
496
+ watch(selected, selectCurrentTargets)
497
+
489
498
  //
490
499
  // methods
491
500
  //
@@ -537,17 +546,25 @@ function attemptMove(san: San, evt?: MouseEvent) {
537
546
  }
538
547
  }
539
548
 
540
- /** check if user is playing the color at a position */
541
- function isPlayingPosition(index: number): boolean {
542
- const piece = props.hexchess?.board[index]
549
+ /** cancel promotion and restore original selection */
550
+ function cancelPromotion() {
551
+ const from = staging.value.promotionFrom
543
552
 
544
- if (!piece) {
545
- return false
553
+ staging.value = {
554
+ hexchess: null,
555
+ promotionEl: null,
556
+ promotionFrom: null,
557
+ promotionTo: null,
558
+ selected: null,
546
559
  }
547
560
 
548
- const pieceColor: Color = piece === piece.toLowerCase() ? 'b' : 'w'
561
+ // Keep the original piece selected
562
+ if (typeof from === 'number') {
563
+ selected.value = from
564
+ }
549
565
 
550
- return props.playing === true || props.playing === pieceColor
566
+ pointerdownPosition.value = null
567
+ skipNextClick = true
551
568
  }
552
569
 
553
570
  /** get fill color of label */
@@ -566,6 +583,19 @@ function getLabelFill(text: string) {
566
583
  return normalizedOptions.value.labelInactiveColor
567
584
  }
568
585
 
586
+ /** check if user is playing the color at a position */
587
+ function isPlayingPosition(index: number): boolean {
588
+ const piece = props.hexchess?.board[index]
589
+
590
+ if (!piece) {
591
+ return false
592
+ }
593
+
594
+ const pieceColor: Color = piece === piece.toLowerCase() ? 'b' : 'w'
595
+
596
+ return props.playing === true || props.playing === pieceColor
597
+ }
598
+
569
599
  /** listen for events */
570
600
  function listen() {
571
601
  pointerCoords.value = { x: 0, y: 0 }
@@ -614,7 +644,6 @@ function onClickPosition(index: number, evt: MouseEvent) {
614
644
  // If autoselect is enabled and clicking an unoccupied position, deselect
615
645
  if (props.autoselect && !props.hexchess.board[index]) {
616
646
  selected.value = null
617
- targets.value = []
618
647
  }
619
648
 
620
649
  emit('clickPosition', index)
@@ -632,11 +661,20 @@ function onKeyupWindow(evt: KeyboardEvent) {
632
661
  // Otherwise deselect if autoselect is enabled
633
662
  if (props.autoselect) {
634
663
  selected.value = null
635
- targets.value = []
636
664
  }
637
665
  }
638
666
  }
639
667
 
668
+ /** mouseenter position */
669
+ function onMouseenter(index: number) {
670
+ mouseoverPosition.value = index
671
+ }
672
+
673
+ /** mouseleave position */
674
+ function onMouseleave() {
675
+ mouseoverPosition.value = null
676
+ }
677
+
640
678
  /** handle piece move */
641
679
  function onPieceMove(san: San) {
642
680
  emit('move', san)
@@ -644,6 +682,60 @@ function onPieceMove(san: San) {
644
682
  resetState()
645
683
  }
646
684
 
685
+ /** pointerdown on position */
686
+ function onPointerdownPosition(index: number, evt: PointerEvent) {
687
+ evt.preventDefault()
688
+
689
+ // Don't start new interactions during promotion
690
+ if (staging.value.hexchess) {
691
+ return
692
+ }
693
+
694
+ // If clicking on a valid target for the selected piece, don't re-select
695
+ // (the move will be handled in onPointerupPosition/onClickPosition)
696
+ if (selected.value !== null && targets.value.includes(index)) {
697
+ return
698
+ }
699
+
700
+ const piece = props.hexchess?.board[index]
701
+
702
+ if (!piece) {
703
+ return
704
+ }
705
+
706
+ if (props.autoselect) {
707
+ selected.value = index
708
+ }
709
+
710
+ if (!isPlayingPosition(index)) {
711
+ return
712
+ }
713
+
714
+ // Only allow dragging if it's the piece's turn (or ignoreTurn is true)
715
+ const pieceColor: Color = piece === piece.toLowerCase() ? 'b' : 'w'
716
+ const isCurrentTurn = props.hexchess?.turn === pieceColor
717
+
718
+ if (!props.ignoreTurn && !isCurrentTurn) {
719
+ return
720
+ }
721
+
722
+ pointerdownPosition.value = index
723
+ pointerCoords.value = { x: evt.clientX, y: evt.clientY }
724
+
725
+ if (svgEl.value instanceof Element) {
726
+ svgRect.value = svgEl.value.getBoundingClientRect()
727
+ }
728
+ }
729
+
730
+ /** pointermove window */
731
+ function onPointermoveWindow(evt: MouseEvent) {
732
+ if (!props.active) {
733
+ return
734
+ }
735
+
736
+ pointerCoords.value = { x: evt.clientX, y: evt.clientY }
737
+ }
738
+
647
739
  /** pointerup position */
648
740
  function onPointerupPosition(index: number, evt: PointerEvent) {
649
741
  evt.stopPropagation()
@@ -707,94 +799,6 @@ function onPointerupPosition(index: number, evt: PointerEvent) {
707
799
  resetState()
708
800
  }
709
801
 
710
- /** cancel promotion and restore original selection */
711
- function cancelPromotion() {
712
- const from = staging.value.promotionFrom
713
-
714
- staging.value = {
715
- hexchess: null,
716
- promotionEl: null,
717
- promotionFrom: null,
718
- promotionTo: null,
719
- selected: null,
720
- }
721
-
722
- // Keep the original piece selected
723
- if (typeof from === 'number') {
724
- selected.value = from
725
- targets.value = props.hexchess.movesFrom(from).map(san => san.to) ?? []
726
- }
727
-
728
- pointerdownPosition.value = null
729
- skipNextClick = true
730
- }
731
-
732
- /** pointerdown on position */
733
- function onPointerdownPosition(index: number, evt: PointerEvent) {
734
- evt.preventDefault()
735
-
736
- // Don't start new interactions during promotion
737
- if (staging.value.hexchess) {
738
- return
739
- }
740
-
741
- const piece = props.hexchess?.board[index]
742
-
743
- if (!piece) {
744
- return
745
- }
746
-
747
- if (props.autoselect) {
748
- selected.value = index
749
- targets.value = props.hexchess?.movesFrom(index).map(san => san.to) ?? []
750
- }
751
-
752
- if (!isPlayingPosition(index)) {
753
- return
754
- }
755
-
756
- // Only allow dragging if it's the piece's turn (or ignoreTurn is true)
757
- const pieceColor: Color = piece === piece.toLowerCase() ? 'b' : 'w'
758
- const isCurrentTurn = props.hexchess?.turn === pieceColor
759
-
760
- if (!props.ignoreTurn && !isCurrentTurn) {
761
- return
762
- }
763
-
764
- pointerdownPosition.value = index
765
- pointerCoords.value = { x: evt.clientX, y: evt.clientY }
766
-
767
- if (svgEl.value instanceof Element) {
768
- svgRect.value = svgEl.value.getBoundingClientRect()
769
- }
770
- }
771
-
772
- /** mouseenter position */
773
- function onMouseenter(index: number) {
774
- mouseoverPosition.value = index
775
- }
776
-
777
- /** mouseleave position */
778
- function onMouseleave() {
779
- mouseoverPosition.value = null
780
- }
781
-
782
- /** pointermove window */
783
- function onPointermoveWindow(evt: MouseEvent) {
784
- if (!props.active) {
785
- return
786
- }
787
-
788
- pointerCoords.value = { x: evt.clientX, y: evt.clientY }
789
- }
790
-
791
- /** touchmove window - prevent scrolling while dragging */
792
- function onTouchmoveWindow(evt: TouchEvent) {
793
- if (pointerdownPosition.value !== null) {
794
- evt.preventDefault()
795
- }
796
- }
797
-
798
802
  /** pointerup window */
799
803
  function onPointerupWindow() {
800
804
  // If staging a promotion, cancel it but keep the original piece selected
@@ -813,6 +817,13 @@ function onPointerupWindow() {
813
817
  resetState()
814
818
  }
815
819
 
820
+ /** touchmove window - prevent scrolling while dragging */
821
+ function onTouchmoveWindow(evt: TouchEvent) {
822
+ if (pointerdownPosition.value !== null) {
823
+ evt.preventDefault()
824
+ }
825
+ }
826
+
816
827
  /** promote piece */
817
828
  function promote(promotion: 'n' | 'b' | 'r' | 'q') {
818
829
  if (
@@ -859,6 +870,16 @@ function resetState() {
859
870
  targets.value = []
860
871
  }
861
872
 
873
+ /** select current targets */
874
+ function selectCurrentTargets() {
875
+ if (typeof selected.value === 'number') {
876
+ targets.value = props.hexchess?.movesFrom(selected.value).map(san => san.to) ?? []
877
+ }
878
+ else {
879
+ targets.value = []
880
+ }
881
+ }
882
+
862
883
  /** stop listening for events */
863
884
  function unlisten() {
864
885
  resetState()
@@ -3,7 +3,7 @@ import { expect, test, vi } from 'vitest'
3
3
  import { Hexboard } from '../lib'
4
4
  import { Hexchess } from '@bedard/hexchess'
5
5
  import { index, San } from '@bedard/hexchess'
6
- import { makeMove, setup } from './utils'
6
+ import { dragMove, clickMove, setup } from './utils'
7
7
  import { page } from 'vitest/browser'
8
8
  import { ref, nextTick } from 'vue'
9
9
  import { userEvent } from 'vitest/browser'
@@ -520,23 +520,7 @@ test('drag and drop piece emits move event', async () => {
520
520
  )
521
521
  })
522
522
 
523
- const fromPosition = page.getByTestId('position-f5')
524
- const toPosition = page.getByTestId('position-f6')
525
-
526
- // Start dragging the piece (pointerdown)
527
- await fromPosition
528
- .element()
529
- .dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
530
- await nextTick()
531
-
532
- // Verify dragging started
533
- await expect.element(page.getByTestId('drag-piece')).toBeVisible()
534
-
535
- // Move pointer to target position and release (pointerup)
536
- await toPosition
537
- .element()
538
- .dispatchEvent(new PointerEvent('pointerup', { bubbles: true }))
539
- await nextTick()
523
+ await dragMove(page, 'f5f6')
540
524
 
541
525
  // Verify move event was emitted with correct San object
542
526
  await expect(onMove).toHaveBeenCalledOnce()
@@ -644,7 +628,7 @@ test('promotion', async () => {
644
628
  )
645
629
  })
646
630
 
647
- await makeMove(page, 'f10f11')
631
+ await clickMove(page, 'f10f11')
648
632
  await page.getByTestId('promote').click()
649
633
  await expect(page.getByTestId('piece-f11')).toHaveAttribute(
650
634
  'data-piece-type',
@@ -682,7 +666,7 @@ test('canceled promotion', async () => {
682
666
  })
683
667
 
684
668
  // Move pawn to promotion square
685
- await makeMove(page, 'f10f11')
669
+ await clickMove(page, 'f10f11')
686
670
 
687
671
  // Promotion UI should be visible
688
672
  await expect.element(page.getByTestId('cancel')).toBeVisible()
@@ -730,7 +714,7 @@ test('clicking position during promotion cancels it', async () => {
730
714
  })
731
715
 
732
716
  // Move pawn to promotion square
733
- await makeMove(page, 'f10f11')
717
+ await clickMove(page, 'f10f11')
734
718
 
735
719
  // Promotion UI should be visible
736
720
  await expect.element(page.getByTestId('promote')).toBeVisible()
@@ -771,14 +755,14 @@ test('ignoreTurn allows moving pieces out of turn', async () => {
771
755
  })
772
756
 
773
757
  // Initial position is white's turn, try to move black's pawn without ignoreTurn
774
- await makeMove(page, 'f7f6')
758
+ await clickMove(page, 'f7f6')
775
759
  await expect(onMove).not.toHaveBeenCalled()
776
760
 
777
761
  // Enable ignoreTurn and try again
778
762
  ignoreTurn.value = true
779
763
  await nextTick()
780
764
 
781
- await makeMove(page, 'f7f6')
765
+ await clickMove(page, 'f7f6')
782
766
  await expect(onMove).toHaveBeenCalledOnce()
783
767
  await expect(onMove).toHaveBeenCalledWith(
784
768
  expect.objectContaining({
@@ -791,6 +775,7 @@ test('ignoreTurn allows moving pieces out of turn', async () => {
791
775
  test('dragging piece to non-target position keeps selection', async () => {
792
776
  const selected = ref<number | null>(null)
793
777
  const targets = ref<number[]>([])
778
+ const onMove = vi.fn()
794
779
 
795
780
  setup(() => {
796
781
  return () => (
@@ -801,32 +786,86 @@ test('dragging piece to non-target position keeps selection', async () => {
801
786
  playing="w"
802
787
  v-model:selected={selected.value}
803
788
  v-model:targets={targets.value}
789
+ onMove={onMove}
804
790
  />
805
791
  <div data-testid="selected-value" v-text={selected.value} />
806
792
  </>
807
793
  )
808
794
  })
809
795
 
810
- const piecePosition = page.getByTestId('position-f5')
811
- const nonTargetPosition = page.getByTestId('position-a1')
796
+ await dragMove(page, 'f5a1')
812
797
 
813
- // Start dragging the piece
814
- await piecePosition
815
- .element()
816
- .dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
817
- await nextTick()
818
-
819
- // Verify piece is selected
798
+ // Piece should still be selected
820
799
  await expect.element(page.getByTestId('selected-f5')).toBeVisible()
821
800
  await expect(selected.value).toBe(index('f5'))
822
801
 
823
- // Release on a non-target position
824
- await nonTargetPosition
825
- .element()
826
- .dispatchEvent(new PointerEvent('pointerup', { bubbles: true }))
802
+ // No move should have been emitted
803
+ await expect(onMove).not.toHaveBeenCalled()
804
+ })
805
+
806
+ test('click capture', async () => {
807
+ const hexchess = ref(
808
+ Hexchess.parse('b/qbk/n1b1n/r5r/ppp1ppppp/11/4pP5/4P1P4/3P1B1P3/2P2B2P2/1PRNQBKNRP1 w e6 0 2'),
809
+ )
810
+
811
+ setup(() => {
812
+ return () => (
813
+ <Hexboard
814
+ active
815
+ autoselect
816
+ playing={true}
817
+ hexchess={hexchess.value}
818
+ onMove={san => hexchess.value.applyMoveUnsafe(san)}
819
+ />
820
+ )
821
+ })
822
+
823
+ await clickMove(page, 'f5e5')
824
+
825
+ await expect(hexchess.value.toString()).toBe('b/qbk/n1b1n/r5r/ppp1ppppp/11/4P6/4P1P4/3P1B1P3/2P2B2P2/1PRNQBKNRP1 b - 0 2')
826
+ })
827
+
828
+ test('drag capture', async () => {
829
+ const hexchess = ref(
830
+ Hexchess.parse('b/qbk/n1b1n/r5r/ppp1ppppp/11/4pP5/4P1P4/3P1B1P3/2P2B2P2/1PRNQBKNRP1 w e6 0 2'),
831
+ )
832
+
833
+ setup(() => {
834
+ return () => (
835
+ <Hexboard
836
+ active
837
+ autoselect
838
+ playing={true}
839
+ hexchess={hexchess.value}
840
+ onMove={san => hexchess.value.applyMoveUnsafe(san)}
841
+ />
842
+ )
843
+ })
844
+
845
+ await dragMove(page, 'f5e5')
846
+
847
+ await expect(hexchess.value.toString()).toBe('b/qbk/n1b1n/r5r/ppp1ppppp/11/4P6/4P1P4/3P1B1P3/2P2B2P2/1PRNQBKNRP1 b - 0 2')
848
+ })
849
+
850
+ test('updates targets when autoselect is true', async () => {
851
+ const selected = ref<number | null>(index('f5'))
852
+ const targets = ref<number[]>([])
853
+
854
+ setup(() => {
855
+ return () => (
856
+ <Hexboard
857
+ v-model:selected={selected.value}
858
+ v-model:targets={targets.value}
859
+ autoselect
860
+ />
861
+ )
862
+ })
863
+
864
+ await expect(targets.value).toEqual([index('f6')])
865
+
866
+ selected.value = index('e4')
867
+
827
868
  await nextTick()
828
869
 
829
- // Piece should still be selected
830
- await expect.element(page.getByTestId('selected-f5')).toBeVisible()
831
- await expect(selected.value).toBe(index('f5'))
870
+ await expect(targets.value).toEqual([index('e5'), index('e6')])
832
871
  })
@@ -1,11 +1,12 @@
1
1
  import { position } from '@bedard/hexchess'
2
2
  import { San } from '@bedard/hexchess'
3
+ import { expect } from 'vitest'
3
4
  import type { BrowserPage } from 'vitest/browser'
4
5
  import { render } from 'vitest-browser-vue'
5
6
  import { nextTick } from 'vue'
6
7
 
7
- /** make a move on the hexboard */
8
- export async function makeMove(
8
+ /** make a move on the hexboard by clicking */
9
+ export async function clickMove(
9
10
  page: BrowserPage,
10
11
  ...sans: string[]
11
12
  ): Promise<void> {
@@ -20,6 +21,31 @@ export async function makeMove(
20
21
  }
21
22
  }
22
23
 
24
+ /** drag and drop a piece on the hexboard */
25
+ export async function dragMove(
26
+ page: BrowserPage,
27
+ ...sans: string[]
28
+ ): Promise<void> {
29
+ for (const str of sans) {
30
+ const san = San.from(str)
31
+
32
+ const fromPosition = page.getByTestId(`position-${position(san.from)}`)
33
+ const toPosition = page.getByTestId(`position-${position(san.to)}`)
34
+
35
+ await fromPosition
36
+ .element()
37
+ .dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
38
+ await nextTick()
39
+
40
+ await expect.element(page.getByTestId('drag-piece')).toBeVisible()
41
+
42
+ await toPosition
43
+ .element()
44
+ .dispatchEvent(new PointerEvent('pointerup', { bubbles: true }))
45
+ await nextTick()
46
+ }
47
+ }
48
+
23
49
  /** setup a component */
24
50
  export function setup(setup: () => any): ReturnType<typeof render> {
25
51
  return render({ setup })
package/vite.config.ts CHANGED
@@ -27,7 +27,24 @@ export default defineConfig({
27
27
  rollupTypes: true,
28
28
  }),
29
29
  tailwindcss(),
30
- vue(),
30
+ vue({
31
+ template: {
32
+ compilerOptions: {
33
+ nodeTransforms: [
34
+ (node) => {
35
+ // remove data-testid attributes from production build
36
+ if (node.type === 1 && node.props) {
37
+ node.props = node.props
38
+ .filter(prop =>
39
+ !(prop.type === 6 && prop.name === 'data-testid')
40
+ && !(prop.type === 7 && prop.name === 'bind' && prop.arg?.type === 4 && prop.arg.content === 'data-testid'),
41
+ )
42
+ }
43
+ },
44
+ ],
45
+ },
46
+ },
47
+ }),
31
48
  ],
32
49
  resolve: {
33
50
  alias: {