@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/CHANGELOG.md +8 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +314 -308
- package/package.json +21 -21
- package/src/lib/components/hexboard/Hexboard.vue +124 -103
- package/src/tests/hexboard.test.tsx +89 -0
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.
|
|
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.
|
|
10
|
-
"@tailwindcss/vite": "^4.1
|
|
11
|
-
"@types/node": "^22.19.
|
|
12
|
-
"@vitejs/plugin-vue": "^6.0.
|
|
13
|
-
"@vitejs/plugin-vue-jsx": "^5.1.
|
|
14
|
-
"@vitest/browser": "^4.0.
|
|
15
|
-
"@vitest/browser-playwright": "^4.0.
|
|
16
|
-
"@vitest/coverage-v8": "^4.0.
|
|
17
|
-
"@vitest/ui": "^4.0.
|
|
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.
|
|
19
|
+
"eslint": "^9.39.3",
|
|
20
20
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
21
|
-
"eslint-plugin-vue": "^10.
|
|
21
|
+
"eslint-plugin-vue": "^10.8.0",
|
|
22
22
|
"globals": "^16.5.0",
|
|
23
|
-
"jsdom": "^27.
|
|
24
|
-
"playwright": "^1.
|
|
25
|
-
"tailwindcss": "^4.1
|
|
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.
|
|
27
|
+
"typescript-eslint": "^8.56.1",
|
|
28
28
|
"vite": "^6.4.1",
|
|
29
29
|
"vite-plugin-dts": "^4.5.4",
|
|
30
|
-
"vitest": "^4.0.
|
|
31
|
-
"vitest-browser-vue": "^2.0.
|
|
32
|
-
"vue": "^3.5.
|
|
33
|
-
"vue-tsc": "^3.
|
|
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.
|
|
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
|
-
/**
|
|
541
|
-
function
|
|
542
|
-
const
|
|
551
|
+
/** cancel promotion and restore original selection */
|
|
552
|
+
function cancelPromotion() {
|
|
553
|
+
const from = staging.value.promotionFrom
|
|
543
554
|
|
|
544
|
-
|
|
545
|
-
|
|
555
|
+
staging.value = {
|
|
556
|
+
hexchess: null,
|
|
557
|
+
promotionEl: null,
|
|
558
|
+
promotionFrom: null,
|
|
559
|
+
promotionTo: null,
|
|
560
|
+
selected: null,
|
|
546
561
|
}
|
|
547
562
|
|
|
548
|
-
|
|
563
|
+
// Keep the original piece selected
|
|
564
|
+
if (typeof from === 'number') {
|
|
565
|
+
selected.value = from
|
|
566
|
+
}
|
|
549
567
|
|
|
550
|
-
|
|
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
|
+
})
|