@saolabs/client 1.0.0 → 1.1.0
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 +52 -195
- package/dist/index.d.ts +20 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -1
- package/dist/plugins.d.ts +2 -2
- package/dist/plugins.js +2 -2
- package/dist/src/core/app/Application.d.ts +25 -15
- package/dist/src/core/app/Application.d.ts.map +1 -1
- package/dist/src/core/app/Application.js +55 -18
- package/dist/src/core/app/Application.js.map +1 -1
- package/dist/src/core/bootstrap/app.d.ts +1 -1
- package/dist/src/core/bootstrap/app.d.ts.map +1 -1
- package/dist/src/core/bootstrap/app.js +77 -4
- package/dist/src/core/bootstrap/app.js.map +1 -1
- package/dist/src/core/bootstrap/bundle.d.ts +41 -0
- package/dist/src/core/bootstrap/bundle.d.ts.map +1 -0
- package/dist/src/core/bootstrap/bundle.js +98 -0
- package/dist/src/core/bootstrap/bundle.js.map +1 -0
- package/dist/src/core/bootstrap/providers/ApiServiceProvider.d.ts +1 -1
- package/dist/src/core/bootstrap/providers/ApiServiceProvider.d.ts.map +1 -1
- package/dist/src/core/bootstrap/providers/ApiServiceProvider.js +3 -3
- package/dist/src/core/bootstrap/providers/ApiServiceProvider.js.map +1 -1
- package/dist/src/core/bootstrap/providers/CoreServiceProvider.d.ts +1 -1
- package/dist/src/core/bootstrap/providers/CoreServiceProvider.d.ts.map +1 -1
- package/dist/src/core/bootstrap/providers/CoreServiceProvider.js +11 -8
- package/dist/src/core/bootstrap/providers/CoreServiceProvider.js.map +1 -1
- package/dist/src/core/bootstrap/providers/HelperServiceProvider.d.ts +3 -1
- package/dist/src/core/bootstrap/providers/HelperServiceProvider.d.ts.map +1 -1
- package/dist/src/core/bootstrap/providers/HelperServiceProvider.js +8 -3
- package/dist/src/core/bootstrap/providers/HelperServiceProvider.js.map +1 -1
- package/dist/src/core/bootstrap/providers/RouteServiceProvider.d.ts +2 -1
- package/dist/src/core/bootstrap/providers/RouteServiceProvider.d.ts.map +1 -1
- package/dist/src/core/bootstrap/providers/RouteServiceProvider.js +14 -4
- package/dist/src/core/bootstrap/providers/RouteServiceProvider.js.map +1 -1
- package/dist/src/core/bootstrap/providers/ServiceProvider.d.ts +4 -4
- package/dist/src/core/bootstrap/providers/ServiceProvider.d.ts.map +1 -1
- package/dist/src/core/bootstrap/providers/ServiceProvider.js +5 -5
- package/dist/src/core/bootstrap/providers/ServiceProvider.js.map +1 -1
- package/dist/src/core/bootstrap/providers/ViewServiceProvider.d.ts +1 -1
- package/dist/src/core/bootstrap/providers/ViewServiceProvider.d.ts.map +1 -1
- package/dist/src/core/bootstrap/providers/ViewServiceProvider.js +3 -3
- package/dist/src/core/bootstrap/providers/ViewServiceProvider.js.map +1 -1
- package/dist/src/core/bootstrap/providers/default-providers.d.ts +1 -1
- package/dist/src/core/bootstrap/providers/default-providers.d.ts.map +1 -1
- package/dist/src/core/bootstrap/providers/default-providers.js +44 -11
- package/dist/src/core/bootstrap/providers/default-providers.js.map +1 -1
- package/dist/src/core/bootstrap/providers/index.d.ts +9 -9
- package/dist/src/core/bootstrap/providers/index.d.ts.map +1 -1
- package/dist/src/core/bootstrap/providers/index.js +8 -8
- package/dist/src/core/bootstrap/providers/index.js.map +1 -1
- package/dist/src/core/bootstrap/providers/provider-order.d.ts +1 -1
- package/dist/src/core/bootstrap/providers/provider-order.d.ts.map +1 -1
- package/dist/src/core/bootstrap/ssr.d.ts +47 -0
- package/dist/src/core/bootstrap/ssr.d.ts.map +1 -0
- package/dist/src/core/bootstrap/ssr.js +129 -0
- package/dist/src/core/bootstrap/ssr.js.map +1 -0
- package/dist/src/core/cache/ViewCache.d.ts +1 -1
- package/dist/src/core/cache/ViewCache.d.ts.map +1 -1
- package/dist/src/core/cache/index.d.ts +1 -1
- package/dist/src/core/cache/index.d.ts.map +1 -1
- package/dist/src/core/cache/index.js +1 -1
- package/dist/src/core/cache/index.js.map +1 -1
- package/dist/src/core/contracts/ApplicationInterface.d.ts +15 -11
- package/dist/src/core/contracts/ApplicationInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/BlockInterface.d.ts +10 -10
- package/dist/src/core/contracts/BlockInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/BundleInterface.d.ts +38 -0
- package/dist/src/core/contracts/BundleInterface.d.ts.map +1 -0
- package/dist/src/core/contracts/BundleInterface.js +2 -0
- package/dist/src/core/contracts/BundleInterface.js.map +1 -0
- package/dist/src/core/contracts/ComponentInterface.d.ts +7 -6
- package/dist/src/core/contracts/ComponentInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/ElementInterface.d.ts +91 -27
- package/dist/src/core/contracts/ElementInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/HeadServiceInterface.d.ts +37 -0
- package/dist/src/core/contracts/HeadServiceInterface.d.ts.map +1 -0
- package/dist/src/core/contracts/HeadServiceInterface.js +3 -0
- package/dist/src/core/contracts/HeadServiceInterface.js.map +1 -0
- package/dist/src/core/contracts/HelperInterface.d.ts +9 -1
- package/dist/src/core/contracts/HelperInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/HttpServiceInterface.d.ts +3 -0
- package/dist/src/core/contracts/HttpServiceInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/MarkerInterface.d.ts +29 -44
- package/dist/src/core/contracts/MarkerInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/ReactiveInterface.d.ts +9 -9
- package/dist/src/core/contracts/ReactiveInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/RouterInterface.d.ts +5 -2
- package/dist/src/core/contracts/RouterInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/SectionInterface.d.ts +48 -0
- package/dist/src/core/contracts/SectionInterface.d.ts.map +1 -0
- package/dist/src/core/contracts/SectionInterface.js +2 -0
- package/dist/src/core/contracts/SectionInterface.js.map +1 -0
- package/dist/src/core/contracts/ServiceProviderInterface.d.ts +3 -3
- package/dist/src/core/contracts/ServiceProviderInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/ViewControllerInterface.d.ts +116 -15
- package/dist/src/core/contracts/ViewControllerInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/ViewInterface.d.ts +31 -5
- package/dist/src/core/contracts/ViewInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/ViewManagerInterface.d.ts +47 -5
- package/dist/src/core/contracts/ViewManagerInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/ViewStateInterface.d.ts +39 -2
- package/dist/src/core/contracts/ViewStateInterface.d.ts.map +1 -1
- package/dist/src/core/contracts/common.d.ts +3 -3
- package/dist/src/core/contracts/common.js +3 -3
- package/dist/src/core/contracts/utils.d.ts +19 -19
- package/dist/src/core/contracts/utils.d.ts.map +1 -1
- package/dist/src/core/contracts/views.d.ts +1 -22
- package/dist/src/core/contracts/views.d.ts.map +1 -1
- package/dist/src/core/devtools/hook.d.ts +66 -0
- package/dist/src/core/devtools/hook.d.ts.map +1 -0
- package/dist/src/core/devtools/hook.js +114 -0
- package/dist/src/core/devtools/hook.js.map +1 -0
- package/dist/src/core/devtools/index.d.ts +34 -0
- package/dist/src/core/devtools/index.d.ts.map +1 -0
- package/dist/src/core/devtools/index.js +33 -0
- package/dist/src/core/devtools/index.js.map +1 -0
- package/dist/src/core/devtools/inspector.d.ts +31 -0
- package/dist/src/core/devtools/inspector.d.ts.map +1 -0
- package/dist/src/core/devtools/inspector.js +249 -0
- package/dist/src/core/devtools/inspector.js.map +1 -0
- package/dist/src/core/elements/Block.d.ts +9 -7
- package/dist/src/core/elements/Block.d.ts.map +1 -1
- package/dist/src/core/elements/Block.js +33 -9
- package/dist/src/core/elements/Block.js.map +1 -1
- package/dist/src/core/elements/BlockOutlet.d.ts +19 -8
- package/dist/src/core/elements/BlockOutlet.d.ts.map +1 -1
- package/dist/src/core/elements/BlockOutlet.js +57 -13
- package/dist/src/core/elements/BlockOutlet.js.map +1 -1
- package/dist/src/core/elements/Component.d.ts +83 -11
- package/dist/src/core/elements/Component.d.ts.map +1 -1
- package/dist/src/core/elements/Component.js +334 -13
- package/dist/src/core/elements/Component.js.map +1 -1
- package/dist/src/core/elements/ForeachSlotCache.d.ts +95 -0
- package/dist/src/core/elements/ForeachSlotCache.d.ts.map +1 -0
- package/dist/src/core/elements/ForeachSlotCache.js +149 -0
- package/dist/src/core/elements/ForeachSlotCache.js.map +1 -0
- package/dist/src/core/elements/Fragment.d.ts +19 -13
- package/dist/src/core/elements/Fragment.d.ts.map +1 -1
- package/dist/src/core/elements/Fragment.js +43 -20
- package/dist/src/core/elements/Fragment.js.map +1 -1
- package/dist/src/core/elements/Html.d.ts +89 -12
- package/dist/src/core/elements/Html.d.ts.map +1 -1
- package/dist/src/core/elements/Html.js +707 -106
- package/dist/src/core/elements/Html.js.map +1 -1
- package/dist/src/core/elements/Output.d.ts +34 -10
- package/dist/src/core/elements/Output.d.ts.map +1 -1
- package/dist/src/core/elements/Output.js +141 -26
- package/dist/src/core/elements/Output.js.map +1 -1
- package/dist/src/core/elements/Reactive.d.ts +85 -14
- package/dist/src/core/elements/Reactive.d.ts.map +1 -1
- package/dist/src/core/elements/Reactive.js +315 -41
- package/dist/src/core/elements/Reactive.js.map +1 -1
- package/dist/src/core/elements/TextElement.d.ts +11 -7
- package/dist/src/core/elements/TextElement.d.ts.map +1 -1
- package/dist/src/core/elements/TextElement.js +18 -11
- package/dist/src/core/elements/TextElement.js.map +1 -1
- package/dist/src/core/elements/Wrapper.d.ts +24 -13
- package/dist/src/core/elements/Wrapper.d.ts.map +1 -1
- package/dist/src/core/elements/Wrapper.js +65 -8
- package/dist/src/core/elements/Wrapper.js.map +1 -1
- package/dist/src/core/elements/Yield.d.ts +10 -9
- package/dist/src/core/elements/Yield.d.ts.map +1 -1
- package/dist/src/core/elements/Yield.js +31 -19
- package/dist/src/core/elements/Yield.js.map +1 -1
- package/dist/src/core/elements/helpers.d.ts +7 -7
- package/dist/src/core/elements/helpers.d.ts.map +1 -1
- package/dist/src/core/elements/helpers.js +4 -4
- package/dist/src/core/elements/index.d.ts +12 -11
- package/dist/src/core/elements/index.d.ts.map +1 -1
- package/dist/src/core/elements/index.js +12 -11
- package/dist/src/core/elements/index.js.map +1 -1
- package/dist/src/core/elements/reconcileChildren.d.ts +7 -0
- package/dist/src/core/elements/reconcileChildren.d.ts.map +1 -0
- package/dist/src/core/elements/reconcileChildren.js +74 -0
- package/dist/src/core/elements/reconcileChildren.js.map +1 -0
- package/dist/src/core/helpers/ApiClient.d.ts.map +1 -0
- package/dist/src/core/{hellpers → helpers}/ApiClient.js +10 -1
- package/dist/src/core/helpers/ApiClient.js.map +1 -0
- package/dist/src/core/helpers/app.d.ts +3 -0
- package/dist/src/core/helpers/app.d.ts.map +1 -0
- package/dist/src/core/{hellpers → helpers}/app.js +11 -1
- package/dist/src/core/helpers/app.js.map +1 -0
- package/dist/src/core/helpers/transition.d.ts +34 -0
- package/dist/src/core/helpers/transition.d.ts.map +1 -0
- package/dist/src/core/helpers/transition.js +118 -0
- package/dist/src/core/helpers/transition.js.map +1 -0
- package/dist/src/core/{hellpers → helpers}/utils.d.ts +2 -2
- package/dist/src/core/helpers/utils.d.ts.map +1 -0
- package/dist/src/core/{hellpers → helpers}/utils.js +4 -4
- package/dist/src/core/helpers/utils.js.map +1 -0
- package/dist/src/core/helpers/view.d.ts +39 -0
- package/dist/src/core/helpers/view.d.ts.map +1 -0
- package/dist/src/core/helpers/view.js +286 -0
- package/dist/src/core/helpers/view.js.map +1 -0
- package/dist/src/core/routers/Router.d.ts +108 -7
- package/dist/src/core/routers/Router.d.ts.map +1 -1
- package/dist/src/core/routers/Router.js +444 -68
- package/dist/src/core/routers/Router.js.map +1 -1
- package/dist/src/core/routers/index.d.ts +2 -2
- package/dist/src/core/routers/index.d.ts.map +1 -1
- package/dist/src/core/routers/index.js +1 -1
- package/dist/src/core/routers/index.js.map +1 -1
- package/dist/src/core/services/AssetManager.d.ts +112 -0
- package/dist/src/core/services/AssetManager.d.ts.map +1 -0
- package/dist/src/core/services/AssetManager.js +361 -0
- package/dist/src/core/services/AssetManager.js.map +1 -0
- package/dist/src/core/services/BlockManager.d.ts +73 -2
- package/dist/src/core/services/BlockManager.d.ts.map +1 -1
- package/dist/src/core/services/BlockManager.js +270 -48
- package/dist/src/core/services/BlockManager.js.map +1 -1
- package/dist/src/core/services/DomService.js +3 -3
- package/dist/src/core/services/DomService.js.map +1 -1
- package/dist/src/core/services/HeadService.d.ts +24 -0
- package/dist/src/core/services/HeadService.d.ts.map +1 -0
- package/dist/src/core/services/HeadService.js +120 -0
- package/dist/src/core/services/HeadService.js.map +1 -0
- package/dist/src/core/services/HelperService.d.ts +14 -3
- package/dist/src/core/services/HelperService.d.ts.map +1 -1
- package/dist/src/core/services/HelperService.js +32 -2
- package/dist/src/core/services/HelperService.js.map +1 -1
- package/dist/src/core/services/HttpService.d.ts +8 -0
- package/dist/src/core/services/HttpService.d.ts.map +1 -1
- package/dist/src/core/services/HttpService.js +121 -47
- package/dist/src/core/services/HttpService.js.map +1 -1
- package/dist/src/core/services/MarkerRegistry.d.ts +51 -8
- package/dist/src/core/services/MarkerRegistry.d.ts.map +1 -1
- package/dist/src/core/services/MarkerRegistry.js +135 -14
- package/dist/src/core/services/MarkerRegistry.js.map +1 -1
- package/dist/src/core/services/PageCache.d.ts +101 -0
- package/dist/src/core/services/PageCache.d.ts.map +1 -0
- package/dist/src/core/services/PageCache.js +184 -0
- package/dist/src/core/services/PageCache.js.map +1 -0
- package/dist/src/core/services/SectionManager.d.ts +100 -0
- package/dist/src/core/services/SectionManager.d.ts.map +1 -0
- package/dist/src/core/services/SectionManager.js +408 -0
- package/dist/src/core/services/SectionManager.js.map +1 -0
- package/dist/src/core/services/StorageService.js +1 -1
- package/dist/src/core/services/TranslationService.d.ts +21 -0
- package/dist/src/core/services/TranslationService.d.ts.map +1 -0
- package/dist/src/core/services/TranslationService.js +95 -0
- package/dist/src/core/services/TranslationService.js.map +1 -0
- package/dist/src/core/services/index.d.ts +11 -8
- package/dist/src/core/services/index.d.ts.map +1 -1
- package/dist/src/core/services/index.js +11 -8
- package/dist/src/core/services/index.js.map +1 -1
- package/dist/src/core/types/utils.d.ts +8 -8
- package/dist/src/core/types/utils.d.ts.map +1 -1
- package/dist/src/core/view/LoopContext.d.ts +21 -1
- package/dist/src/core/view/LoopContext.d.ts.map +1 -1
- package/dist/src/core/view/LoopContext.js +36 -0
- package/dist/src/core/view/LoopContext.js.map +1 -1
- package/dist/src/core/view/ResourceScope.d.ts +38 -0
- package/dist/src/core/view/ResourceScope.d.ts.map +1 -0
- package/dist/src/core/view/ResourceScope.js +167 -0
- package/dist/src/core/view/ResourceScope.js.map +1 -0
- package/dist/src/core/view/Section.d.ts +17 -32
- package/dist/src/core/view/Section.d.ts.map +1 -1
- package/dist/src/core/view/Section.js +14 -55
- package/dist/src/core/view/Section.js.map +1 -1
- package/dist/src/core/view/View.d.ts +43 -7
- package/dist/src/core/view/View.d.ts.map +1 -1
- package/dist/src/core/view/View.js +22 -4
- package/dist/src/core/view/View.js.map +1 -1
- package/dist/src/core/view/ViewController.d.ts +313 -40
- package/dist/src/core/view/ViewController.d.ts.map +1 -1
- package/dist/src/core/view/ViewController.js +1002 -147
- package/dist/src/core/view/ViewController.js.map +1 -1
- package/dist/src/core/view/ViewManager.d.ts +318 -27
- package/dist/src/core/view/ViewManager.d.ts.map +1 -1
- package/dist/src/core/view/ViewManager.js +1199 -258
- package/dist/src/core/view/ViewManager.js.map +1 -1
- package/dist/src/core/view/ViewState.d.ts +185 -6
- package/dist/src/core/view/ViewState.d.ts.map +1 -1
- package/dist/src/core/view/ViewState.js +851 -123
- package/dist/src/core/view/ViewState.js.map +1 -1
- package/dist/src/core/view/index.d.ts +5 -5
- package/dist/src/core/view/index.d.ts.map +1 -1
- package/dist/src/core/view/index.js +5 -5
- package/dist/src/core/view/index.js.map +1 -1
- package/dist/src/testing/index.d.ts +69 -0
- package/dist/src/testing/index.d.ts.map +1 -0
- package/dist/src/testing/index.js +140 -0
- package/dist/src/testing/index.js.map +1 -0
- package/index.ts +29 -5
- package/package.json +30 -21
- package/plugins.ts +2 -2
- package/src/core/app/Application.ts +72 -31
- package/src/core/bootstrap/app.ts +84 -6
- package/src/core/bootstrap/bundle.ts +101 -0
- package/src/core/bootstrap/providers/ApiServiceProvider.ts +4 -4
- package/src/core/bootstrap/providers/CoreServiceProvider.ts +14 -8
- package/src/core/bootstrap/providers/HelperServiceProvider.ts +8 -3
- package/src/core/bootstrap/providers/RouteServiceProvider.ts +15 -4
- package/src/core/bootstrap/providers/ServiceProvider.ts +8 -8
- package/src/core/bootstrap/providers/ViewServiceProvider.ts +3 -3
- package/src/core/bootstrap/providers/default-providers.ts +51 -16
- package/src/core/bootstrap/providers/index.ts +9 -9
- package/src/core/bootstrap/providers/provider-order.test.ts +16 -27
- package/src/core/bootstrap/providers/provider-order.ts +1 -1
- package/src/core/bootstrap/ssr.ts +141 -0
- package/src/core/cache/ViewCache.ts +1 -1
- package/src/core/cache/index.ts +1 -1
- package/src/core/contracts/ApplicationInterface.ts +15 -11
- package/src/core/contracts/BlockInterface.ts +18 -18
- package/src/core/contracts/BundleInterface.ts +40 -0
- package/src/core/contracts/ComponentInterface.ts +7 -6
- package/src/core/contracts/ElementInterface.ts +95 -27
- package/src/core/contracts/HeadServiceInterface.ts +41 -0
- package/src/core/contracts/HelperInterface.ts +9 -1
- package/src/core/contracts/HttpServiceInterface.ts +3 -0
- package/src/core/contracts/MarkerInterface.ts +22 -49
- package/src/core/contracts/ReactiveInterface.ts +9 -9
- package/src/core/contracts/RouterInterface.ts +5 -2
- package/src/core/contracts/SectionInterface.ts +49 -0
- package/src/core/contracts/ServiceProviderInterface.ts +3 -3
- package/src/core/contracts/ViewControllerInterface.ts +124 -16
- package/src/core/contracts/ViewInterface.ts +34 -5
- package/src/core/contracts/ViewManagerInterface.ts +40 -5
- package/src/core/contracts/ViewStateInterface.ts +39 -2
- package/src/core/contracts/common.ts +3 -3
- package/src/core/contracts/utils.ts +19 -19
- package/src/core/contracts/views.ts +2 -22
- package/src/core/devtools/hook.ts +142 -0
- package/src/core/devtools/index.ts +38 -0
- package/src/core/devtools/inspector.ts +270 -0
- package/src/core/elements/Block.ts +43 -21
- package/src/core/elements/BlockOutlet.ts +61 -21
- package/src/core/elements/Component.ts +352 -18
- package/src/core/elements/ElementManager.ts +9 -9
- package/src/core/elements/ForeachSlotCache.ts +170 -0
- package/src/core/elements/Fragment.ts +50 -32
- package/src/core/elements/Html.ts +696 -117
- package/src/core/elements/Output.ts +147 -30
- package/src/core/elements/Reactive.ts +329 -52
- package/src/core/elements/TextElement.ts +25 -16
- package/src/core/elements/Wrapper.ts +84 -18
- package/src/core/elements/Yield.ts +35 -24
- package/src/core/elements/helpers.ts +12 -12
- package/src/core/elements/index.ts +25 -24
- package/src/core/elements/reconcileChildren.ts +70 -0
- package/src/core/{hellpers → helpers}/ApiClient.ts +8 -1
- package/src/core/{hellpers → helpers}/app.ts +13 -2
- package/src/core/helpers/transition.ts +127 -0
- package/src/core/{hellpers → helpers}/utils.ts +4 -4
- package/src/core/helpers/view.ts +283 -0
- package/src/core/routers/Router.ts +458 -64
- package/src/core/routers/index.ts +2 -2
- package/src/core/services/AssetManager.ts +387 -0
- package/src/core/services/BlockManager.ts +272 -50
- package/src/core/services/DomService.ts +3 -3
- package/src/core/services/HeadService.ts +124 -0
- package/src/core/services/HelperService.ts +34 -4
- package/src/core/services/HttpService.ts +116 -55
- package/src/core/services/MarkerRegistry.ts +142 -16
- package/src/core/services/PageCache.ts +239 -0
- package/src/core/services/SectionManager.ts +387 -0
- package/src/core/services/StorageService.ts +1 -1
- package/src/core/services/TranslationService.ts +91 -0
- package/src/core/services/index.ts +11 -8
- package/src/core/types/utils.ts +8 -8
- package/src/core/view/LoopContext.ts +38 -1
- package/src/core/view/ResourceScope.ts +174 -0
- package/src/core/view/Section.ts +21 -68
- package/src/core/view/View.ts +57 -8
- package/src/core/view/ViewController.ts +1070 -186
- package/src/core/view/ViewManager.ts +1346 -272
- package/src/core/view/ViewState.ts +834 -116
- package/src/core/view/index.ts +5 -5
- package/src/testing/index.ts +184 -0
- package/dist/src/core/hellpers/ApiClient.d.ts.map +0 -1
- package/dist/src/core/hellpers/ApiClient.js.map +0 -1
- package/dist/src/core/hellpers/app.d.ts +0 -3
- package/dist/src/core/hellpers/app.d.ts.map +0 -1
- package/dist/src/core/hellpers/app.js.map +0 -1
- package/dist/src/core/hellpers/utils.d.ts.map +0 -1
- package/dist/src/core/hellpers/utils.js.map +0 -1
- package/dist/src/core/services/MarkerService.d.ts +0 -299
- package/dist/src/core/services/MarkerService.d.ts.map +0 -1
- package/dist/src/core/services/MarkerService.js +0 -723
- package/dist/src/core/services/MarkerService.js.map +0 -1
- package/src/core/services/MarkerService.ts +0 -923
- /package/dist/src/core/{hellpers → helpers}/ApiClient.d.ts +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Router — SPA Router cho
|
|
2
|
+
* Router — SPA Router cho SaoView v3.
|
|
3
3
|
*
|
|
4
4
|
* Port từ core/Router nhưng nhẹ hơn (~400 dòng thay vì 960),
|
|
5
5
|
* tích hợp trực tiếp với ViewManager mới.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* - Browser back/forward handling
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import type { ViewManagerInterface } from "../contracts/ViewManagerInterface";
|
|
17
|
+
import type { ViewManagerInterface } from "../contracts/ViewManagerInterface.js";
|
|
18
18
|
|
|
19
19
|
// ─── Types ──────────────────────────────────────────────────────
|
|
20
20
|
|
|
@@ -23,12 +23,24 @@ export interface RouteDefinition {
|
|
|
23
23
|
path: string;
|
|
24
24
|
/** View name: 'web.home', 'layouts.main' */
|
|
25
25
|
component?: string;
|
|
26
|
+
/** Worker-stable alias retained by the server route registry. */
|
|
27
|
+
logicalComponent?: string;
|
|
26
28
|
/** @deprecated Use component */
|
|
27
29
|
view?: string;
|
|
28
30
|
/** Named route identifier */
|
|
29
31
|
name?: string;
|
|
30
32
|
/** Route metadata (auth, roles, etc.) */
|
|
31
33
|
meta?: Record<string, any>;
|
|
34
|
+
/**
|
|
35
|
+
* Route con — path nối vào path cha, `meta` kế thừa từ cha (con ghi đè khi
|
|
36
|
+
* trùng key). Chỉ là tầng CẤU HÌNH: bảng route vẫn phẳng sau khi flatten.
|
|
37
|
+
*
|
|
38
|
+
* Việc giữ nguyên view cha khi chuyển giữa các con là do chuỗi layout lo
|
|
39
|
+
* (`@extends` + `@useBlock`) — view con khai báo cha bằng `@extends`, và
|
|
40
|
+
* `ViewManager` tái dùng đúng instance layout đang active thay vì render
|
|
41
|
+
* lại. Xem docs/GAPS_AND_ROADMAP.md §2.17.
|
|
42
|
+
*/
|
|
43
|
+
children?: RouteDefinition[];
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
export interface Route {
|
|
@@ -59,12 +71,16 @@ export interface RouterConfig {
|
|
|
59
71
|
|
|
60
72
|
export type NavigationGuard = (to: Route, from: ActiveRoute | null, urlPath: string) => boolean | Promise<boolean>;
|
|
61
73
|
export type AfterNavigationHook = (to: Route, from: ActiveRoute | null) => void;
|
|
74
|
+
type NavigationType = 'push' | 'replace' | 'pop' | 'initial';
|
|
75
|
+
type NavigationRequest = { id: number; path: string; type: NavigationType };
|
|
62
76
|
|
|
63
77
|
// ─── ActiveRoute ────────────────────────────────────────────────
|
|
64
78
|
|
|
65
79
|
export class ActiveRoute {
|
|
66
80
|
public readonly $route: Route;
|
|
67
81
|
public readonly $urlPath: string;
|
|
82
|
+
/** Request URI = path + query string (KHÔNG gồm hash) — dùng làm cache key */
|
|
83
|
+
public readonly $uri: string;
|
|
68
84
|
public readonly $params: Record<string, string>;
|
|
69
85
|
public readonly $query: Record<string, string>;
|
|
70
86
|
public readonly $fragment: string;
|
|
@@ -74,10 +90,12 @@ export class ActiveRoute {
|
|
|
74
90
|
urlPath: string,
|
|
75
91
|
params: Record<string, string> = {},
|
|
76
92
|
query: Record<string, string> = {},
|
|
77
|
-
fragment: string = ''
|
|
93
|
+
fragment: string = '',
|
|
94
|
+
uri?: string
|
|
78
95
|
) {
|
|
79
96
|
this.$route = route;
|
|
80
97
|
this.$urlPath = urlPath;
|
|
98
|
+
this.$uri = uri ?? urlPath;
|
|
81
99
|
this.$params = params;
|
|
82
100
|
this.$query = query;
|
|
83
101
|
this.$fragment = fragment;
|
|
@@ -158,7 +176,15 @@ export class Router {
|
|
|
158
176
|
|
|
159
177
|
/** Navigation guards */
|
|
160
178
|
private _beforeEach: NavigationGuard | null = null;
|
|
161
|
-
|
|
179
|
+
/**
|
|
180
|
+
* NHIỀU hook, không phải một.
|
|
181
|
+
*
|
|
182
|
+
* Trước đây đây là một slot duy nhất, và mỗi layout muốn biết "đã điều hướng
|
|
183
|
+
* xong" đều phải giành lấy nó: layout mới đăng ký đè hook của layout cũ, rồi
|
|
184
|
+
* layout cũ destroy lại xoá hook của layout mới → không còn hook nào. Đúng
|
|
185
|
+
* chuỗi docs → demo → docs. Hook giờ là tập hợp, `afterEach()` trả về hàm huỷ.
|
|
186
|
+
*/
|
|
187
|
+
private afterHooks = new Set<AfterNavigationHook>();
|
|
162
188
|
|
|
163
189
|
/** Caches */
|
|
164
190
|
private routeCache: Map<string, RouteMatch | null> = new Map();
|
|
@@ -166,10 +192,13 @@ export class Router {
|
|
|
166
192
|
/** State */
|
|
167
193
|
private isStarted = false;
|
|
168
194
|
private isNavigating = false;
|
|
195
|
+
private navigationSequence = 0;
|
|
196
|
+
private activeNavigationUrl: string | null = null;
|
|
169
197
|
|
|
170
198
|
/** Bound handlers for cleanup */
|
|
171
199
|
private _handlePopState: () => void;
|
|
172
200
|
private _handleAutoNavigation: (e: MouseEvent) => void;
|
|
201
|
+
private _handleViewContextChange: (e: Event) => void;
|
|
173
202
|
|
|
174
203
|
constructor(app?: any) {
|
|
175
204
|
this.App = app || null;
|
|
@@ -179,12 +208,30 @@ export class Router {
|
|
|
179
208
|
|
|
180
209
|
this._handlePopState = this.handlePopState.bind(this);
|
|
181
210
|
this._handleAutoNavigation = this.handleAutoNavigation.bind(this);
|
|
211
|
+
this._handleViewContextChange = this.handleViewContextChange.bind(this);
|
|
182
212
|
}
|
|
183
213
|
|
|
184
214
|
// ─── Configuration ──────────────────────────────────────────
|
|
185
215
|
|
|
216
|
+
/**
|
|
217
|
+
* init — nạp config và wire các dependency.
|
|
218
|
+
* Gọi bởi RouteServiceProvider.boot() sau khi tất cả providers đã boot.
|
|
219
|
+
*
|
|
220
|
+
* Thực hiện:
|
|
221
|
+
* 1. configure() — load routes, mode, guards từ config
|
|
222
|
+
* 2. Wire ViewManager — lấy từ App.View nếu chưa set thủ công
|
|
223
|
+
*/
|
|
186
224
|
init(config: RouterConfig): this {
|
|
187
|
-
|
|
225
|
+
if (config && Object.keys(config).length > 0) {
|
|
226
|
+
this.configure(config);
|
|
227
|
+
}
|
|
228
|
+
// Auto-wire ViewManager nếu App đã có View registered
|
|
229
|
+
if (this.App && !this.viewManager) {
|
|
230
|
+
try {
|
|
231
|
+
const vm = (this.App as any).View ?? this.App.get?.('View') ?? null;
|
|
232
|
+
if (vm) this.viewManager = vm;
|
|
233
|
+
} catch (_) { /* View chưa sẵn sàng — sẽ fallback lúc handleRoute */ }
|
|
234
|
+
}
|
|
188
235
|
return this;
|
|
189
236
|
}
|
|
190
237
|
|
|
@@ -235,10 +282,71 @@ export class Router {
|
|
|
235
282
|
}
|
|
236
283
|
|
|
237
284
|
/**
|
|
238
|
-
*
|
|
285
|
+
* Nối path con vào path cha. Con bắt đầu bằng '/' là ĐƯỜNG TUYỆT ĐỐI —
|
|
286
|
+
* bỏ qua prefix cha (lối thoát cho route lệch khỏi cây, như Vue Router).
|
|
287
|
+
* Con rỗng ('') = index route, trùng đúng path cha.
|
|
288
|
+
*/
|
|
289
|
+
private static joinRoutePath(parent: string, child: string): string {
|
|
290
|
+
if (child.startsWith('/')) return child;
|
|
291
|
+
const base = parent.replace(/\/+$/, '');
|
|
292
|
+
if (!child) return base || '/';
|
|
293
|
+
return `${base}/${child}`.replace(/\/{2,}/g, '/');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Trải cây route thành bảng phẳng — bảng phẳng là thứ `matchRoute()` duyệt,
|
|
298
|
+
* và nó khớp THEO THỨ TỰ (first match wins), nên thứ tự emit ở đây chính là
|
|
299
|
+
* độ ưu tiên: giữ nguyên thứ tự khai báo để `/users/profile` đứng trước
|
|
300
|
+
* `/users/{id}` đúng như người viết mong đợi.
|
|
301
|
+
*
|
|
302
|
+
* Cha có `children` mà KHÔNG có `component` = nhóm thuần tuý (gom prefix +
|
|
303
|
+
* meta dùng chung), không sinh route nào cho chính nó.
|
|
304
|
+
*
|
|
305
|
+
* Index child (`path: ''`) sinh ra đúng path của cha → khi đó bỏ route
|
|
306
|
+
* riêng của cha, vì khai báo con là chỉ định cụ thể hơn cho URL đó.
|
|
307
|
+
*/
|
|
308
|
+
private flattenRouteTree(
|
|
309
|
+
routes: RouteDefinition[],
|
|
310
|
+
parentPath = '',
|
|
311
|
+
parentMeta: Record<string, any> = {},
|
|
312
|
+
): RouteDefinition[] {
|
|
313
|
+
const flat: RouteDefinition[] = [];
|
|
314
|
+
|
|
315
|
+
for (const route of routes) {
|
|
316
|
+
const { children, ...own } = route;
|
|
317
|
+
const path = Router.joinRoutePath(parentPath, route.path ?? '');
|
|
318
|
+
const meta = { ...parentMeta, ...(route.meta ?? {}) };
|
|
319
|
+
|
|
320
|
+
const flatChildren = children && children.length
|
|
321
|
+
? this.flattenRouteTree(children, path, meta)
|
|
322
|
+
: [];
|
|
323
|
+
|
|
324
|
+
const component = route.component || route.view || '';
|
|
325
|
+
const coveredByIndexChild = flatChildren.some(c => c.path === path);
|
|
326
|
+
if (component && !coveredByIndexChild) {
|
|
327
|
+
flat.push({ ...own, path, meta });
|
|
328
|
+
}
|
|
329
|
+
flat.push(...flatChildren);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return flat;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Add multiple routes at once. Chấp nhận cây lồng qua `children` — mọi
|
|
337
|
+
* đường vào (`configure`, `replaceRoutes`) đều đi qua đây nên chỉ cần
|
|
338
|
+
* flatten một chỗ.
|
|
239
339
|
*/
|
|
240
340
|
addRoutes(routes: RouteDefinition[]): this {
|
|
341
|
+
// Route KHÔNG có component (API, redirect, fallback) vẫn cần TÊN để
|
|
342
|
+
// sinh URL — chỉ không vào bảng khớp path. flattenRouteTree lọc chúng
|
|
343
|
+
// ra khỏi nhánh khớp, nên đăng ký tên riêng ở đây.
|
|
241
344
|
for (const route of routes) {
|
|
345
|
+
if (route.name && !(route.component || route.view)) {
|
|
346
|
+
this.addRouteConfig(route);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
for (const route of this.flattenRouteTree(routes)) {
|
|
242
350
|
const component = route.component || route.view || '';
|
|
243
351
|
this.addRoute(route.path, component, route.meta || {});
|
|
244
352
|
if (route.name) {
|
|
@@ -248,6 +356,14 @@ export class Router {
|
|
|
248
356
|
return this;
|
|
249
357
|
}
|
|
250
358
|
|
|
359
|
+
/** Atomically replace the materialized route table for a new context revision. */
|
|
360
|
+
replaceRoutes(routes: RouteDefinition[]): this {
|
|
361
|
+
this.routes = [];
|
|
362
|
+
this.routeConfigs = {};
|
|
363
|
+
this.routeCache.clear();
|
|
364
|
+
return this.addRoutes(routes);
|
|
365
|
+
}
|
|
366
|
+
|
|
251
367
|
/**
|
|
252
368
|
* Configure router from a config object.
|
|
253
369
|
*/
|
|
@@ -255,10 +371,14 @@ export class Router {
|
|
|
255
371
|
if (config.mode) this.mode = config.mode;
|
|
256
372
|
if (config.base) this.base = config.base;
|
|
257
373
|
if (config.defaultRoute) this.defaultRoute = config.defaultRoute;
|
|
258
|
-
|
|
259
|
-
|
|
374
|
+
// MỘT danh sách thôi. Server từng phát cả `routes` (chỉ route có
|
|
375
|
+
// component) lẫn `allRoutes` (đủ) — tập con của nhau, nên đăng ký cả
|
|
376
|
+
// hai là mỗi route vào bảng hai lần. `allRoutes` giữ lại làm bí danh
|
|
377
|
+
// cho payload cũ.
|
|
378
|
+
const routes = config.routes ?? config.allRoutes;
|
|
379
|
+
if (routes) this.addRoutes(routes);
|
|
260
380
|
if (config.beforeEach) this._beforeEach = config.beforeEach;
|
|
261
|
-
if (config.afterEach) this.
|
|
381
|
+
if (config.afterEach) this.afterEach(config.afterEach);
|
|
262
382
|
return this;
|
|
263
383
|
}
|
|
264
384
|
|
|
@@ -269,24 +389,21 @@ export class Router {
|
|
|
269
389
|
return this;
|
|
270
390
|
}
|
|
271
391
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
392
|
+
/** Đăng ký hook chạy sau mỗi lần điều hướng. Trả về hàm HUỶ đăng ký. */
|
|
393
|
+
afterEach(hook: AfterNavigationHook): () => void {
|
|
394
|
+
this.afterHooks.add(hook);
|
|
395
|
+
return () => { this.afterHooks.delete(hook); };
|
|
275
396
|
}
|
|
276
397
|
|
|
277
398
|
// ─── Navigation ─────────────────────────────────────────────
|
|
278
399
|
|
|
279
400
|
/**
|
|
280
401
|
* Navigate to a URL path.
|
|
402
|
+
* History chỉ được cập nhật SAU khi guard cho phép (trong handleRoute) —
|
|
403
|
+
* guard chặn thì URL không đổi, tránh desync URL ↔ view.
|
|
281
404
|
*/
|
|
282
405
|
navigate(path: string): void {
|
|
283
|
-
|
|
284
|
-
window.history.pushState({}, '', path);
|
|
285
|
-
} else {
|
|
286
|
-
window.location.hash = path;
|
|
287
|
-
}
|
|
288
|
-
this.handleRoute(path);
|
|
289
|
-
this.currentUri = path;
|
|
406
|
+
this.requestNavigation(path, 'push');
|
|
290
407
|
}
|
|
291
408
|
|
|
292
409
|
/** Alias for navigate */
|
|
@@ -310,13 +427,7 @@ export class Router {
|
|
|
310
427
|
* Replace current history entry without adding to stack.
|
|
311
428
|
*/
|
|
312
429
|
replace(path: string): void {
|
|
313
|
-
|
|
314
|
-
window.history.replaceState({}, '', path);
|
|
315
|
-
} else {
|
|
316
|
-
window.location.hash = path;
|
|
317
|
-
}
|
|
318
|
-
this.handleRoute(path);
|
|
319
|
-
this.currentUri = path;
|
|
430
|
+
this.requestNavigation(path, 'replace');
|
|
320
431
|
}
|
|
321
432
|
|
|
322
433
|
/**
|
|
@@ -446,10 +557,11 @@ export class Router {
|
|
|
446
557
|
window.addEventListener('hashchange', this._handlePopState);
|
|
447
558
|
}
|
|
448
559
|
document.addEventListener('click', this._handleAutoNavigation);
|
|
560
|
+
window.addEventListener('saola:view-context', this._handleViewContextChange);
|
|
449
561
|
|
|
450
562
|
// Handle initial route
|
|
451
563
|
if (!skipInitial) {
|
|
452
|
-
this.
|
|
564
|
+
this.requestNavigation(initialPath, 'initial');
|
|
453
565
|
}
|
|
454
566
|
|
|
455
567
|
this.isStarted = true;
|
|
@@ -462,41 +574,168 @@ export class Router {
|
|
|
462
574
|
window.removeEventListener('popstate', this._handlePopState);
|
|
463
575
|
window.removeEventListener('hashchange', this._handlePopState);
|
|
464
576
|
document.removeEventListener('click', this._handleAutoNavigation);
|
|
577
|
+
window.removeEventListener('saola:view-context', this._handleViewContextChange);
|
|
465
578
|
this.isStarted = false;
|
|
466
579
|
}
|
|
467
580
|
|
|
581
|
+
private handleViewContextChange(event: Event): void {
|
|
582
|
+
const state = (event as CustomEvent<Record<string, any>>).detail;
|
|
583
|
+
if (!state || state.changed !== true) return;
|
|
584
|
+
|
|
585
|
+
const vm = this.viewManager ?? this.App?.View;
|
|
586
|
+
const target = this.activeNavigationUrl || this.currentUri
|
|
587
|
+
|| (typeof window !== 'undefined'
|
|
588
|
+
? window.location.pathname + window.location.search + window.location.hash
|
|
589
|
+
: '/');
|
|
590
|
+
|
|
591
|
+
if (vm?.requiresReloadForViewContext?.(state)) {
|
|
592
|
+
vm.cancelNavigation?.();
|
|
593
|
+
this.reloadForViewContext(target, state);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const applied = vm?.applyViewContext?.(state) ?? false;
|
|
598
|
+
if (!applied) return;
|
|
599
|
+
|
|
600
|
+
if (Array.isArray(state.routes)) {
|
|
601
|
+
this.replaceRoutes(state.routes);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// If a fetch discovered the change mid-navigation, requestNavigation
|
|
605
|
+
// queues the same target and invalidates the old render generation.
|
|
606
|
+
if (target) this.requestNavigation(target, 'replace');
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Switch to a coherent server-rendered document when the view namespace
|
|
611
|
+
* changes. A short-lived token prevents a bad deployment from reloading the
|
|
612
|
+
* same URL/revision forever.
|
|
613
|
+
*/
|
|
614
|
+
private reloadForViewContext(target: string, state: Record<string, any>): void {
|
|
615
|
+
if (typeof window === 'undefined') return;
|
|
616
|
+
|
|
617
|
+
const url = new URL(target || '/', window.location.href);
|
|
618
|
+
const revision = typeof state.revision === 'string' ? state.revision : 'unknown';
|
|
619
|
+
const token = `${revision}:${url.pathname}${url.search}`;
|
|
620
|
+
const storageKey = 'saola:view-context-reload';
|
|
621
|
+
const now = Date.now();
|
|
622
|
+
|
|
623
|
+
try {
|
|
624
|
+
const previousRaw = window.sessionStorage.getItem(storageKey);
|
|
625
|
+
const previous = previousRaw ? JSON.parse(previousRaw) : null;
|
|
626
|
+
if (previous?.token === token && typeof previous.at === 'number' && now - previous.at < 15_000) {
|
|
627
|
+
const message = 'Không thể đồng bộ view context sau khi tải lại. Vui lòng kiểm tra revision và asset đã triển khai.';
|
|
628
|
+
console.error(`[Router] ${message}`, { revision, target: url.href });
|
|
629
|
+
(this.viewManager ?? this.App?.View)?.showError?.(message, {
|
|
630
|
+
revision,
|
|
631
|
+
target: url.href,
|
|
632
|
+
});
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
window.sessionStorage.setItem(storageKey, JSON.stringify({ token, at: now }));
|
|
636
|
+
} catch {
|
|
637
|
+
// Storage có thể bị chặn; coherence vẫn quan trọng hơn loop guard.
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
window.dispatchEvent(new CustomEvent('saola:view-context-reload', {
|
|
641
|
+
detail: { ...state, target: url.href },
|
|
642
|
+
}));
|
|
643
|
+
window.location.replace(url.href);
|
|
644
|
+
}
|
|
645
|
+
|
|
468
646
|
/**
|
|
469
647
|
* Full destroy — cleanup everything.
|
|
470
648
|
*/
|
|
471
649
|
destroy(): void {
|
|
472
650
|
this.stop();
|
|
651
|
+
this.navigationSequence++;
|
|
652
|
+
this.pendingNavigation = null;
|
|
653
|
+
this.activeNavigationUrl = null;
|
|
654
|
+
this.viewManager?.cancelNavigation?.();
|
|
473
655
|
this.routes = [];
|
|
474
656
|
this.routeConfigs = {};
|
|
475
657
|
this.routeCache.clear();
|
|
476
658
|
this._beforeEach = null;
|
|
477
|
-
this.
|
|
659
|
+
this.afterHooks.clear();
|
|
478
660
|
this.currentRoute = null;
|
|
661
|
+
this.liveRegion?.remove();
|
|
662
|
+
this.liveRegion = null;
|
|
479
663
|
Router.activeRoute = null;
|
|
480
664
|
}
|
|
481
665
|
|
|
482
666
|
// ─── Helper ────────────────────────────────
|
|
483
667
|
getFullUrl(): string {
|
|
484
668
|
if (typeof window === 'undefined') return '';
|
|
669
|
+
if (this.activeNavigationUrl) {
|
|
670
|
+
return new URL(this.activeNavigationUrl, window.location.href).href;
|
|
671
|
+
}
|
|
485
672
|
return window.location.href;
|
|
486
673
|
}
|
|
487
674
|
// ─── Internal: Route Handling ────────────────────────────────
|
|
488
675
|
|
|
676
|
+
/** Kiểu navigation nội bộ — quyết định thao tác history + nav type xuống ViewManager */
|
|
677
|
+
private pendingNavigation: NavigationRequest | null = null;
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Tách một URL string thành pathname / query string / fragment.
|
|
681
|
+
* PHẢI tách query + hash TRƯỚC khi match route — pattern chỉ match pathname.
|
|
682
|
+
*/
|
|
683
|
+
private splitLocation(raw: string): { pathname: string; queryString: string; fragment: string } {
|
|
684
|
+
let rest = raw ?? '';
|
|
685
|
+
let fragment = '';
|
|
686
|
+
const hashIdx = rest.indexOf('#');
|
|
687
|
+
if (hashIdx !== -1) {
|
|
688
|
+
fragment = rest.slice(hashIdx + 1);
|
|
689
|
+
rest = rest.slice(0, hashIdx);
|
|
690
|
+
}
|
|
691
|
+
let queryString = '';
|
|
692
|
+
const queryIdx = rest.indexOf('?');
|
|
693
|
+
if (queryIdx !== -1) {
|
|
694
|
+
queryString = rest.slice(queryIdx + 1);
|
|
695
|
+
rest = rest.slice(0, queryIdx);
|
|
696
|
+
}
|
|
697
|
+
return { pathname: rest, queryString, fragment };
|
|
698
|
+
}
|
|
699
|
+
|
|
489
700
|
/**
|
|
490
|
-
*
|
|
701
|
+
* Điểm vào duy nhất cho mọi navigation (navigate/replace/popstate/initial).
|
|
702
|
+
* Đang navigate dở → ghi nhận request MỚI NHẤT, xử lý sau khi xong
|
|
703
|
+
* (không drop im lặng như trước).
|
|
491
704
|
*/
|
|
492
|
-
private
|
|
493
|
-
|
|
705
|
+
private requestNavigation(path: string, type: NavigationType): void {
|
|
706
|
+
const request: NavigationRequest = { id: ++this.navigationSequence, path, type };
|
|
707
|
+
if (this.isNavigating) {
|
|
708
|
+
this.pendingNavigation = request;
|
|
709
|
+
// Cho ViewManager thoát sớm khỏi fetch/render cũ. Request mới nhất
|
|
710
|
+
// sẽ được chạy trong finally, không commit DOM/history trung gian.
|
|
711
|
+
(this.viewManager ?? this.App?.View)?.cancelNavigation?.();
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
void this.handleRoute(path, type, request.id);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Core route handler — prepare/guard/render trước, chỉ commit history +
|
|
719
|
+
* active route sau khi ViewManager đã mount/hydrate thành công.
|
|
720
|
+
*/
|
|
721
|
+
private async handleRoute(
|
|
722
|
+
path: string,
|
|
723
|
+
type: NavigationType = 'push',
|
|
724
|
+
requestId: number = ++this.navigationSequence,
|
|
725
|
+
): Promise<void> {
|
|
494
726
|
this.isNavigating = true;
|
|
727
|
+
// Khai ngoài try: `finally` phải nhả được kể cả khi render ném giữa chừng.
|
|
728
|
+
let releaseHeight: () => void = () => {};
|
|
495
729
|
|
|
496
730
|
try {
|
|
497
|
-
const
|
|
498
|
-
const
|
|
499
|
-
|
|
731
|
+
const { pathname, queryString, fragment } = this.splitLocation(path);
|
|
732
|
+
const normalizedPath = this.normalizePath(pathname);
|
|
733
|
+
// Request URI = path + query, KHÔNG gồm hash — cache key của ViewManager
|
|
734
|
+
const uri = queryString ? `${normalizedPath}?${queryString}` : normalizedPath;
|
|
735
|
+
const query = this.parseQuery(queryString);
|
|
736
|
+
|
|
737
|
+
// popstate/hashchange echo về đúng URI đang đứng → bỏ qua
|
|
738
|
+
if (type === 'pop' && uri === this.currentUri) return;
|
|
500
739
|
|
|
501
740
|
const match = this.matchRoute(normalizedPath);
|
|
502
741
|
if (!match) {
|
|
@@ -508,64 +747,208 @@ export class Router {
|
|
|
508
747
|
const from = this.currentRoute;
|
|
509
748
|
|
|
510
749
|
// Create ActiveRoute
|
|
511
|
-
const activeRoute = new ActiveRoute(route, normalizedPath, params, query, fragment);
|
|
750
|
+
const activeRoute = new ActiveRoute(route, normalizedPath, params, query, fragment, uri);
|
|
512
751
|
|
|
513
|
-
// Before guard
|
|
752
|
+
// Before guard — chạy TRƯỚC khi đụng vào history: guard chặn thì URL giữ nguyên
|
|
514
753
|
if (this._beforeEach) {
|
|
515
754
|
const allow = await this._beforeEach(route, from, normalizedPath);
|
|
516
755
|
if (allow === false) return;
|
|
517
756
|
}
|
|
757
|
+
if (requestId !== this.navigationSequence) return;
|
|
518
758
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
this.currentRoute = activeRoute;
|
|
759
|
+
const historyTarget = fragment ? `${uri}#${fragment}` : uri;
|
|
760
|
+
this.activeNavigationUrl = historyTarget;
|
|
522
761
|
|
|
523
|
-
// Mount view via ViewManager
|
|
762
|
+
// Mount view via ViewManager.
|
|
763
|
+
// Route ĐẦU TIÊN sau SSR: nếu view khớp entry server đã render →
|
|
764
|
+
// hydrateView (claim DOM) thay vì mountView. consumeSSRViewId() chỉ
|
|
765
|
+
// trả id 1 lần → các navigate sau là CSR (SPA takeover).
|
|
524
766
|
const viewComponent = route.component || route.view;
|
|
767
|
+
let transitionSucceeded = true;
|
|
768
|
+
// View cũ bị gỡ trước khi cây mới dựng xong, nên tài liệu sập chiều
|
|
769
|
+
// cao trong vài frame — footer nhảy vọt lên, thanh cuộn biến mất,
|
|
770
|
+
// trình duyệt kẹp lại scrollY. Giữ chiều cao cũ tới khi thay xong.
|
|
771
|
+
releaseHeight = this.freezeDocumentHeight();
|
|
525
772
|
if (viewComponent) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
773
|
+
const vm = this.viewManager ?? this.App?.View;
|
|
774
|
+
if (vm) {
|
|
775
|
+
const ssrViewId = vm.consumeSSRViewId?.(viewComponent) ?? null;
|
|
776
|
+
let result: any;
|
|
777
|
+
if (ssrViewId) {
|
|
778
|
+
result = await vm.hydrateView(viewComponent, { __SSR_VIEW_ID__: ssrViewId, ...params }, activeRoute);
|
|
779
|
+
} else {
|
|
780
|
+
result = await vm.mountView(viewComponent, params, activeRoute, type === 'pop' ? 'pop' : 'push');
|
|
781
|
+
}
|
|
782
|
+
// mountView(null) có thể là duplicate no-op; xác nhận chain
|
|
783
|
+
// active đang thật sự thuộc URI đích trước khi commit Router.
|
|
784
|
+
transitionSucceeded = result != null
|
|
785
|
+
|| vm.getCurrentView?.()?.__ctrl__?.urlPath === uri;
|
|
786
|
+
} else {
|
|
787
|
+
transitionSucceeded = false;
|
|
531
788
|
}
|
|
532
789
|
}
|
|
790
|
+
if (requestId !== this.navigationSequence) return;
|
|
791
|
+
if (!transitionSucceeded) {
|
|
792
|
+
if (type === 'pop') this.restoreUrlAfterFailedPop(from);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
533
795
|
|
|
534
|
-
//
|
|
535
|
-
|
|
536
|
-
|
|
796
|
+
// Commit point: từ đây URL, Router state và mounted view cùng
|
|
797
|
+
// đại diện cho một navigation duy nhất.
|
|
798
|
+
if (this.mode === 'history') {
|
|
799
|
+
if (type === 'push') window.history.pushState({}, '', historyTarget);
|
|
800
|
+
else if (type === 'replace') window.history.replaceState({}, '', historyTarget);
|
|
801
|
+
} else if (type === 'push' || type === 'replace') {
|
|
802
|
+
window.location.hash = historyTarget;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
Router.activeRoute = activeRoute;
|
|
806
|
+
this.currentRoute = activeRoute;
|
|
807
|
+
this.currentUri = uri;
|
|
808
|
+
this.applyScroll(type, fragment);
|
|
809
|
+
this.announceNavigation(type);
|
|
810
|
+
|
|
811
|
+
// After hook — chạy trên BẢN SAO: hook được phép tự huỷ đăng ký trong
|
|
812
|
+
// lúc chạy. Bọc try/catch từng hook: một hook hỏng (view đã destroy
|
|
813
|
+
// chẳng hạn) không được phép làm chết điều hướng của tất cả phần còn lại.
|
|
814
|
+
for (const hook of [...this.afterHooks]) {
|
|
815
|
+
try {
|
|
816
|
+
hook({ ...route, path: normalizedPath }, from);
|
|
817
|
+
} catch (error) {
|
|
818
|
+
console.error('[Router] afterEach hook lỗi:', error);
|
|
819
|
+
}
|
|
537
820
|
}
|
|
538
821
|
} catch (error) {
|
|
539
822
|
console.error('[Router] Navigation error:', error);
|
|
540
823
|
} finally {
|
|
824
|
+
releaseHeight();
|
|
541
825
|
this.isNavigating = false;
|
|
826
|
+
if (requestId === this.navigationSequence) this.activeNavigationUrl = null;
|
|
827
|
+
// Có navigation đến trong lúc đang xử lý → chạy request mới nhất
|
|
828
|
+
const pending = this.pendingNavigation;
|
|
829
|
+
this.pendingNavigation = null;
|
|
830
|
+
if (pending) {
|
|
831
|
+
void this.handleRoute(pending.path, pending.type, pending.id);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** Pop đã đổi address bar; render fail thì đưa URL về chain còn active. */
|
|
837
|
+
private restoreUrlAfterFailedPop(from: ActiveRoute | null): void {
|
|
838
|
+
if (this.mode !== 'history' || !from) return;
|
|
839
|
+
const target = from.$fragment ? `${from.$uri}#${from.$fragment}` : from.$uri;
|
|
840
|
+
window.history.replaceState(window.history.state, '', target);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Ghim chiều cao tài liệu bằng đúng chiều cao hiện tại, trả về hàm nhả.
|
|
845
|
+
* Chỉ chạm `min-height` của body nên không đụng layout khi đã nhả.
|
|
846
|
+
*/
|
|
847
|
+
private freezeDocumentHeight(): () => void {
|
|
848
|
+
if (typeof document === 'undefined' || !document.body) return () => {};
|
|
849
|
+
const body = document.body;
|
|
850
|
+
const previous = body.style.minHeight;
|
|
851
|
+
try {
|
|
852
|
+
body.style.minHeight = `${document.documentElement.scrollHeight}px`;
|
|
853
|
+
} catch {
|
|
854
|
+
return () => {};
|
|
855
|
+
}
|
|
856
|
+
return () => { try { body.style.minHeight = previous; } catch { /* noop */ } };
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
private applyScroll(type: NavigationType, fragment: string): void {
|
|
860
|
+
if (typeof window === 'undefined') return;
|
|
861
|
+
if (fragment) {
|
|
862
|
+
let id = fragment;
|
|
863
|
+
try { id = decodeURIComponent(fragment); } catch { /* malformed fragment */ }
|
|
864
|
+
const target = document.getElementById(id) || document.getElementsByName(id)[0];
|
|
865
|
+
target?.scrollIntoView?.();
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
// PageCache tự khôi phục pop position; initial giữ vị trí SSR/browser.
|
|
869
|
+
if (type === 'push' || type === 'replace') {
|
|
870
|
+
// scrollTo(0,0) tuân theo `html { scroll-behavior: smooth }` nếu trang
|
|
871
|
+
// có đặt — biến cú nhảy về đầu thành cuộn chậm ~0.4s chạy SAU khi nội
|
|
872
|
+
// dung đã thay xong, nhìn như treo. Đổi route phải tức thì; smooth để
|
|
873
|
+
// dành cho anchor trong trang (nhánh fragment ở trên).
|
|
874
|
+
try {
|
|
875
|
+
window.scrollTo({ top: 0, left: 0, behavior: 'instant' as ScrollBehavior });
|
|
876
|
+
} catch {
|
|
877
|
+
// Engine cũ chưa biết 'instant' sẽ ném khi validate enum.
|
|
878
|
+
try { window.scrollTo(0, 0); } catch { /* non-browser/test runtime */ }
|
|
879
|
+
}
|
|
542
880
|
}
|
|
543
881
|
}
|
|
544
882
|
|
|
883
|
+
/** Live region dùng lại giữa các lần điều hướng — tạo lười, chỉ 1 node. */
|
|
884
|
+
private liveRegion: HTMLElement | null = null;
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* A11y sau khi điều hướng xong (SPA không tự làm được như full page load):
|
|
888
|
+
* 1. Đưa focus về container view — không làm thì bàn phím vẫn ở link cũ
|
|
889
|
+
* của trang TRƯỚC, Tab tiếp tục từ vị trí không còn tồn tại.
|
|
890
|
+
* 2. Đọc tên trang mới qua live region — screen reader không hề biết
|
|
891
|
+
* nội dung đã đổi vì document không reload.
|
|
892
|
+
*
|
|
893
|
+
* `preventScroll` để không phá `applyScroll()` vừa chạy. Bỏ qua `initial`:
|
|
894
|
+
* lần paint đầu/hydrate không được cướp focus khỏi thứ user đang thao tác.
|
|
895
|
+
*/
|
|
896
|
+
private announceNavigation(type: NavigationType): void {
|
|
897
|
+
if (typeof document === 'undefined' || type === 'initial') return;
|
|
898
|
+
|
|
899
|
+
const container = (this.viewManager ?? this.App?.View)?.getContainer?.();
|
|
900
|
+
if (container instanceof HTMLElement) {
|
|
901
|
+
// tabindex=-1: focus được bằng script nhưng KHÔNG chen vào thứ tự Tab.
|
|
902
|
+
if (!container.hasAttribute('tabindex')) container.setAttribute('tabindex', '-1');
|
|
903
|
+
try { container.focus({ preventScroll: true }); } catch { /* jsdom/older browsers */ }
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (!this.liveRegion || !this.liveRegion.isConnected) {
|
|
907
|
+
const region = document.createElement('div');
|
|
908
|
+
region.setAttribute('aria-live', 'polite');
|
|
909
|
+
region.setAttribute('aria-atomic', 'true');
|
|
910
|
+
// Ẩn khỏi mắt nhưng KHÔNG display:none / visibility:hidden —
|
|
911
|
+
// hai cái đó làm screen reader bỏ qua luôn.
|
|
912
|
+
region.style.cssText =
|
|
913
|
+
'position:absolute;width:1px;height:1px;margin:-1px;padding:0;'
|
|
914
|
+
+ 'overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0';
|
|
915
|
+
document.body.appendChild(region);
|
|
916
|
+
this.liveRegion = region;
|
|
917
|
+
}
|
|
918
|
+
// Ghi ở frame sau: nội dung mới phải khác nội dung cũ thì AT mới đọc lại.
|
|
919
|
+
const label = document.title || this.currentUri || '';
|
|
920
|
+
this.liveRegion.textContent = '';
|
|
921
|
+
requestAnimationFrame(() => {
|
|
922
|
+
if (this.liveRegion) this.liveRegion.textContent = label;
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
|
|
545
926
|
/**
|
|
546
927
|
* Handle browser back/forward.
|
|
547
928
|
*/
|
|
548
929
|
private handlePopState(): void {
|
|
549
930
|
const path = this.mode === 'history'
|
|
550
|
-
? window.location.pathname + window.location.search
|
|
931
|
+
? window.location.pathname + window.location.search + window.location.hash
|
|
551
932
|
: window.location.hash.slice(1) || this.defaultRoute;
|
|
552
|
-
this.
|
|
933
|
+
this.requestNavigation(path, 'pop');
|
|
553
934
|
}
|
|
554
935
|
|
|
555
936
|
/**
|
|
556
937
|
* Auto-navigation: intercept clicks on <a>, [data-nav-link], [data-navigate].
|
|
557
938
|
*/
|
|
558
939
|
private handleAutoNavigation(e: MouseEvent): void {
|
|
940
|
+
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
559
941
|
const target = e.target as HTMLElement;
|
|
942
|
+
if (!target?.closest) return;
|
|
560
943
|
|
|
561
944
|
// 1. [data-nav-link] — highest priority
|
|
562
945
|
const navLinkEl = target.closest('[data-nav-link]') as HTMLElement;
|
|
563
946
|
if (navLinkEl) {
|
|
564
947
|
if (navLinkEl.hasAttribute('data-nav-disabled')) return;
|
|
565
948
|
const navPath = navLinkEl.getAttribute('data-nav-link');
|
|
566
|
-
if (navPath && navPath.trim() !== ''
|
|
949
|
+
if (navPath && navPath.trim() !== '') {
|
|
567
950
|
e.preventDefault();
|
|
568
|
-
this.navigate(navPath);
|
|
951
|
+
if (navPath !== this.currentUri) this.navigate(navPath);
|
|
569
952
|
return;
|
|
570
953
|
}
|
|
571
954
|
}
|
|
@@ -575,9 +958,9 @@ export class Router {
|
|
|
575
958
|
if (navigateEl) {
|
|
576
959
|
if (navigateEl.hasAttribute('data-nav-disabled')) return;
|
|
577
960
|
const navPath = navigateEl.getAttribute('data-navigate');
|
|
578
|
-
if (navPath && navPath.trim() !== ''
|
|
961
|
+
if (navPath && navPath.trim() !== '') {
|
|
579
962
|
e.preventDefault();
|
|
580
|
-
this.navigate(navPath);
|
|
963
|
+
if (navPath !== this.currentUri) this.navigate(navPath);
|
|
581
964
|
return;
|
|
582
965
|
}
|
|
583
966
|
}
|
|
@@ -587,7 +970,8 @@ export class Router {
|
|
|
587
970
|
if (!link) return;
|
|
588
971
|
|
|
589
972
|
// Skip: target="_blank", disabled, special protocols
|
|
590
|
-
if (link.target
|
|
973
|
+
if (link.target && link.target !== '_self') return;
|
|
974
|
+
if (link.hasAttribute('download')) return;
|
|
591
975
|
if (link.dataset.nav === 'disabled' || link.dataset.nav === 'false') return;
|
|
592
976
|
const href = link.href;
|
|
593
977
|
if (href.startsWith('mailto:') || href.startsWith('tel:') || href.startsWith('javascript:')) return;
|
|
@@ -598,15 +982,18 @@ export class Router {
|
|
|
598
982
|
const currentUrl = new URL(window.location.href);
|
|
599
983
|
if (linkUrl.host !== currentUrl.host) return; // External
|
|
600
984
|
|
|
601
|
-
const path = linkUrl.pathname + linkUrl.search;
|
|
602
|
-
|
|
985
|
+
const path = linkUrl.pathname + linkUrl.search + linkUrl.hash;
|
|
986
|
+
// Link trỏ về đúng route đang đứng vẫn phải nuốt click. Thoát sớm mà
|
|
987
|
+
// không preventDefault là để browser tự điều hướng — reload cả tài
|
|
988
|
+
// liệu, mất sạch state SPA. Đây là logo và mục menu đang active.
|
|
603
989
|
e.preventDefault();
|
|
990
|
+
if (path === this.currentUri && !linkUrl.hash) return; // Same page — no-op
|
|
604
991
|
this.navigate(path);
|
|
605
992
|
} catch {
|
|
606
993
|
// Relative URL
|
|
607
994
|
if (href && !href.startsWith('http') && !href.startsWith('//')) {
|
|
608
|
-
if (href === this.currentUri) return;
|
|
609
995
|
e.preventDefault();
|
|
996
|
+
if (href === this.currentUri) return; // Same page — no-op
|
|
610
997
|
this.navigate(href);
|
|
611
998
|
}
|
|
612
999
|
}
|
|
@@ -615,7 +1002,8 @@ export class Router {
|
|
|
615
1002
|
// ─── Internal: Pattern Matching ─────────────────────────────
|
|
616
1003
|
|
|
617
1004
|
private matchRoute(path: string): RouteMatch | null {
|
|
618
|
-
|
|
1005
|
+
// Defensive: strip query/hash nếu caller truyền URI đầy đủ
|
|
1006
|
+
const normalizedPath = this.normalizePath(this.splitLocation(path).pathname);
|
|
619
1007
|
|
|
620
1008
|
if (this.routeCache.has(normalizedPath)) {
|
|
621
1009
|
return this.routeCache.get(normalizedPath)!;
|
|
@@ -681,7 +1069,9 @@ export class Router {
|
|
|
681
1069
|
regexParts.push('\\/' + escaped);
|
|
682
1070
|
}
|
|
683
1071
|
|
|
684
|
-
|
|
1072
|
+
// Root path '/' → mọi segment rỗng → regexParts rỗng. Phải khớp '\/'
|
|
1073
|
+
// (nếu để '^$' thì route '/' không bao giờ match — bug gốc).
|
|
1074
|
+
const regex = new RegExp(`^${regexParts.join('') || '\\/'}$`);
|
|
685
1075
|
const match = normalizedPath.match(regex);
|
|
686
1076
|
if (!match) return null;
|
|
687
1077
|
|
|
@@ -714,12 +1104,16 @@ export class Router {
|
|
|
714
1104
|
}
|
|
715
1105
|
|
|
716
1106
|
private setActiveRouteForPath(path: string): void {
|
|
717
|
-
const
|
|
1107
|
+
const { pathname, queryString, fragment } = this.splitLocation(path);
|
|
1108
|
+
const normalizedPath = this.normalizePath(pathname);
|
|
718
1109
|
const match = this.matchRoute(normalizedPath);
|
|
719
1110
|
if (match) {
|
|
720
|
-
const
|
|
721
|
-
const
|
|
722
|
-
const activeRoute = new ActiveRoute(
|
|
1111
|
+
const uri = queryString ? `${normalizedPath}?${queryString}` : normalizedPath;
|
|
1112
|
+
const query = this.parseQuery(queryString || window.location.search);
|
|
1113
|
+
const activeRoute = new ActiveRoute(
|
|
1114
|
+
match.route, normalizedPath, match.params, query,
|
|
1115
|
+
fragment || window.location.hash.substring(1), uri
|
|
1116
|
+
);
|
|
723
1117
|
Router.activeRoute = activeRoute;
|
|
724
1118
|
this.currentRoute = activeRoute;
|
|
725
1119
|
}
|