11import { Controller } from "@hotwired/stimulus"
22
33export default class extends Controller {
4- static targets = [ "map" , "select" , "surfaceSelect" , "environmentSelect" ]
4+ static targets = [ "map" , "select" , "surfaceSelect" , "environmentSelect" , "countrySelect" , "citySelect" ]
55 static values = {
66 courts : Array ,
7+ countryCities : Object ,
8+ cityPlaceholder : String ,
79 surfaceLabels : Object ,
810 environmentLabels : Object ,
911 surfaceSelected : String ,
@@ -14,6 +16,7 @@ export default class extends Controller {
1416 }
1517
1618 connect ( ) {
19+ this . filterCourts ( )
1720 this . initMap ( )
1821 this . updateSurfaceOptions ( { initial : true } )
1922 }
@@ -48,14 +51,50 @@ export default class extends Controller {
4851 this . markersById . set ( String ( c . id ) , marker )
4952 } )
5053
54+ this . _syncMarkers ( )
5155 if ( this . selectTarget . value ) this . _focusSelected ( )
5256 }
5357
58+ // Смена страны или города: пересобираем города под страну и список кортов.
59+ locationChanged ( event ) {
60+ if ( this . hasCountrySelectTarget && event . target === this . countrySelectTarget ) this . _syncCityOptions ( )
61+ this . filterCourts ( )
62+ }
63+
5464 selectChanged ( ) {
5565 this . _focusSelected ( )
5666 this . updateSurfaceOptions ( )
5767 }
5868
69+ // Оставляет в списке корты выбранной страны/города, группируя их по городам.
70+ // Если выбранный корт отфильтровался, берём первый доступный.
71+ filterCourts ( ) {
72+ const country = this . hasCountrySelectTarget ? this . countrySelectTarget . value : ""
73+ const city = this . hasCitySelectTarget ? this . citySelectTarget . value : ""
74+ const previous = this . selectTarget . value
75+ const courts = ( this . courtsValue || [ ] ) . filter ( c => this . _matchesLocation ( c , country , city ) )
76+ const groups = this . _groupByCity ( courts )
77+ // Города разделяем заголовками только когда их несколько — иначе это лишний шум.
78+ const grouped = groups . length > 1
79+
80+ this . selectTarget . innerHTML = ""
81+ groups . forEach ( ( [ cityName , cityCourts ] ) => {
82+ const parent = grouped && cityName ? this . _appendGroup ( cityName ) : this . selectTarget
83+ cityCourts . forEach ( c => parent . appendChild ( this . _buildOption ( String ( c . id ) , c . name ) ) )
84+ } )
85+
86+ // Порядок опций задают группы, поэтому запасной вариант берём из самого списка.
87+ const stillListed = courts . some ( c => String ( c . id ) === String ( previous ) )
88+ if ( stillListed ) this . selectTarget . value = previous
89+ else this . selectTarget . selectedIndex = 0
90+
91+ this . _syncMarkers ( )
92+ if ( ! stillListed ) {
93+ this . updateSurfaceOptions ( )
94+ this . _focusSelected ( )
95+ }
96+ }
97+
5998 // Заполняет селекты покрытия и среды вариантами выбранного корта.
6099 // preselect (сохранённое значение игры) применяется только при initial connect;
61100 // после смены корта пользователем ориентируемся только на текущее значение селекта.
@@ -81,6 +120,59 @@ export default class extends Controller {
81120 }
82121 }
83122
123+ _matchesLocation ( court , country , city ) {
124+ if ( city ) return court . city === city
125+ if ( country ) return court . country === country
126+ return true
127+ }
128+
129+ _groupByCity ( courts ) {
130+ const groups = new Map ( )
131+ courts . forEach ( c => {
132+ const key = c . city || ""
133+ if ( ! groups . has ( key ) ) groups . set ( key , [ ] )
134+ groups . get ( key ) . push ( c )
135+ } )
136+
137+ return [ ...groups . entries ( ) ]
138+ . sort ( ( a , b ) => a [ 0 ] . localeCompare ( b [ 0 ] ) )
139+ . map ( ( [ cityName , cityCourts ] ) => [ cityName , cityCourts . sort ( ( a , b ) => a . name . localeCompare ( b . name ) ) ] )
140+ }
141+
142+ _appendGroup ( label ) {
143+ const group = document . createElement ( "optgroup" )
144+ group . label = label
145+ this . selectTarget . appendChild ( group )
146+ return group
147+ }
148+
149+ _syncCityOptions ( ) {
150+ if ( ! this . hasCitySelectTarget ) return
151+
152+ const selectedCity = this . citySelectTarget . value
153+ const cities = ( this . countryCitiesValue || { } ) [ this . countrySelectTarget . value ] || [ ]
154+
155+ this . citySelectTarget . innerHTML = ""
156+ this . citySelectTarget . appendChild ( this . _buildOption ( "" , this . cityPlaceholderValue ) )
157+ cities . forEach ( city => this . citySelectTarget . appendChild ( this . _buildOption ( city , city ) ) )
158+ this . citySelectTarget . value = cities . includes ( selectedCity ) ? selectedCity : ""
159+ }
160+
161+ // Маркеры отфильтрованных кортов убираем с карты, чтобы она совпадала со списком.
162+ _syncMarkers ( ) {
163+ if ( ! this . markersById || ! this . map ) return
164+
165+ const listed = new Set ( [ ...this . selectTarget . options ] . map ( option => option . value ) )
166+ this . markersById . forEach ( ( marker , id ) => marker . setMap ( listed . has ( id ) ? this . map : null ) )
167+ }
168+
169+ _buildOption ( value , label ) {
170+ const option = document . createElement ( "option" )
171+ option . value = value
172+ option . textContent = label
173+ return option
174+ }
175+
84176 _fillSelect ( select , values , labels , preselect ) {
85177 const previous = select . value || preselect || ""
86178 const blank = select . querySelector ( 'option[value=""]' )
@@ -99,15 +191,16 @@ export default class extends Controller {
99191 }
100192
101193 _focusSelected ( ) {
102- const marker = this . markersById . get ( String ( this . selectTarget . value ) )
194+ const marker = this . markersById && this . markersById . get ( String ( this . selectTarget . value ) )
103195 if ( ! marker || ! this . map ) return
104196 this . map . setCenter ( marker . getPosition ( ) )
105197 this . map . setZoom ( Math . max ( this . map . getZoom ( ) , 14 ) )
106198 // Можно открыть infoWindow, но для простоты пропустим
107199 }
108200
109201 _center ( ) {
110- const withCoords = ( this . courtsValue || [ ] ) . find ( this . _valid )
202+ const selected = ( this . courtsValue || [ ] ) . find ( c => String ( c . id ) === String ( this . selectTarget . value ) )
203+ const withCoords = this . _valid ( selected ) ? selected : ( this . courtsValue || [ ] ) . find ( this . _valid )
111204 return withCoords ? [ withCoords . lat , withCoords . lng ] : [ this . defaultLatValue , this . defaultLngValue ]
112205 }
113206
@@ -118,4 +211,4 @@ export default class extends Controller {
118211 disconnect ( ) {
119212 // Очистка не требуется
120213 }
121- }
214+ }
0 commit comments