@@ -845,6 +845,184 @@ <h3>Loading Error</h3>
845
845
// Initialize theme
846
846
initializeThemeSelector ( ) ;
847
847
848
+ // Cities search functionality
849
+ let citiesData = [ ] ;
850
+ let searchTimeout = null ;
851
+
852
+ // Load cities data
853
+ async function loadCitiesData ( ) {
854
+ try {
855
+ const response = await fetch (
856
+ "https://unpkg.com/[email protected] /cities.json" ,
857
+ ) ;
858
+ citiesData = await response . json ( ) ;
859
+ console . log (
860
+ "Cities data loaded:" ,
861
+ citiesData . length ,
862
+ "cities" ,
863
+ ) ;
864
+ } catch ( error ) {
865
+ console . error ( "Error loading cities data:" , error ) ;
866
+ showError ( "Failed to load cities data" ) ;
867
+ }
868
+ }
869
+
870
+ // Search cities function
871
+ function searchCities ( query ) {
872
+ if ( ! query || query . length < 2 ) {
873
+ return [ ] ;
874
+ }
875
+
876
+ query = query . toLowerCase ( ) ;
877
+ return citiesData
878
+ . filter ( ( city ) => city . name . toLowerCase ( ) . includes ( query ) )
879
+ . slice ( 0 , 10 ) ; // Limit to 10 results
880
+ }
881
+
882
+ // Display search results
883
+ function displaySearchResults ( results ) {
884
+ const resultsContainer = document . getElementById (
885
+ "city-search-results" ,
886
+ ) ;
887
+ resultsContainer . innerHTML = "" ;
888
+
889
+ if ( results . length === 0 ) {
890
+ resultsContainer . innerHTML =
891
+ '<div class="city-result">No cities found</div>' ;
892
+ resultsContainer . classList . add ( "active" ) ;
893
+ return ;
894
+ }
895
+
896
+ results . forEach ( ( city ) => {
897
+ const resultElement = document . createElement ( "div" ) ;
898
+ resultElement . className = "city-result" ;
899
+ resultElement . innerHTML = `
900
+ <div class="city-name">${ city . name } </div>
901
+ <div class="city-info">${ city . country } • ${ city . lat } , ${ city . lng } </div>
902
+ ` ;
903
+
904
+ resultElement . addEventListener ( "click" , ( ) => {
905
+ showTimezone (
906
+ parseFloat ( city . lat ) ,
907
+ parseFloat ( city . lng ) ,
908
+ map ,
909
+ ) ;
910
+ document . getElementById ( "city-search" ) . value = "" ;
911
+ resultsContainer . classList . remove ( "active" ) ;
912
+ } ) ;
913
+
914
+ resultsContainer . appendChild ( resultElement ) ;
915
+ } ) ;
916
+
917
+ resultsContainer . classList . add ( "active" ) ;
918
+ }
919
+
920
+ // Handle search input
921
+ document . getElementById ( "city-search" ) . addEventListener (
922
+ "input" ,
923
+ ( e ) => {
924
+ const query = e . target . value . trim ( ) ;
925
+ const resultsContainer = document . getElementById (
926
+ "city-search-results" ,
927
+ ) ;
928
+
929
+ if ( ! query ) {
930
+ resultsContainer . classList . remove ( "active" ) ;
931
+ return ;
932
+ }
933
+
934
+ if ( searchTimeout ) {
935
+ clearTimeout ( searchTimeout ) ;
936
+ }
937
+
938
+ searchTimeout = setTimeout ( ( ) => {
939
+ const results = searchCities ( query ) ;
940
+ displaySearchResults ( results ) ;
941
+ } , 300 ) ;
942
+ } ,
943
+ ) ;
944
+
945
+ // Close search results when clicking outside
946
+ document . addEventListener ( "click" , ( e ) => {
947
+ const resultsContainer = document . getElementById (
948
+ "city-search-results" ,
949
+ ) ;
950
+ const searchInput = document . getElementById ( "city-search" ) ;
951
+
952
+ if (
953
+ ! resultsContainer . contains ( e . target ) &&
954
+ e . target !== searchInput
955
+ ) {
956
+ resultsContainer . classList . remove ( "active" ) ;
957
+ }
958
+ } ) ;
959
+
960
+ // Load cities data when the page loads
961
+ loadCitiesData ( ) ;
962
+
963
+ // Add keyboard navigation for search results
964
+ document . getElementById ( "city-search" ) . addEventListener (
965
+ "keydown" ,
966
+ ( e ) => {
967
+ const resultsContainer = document . getElementById (
968
+ "city-search-results" ,
969
+ ) ;
970
+ const results = resultsContainer . getElementsByClassName (
971
+ "city-result" ,
972
+ ) ;
973
+ let currentFocus = - 1 ;
974
+
975
+ // Get currently focused element
976
+ for ( let i = 0 ; i < results . length ; i ++ ) {
977
+ if ( results [ i ] . classList . contains ( "focused" ) ) {
978
+ currentFocus = i ;
979
+ break ;
980
+ }
981
+ }
982
+
983
+ if ( e . key === "ArrowDown" ) {
984
+ e . preventDefault ( ) ;
985
+ currentFocus ++ ;
986
+ if ( currentFocus >= results . length ) currentFocus = 0 ;
987
+ } else if ( e . key === "ArrowUp" ) {
988
+ e . preventDefault ( ) ;
989
+ currentFocus -- ;
990
+ if ( currentFocus < 0 ) currentFocus = results . length - 1 ;
991
+ } else if ( e . key === "Enter" && currentFocus > - 1 ) {
992
+ e . preventDefault ( ) ;
993
+ results [ currentFocus ] . click ( ) ;
994
+ } else if ( e . key === "Escape" ) {
995
+ resultsContainer . classList . remove ( "active" ) ;
996
+ e . target . blur ( ) ;
997
+ } else {
998
+ return ;
999
+ }
1000
+
1001
+ // Update focus
1002
+ Array . from ( results ) . forEach ( ( result ) =>
1003
+ result . classList . remove ( "focused" )
1004
+ ) ;
1005
+ if ( currentFocus > - 1 ) {
1006
+ results [ currentFocus ] . classList . add ( "focused" ) ;
1007
+ results [ currentFocus ] . scrollIntoView ( { block : "nearest" } ) ;
1008
+ }
1009
+ } ,
1010
+ ) ;
1011
+
1012
+ // Add focus style for keyboard navigation
1013
+ const focusStyle = document . createElement ( "style" ) ;
1014
+ focusStyle . textContent = `
1015
+ .city-result.focused {
1016
+ background-color: var(--button-bg);
1017
+ color: white;
1018
+ }
1019
+ [data-theme="high-contrast"] .city-result.focused {
1020
+ background-color: yellow;
1021
+ color: black;
1022
+ }
1023
+ ` ;
1024
+ document . head . appendChild ( focusStyle ) ;
1025
+
848
1026
// 添加键盘快捷键面板切换功能
849
1027
window . toggleShortcutsHelp = function ( ) {
850
1028
const panel = document . getElementById ( "shortcuts-panel" ) ;
@@ -2649,6 +2827,98 @@ <h3>Loading Error</h3>
2649
2827
~ .leaflet-tile-container {
2650
2828
filter : brightness (0.65 );
2651
2829
}
2830
+
2831
+ /* City Search Styles */
2832
+ .city-search-results {
2833
+ max-height : 200px ;
2834
+ overflow-y : auto;
2835
+ margin-top : 8px ;
2836
+ border : 1px solid var (--input-border );
2837
+ border-radius : 4px ;
2838
+ background : var (--panel-bg );
2839
+ display : none;
2840
+ }
2841
+
2842
+ .city-search-results .active {
2843
+ display : block;
2844
+ }
2845
+
2846
+ .city-result {
2847
+ padding : 8px 12px ;
2848
+ cursor : pointer;
2849
+ border-bottom : 1px solid var (--input-border );
2850
+ transition : background-color 0.2s ;
2851
+ }
2852
+
2853
+ .city-result : last-child {
2854
+ border-bottom : none;
2855
+ }
2856
+
2857
+ .city-result : hover {
2858
+ background-color : var (--button-bg );
2859
+ color : white;
2860
+ }
2861
+
2862
+ .city-name {
2863
+ font-weight : 500 ;
2864
+ margin-bottom : 2px ;
2865
+ }
2866
+
2867
+ .city-info {
2868
+ font-size : 12px ;
2869
+ opacity : 0.8 ;
2870
+ }
2871
+
2872
+ [data-theme = "dark" ] .city-result : hover {
2873
+ background-color : var (--button-hover );
2874
+ }
2875
+
2876
+ [data-theme = "high-contrast" ] .city-result : hover {
2877
+ background-color : yellow;
2878
+ color : black;
2879
+ }
2880
+
2881
+ # city-search {
2882
+ width : 100% ;
2883
+ padding : 8px 12px ;
2884
+ border : 1px solid var (--input-border );
2885
+ border-radius : 4px ;
2886
+ font-size : 14px ;
2887
+ background : var (--panel-bg );
2888
+ color : var (--text-color );
2889
+ }
2890
+
2891
+ # city-search : focus {
2892
+ outline : none;
2893
+ border-color : var (--input-focus );
2894
+ box-shadow : 0 0 0 3px rgba (66 , 153 , 225 , 0.1 );
2895
+ }
2896
+
2897
+ /* Scrollbar styles for city search results */
2898
+ .city-search-results ::-webkit-scrollbar {
2899
+ width : 8px ;
2900
+ }
2901
+
2902
+ .city-search-results ::-webkit-scrollbar-track {
2903
+ background : var (--panel-bg );
2904
+ }
2905
+
2906
+ .city-search-results ::-webkit-scrollbar-thumb {
2907
+ background : var (--input-border );
2908
+ border-radius : 4px ;
2909
+ }
2910
+
2911
+ .city-search-results ::-webkit-scrollbar-thumb : hover {
2912
+ background : var (--button-bg );
2913
+ }
2914
+
2915
+ /* Loading indicator for city search */
2916
+ .city-search-loading {
2917
+ padding : 8px 12px ;
2918
+ text-align : center;
2919
+ color : var (--text-color );
2920
+ font-style : italic;
2921
+ }
2652
2922
</ style >
2653
2923
</ head >
2654
2924
@@ -2729,6 +2999,23 @@ <h2 id="coordinates-title" class="panel-section-title">
2729
2999
</ button >
2730
3000
</ section >
2731
3001
3002
+ < section class ="panel-section " aria-labelledby ="city-search-title ">
3003
+ < h2 id ="city-search-title " class ="panel-section-title ">
3004
+ City Search
3005
+ </ h2 >
3006
+ < div class ="input-group ">
3007
+ < input
3008
+ type ="text "
3009
+ id ="city-search "
3010
+ name ="city-search "
3011
+ placeholder ="Search for a city... "
3012
+ aria-label ="Search for a city "
3013
+ autocomplete ="off "
3014
+ />
3015
+ </ div >
3016
+ < div id ="city-search-results " class ="city-search-results "> </ div >
3017
+ </ section >
3018
+
2732
3019
< section class ="panel-section " aria-labelledby ="marker-controls-title ">
2733
3020
< h2 id ="marker-controls-title " class ="panel-section-title ">
2734
3021
Marker Controls
0 commit comments