@bedard/hexboard 0.0.9 → 0.0.11

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
@@ -3,34 +3,34 @@
3
3
  "description": "Component library for hexchess.club",
4
4
  "devDependencies": {
5
5
  "@bedard/hexchess": "^2.5.1",
6
- "@eslint/js": "^9.39.1",
6
+ "@eslint/js": "^9.39.3",
7
7
  "@headlessui/vue": "^1.7.23",
8
8
  "@heroicons/vue": "^2.2.0",
9
- "@stylistic/eslint-plugin": "^5.6.1",
10
- "@tailwindcss/vite": "^4.1.17",
11
- "@types/node": "^22.19.2",
12
- "@vitejs/plugin-vue": "^6.0.2",
13
- "@vitejs/plugin-vue-jsx": "^5.1.2",
14
- "@vitest/browser": "^4.0.15",
15
- "@vitest/browser-playwright": "^4.0.15",
16
- "@vitest/coverage-v8": "^4.0.15",
17
- "@vitest/ui": "^4.0.15",
9
+ "@stylistic/eslint-plugin": "^5.9.0",
10
+ "@tailwindcss/vite": "^4.2.1",
11
+ "@types/node": "^22.19.12",
12
+ "@vitejs/plugin-vue": "^6.0.4",
13
+ "@vitejs/plugin-vue-jsx": "^5.1.4",
14
+ "@vitest/browser": "^4.0.18",
15
+ "@vitest/browser-playwright": "^4.0.18",
16
+ "@vitest/coverage-v8": "^4.0.18",
17
+ "@vitest/ui": "^4.0.18",
18
18
  "@vue/test-utils": "^2.4.6",
19
- "eslint": "^9.39.1",
19
+ "eslint": "^9.39.3",
20
20
  "eslint-plugin-simple-import-sort": "^12.1.1",
21
- "eslint-plugin-vue": "^10.6.2",
21
+ "eslint-plugin-vue": "^10.8.0",
22
22
  "globals": "^16.5.0",
23
- "jsdom": "^27.3.0",
24
- "playwright": "^1.57.0",
25
- "tailwindcss": "^4.1.17",
23
+ "jsdom": "^27.4.0",
24
+ "playwright": "^1.58.2",
25
+ "tailwindcss": "^4.2.1",
26
26
  "typescript": "^5.9.3",
27
- "typescript-eslint": "^8.49.0",
27
+ "typescript-eslint": "^8.56.1",
28
28
  "vite": "^6.4.1",
29
29
  "vite-plugin-dts": "^4.5.4",
30
- "vitest": "^4.0.15",
31
- "vitest-browser-vue": "^2.0.1",
32
- "vue": "^3.5.25",
33
- "vue-tsc": "^3.1.8"
30
+ "vitest": "^4.0.18",
31
+ "vitest-browser-vue": "^2.0.2",
32
+ "vue": "^3.5.29",
33
+ "vue-tsc": "^3.2.5"
34
34
  },
