@bquery/bquery 1.6.0 → 1.8.1
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/README.md +192 -18
- package/dist/a11y/announce.d.ts +43 -0
- package/dist/a11y/announce.d.ts.map +1 -0
- package/dist/a11y/audit.d.ts +42 -0
- package/dist/a11y/audit.d.ts.map +1 -0
- package/dist/a11y/index.d.ts +53 -0
- package/dist/a11y/index.d.ts.map +1 -0
- package/dist/a11y/media-preferences.d.ts +77 -0
- package/dist/a11y/media-preferences.d.ts.map +1 -0
- package/dist/a11y/roving-tab-index.d.ts +38 -0
- package/dist/a11y/roving-tab-index.d.ts.map +1 -0
- package/dist/a11y/skip-link.d.ts +37 -0
- package/dist/a11y/skip-link.d.ts.map +1 -0
- package/dist/a11y/trap-focus.d.ts +49 -0
- package/dist/a11y/trap-focus.d.ts.map +1 -0
- package/dist/a11y/types.d.ts +152 -0
- package/dist/a11y/types.d.ts.map +1 -0
- package/dist/a11y-DVBCy09c.js +421 -0
- package/dist/a11y-DVBCy09c.js.map +1 -0
- package/dist/a11y.es.mjs +14 -0
- package/dist/component/component.d.ts.map +1 -1
- package/dist/component/html.d.ts.map +1 -1
- package/dist/component/index.d.ts +2 -1
- package/dist/component/index.d.ts.map +1 -1
- package/dist/component/library.d.ts.map +1 -1
- package/dist/component/scope.d.ts +138 -0
- package/dist/component/scope.d.ts.map +1 -0
- package/dist/component/types.d.ts +53 -1
- package/dist/component/types.d.ts.map +1 -1
- package/dist/component-L3-JfOFz.js +684 -0
- package/dist/component-L3-JfOFz.js.map +1 -0
- package/dist/component.es.mjs +9 -6
- package/dist/{config-DRmZZno3.js → config-DhT9auRm.js} +4 -4
- package/dist/{config-DRmZZno3.js.map → config-DhT9auRm.js.map} +1 -1
- package/dist/constraints-D5RHQLmP.js +100 -0
- package/dist/constraints-D5RHQLmP.js.map +1 -0
- package/dist/core/collection.d.ts +134 -0
- package/dist/core/collection.d.ts.map +1 -1
- package/dist/core/element.d.ts +120 -0
- package/dist/core/element.d.ts.map +1 -1
- package/dist/core/env.d.ts +18 -0
- package/dist/core/env.d.ts.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/shared.d.ts +14 -0
- package/dist/core/shared.d.ts.map +1 -1
- package/dist/core/utils/index.d.ts +52 -41
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core-DdtZHzsS.js +168 -0
- package/dist/core-DdtZHzsS.js.map +1 -0
- package/dist/{core-CCEabVHl.js → core-EMYSLzaT.js} +293 -194
- package/dist/core-EMYSLzaT.js.map +1 -0
- package/dist/core.es.mjs +48 -46
- package/dist/custom-directives-Dr4C5lVV.js +9 -0
- package/dist/custom-directives-Dr4C5lVV.js.map +1 -0
- package/dist/devtools/devtools.d.ts +212 -0
- package/dist/devtools/devtools.d.ts.map +1 -0
- package/dist/devtools/index.d.ts +20 -0
- package/dist/devtools/index.d.ts.map +1 -0
- package/dist/devtools/types.d.ts +69 -0
- package/dist/devtools/types.d.ts.map +1 -0
- package/dist/devtools-BhB2iDPT.js +122 -0
- package/dist/devtools-BhB2iDPT.js.map +1 -0
- package/dist/devtools.es.mjs +19 -0
- package/dist/dnd/draggable.d.ts +51 -0
- package/dist/dnd/draggable.d.ts.map +1 -0
- package/dist/dnd/droppable.d.ts +38 -0
- package/dist/dnd/droppable.d.ts.map +1 -0
- package/dist/dnd/index.d.ts +47 -0
- package/dist/dnd/index.d.ts.map +1 -0
- package/dist/dnd/sortable.d.ts +43 -0
- package/dist/dnd/sortable.d.ts.map +1 -0
- package/dist/dnd/types.d.ts +250 -0
- package/dist/dnd/types.d.ts.map +1 -0
- package/dist/dnd-NwZBYh4l.js +244 -0
- package/dist/dnd-NwZBYh4l.js.map +1 -0
- package/dist/dnd.es.mjs +6 -0
- package/dist/env-CTdvLaH2.js +19 -0
- package/dist/env-CTdvLaH2.js.map +1 -0
- package/dist/forms/create-form.d.ts +49 -0
- package/dist/forms/create-form.d.ts.map +1 -0
- package/dist/forms/index.d.ts +40 -0
- package/dist/forms/index.d.ts.map +1 -0
- package/dist/forms/types.d.ts +185 -0
- package/dist/forms/types.d.ts.map +1 -0
- package/dist/forms/use-field.d.ts +34 -0
- package/dist/forms/use-field.d.ts.map +1 -0
- package/dist/forms/validators.d.ts +204 -0
- package/dist/forms/validators.d.ts.map +1 -0
- package/dist/forms-UcRHsYxC.js +227 -0
- package/dist/forms-UcRHsYxC.js.map +1 -0
- package/dist/forms.es.mjs +16 -0
- package/dist/full.d.ts +30 -11
- package/dist/full.d.ts.map +1 -1
- package/dist/full.es.mjs +209 -93
- package/dist/full.iife.js +47 -31
- package/dist/full.iife.js.map +1 -1
- package/dist/full.umd.js +47 -31
- package/dist/full.umd.js.map +1 -1
- package/dist/function-Cybd57JV.js +33 -0
- package/dist/function-Cybd57JV.js.map +1 -0
- package/dist/i18n/formatting.d.ts +40 -0
- package/dist/i18n/formatting.d.ts.map +1 -0
- package/dist/i18n/i18n.d.ts +48 -0
- package/dist/i18n/i18n.d.ts.map +1 -0
- package/dist/i18n/index.d.ts +57 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/translate.d.ts +83 -0
- package/dist/i18n/translate.d.ts.map +1 -0
- package/dist/i18n/types.d.ts +156 -0
- package/dist/i18n/types.d.ts.map +1 -0
- package/dist/i18n-kuF6Ekj6.js +89 -0
- package/dist/i18n-kuF6Ekj6.js.map +1 -0
- package/dist/i18n.es.mjs +6 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.es.mjs +257 -143
- package/dist/media/battery.d.ts +35 -0
- package/dist/media/battery.d.ts.map +1 -0
- package/dist/media/breakpoints.d.ts +51 -0
- package/dist/media/breakpoints.d.ts.map +1 -0
- package/dist/media/clipboard.d.ts +30 -0
- package/dist/media/clipboard.d.ts.map +1 -0
- package/dist/media/device-sensors.d.ts +54 -0
- package/dist/media/device-sensors.d.ts.map +1 -0
- package/dist/media/geolocation.d.ts +38 -0
- package/dist/media/geolocation.d.ts.map +1 -0
- package/dist/media/index.d.ts +42 -0
- package/dist/media/index.d.ts.map +1 -0
- package/dist/media/media-query.d.ts +36 -0
- package/dist/media/media-query.d.ts.map +1 -0
- package/dist/media/network.d.ts +35 -0
- package/dist/media/network.d.ts.map +1 -0
- package/dist/media/types.d.ts +173 -0
- package/dist/media/types.d.ts.map +1 -0
- package/dist/media/viewport.d.ts +32 -0
- package/dist/media/viewport.d.ts.map +1 -0
- package/dist/media-i-fB5WxI.js +340 -0
- package/dist/media-i-fB5WxI.js.map +1 -0
- package/dist/media.es.mjs +12 -0
- package/dist/motion/index.d.ts +7 -3
- package/dist/motion/index.d.ts.map +1 -1
- package/dist/motion/morph.d.ts +27 -0
- package/dist/motion/morph.d.ts.map +1 -0
- package/dist/motion/parallax.d.ts +30 -0
- package/dist/motion/parallax.d.ts.map +1 -0
- package/dist/motion/reduced-motion.d.ts +36 -3
- package/dist/motion/reduced-motion.d.ts.map +1 -1
- package/dist/motion/types.d.ts +58 -0
- package/dist/motion/types.d.ts.map +1 -1
- package/dist/motion/typewriter.d.ts +31 -0
- package/dist/motion/typewriter.d.ts.map +1 -0
- package/dist/motion-BJsAuULb.js +530 -0
- package/dist/motion-BJsAuULb.js.map +1 -0
- package/dist/motion.es.mjs +27 -23
- package/dist/{view-C70lA3vf.js → mount-B4Y8bk8Z.js} +166 -160
- package/dist/mount-B4Y8bk8Z.js.map +1 -0
- package/dist/{object-qGpWr6-J.js → object-BCk-1c8T.js} +5 -4
- package/dist/{object-qGpWr6-J.js.map → object-BCk-1c8T.js.map} +1 -1
- package/dist/{platform-Dr9b6fsq.js → platform-Dw2gE3zI.js} +21 -22
- package/dist/{platform-Dr9b6fsq.js.map → platform-Dw2gE3zI.js.map} +1 -1
- package/dist/platform.es.mjs +2 -2
- package/dist/plugin/index.d.ts +22 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/registry.d.ts +108 -0
- package/dist/plugin/registry.d.ts.map +1 -0
- package/dist/plugin/types.d.ts +110 -0
- package/dist/plugin/types.d.ts.map +1 -0
- package/dist/plugin-C2WuC8SF.js +66 -0
- package/dist/plugin-C2WuC8SF.js.map +1 -0
- package/dist/plugin.es.mjs +9 -0
- package/dist/reactive/async-data.d.ts +28 -3
- package/dist/reactive/async-data.d.ts.map +1 -1
- package/dist/reactive/computed.d.ts +10 -0
- package/dist/reactive/computed.d.ts.map +1 -1
- package/dist/reactive/effect.d.ts +3 -0
- package/dist/reactive/effect.d.ts.map +1 -1
- package/dist/reactive/http.d.ts +194 -0
- package/dist/reactive/http.d.ts.map +1 -0
- package/dist/reactive/index.d.ts +2 -2
- package/dist/reactive/index.d.ts.map +1 -1
- package/dist/reactive/pagination.d.ts +126 -0
- package/dist/reactive/pagination.d.ts.map +1 -0
- package/dist/reactive/polling.d.ts +55 -0
- package/dist/reactive/polling.d.ts.map +1 -0
- package/dist/reactive/readonly.d.ts +20 -1
- package/dist/reactive/readonly.d.ts.map +1 -1
- package/dist/reactive/rest.d.ts +293 -0
- package/dist/reactive/rest.d.ts.map +1 -0
- package/dist/reactive/scope.d.ts +140 -0
- package/dist/reactive/scope.d.ts.map +1 -0
- package/dist/reactive/signal.d.ts +16 -2
- package/dist/reactive/signal.d.ts.map +1 -1
- package/dist/reactive/to-value.d.ts +57 -0
- package/dist/reactive/to-value.d.ts.map +1 -0
- package/dist/reactive/websocket.d.ts +285 -0
- package/dist/reactive/websocket.d.ts.map +1 -0
- package/dist/reactive-DwkhUJfP.js +1148 -0
- package/dist/reactive-DwkhUJfP.js.map +1 -0
- package/dist/reactive.es.mjs +38 -20
- package/dist/registry-B08iilIh.js +26 -0
- package/dist/registry-B08iilIh.js.map +1 -0
- package/dist/router/bq-link.d.ts +112 -0
- package/dist/router/bq-link.d.ts.map +1 -0
- package/dist/router/constraints.d.ts +9 -0
- package/dist/router/constraints.d.ts.map +1 -0
- package/dist/router/index.d.ts +15 -7
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/match.d.ts +0 -1
- package/dist/router/match.d.ts.map +1 -1
- package/dist/router/path-pattern.d.ts +14 -0
- package/dist/router/path-pattern.d.ts.map +1 -0
- package/dist/router/query.d.ts.map +1 -1
- package/dist/router/router.d.ts +3 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/state.d.ts +25 -2
- package/dist/router/state.d.ts.map +1 -1
- package/dist/router/types.d.ts +48 -4
- package/dist/router/types.d.ts.map +1 -1
- package/dist/router/use-route.d.ts +50 -0
- package/dist/router/use-route.d.ts.map +1 -0
- package/dist/router/utils.d.ts +3 -0
- package/dist/router/utils.d.ts.map +1 -1
- package/dist/router-CQikC9Ed.js +492 -0
- package/dist/router-CQikC9Ed.js.map +1 -0
- package/dist/router.es.mjs +14 -10
- package/dist/{sanitize-Bs2dkMby.js → sanitize-B1V4JswB.js} +2 -1
- package/dist/{sanitize-Bs2dkMby.js.map → sanitize-B1V4JswB.js.map} +1 -1
- package/dist/security/index.d.ts +2 -2
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security.es.mjs +1 -1
- package/dist/ssr/hydrate.d.ts +65 -0
- package/dist/ssr/hydrate.d.ts.map +1 -0
- package/dist/ssr/index.d.ts +59 -0
- package/dist/ssr/index.d.ts.map +1 -0
- package/dist/ssr/render.d.ts +62 -0
- package/dist/ssr/render.d.ts.map +1 -0
- package/dist/ssr/serialize.d.ts +118 -0
- package/dist/ssr/serialize.d.ts.map +1 -0
- package/dist/ssr/types.d.ts +70 -0
- package/dist/ssr/types.d.ts.map +1 -0
- package/dist/ssr-_dAcGdzu.js +248 -0
- package/dist/ssr-_dAcGdzu.js.map +1 -0
- package/dist/ssr.es.mjs +9 -0
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/index.d.ts +1 -1
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/persisted.d.ts +38 -4
- package/dist/store/persisted.d.ts.map +1 -1
- package/dist/store/types.d.ts +138 -1
- package/dist/store/types.d.ts.map +1 -1
- package/dist/store/utils.d.ts +2 -2
- package/dist/store/utils.d.ts.map +1 -1
- package/dist/store-Cb3gPRve.js +338 -0
- package/dist/store-Cb3gPRve.js.map +1 -0
- package/dist/store.es.mjs +11 -10
- package/dist/storybook/index.d.ts.map +1 -1
- package/dist/storybook.es.mjs +1 -1
- package/dist/storybook.es.mjs.map +1 -1
- package/dist/testing/index.d.ts +23 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/testing.d.ts +156 -0
- package/dist/testing/testing.d.ts.map +1 -0
- package/dist/testing/types.d.ts +134 -0
- package/dist/testing/types.d.ts.map +1 -0
- package/dist/testing-C5Sjfsna.js +224 -0
- package/dist/testing-C5Sjfsna.js.map +1 -0
- package/dist/testing.es.mjs +9 -0
- package/dist/type-guards-BMX2c0LP.js +44 -0
- package/dist/type-guards-BMX2c0LP.js.map +1 -0
- package/dist/untrack-D0fnO5k2.js +36 -0
- package/dist/untrack-D0fnO5k2.js.map +1 -0
- package/dist/view/custom-directives.d.ts +20 -0
- package/dist/view/custom-directives.d.ts.map +1 -0
- package/dist/view/evaluate.d.ts.map +1 -1
- package/dist/view/process.d.ts.map +1 -1
- package/dist/view.es.mjs +9 -9
- package/package.json +47 -11
- package/src/a11y/announce.ts +131 -0
- package/src/a11y/audit.ts +314 -0
- package/src/a11y/index.ts +68 -0
- package/src/a11y/media-preferences.ts +255 -0
- package/src/a11y/roving-tab-index.ts +164 -0
- package/src/a11y/skip-link.ts +255 -0
- package/src/a11y/trap-focus.ts +184 -0
- package/src/a11y/types.ts +183 -0
- package/src/component/component.ts +599 -524
- package/src/component/html.ts +153 -153
- package/src/component/index.ts +52 -50
- package/src/component/library.ts +540 -518
- package/src/component/scope.ts +212 -0
- package/src/component/types.ts +310 -256
- package/src/core/collection.ts +249 -1
- package/src/core/element.ts +252 -11
- package/src/core/env.ts +60 -0
- package/src/core/index.ts +1 -0
- package/src/core/shared.ts +64 -0
- package/src/core/utils/index.ts +66 -1
- package/src/devtools/devtools.ts +410 -0
- package/src/devtools/index.ts +48 -0
- package/src/devtools/types.ts +104 -0
- package/src/dnd/draggable.ts +296 -0
- package/src/dnd/droppable.ts +228 -0
- package/src/dnd/index.ts +62 -0
- package/src/dnd/sortable.ts +307 -0
- package/src/dnd/types.ts +293 -0
- package/src/forms/create-form.ts +320 -0
- package/src/forms/index.ts +70 -0
- package/src/forms/types.ts +203 -0
- package/src/forms/use-field.ts +231 -0
- package/src/forms/validators.ts +294 -0
- package/src/full.ts +554 -229
- package/src/i18n/formatting.ts +67 -0
- package/src/i18n/i18n.ts +200 -0
- package/src/i18n/index.ts +67 -0
- package/src/i18n/translate.ts +182 -0
- package/src/i18n/types.ts +171 -0
- package/src/index.ts +72 -0
- package/src/media/battery.ts +116 -0
- package/src/media/breakpoints.ts +129 -0
- package/src/media/clipboard.ts +80 -0
- package/src/media/device-sensors.ts +158 -0
- package/src/media/geolocation.ts +119 -0
- package/src/media/index.ts +76 -0
- package/src/media/media-query.ts +92 -0
- package/src/media/network.ts +115 -0
- package/src/media/types.ts +177 -0
- package/src/media/viewport.ts +84 -0
- package/src/motion/index.ts +11 -2
- package/src/motion/morph.ts +151 -0
- package/src/motion/parallax.ts +120 -0
- package/src/motion/reduced-motion.ts +52 -3
- package/src/motion/types.ts +63 -0
- package/src/motion/typewriter.ts +164 -0
- package/src/plugin/index.ts +37 -0
- package/src/plugin/registry.ts +284 -0
- package/src/plugin/types.ts +137 -0
- package/src/reactive/async-data.ts +250 -29
- package/src/reactive/computed.ts +53 -1
- package/src/reactive/effect.ts +29 -6
- package/src/reactive/http.ts +790 -0
- package/src/reactive/index.ts +60 -0
- package/src/reactive/pagination.ts +317 -0
- package/src/reactive/polling.ts +179 -0
- package/src/reactive/readonly.ts +52 -8
- package/src/reactive/rest.ts +859 -0
- package/src/reactive/scope.ts +276 -0
- package/src/reactive/signal.ts +61 -1
- package/src/reactive/to-value.ts +71 -0
- package/src/reactive/websocket.ts +849 -0
- package/src/router/bq-link.ts +279 -0
- package/src/router/constraints.ts +204 -0
- package/src/router/index.ts +15 -7
- package/src/router/match.ts +255 -49
- package/src/router/path-pattern.ts +52 -0
- package/src/router/query.ts +3 -0
- package/src/router/router.ts +258 -48
- package/src/router/state.ts +51 -3
- package/src/router/types.ts +50 -4
- package/src/router/use-route.ts +68 -0
- package/src/router/utils.ts +44 -3
- package/src/security/index.ts +12 -17
- package/src/security/sanitize.ts +70 -70
- package/src/security/trusted-html.ts +71 -71
- package/src/ssr/hydrate.ts +84 -0
- package/src/ssr/index.ts +70 -0
- package/src/ssr/render.ts +508 -0
- package/src/ssr/serialize.ts +296 -0
- package/src/ssr/types.ts +81 -0
- package/src/store/create-store.ts +146 -8
- package/src/store/define-store.ts +49 -49
- package/src/store/index.ts +5 -0
- package/src/store/mapping.ts +74 -74
- package/src/store/persisted.ts +245 -62
- package/src/store/types.ts +247 -92
- package/src/store/utils.ts +4 -10
- package/src/store/watch.ts +53 -53
- package/src/storybook/index.ts +480 -479
- package/src/testing/index.ts +42 -0
- package/src/testing/testing.ts +593 -0
- package/src/testing/types.ts +170 -0
- package/src/view/custom-directives.ts +28 -0
- package/src/view/evaluate.ts +2 -0
- package/src/view/process.ts +19 -3
- package/dist/component-BEQgt5hl.js +0 -600
- package/dist/component-BEQgt5hl.js.map +0 -1
- package/dist/core-BGQJVw0-.js +0 -35
- package/dist/core-BGQJVw0-.js.map +0 -1
- package/dist/core-CCEabVHl.js.map +0 -1
- package/dist/effect-AFRW_Plg.js +0 -84
- package/dist/effect-AFRW_Plg.js.map +0 -1
- package/dist/motion-D9TcHxOF.js +0 -415
- package/dist/motion-D9TcHxOF.js.map +0 -1
- package/dist/reactive-DSkct0dO.js +0 -254
- package/dist/reactive-DSkct0dO.js.map +0 -1
- package/dist/router-CbDhl8rS.js +0 -188
- package/dist/router-CbDhl8rS.js.map +0 -1
- package/dist/store-BwDvI45q.js +0 -263
- package/dist/store-BwDvI45q.js.map +0 -1
- package/dist/untrack-B0rVscTc.js +0 -7
- package/dist/untrack-B0rVscTc.js.map +0 -1
- package/dist/view-C70lA3vf.js.map +0 -1
package/src/router/router.ts
CHANGED
|
@@ -3,8 +3,17 @@
|
|
|
3
3
|
* @module bquery/router
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { isPrototypePollutionKey } from '../core/utils/object';
|
|
6
7
|
import { createRoute } from './match';
|
|
7
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
beginNavigation,
|
|
10
|
+
currentRoute,
|
|
11
|
+
endNavigation,
|
|
12
|
+
getActiveRouter,
|
|
13
|
+
resetNavigationState,
|
|
14
|
+
routeSignal,
|
|
15
|
+
setActiveRouter,
|
|
16
|
+
} from './state';
|
|
8
17
|
import type { NavigationGuard, Route, Router, RouterOptions } from './types';
|
|
9
18
|
import { flattenRoutes } from './utils';
|
|
10
19
|
|
|
@@ -12,6 +21,19 @@ import { flattenRoutes } from './utils';
|
|
|
12
21
|
// Router Creation
|
|
13
22
|
// ============================================================================
|
|
14
23
|
|
|
24
|
+
const MAX_SCROLL_POSITION_ENTRIES = 100;
|
|
25
|
+
|
|
26
|
+
const sanitizeHistoryState = (state: Record<string, unknown>): Record<string, unknown> => {
|
|
27
|
+
const sanitized: Record<string, unknown> = {};
|
|
28
|
+
|
|
29
|
+
for (const [key, value] of Object.entries(state)) {
|
|
30
|
+
if (isPrototypePollutionKey(key)) continue;
|
|
31
|
+
sanitized[key] = value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return sanitized;
|
|
35
|
+
};
|
|
36
|
+
|
|
15
37
|
/**
|
|
16
38
|
* Creates and initializes a router instance.
|
|
17
39
|
*
|
|
@@ -26,10 +48,12 @@ import { flattenRoutes } from './utils';
|
|
|
26
48
|
* routes: [
|
|
27
49
|
* { path: '/', component: () => import('./pages/Home') },
|
|
28
50
|
* { path: '/about', component: () => import('./pages/About') },
|
|
29
|
-
* { path: '/user/:id', component: () => import('./pages/User') },
|
|
51
|
+
* { path: '/user/:id(\\d+)', component: () => import('./pages/User') },
|
|
52
|
+
* { path: '/old-page', redirectTo: '/new-page' },
|
|
30
53
|
* { path: '*', component: () => import('./pages/NotFound') },
|
|
31
54
|
* ],
|
|
32
55
|
* base: '/app',
|
|
56
|
+
* scrollRestoration: true,
|
|
33
57
|
* });
|
|
34
58
|
*
|
|
35
59
|
* router.beforeEach((to, from) => {
|
|
@@ -46,7 +70,7 @@ export const createRouter = (options: RouterOptions): Router => {
|
|
|
46
70
|
existingRouter.destroy();
|
|
47
71
|
}
|
|
48
72
|
|
|
49
|
-
const { routes, base = '', hash: useHash = false } = options;
|
|
73
|
+
const { routes, base = '', hash: useHash = false, scrollRestoration = false } = options;
|
|
50
74
|
|
|
51
75
|
// Instance-specific guards and hooks (not shared globally)
|
|
52
76
|
const beforeGuards: NavigationGuard[] = [];
|
|
@@ -55,6 +79,98 @@ export const createRouter = (options: RouterOptions): Router => {
|
|
|
55
79
|
// Flatten nested routes (base-relative, not including the base path)
|
|
56
80
|
const flatRoutes = flattenRoutes(routes);
|
|
57
81
|
|
|
82
|
+
// Scroll position storage keyed by history state id
|
|
83
|
+
const scrollPositions = new Map<string, { x: number; y: number }>();
|
|
84
|
+
let currentScrollKey = '0';
|
|
85
|
+
let scrollKeyCounter = 0;
|
|
86
|
+
let previousScrollRestoration: History['scrollRestoration'] | null = null;
|
|
87
|
+
|
|
88
|
+
// Enable manual scroll restoration if scrollRestoration is configured
|
|
89
|
+
if (scrollRestoration && typeof history !== 'undefined' && 'scrollRestoration' in history) {
|
|
90
|
+
previousScrollRestoration = history.scrollRestoration;
|
|
91
|
+
if (history.scrollRestoration !== 'manual') {
|
|
92
|
+
history.scrollRestoration = 'manual';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const state =
|
|
96
|
+
history.state && typeof history.state === 'object'
|
|
97
|
+
? (history.state as Record<string, unknown>)
|
|
98
|
+
: {};
|
|
99
|
+
|
|
100
|
+
if (typeof state.__bqScrollKey !== 'string') {
|
|
101
|
+
const currentUrl = useHash
|
|
102
|
+
? window.location.hash || '#/'
|
|
103
|
+
: `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
104
|
+
history.replaceState({ ...state, __bqScrollKey: currentScrollKey }, '', currentUrl);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Generates a unique key for the current history entry.
|
|
110
|
+
* @internal
|
|
111
|
+
*/
|
|
112
|
+
const getScrollKey = (): string => {
|
|
113
|
+
return (history.state && history.state.__bqScrollKey) || currentScrollKey;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Generates a unique key for a new history entry.
|
|
118
|
+
* @internal
|
|
119
|
+
*/
|
|
120
|
+
const createScrollKey = (): string => `${Date.now()}-${scrollKeyCounter++}`;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Saves current scroll position for the current history entry.
|
|
124
|
+
* @internal
|
|
125
|
+
*/
|
|
126
|
+
const saveScrollPosition = (key = getScrollKey()): void => {
|
|
127
|
+
if (!scrollRestoration) return;
|
|
128
|
+
if (scrollPositions.has(key)) {
|
|
129
|
+
// Refresh the insertion order so pruning behaves like an LRU cache.
|
|
130
|
+
scrollPositions.delete(key);
|
|
131
|
+
}
|
|
132
|
+
scrollPositions.set(key, { x: window.scrollX, y: window.scrollY });
|
|
133
|
+
while (scrollPositions.size > MAX_SCROLL_POSITION_ENTRIES) {
|
|
134
|
+
const oldestKey = scrollPositions.keys().next().value as string | undefined;
|
|
135
|
+
if (oldestKey === undefined) {
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
scrollPositions.delete(oldestKey);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Restores scroll position for the current history entry.
|
|
144
|
+
* @internal
|
|
145
|
+
*/
|
|
146
|
+
const restoreScrollPosition = (key = getScrollKey()): void => {
|
|
147
|
+
if (!scrollRestoration) return;
|
|
148
|
+
const pos = scrollPositions.get(key);
|
|
149
|
+
if (pos) {
|
|
150
|
+
window.scrollTo(pos.x, pos.y);
|
|
151
|
+
} else {
|
|
152
|
+
window.scrollTo(0, 0);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Builds history state for canceled navigations without dropping
|
|
158
|
+
* the scroll restoration key for the current entry.
|
|
159
|
+
* @internal
|
|
160
|
+
*/
|
|
161
|
+
const getRestoreHistoryState = (): Record<string, unknown> => {
|
|
162
|
+
const state =
|
|
163
|
+
history.state && typeof history.state === 'object'
|
|
164
|
+
? { ...(history.state as Record<string, unknown>) }
|
|
165
|
+
: {};
|
|
166
|
+
|
|
167
|
+
if (scrollRestoration) {
|
|
168
|
+
state.__bqScrollKey = currentScrollKey;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return state;
|
|
172
|
+
};
|
|
173
|
+
|
|
58
174
|
/**
|
|
59
175
|
* Gets the current path from the URL.
|
|
60
176
|
*/
|
|
@@ -99,68 +215,152 @@ export const createRouter = (options: RouterOptions): Router => {
|
|
|
99
215
|
*/
|
|
100
216
|
const performNavigation = async (
|
|
101
217
|
path: string,
|
|
102
|
-
method: 'pushState' | 'replaceState'
|
|
218
|
+
method: 'pushState' | 'replaceState',
|
|
219
|
+
visitedPaths: Set<string> = new Set()
|
|
103
220
|
): Promise<void> => {
|
|
104
|
-
|
|
105
|
-
|
|
221
|
+
beginNavigation();
|
|
222
|
+
try {
|
|
223
|
+
const { pathname, search, hash } = getCurrentPath();
|
|
224
|
+
const from = createRoute(pathname, search, hash, flatRoutes);
|
|
106
225
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
226
|
+
// Parse the target path
|
|
227
|
+
const url = new URL(path, window.location.origin);
|
|
228
|
+
const resolvedPath = `${url.pathname}${url.search}${url.hash}`;
|
|
229
|
+
if (visitedPaths.has(resolvedPath)) {
|
|
230
|
+
throw new Error(`bQuery router: redirect loop detected for path "${resolvedPath}"`);
|
|
231
|
+
}
|
|
232
|
+
visitedPaths.add(resolvedPath);
|
|
233
|
+
const to = createRoute(url.pathname, url.search, url.hash, flatRoutes);
|
|
110
234
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return;
|
|
235
|
+
// Check for redirectTo on the matched route
|
|
236
|
+
if (to.matched?.redirectTo) {
|
|
237
|
+
// Navigate to the redirect target instead
|
|
238
|
+
await performNavigation(to.matched.redirectTo, method, visitedPaths);
|
|
239
|
+
return;
|
|
116
240
|
}
|
|
117
|
-
}
|
|
118
241
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
242
|
+
// Run route-level beforeEnter guard
|
|
243
|
+
if (to.matched?.beforeEnter) {
|
|
244
|
+
const result = await to.matched.beforeEnter(to, from);
|
|
245
|
+
if (result === false) {
|
|
246
|
+
return; // Cancel navigation
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Run beforeEach guards
|
|
251
|
+
for (const guard of beforeGuards) {
|
|
252
|
+
const result = await guard(to, from);
|
|
253
|
+
if (result === false) {
|
|
254
|
+
return; // Cancel navigation
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Save scroll position before navigation
|
|
259
|
+
saveScrollPosition();
|
|
260
|
+
|
|
261
|
+
// Update browser history
|
|
262
|
+
const existingScrollKey = scrollRestoration ? getScrollKey() : undefined;
|
|
263
|
+
const scrollKey =
|
|
264
|
+
method === 'replaceState' && existingScrollKey ? existingScrollKey : createScrollKey();
|
|
265
|
+
const fullPath = useHash ? `#${path}` : `${base}${path}`;
|
|
266
|
+
const baseState =
|
|
267
|
+
scrollRestoration && history.state && typeof history.state === 'object'
|
|
268
|
+
? sanitizeHistoryState(history.state as Record<string, unknown>)
|
|
269
|
+
: {};
|
|
270
|
+
const state = scrollRestoration ? { ...baseState, __bqScrollKey: scrollKey } : {};
|
|
271
|
+
history[method](state, '', fullPath);
|
|
272
|
+
currentScrollKey = scrollKey;
|
|
122
273
|
|
|
123
|
-
|
|
124
|
-
|
|
274
|
+
// Update route signal
|
|
275
|
+
syncRoute();
|
|
125
276
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
277
|
+
// Scroll to top on push navigation
|
|
278
|
+
if (scrollRestoration && method === 'pushState') {
|
|
279
|
+
window.scrollTo(0, 0);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Run afterEach hooks
|
|
283
|
+
for (const hook of afterHooks) {
|
|
284
|
+
hook(routeSignal.value, from);
|
|
285
|
+
}
|
|
286
|
+
} finally {
|
|
287
|
+
endNavigation();
|
|
129
288
|
}
|
|
130
289
|
};
|
|
131
290
|
|
|
132
291
|
/**
|
|
133
292
|
* Handle popstate events (back/forward).
|
|
134
293
|
*/
|
|
135
|
-
const handlePopState = async (): Promise<void> => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
const queryString = new URLSearchParams(
|
|
146
|
-
Object.entries(from.query).flatMap(([key, value]) =>
|
|
147
|
-
Array.isArray(value) ? value.map((v) => [key, v]) : [[key, value]]
|
|
148
|
-
)
|
|
149
|
-
).toString();
|
|
150
|
-
const search = queryString ? `?${queryString}` : '';
|
|
151
|
-
const hash = from.hash ? `#${from.hash}` : '';
|
|
152
|
-
const restorePath = useHash
|
|
153
|
-
? `#${from.path}${search}${hash}`
|
|
154
|
-
: `${base}${from.path}${search}${hash}`;
|
|
155
|
-
history.replaceState({}, '', restorePath);
|
|
294
|
+
const handlePopState = async (event: PopStateEvent): Promise<void> => {
|
|
295
|
+
beginNavigation();
|
|
296
|
+
try {
|
|
297
|
+
const { pathname, search, hash } = getCurrentPath();
|
|
298
|
+
const from = routeSignal.value;
|
|
299
|
+
const to = createRoute(pathname, search, hash, flatRoutes);
|
|
300
|
+
|
|
301
|
+
// Check for redirectTo on the matched route
|
|
302
|
+
if (to.matched?.redirectTo) {
|
|
303
|
+
await performNavigation(to.matched.redirectTo, 'replaceState');
|
|
156
304
|
return;
|
|
157
305
|
}
|
|
158
|
-
}
|
|
159
306
|
|
|
160
|
-
|
|
307
|
+
// Run route-level beforeEnter guard
|
|
308
|
+
if (to.matched?.beforeEnter) {
|
|
309
|
+
const result = await to.matched.beforeEnter(to, from);
|
|
310
|
+
if (result === false) {
|
|
311
|
+
// Restore previous state with full URL (including query/hash)
|
|
312
|
+
const queryString = new URLSearchParams(
|
|
313
|
+
Object.entries(from.query).flatMap(([key, value]) =>
|
|
314
|
+
Array.isArray(value) ? value.map((v) => [key, v]) : [[key, value]]
|
|
315
|
+
)
|
|
316
|
+
).toString();
|
|
317
|
+
const searchStr = queryString ? `?${queryString}` : '';
|
|
318
|
+
const hashStr = from.hash ? `#${from.hash}` : '';
|
|
319
|
+
const restorePath = useHash
|
|
320
|
+
? `#${from.path}${searchStr}${hashStr}`
|
|
321
|
+
: `${base}${from.path}${searchStr}${hashStr}`;
|
|
322
|
+
history.replaceState(getRestoreHistoryState(), '', restorePath);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
161
326
|
|
|
162
|
-
|
|
163
|
-
|
|
327
|
+
// Run beforeEach guards (supports async guards)
|
|
328
|
+
for (const guard of beforeGuards) {
|
|
329
|
+
const result = await guard(to, from);
|
|
330
|
+
if (result === false) {
|
|
331
|
+
// Restore previous state with full URL (including query/hash)
|
|
332
|
+
const queryString = new URLSearchParams(
|
|
333
|
+
Object.entries(from.query).flatMap(([key, value]) =>
|
|
334
|
+
Array.isArray(value) ? value.map((v) => [key, v]) : [[key, value]]
|
|
335
|
+
)
|
|
336
|
+
).toString();
|
|
337
|
+
const search = queryString ? `?${queryString}` : '';
|
|
338
|
+
const hash = from.hash ? `#${from.hash}` : '';
|
|
339
|
+
const restorePath = useHash
|
|
340
|
+
? `#${from.path}${search}${hash}`
|
|
341
|
+
: `${base}${from.path}${search}${hash}`;
|
|
342
|
+
history.replaceState(getRestoreHistoryState(), '', restorePath);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Save scroll position of the page we're leaving
|
|
348
|
+
saveScrollPosition(currentScrollKey);
|
|
349
|
+
|
|
350
|
+
// Update scroll key from history state
|
|
351
|
+
currentScrollKey =
|
|
352
|
+
(event.state as { __bqScrollKey?: string } | null)?.__bqScrollKey ?? getScrollKey();
|
|
353
|
+
|
|
354
|
+
syncRoute();
|
|
355
|
+
|
|
356
|
+
// Restore scroll position for the entry we're navigating to
|
|
357
|
+
restoreScrollPosition(currentScrollKey);
|
|
358
|
+
|
|
359
|
+
for (const hook of afterHooks) {
|
|
360
|
+
hook(routeSignal.value, from);
|
|
361
|
+
}
|
|
362
|
+
} finally {
|
|
363
|
+
endNavigation();
|
|
164
364
|
}
|
|
165
365
|
};
|
|
166
366
|
|
|
@@ -202,6 +402,16 @@ export const createRouter = (options: RouterOptions): Router => {
|
|
|
202
402
|
window.removeEventListener('popstate', handlePopState);
|
|
203
403
|
beforeGuards.length = 0;
|
|
204
404
|
afterHooks.length = 0;
|
|
405
|
+
scrollPositions.clear();
|
|
406
|
+
// Restore the previous scroll restoration mode on destroy
|
|
407
|
+
if (
|
|
408
|
+
previousScrollRestoration !== null &&
|
|
409
|
+
typeof history !== 'undefined' &&
|
|
410
|
+
'scrollRestoration' in history
|
|
411
|
+
) {
|
|
412
|
+
history.scrollRestoration = previousScrollRestoration;
|
|
413
|
+
}
|
|
414
|
+
resetNavigationState();
|
|
205
415
|
setActiveRouter(null);
|
|
206
416
|
},
|
|
207
417
|
};
|
package/src/router/state.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @module bquery/router
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { computed, signal, type ReadonlySignal, type Signal } from '../reactive/index';
|
|
6
|
+
import { computed, readonly, signal, type ReadonlySignal, type Signal } from '../reactive/index';
|
|
7
7
|
import type { Route, Router } from './types';
|
|
8
8
|
|
|
9
9
|
// ============================================================================
|
|
@@ -27,8 +27,8 @@ export const routeSignal: Signal<Route> = signal<Route>({
|
|
|
27
27
|
*
|
|
28
28
|
* @example
|
|
29
29
|
* ```ts
|
|
30
|
-
* import { currentRoute } from 'bquery/router';
|
|
31
|
-
* import { effect } from 'bquery/reactive';
|
|
30
|
+
* import { currentRoute } from '@bquery/bquery/router';
|
|
31
|
+
* import { effect } from '@bquery/bquery/reactive';
|
|
32
32
|
*
|
|
33
33
|
* effect(() => {
|
|
34
34
|
* document.title = `Page: ${currentRoute.value.path}`;
|
|
@@ -37,6 +37,54 @@ export const routeSignal: Signal<Route> = signal<Route>({
|
|
|
37
37
|
*/
|
|
38
38
|
export const currentRoute: ReadonlySignal<Route> = computed(() => routeSignal.value);
|
|
39
39
|
|
|
40
|
+
/** @internal */
|
|
41
|
+
const navigationCountSignal: Signal<number> = signal(0);
|
|
42
|
+
|
|
43
|
+
/** @internal */
|
|
44
|
+
const isNavigatingSignal: Signal<boolean> = signal(false);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Reactive signal indicating whether a navigation is currently in progress.
|
|
48
|
+
*
|
|
49
|
+
* This becomes `true` while async guards or redirect resolution are running,
|
|
50
|
+
* then flips back to `false` once navigation finishes or is canceled.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* import { isNavigating } from '@bquery/bquery/router';
|
|
55
|
+
* import { effect } from '@bquery/bquery/reactive';
|
|
56
|
+
*
|
|
57
|
+
* effect(() => {
|
|
58
|
+
* document.body.toggleAttribute('data-loading-route', isNavigating.value);
|
|
59
|
+
* });
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export const isNavigating: ReadonlySignal<boolean> = readonly(isNavigatingSignal);
|
|
63
|
+
|
|
64
|
+
/** @internal */
|
|
65
|
+
export const beginNavigation = (): void => {
|
|
66
|
+
if (navigationCountSignal.value === 0) {
|
|
67
|
+
isNavigatingSignal.value = true;
|
|
68
|
+
}
|
|
69
|
+
navigationCountSignal.value += 1;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** @internal */
|
|
73
|
+
export const endNavigation = (): void => {
|
|
74
|
+
const nextCount = Math.max(0, navigationCountSignal.value - 1);
|
|
75
|
+
navigationCountSignal.value = nextCount;
|
|
76
|
+
|
|
77
|
+
if (nextCount === 0) {
|
|
78
|
+
isNavigatingSignal.value = false;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** @internal */
|
|
83
|
+
export const resetNavigationState = (): void => {
|
|
84
|
+
navigationCountSignal.value = 0;
|
|
85
|
+
isNavigatingSignal.value = false;
|
|
86
|
+
};
|
|
87
|
+
|
|
40
88
|
/** @internal */
|
|
41
89
|
export const getActiveRouter = (): Router | null => activeRouter;
|
|
42
90
|
|
package/src/router/types.ts
CHANGED
|
@@ -32,19 +32,59 @@ export type Route = {
|
|
|
32
32
|
/**
|
|
33
33
|
* Route definition for configuration.
|
|
34
34
|
*/
|
|
35
|
-
|
|
36
|
-
/**
|
|
35
|
+
type BaseRouteDefinition = {
|
|
36
|
+
/**
|
|
37
|
+
* Path pattern (e.g., '/user/:id', '/posts/*').
|
|
38
|
+
* Supports regex constraints on params: `/user/:id(\\d+)`.
|
|
39
|
+
* Constraint backreferences are not supported.
|
|
40
|
+
*/
|
|
37
41
|
path: string;
|
|
38
|
-
/** Component loader (sync or async) */
|
|
39
|
-
component: () => unknown | Promise<unknown>;
|
|
40
42
|
/** Optional route name for programmatic navigation */
|
|
41
43
|
name?: string;
|
|
42
44
|
/** Optional metadata */
|
|
43
45
|
meta?: Record<string, unknown>;
|
|
44
46
|
/** Nested child routes */
|
|
45
47
|
children?: RouteDefinition[];
|
|
48
|
+
/**
|
|
49
|
+
* Per-route navigation guard. Called before entering this route.
|
|
50
|
+
* Return `false` to cancel navigation.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* {
|
|
55
|
+
* path: '/admin',
|
|
56
|
+
* component: () => import('./Admin'),
|
|
57
|
+
* beforeEnter: (to, from) => isAuthenticated() || false,
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
beforeEnter?: NavigationGuard;
|
|
46
62
|
};
|
|
47
63
|
|
|
64
|
+
type ComponentRouteDefinition = BaseRouteDefinition & {
|
|
65
|
+
/** Component loader (sync or async) */
|
|
66
|
+
component: () => unknown | Promise<unknown>;
|
|
67
|
+
redirectTo?: never;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
type RedirectRouteDefinition = BaseRouteDefinition & {
|
|
71
|
+
/**
|
|
72
|
+
* Redirect target path. When the route is matched, the router
|
|
73
|
+
* automatically navigates to this path instead.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* { path: '/old-page', redirectTo: '/new-page' }
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
redirectTo: string;
|
|
81
|
+
component?: never;
|
|
82
|
+
children?: never;
|
|
83
|
+
beforeEnter?: never;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type RouteDefinition = ComponentRouteDefinition | RedirectRouteDefinition;
|
|
87
|
+
|
|
48
88
|
/**
|
|
49
89
|
* Router configuration options.
|
|
50
90
|
*/
|
|
@@ -55,6 +95,12 @@ export type RouterOptions = {
|
|
|
55
95
|
base?: string;
|
|
56
96
|
/** Use hash-based routing instead of history (default: false) */
|
|
57
97
|
hash?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Restore scroll position on back/forward navigation (default: false).
|
|
100
|
+
* When enabled, the router saves scroll positions for each history entry
|
|
101
|
+
* and restores them on popstate events.
|
|
102
|
+
*/
|
|
103
|
+
scrollRestoration?: boolean;
|
|
58
104
|
};
|
|
59
105
|
|
|
60
106
|
/**
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive route composable.
|
|
3
|
+
* @module bquery/router
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { computed, type ReadonlySignal } from '../reactive/index';
|
|
7
|
+
import { routeSignal } from './state';
|
|
8
|
+
import type { Route, RouteDefinition } from './types';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// useRoute Composable
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Return type for {@link useRoute}.
|
|
16
|
+
* Provides reactive access to individual route properties.
|
|
17
|
+
*/
|
|
18
|
+
export type UseRouteReturn = {
|
|
19
|
+
/** Full reactive route object */
|
|
20
|
+
route: ReadonlySignal<Route>;
|
|
21
|
+
/** Reactive current path */
|
|
22
|
+
path: ReadonlySignal<string>;
|
|
23
|
+
/** Reactive route params */
|
|
24
|
+
params: ReadonlySignal<Record<string, string>>;
|
|
25
|
+
/** Reactive query params */
|
|
26
|
+
query: ReadonlySignal<Record<string, string | string[]>>;
|
|
27
|
+
/** Reactive hash fragment (without #) */
|
|
28
|
+
hash: ReadonlySignal<string>;
|
|
29
|
+
/** Reactive matched route definition */
|
|
30
|
+
matched: ReadonlySignal<RouteDefinition | null>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const route = computed(() => routeSignal.value);
|
|
34
|
+
const path = computed(() => route.value.path);
|
|
35
|
+
const params = computed(() => route.value.params);
|
|
36
|
+
const query = computed(() => route.value.query);
|
|
37
|
+
const hash = computed(() => route.value.hash);
|
|
38
|
+
const matched = computed(() => route.value.matched);
|
|
39
|
+
|
|
40
|
+
const routeHandle: UseRouteReturn = { route, path, params, query, hash, matched };
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Returns reactive access to the current route, params, query, and hash.
|
|
44
|
+
*
|
|
45
|
+
* Each property is a readonly computed signal that updates automatically
|
|
46
|
+
* when the route changes. This is useful for fine-grained reactivity
|
|
47
|
+
* where you only need to subscribe to specific route parts.
|
|
48
|
+
*
|
|
49
|
+
* @returns An object with reactive route properties
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { useRoute } from '@bquery/bquery/router';
|
|
54
|
+
* import { effect } from '@bquery/bquery/reactive';
|
|
55
|
+
*
|
|
56
|
+
* const { path, params, query, hash } = useRoute();
|
|
57
|
+
*
|
|
58
|
+
* effect(() => {
|
|
59
|
+
* console.log('Path:', path.value);
|
|
60
|
+
* console.log('Params:', params.value);
|
|
61
|
+
* console.log('Query:', query.value);
|
|
62
|
+
* console.log('Hash:', hash.value);
|
|
63
|
+
* });
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export const useRoute = (): UseRouteReturn => {
|
|
67
|
+
return routeHandle;
|
|
68
|
+
};
|
package/src/router/utils.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { computed, type ReadonlySignal } from '../reactive/index';
|
|
7
|
+
import { getRouteConstraintRegex } from './constraints';
|
|
8
|
+
import { isParamChar, isParamStart, readConstraint } from './path-pattern';
|
|
7
9
|
import { getActiveRouter, routeSignal } from './state';
|
|
8
10
|
import type { RouteDefinition } from './types';
|
|
9
11
|
|
|
@@ -41,6 +43,9 @@ export const flattenRoutes = (routes: RouteDefinition[], parentPath = ''): Route
|
|
|
41
43
|
* @param name - The route name
|
|
42
44
|
* @param params - Route params to interpolate
|
|
43
45
|
* @returns The resolved path
|
|
46
|
+
* @throws {Error} If no router is initialized, the route name is unknown,
|
|
47
|
+
* a required path param is missing from `params`, a param value does not satisfy
|
|
48
|
+
* its route regex constraint, or a route param constraint has invalid syntax
|
|
44
49
|
*
|
|
45
50
|
* @example
|
|
46
51
|
* ```ts
|
|
@@ -61,9 +66,45 @@ export const resolve = (name: string, params: Record<string, string> = {}): stri
|
|
|
61
66
|
throw new Error(`bQuery router: Route "${name}" not found.`);
|
|
62
67
|
}
|
|
63
68
|
|
|
64
|
-
let path =
|
|
65
|
-
for (
|
|
66
|
-
path
|
|
69
|
+
let path = '';
|
|
70
|
+
for (let i = 0; i < route.path.length; ) {
|
|
71
|
+
if (route.path[i] === ':' && isParamStart(route.path[i + 1])) {
|
|
72
|
+
let nameEnd = i + 2;
|
|
73
|
+
while (nameEnd < route.path.length && isParamChar(route.path[nameEnd])) {
|
|
74
|
+
nameEnd++;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let nextIndex = nameEnd;
|
|
78
|
+
let constraint: string | null = null;
|
|
79
|
+
if (route.path[nameEnd] === '(') {
|
|
80
|
+
const parsedConstraint = readConstraint(route.path, nameEnd);
|
|
81
|
+
if (!parsedConstraint) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`bQuery router: Invalid constraint syntax in path "${route.path}" for route "${name}".`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
constraint = parsedConstraint.constraint;
|
|
87
|
+
nextIndex = parsedConstraint.endIndex;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const key = route.path.slice(i + 1, nameEnd);
|
|
91
|
+
const value = params[key];
|
|
92
|
+
if (value === undefined) {
|
|
93
|
+
throw new Error(`bQuery router: Missing required param "${key}" for route "${name}".`);
|
|
94
|
+
}
|
|
95
|
+
if (constraint && !getRouteConstraintRegex(constraint).test(value)) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`bQuery router: Param "${key}" with value "${value}" does not satisfy the route constraint "${constraint}" for route "${name}".`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
path += encodeURIComponent(value);
|
|
102
|
+
i = nextIndex;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
path += route.path[i];
|
|
107
|
+
i++;
|
|
67
108
|
}
|
|
68
109
|
|
|
69
110
|
return path;
|
package/src/security/index.ts
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Security module providing sanitization, CSP compatibility, and Trusted Types.
|
|
3
|
-
*
|
|
4
|
-
* @module bquery/security
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export { generateNonce, hasCSPDirective } from './csp';
|
|
8
|
-
export {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from './sanitize';
|
|
14
|
-
export { trusted } from './trusted-html';
|
|
15
|
-
export { createTrustedHtml, getTrustedTypesPolicy, isTrustedTypesSupported } from './trusted-types';
|
|
16
|
-
export type { SanitizedHtml, TrustedHtml } from './trusted-html';
|
|
17
|
-
export type { SanitizeOptions } from './sanitize';
|
|
1
|
+
/**
|
|
2
|
+
* Security module providing sanitization, CSP compatibility, and Trusted Types.
|
|
3
|
+
*
|
|
4
|
+
* @module bquery/security
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { generateNonce, hasCSPDirective } from './csp';
|
|
8
|
+
export { escapeHtml, sanitizeHtml as sanitize, sanitizeHtml, stripTags } from './sanitize';
|
|
9
|
+
export { trusted } from './trusted-html';
|
|
10
|
+
export { createTrustedHtml, getTrustedTypesPolicy, isTrustedTypesSupported } from './trusted-types';
|
|
11
|
+
export type { SanitizedHtml, TrustedHtml } from './trusted-html';
|
|
12
|
+
export type { SanitizeOptions } from './types';
|