1
1
use std:: cell:: Cell ;
2
+ use std:: collections:: HashMap ;
2
3
use std:: marker:: PhantomData ;
3
4
use std:: rc:: Rc ;
4
5
5
6
use sycamore:: prelude:: * ;
6
7
use wasm_bindgen:: prelude:: * ;
7
- use web_sys:: { Element , HtmlAnchorElement , HtmlBaseElement , KeyboardEvent } ;
8
+ use web_sys:: js_sys:: Array ;
9
+ use web_sys:: { Element , Event , HtmlAnchorElement , HtmlBaseElement , KeyboardEvent , UrlSearchParams } ;
8
10
9
11
use crate :: Route ;
10
12
@@ -24,6 +26,7 @@ pub trait Integration {
24
26
25
27
thread_local ! {
26
28
static PATHNAME : Cell <Option <Signal <String >>> = const { Cell :: new( None ) } ;
29
+ static QUERY : Cell <Option <Signal <( ) >>> = const { Cell :: new( None ) } ;
27
30
}
28
31
29
32
/// A router integration that uses the
@@ -78,28 +81,55 @@ impl Integration for HistoryIntegration {
78
81
let origin = a. origin ( ) ;
79
82
let a_pathname = a. pathname ( ) ;
80
83
let hash = a. hash ( ) ;
84
+ let query = a. search ( ) ;
81
85
82
86
let meta_keys_pressed = meta_keys_pressed ( ev. unchecked_ref :: < KeyboardEvent > ( ) ) ;
83
87
if !meta_keys_pressed && location. origin ( ) == Ok ( origin) {
84
88
if location. pathname ( ) . as_ref ( ) != Ok ( & a_pathname) {
85
89
// Same origin, different path. Navigate to new page.
86
90
ev. prevent_default ( ) ;
87
91
PATHNAME . with ( |pathname| {
92
+ // Update History API.
93
+ let history = window ( ) . history ( ) . unwrap_throw ( ) ;
94
+ history
95
+ . push_state_with_url ( & JsValue :: UNDEFINED , "" , Some ( & a_pathname) )
96
+ . unwrap_throw ( ) ;
97
+ window ( ) . scroll_to_with_x_and_y ( 0.0 , 0.0 ) ;
98
+
88
99
let pathname = pathname. get ( ) . unwrap_throw ( ) ;
89
100
let path = a_pathname
90
101
. strip_prefix ( & base_pathname ( ) )
91
102
. unwrap_or ( & a_pathname) ;
92
103
pathname. set ( path. to_string ( ) ) ;
93
-
94
- // Update History API.
104
+ } ) ;
105
+ } else if location. search ( ) . as_ref ( ) != Ok ( & query) {
106
+ // Same origin, same pathname, different query.
107
+ ev. prevent_default ( ) ;
108
+ let history = window ( ) . history ( ) . unwrap_throw ( ) ;
109
+ if query. is_empty ( ) {
110
+ history
111
+ . push_state_with_url ( & JsValue :: UNDEFINED , "" , Some ( & a. href ( ) ) )
112
+ . unwrap_throw ( ) ;
113
+ } else {
95
114
let history = window ( ) . history ( ) . unwrap_throw ( ) ;
96
115
history
97
- . push_state_with_url ( & JsValue :: UNDEFINED , "" , Some ( & a_pathname ) )
116
+ . push_state_with_url ( & JsValue :: UNDEFINED , "" , Some ( & query ) )
98
117
. unwrap_throw ( ) ;
99
- window ( ) . scroll_to_with_x_and_y ( 0.0 , 0.0 ) ;
100
- } ) ;
118
+ }
119
+ QUERY . with ( |query| query . get ( ) . unwrap_throw ( ) . update ( |_| { } ) ) ;
101
120
} else if location. hash ( ) . as_ref ( ) != Ok ( & hash) {
102
- // Same origin, same pathname, different hash. Use default browser behavior.
121
+ // Same origin, same pathname, same query, different hash. Use default
122
+ // browser behavior.
123
+ if hash. is_empty ( ) {
124
+ ev. prevent_default ( ) ;
125
+ let history = window ( ) . history ( ) . unwrap_throw ( ) ;
126
+ history
127
+ . push_state_with_url ( & JsValue :: UNDEFINED , "" , Some ( & a. href ( ) ) )
128
+ . unwrap_throw ( ) ;
129
+ window ( )
130
+ . dispatch_event ( & Event :: new ( "hashchange" ) . unwrap ( ) )
131
+ . unwrap_throw ( ) ;
132
+ }
103
133
} else {
104
134
// Same page. Do nothing.
105
135
ev. prevent_default ( ) ;
@@ -233,6 +263,7 @@ where
233
263
let path = integration. current_pathname ( ) ;
234
264
let path = path. strip_prefix ( & base_pathname) . unwrap_or ( & path) ;
235
265
pathname. set ( Some ( create_signal ( path. to_string ( ) ) ) ) ;
266
+ QUERY . set ( Some ( create_signal ( ( ) ) ) ) ;
236
267
} ) ;
237
268
let pathname = PATHNAME . with ( |p| p. get ( ) . unwrap_throw ( ) ) ;
238
269
@@ -329,16 +360,16 @@ pub fn navigate(url: &str) {
329
360
"navigate can only be used with a Router"
330
361
) ;
331
362
332
- let pathname = pathname. get ( ) . unwrap_throw ( ) ;
333
- let path = url. strip_prefix ( & base_pathname ( ) ) . unwrap_or ( url) ;
334
- pathname. set ( path. to_string ( ) ) ;
335
-
336
363
// Update History API.
337
364
let history = window ( ) . history ( ) . unwrap_throw ( ) ;
338
365
history
339
366
. push_state_with_url ( & JsValue :: UNDEFINED , "" , Some ( url) )
340
367
. unwrap_throw ( ) ;
341
368
window ( ) . scroll_to_with_x_and_y ( 0.0 , 0.0 ) ;
369
+
370
+ let pathname = pathname. get ( ) . unwrap_throw ( ) ;
371
+ let path = url. strip_prefix ( & base_pathname ( ) ) . unwrap_or ( url) ;
372
+ pathname. set ( path. to_string ( ) ) ;
342
373
} ) ;
343
374
}
344
375
@@ -357,16 +388,16 @@ pub fn navigate_replace(url: &str) {
357
388
"navigate_replace can only be used with a Router"
358
389
) ;
359
390
360
- let pathname = pathname. get ( ) . unwrap_throw ( ) ;
361
- let path = url. strip_prefix ( & base_pathname ( ) ) . unwrap_or ( url) ;
362
- pathname. set ( path. to_string ( ) ) ;
363
-
364
391
// Update History API.
365
392
let history = window ( ) . history ( ) . unwrap_throw ( ) ;
366
393
history
367
394
. replace_state_with_url ( & JsValue :: UNDEFINED , "" , Some ( url) )
368
395
. unwrap_throw ( ) ;
369
396
window ( ) . scroll_to_with_x_and_y ( 0.0 , 0.0 ) ;
397
+
398
+ let pathname = pathname. get ( ) . unwrap_throw ( ) ;
399
+ let path = url. strip_prefix ( & base_pathname ( ) ) . unwrap_or ( url) ;
400
+ pathname. set ( path. to_string ( ) ) ;
370
401
} ) ;
371
402
}
372
403
@@ -383,17 +414,18 @@ pub fn navigate_no_history(url: &str) {
383
414
"navigate_no_history can only be used with a Router"
384
415
) ;
385
416
417
+ window ( ) . scroll_to_with_x_and_y ( 0.0 , 0.0 ) ;
418
+
386
419
let pathname = pathname. get ( ) . unwrap_throw ( ) ;
387
420
let path = url. strip_prefix ( & base_pathname ( ) ) . unwrap_or ( url) ;
388
421
pathname. set ( path. to_string ( ) ) ;
389
-
390
- window ( ) . scroll_to_with_x_and_y ( 0.0 , 0.0 ) ;
391
422
} ) ;
392
423
}
393
424
394
425
/// Preform a "soft" refresh of the current page.
395
426
///
396
- /// Unlike a "hard" refresh which corresponds to clicking on the refresh button, this simply forces a re-render of the view for the current page.
427
+ /// Unlike a "hard" refresh which corresponds to clicking on the refresh button, this simply forces
428
+ /// a re-render of the view for the current page.
397
429
///
398
430
/// # Panic
399
431
/// This function will `panic!()` if a [`Router`] has not yet been created.
@@ -404,12 +436,92 @@ pub fn refresh() {
404
436
"refresh can only be used with a Router"
405
437
) ;
406
438
407
- pathname. get ( ) . unwrap_throw ( ) . update ( |_| { } ) ;
408
-
409
439
window ( ) . scroll_to_with_x_and_y ( 0.0 , 0.0 ) ;
440
+
441
+ pathname. get ( ) . unwrap_throw ( ) . update ( |_| { } ) ;
410
442
} ) ;
411
443
}
412
444
445
+ /// Creates a ReadSignal that tracks the url query provided.
446
+ pub fn use_search_query ( query : & ' static str ) -> ReadSignal < Option < String > > {
447
+ PATHNAME . with ( |pathname| {
448
+ assert ! (
449
+ pathname. get( ) . is_some( ) ,
450
+ "create_query can only be used with a Router"
451
+ ) ;
452
+
453
+ let pathname = pathname. get ( ) . unwrap_throw ( ) ;
454
+
455
+ create_memo ( move || {
456
+ QUERY . with ( |query| query. get ( ) . unwrap_throw ( ) ) . track ( ) ;
457
+ pathname. track ( ) ;
458
+ UrlSearchParams :: new_with_str ( & window ( ) . location ( ) . search ( ) . unwrap_throw ( ) )
459
+ . unwrap_throw ( )
460
+ . get ( query)
461
+ } )
462
+ } )
463
+ }
464
+
465
+ /// Creates a ReadSignal that tracks the url query string.
466
+ pub fn use_search_queries ( ) -> ReadSignal < HashMap < String , String > > {
467
+ PATHNAME . with ( |pathname| {
468
+ assert ! (
469
+ pathname. get( ) . is_some( ) ,
470
+ "create_queries can only be used with a Router"
471
+ ) ;
472
+
473
+ let pathname = pathname. get ( ) . unwrap_throw ( ) ;
474
+
475
+ create_memo ( move || {
476
+ QUERY . with ( |query| query. get ( ) . unwrap_throw ( ) ) . track ( ) ;
477
+ pathname. track ( ) ;
478
+ UrlSearchParams :: new_with_str ( & window ( ) . location ( ) . search ( ) . unwrap_throw ( ) )
479
+ . unwrap_throw ( )
480
+ . entries ( )
481
+ . into_iter ( )
482
+ . map ( |e| {
483
+ let e: Array = e. unwrap_throw ( ) . into ( ) ;
484
+ let e = e
485
+ . into_iter ( )
486
+ . map ( |s| s. as_string ( ) . unwrap_throw ( ) )
487
+ . collect :: < Vec < String > > ( ) ;
488
+ ( e[ 0 ] . clone ( ) , e[ 1 ] . clone ( ) )
489
+ } )
490
+ . collect ( )
491
+ } )
492
+ } )
493
+ }
494
+
495
+ /// Creates a ReadSignal that tracks the url fragment.
496
+ pub fn use_location_hash ( ) -> ReadSignal < String > {
497
+ PATHNAME . with ( |pathname| {
498
+ assert ! (
499
+ pathname. get( ) . is_some( ) ,
500
+ "create_fragment can only be used with a Router"
501
+ ) ;
502
+
503
+ let pathname = pathname. get ( ) . unwrap_throw ( ) ;
504
+
505
+ let on_hashchange = create_signal ( ( ) ) ;
506
+ window ( )
507
+ . add_event_listener_with_callback (
508
+ "hashchange" ,
509
+ Closure :: wrap ( Box :: new ( move || {
510
+ on_hashchange. update ( |_| { } ) ;
511
+ } ) as Box < dyn FnMut ( ) > )
512
+ . into_js_value ( )
513
+ . unchecked_ref ( ) ,
514
+ )
515
+ . unwrap_throw ( ) ;
516
+
517
+ create_memo ( move || {
518
+ on_hashchange. track ( ) ;
519
+ pathname. track ( ) ;
520
+ window ( ) . location ( ) . hash ( ) . unwrap_throw ( )
521
+ } )
522
+ } )
523
+ }
524
+
413
525
fn meta_keys_pressed ( kb_event : & KeyboardEvent ) -> bool {
414
526
kb_event. meta_key ( ) || kb_event. ctrl_key ( ) || kb_event. shift_key ( ) || kb_event. alt_key ( )
415
527
}
0 commit comments