35
35
  "keywords": [
36
36
  "hexchess"
@@ -58,5 +58,5 @@
58
58
  "license": "MIT",
59
59
  "type": "module",
60
60
  "types": "dist/index.d.ts",
61
- "version": "0.0.9"
61
+ "version": "0.0.11"
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,
@@ -230,6 +231,8 @@ const props = withDefaults(
230
231
 
231
232
  const emit = defineEmits<{
232
233
  clickPosition: [position: number]
234
+ dragendPosition: [position: number]
235
+ dragstartPosition: [position: number]
233
236
  move: [san: San]
234
237
  }>()
235
238
 
@@ -462,6 +465,12 @@ const promotionPieces = computed(() => {
462
465
  // lifecycle
463
466
  //
464
467
 
468
+ onBeforeMount(() => {
469
+ if (props.autoselect) {
470
+ selectCurrentTargets()
471
+ }
472
+ })
473
+
465
474
  onMounted(() => {
466
475
  if (props.active) {
467
476
  listen()
@@ -486,6 +495,8 @@ watch(
486
495
  val => (val ? listen() : unlisten()),
487
496
  )
488
497
 
498
+ watch(selected, selectCurrentTargets)
499
+
489
500
  //
490
501
  // methods
491
502
  //
@@ -537,17 +548,25 @@ function attemptMove(san: San, evt?: MouseEvent) {
537
548
  }
538
549
  }
539
550
 
540
- /** check if user is playing the color at a position */
541
- function isPlayingPosition(index: number): boolean {
542
- const piece = props.hexchess?.board[index]
551
+ /** cancel promotion and restore original selection */
552
+ function cancelPromotion() {
553
+ const from = staging.value.promotionFrom
543
554
 
544
- if (!piece) {
545
- return false
555
+ staging.value = {
556
+ hexchess: null,
557
+ promotionEl: null,
558
+ promotionFrom: null,
559
+ promotionTo: null,
560
+ selected: null,
546
561
  }
547
562
 
548
- const pieceColor: Color = piece === piece.toLowerCase() ? 'b' : 'w'
563
+ // Keep the original piece selected
564
+ if (typeof from === 'number') {
565
+ selected.value = from
566
+ }
549
567
 
550
- return props.playing === true || props.playing === pieceColor
568
+ pointerdownPosition.value = null
569
+ skipNextClick = true
551
570
  }
552
571
 
553
572
  /** get fill color of label */
@@ -566,6 +585,19 @@ function getLabelFill(text: string) {
566
585
  return normalizedOptions.value.labelInactiveColor
567
586
  }
568
587
 
588
+ /** check if user is playing the color at a position */
589
+ function isPlayingPosition(index: number): boolean {
590
+ const piece = props.hexchess?.board[index]
591
+
592
+ if (!piece) {
593
+ return false
594
+ }
595
+
596
+ const pieceColor: Color = piece === piece.toLowerCase() ? 'b' : 'w'
597
+
598
+ return props.playing === true || props.playing === pieceColor
599
+ }
600
+
569
601
  /** listen for events */
570
602
  function listen() {
571
603
  pointerCoords.value = { x: 0, y: 0 }
@@ -614,7 +646,6 @@ function onClickPosition(index: number, evt: MouseEvent) {
614
646
  // If autoselect is enabled and clicking an unoccupied position, deselect
615
647
  if (props.autoselect && !props.hexchess.board[index]) {
616
648
  selected.value = null
617
- targets.value = []
618
649
  }
619
650
 
620
651
  emit('clickPosition', index)
@@ -632,11 +663,20 @@ function onKeyupWindow(evt: KeyboardEvent) {
632
663
  // Otherwise deselect if autoselect is enabled
633
664
  if (props.autoselect) {
634
665
  selected.value = null
635
- targets.value = []
636
666
  }
637
667
  }
638
668
  }
639
669
 
670
+ /** mouseenter position */
671
+ function onMouseenter(index: number) {
672
+ mouseoverPosition.value = index
673
+ }
674
+
675
+ /** mouseleave position */
676
+ function onMouseleave() {
677
+ mouseoverPosition.value = null
678
+ }
679
+
640
680
  /** handle piece move */
641
681
  function onPieceMove(san: San) {
642
682
  emit('move', san)
@@ -644,6 +684,62 @@ function onPieceMove(san: San) {
644
684
  resetState()
645
685
  }
646
686
 
687
+ /** pointerdown on position */
688
+ function onPointerdownPosition(index: number, evt: PointerEvent) {
689
+ evt.preventDefault()
690
+
691
+ // Don't start new interactions during promotion
692
+ if (staging.value.hexchess) {
693
+ return
694
+ }
695
+
696
+ // If clicking on a valid target for the selected piece, don't re-select
697
+ // (the move will be handled in onPointerupPosition/onClickPosition)
698
+ if (selected.value !== null && targets.value.includes(index)) {
699
+ return
700
+ }
701
+
702
+ const piece = props.hexchess?.board[index]
703
+
704
+ if (!piece) {
705
+ return
706
+ }
707
+
708
+ if (props.autoselect) {
709
+ selected.value = index
710
+ }
711
+
712
+ if (!isPlayingPosition(index)) {
713
+ return
714
+ }
715
+
716
+ // Only allow dragging if it's the piece's turn (or ignoreTurn is true)
717
+ const pieceColor: Color = piece === piece.toLowerCase() ? 'b' : 'w'
718
+ const isCurrentTurn = props.hexchess?.turn === pieceColor
719
+
720
+ if (!props.ignoreTurn && !isCurrentTurn) {
721
+ return
722
+ }
723
+
724
+ pointerdownPosition.value = index
725
+ pointerCoords.value = { x: evt.clientX, y: evt.clientY }
726
+
727
+ if (svgEl.value instanceof Element) {
728
+ svgRect.value = svgEl.value.getBoundingClientRect()
729
+ }
730
+
731
+ emit('dragstartPosition', index)
732
+ }
733
+
734
+ /** pointermove window */
735
+ function onPointermoveWindow(evt: MouseEvent) {
736
+ if (!props.active) {
737
+ return
738
+ }
739
+
740
+ pointerCoords.value = { x: evt.clientX, y: evt.clientY }
741
+ }
742
+
647
743
  /** pointerup position */
648
744
  function onPointerupPosition(index: number, evt: PointerEvent) {
649
745
  evt.stopPropagation()
@@ -663,6 +759,8 @@ function onPointerupPosition(index: number, evt: PointerEvent) {
663
759
  targetIndex = Number(posAttr)
664
760
  }
665
761
 
762
+ emit('dragendPosition', targetIndex)
763
+
666
764
  const san = new San({ from: pointerdownPosition.value, to: targetIndex })
667
765
  attemptMove(san, evt)
668
766
 
@@ -707,100 +805,6 @@ function onPointerupPosition(index: number, evt: PointerEvent) {
707
805
  resetState()
708
806
  }
709
807
 
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
- // If clicking on a valid target for the selected piece, don't re-select
742
- // (the move will be handled in onPointerupPosition/onClickPosition)
743
- if (selected.value !== null && targets.value.includes(index)) {
744
- return
745
- }
746
-
747
- const piece = props.hexchess?.board[index]
748
-
749
- if (!piece) {
750
- return
751
- }
752
-
753
- if (props.autoselect) {
754
- selected.value = index
755
- targets.value = props.hexchess?.movesFrom(index).map(san => san.to) ?? []
756
- }
757
-
758
- if (!isPlayingPosition(index)) {
759
- return
760
- }
761
-
762
- // Only allow dragging if it's the piece's turn (or ignoreTurn is true)
763
- const pieceColor: Color = piece === piece.toLowerCase() ? 'b' : 'w'
764
- const isCurrentTurn = props.hexchess?.turn === pieceColor
765
-
766
- if (!props.ignoreTurn && !isCurrentTurn) {
767
- return
768
- }
769
-
770
- pointerdownPosition.value = index
771
- pointerCoords.value = { x: evt.clientX, y: evt.clientY }
772
-
773
- if (svgEl.value instanceof Element) {
774
- svgRect.value = svgEl.value.getBoundingClientRect()
775
- }
776
- }
777
-
778
- /** mouseenter position */
779
- function onMouseenter(index: number) {
780
- mouseoverPosition.value = index
781
- }
782
-
783
- /** mouseleave position */
784
- function onMouseleave() {
785
- mouseoverPosition.value = null
786
- }
787
-
788
- /** pointermove window */
789
- function onPointermoveWindow(evt: MouseEvent) {
790
- if (!props.active) {
791
- return
792
- }
793
-
794
- pointerCoords.value = { x: evt.clientX, y: evt.clientY }
795
- }
796
-
797
- /** touchmove window - prevent scrolling while dragging */
798
- function onTouchmoveWindow(evt: TouchEvent) {
799
- if (pointerdownPosition.value !== null) {
800
- evt.preventDefault()
801
- }
802
- }
803
-
804
808
  /** pointerup window */
805
809
  function onPointerupWindow() {
806
810
  // If staging a promotion, cancel it but keep the original piece selected
@@ -819,6 +823,13 @@ function onPointerupWindow() {
819
823
  resetState()
820
824
  }
821
825
 
826
+ /** touchmove window - prevent scrolling while dragging */
827
+ function onTouchmoveWindow(evt: TouchEvent) {
828
+ if (pointerdownPosition.value !== null) {
829
+ evt.preventDefault()
830
+ }
831
+ }
832
+
822
833
  /** promote piece */
823
834
  function promote(promotion: 'n' | 'b' | 'r' | 'q') {
824
835
  if (
@@ -865,6 +876,16 @@ function resetState() {
865
876
  targets.value = []
866
877
  }
867
878
 
879
+ /** select current targets */
880
+ function selectCurrentTargets() {
881
+ if (typeof selected.value === 'number') {
882
+ targets.value = props.hexchess?.movesFrom(selected.value).map(san => san.to) ?? []
883
+ }
884
+ else {
885
+ targets.value = []
886
+ }
887
+ }
888
+
868
889
  /** stop listening for events */
869
890
  function unlisten() {
870
891
  resetState()
@@ -502,6 +502,72 @@ test('dragging piece off board results in selection only, dragging state resets'
502
502
  await expect.element(page.getByTestId('drag-piece')).not.toBeInTheDocument()
503
503
  })
504
504
 
505
+ test('drag emits dragstartPosition and dragendPosition', async () => {
506
+ const selected = ref<number | null>(null)
507
+ const targets = ref<number[]>([])
508
+ const onDragstartPosition = vi.fn()
509
+ const onDragendPosition = vi.fn()
510
+
511
+ setup(() => {
512
+ return () => (
513
+ <Hexboard
514
+ active
515
+ autoselect
516
+ playing="w"
517
+ v-model:selected={selected.value}
518
+ v-model:targets={targets.value}
519
+ onDragendPosition={onDragendPosition}
520
+ onDragstartPosition={onDragstartPosition}
521
+ />
522
+ )
523
+ })
524
+
525
+ await dragMove(page, 'f5f6')
526
+
527
+ await expect(onDragstartPosition).toHaveBeenCalledOnce()
528
+ await expect(onDragstartPosition).toHaveBeenCalledWith(index('f5'))
529
+ await expect(onDragendPosition).toHaveBeenCalledOnce()
530
+ await expect(onDragendPosition).toHaveBeenCalledWith(index('f6'))
531
+ })
532
+
533
+ test('drag start emits dragstartPosition; release off board does not emit dragendPosition', async () => {
534
+ const selected = ref<number | null>(null)
535
+ const targets = ref<number[]>([])
536
+ const onDragstartPosition = vi.fn()
537
+ const onDragendPosition = vi.fn()
538
+
539
+ setup(() => {
540
+ return () => (
541
+ <Hexboard
542
+ active
543
+ autoselect
544
+ playing="w"
545
+ v-model:selected={selected.value}
546
+ v-model:targets={targets.value}
547
+ onDragendPosition={onDragendPosition}
548
+ onDragstartPosition={onDragstartPosition}
549
+ />
550
+ )
551
+ })
552
+
553
+ const whitePiecePosition = page.getByTestId('position-f5')
554
+
555
+ await whitePiecePosition
556
+ .element()
557
+ .dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
558
+ await nextTick()
559
+
560
+ await expect(onDragstartPosition).toHaveBeenCalledOnce()
561
+ await expect(onDragstartPosition).toHaveBeenCalledWith(index('f5'))
562
+ await expect(onDragendPosition).not.toHaveBeenCalled()
563
+
564
+ window.dispatchEvent(new PointerEvent('pointerup', { bubbles: true }))
565
+ await nextTick()
566
+
567
+ await expect(onDragstartPosition).toHaveBeenCalledOnce()
568
+ await expect(onDragendPosition).not.toHaveBeenCalled()
569
+ })
570
+
505
571
  test('drag and drop piece emits move event', async () => {
506
572
  const selected = ref<number | null>(null)
507
573
  const targets = ref<number[]>([])
@@ -846,3 +912,26 @@ test('drag capture', async () => {
846
912
 
847
913
  await expect(hexchess.value.toString()).toBe('b/qbk/n1b1n/r5r/ppp1ppppp/11/4P6/4P1P4/3P1B1P3/2P2B2P2/1PRNQBKNRP1 b - 0 2')
848
914
  })
915
+
916
+ test('updates targets when autoselect is true', async () => {
917
+ const selected = ref<number | null>(index('f5'))
918
+ const targets = ref<number[]>([])
919
+
920
+ setup(() => {
921
+ return () => (
922
+ <Hexboard
923
+ v-model:selected={selected.value}
924
+ v-model:targets={targets.value}
925
+ autoselect
926
+ />
927
+ )
928
+ })
929
+
930
+ await expect(targets.value).toEqual([index('f6')])
931
+
932
+ selected.value = index('e4')
933
+
934
+ await nextTick()
935
+
936
+ await expect(targets.value).toEqual([index('e5'), index('e6')])
937
+ })