@saolabs/client 1.0.0 → 1.0.2
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 +17 -15
- package/dist/src/core/app/Application.d.ts.map +1 -1
- package/dist/src/core/app/Application.js +41 -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 +74 -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 +125 -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 +13 -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/hellpers/utils.d.ts +2 -2
- package/dist/src/core/hellpers/utils.js +4 -4
- package/dist/src/core/helpers/ApiClient.d.ts +58 -0
- package/dist/src/core/helpers/ApiClient.d.ts.map +1 -0
- package/dist/src/core/helpers/ApiClient.js +131 -0
- 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/helpers/app.js +63 -0
- 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/helpers/utils.d.ts +26 -0
- package/dist/src/core/helpers/utils.d.ts.map +1 -0
- package/dist/src/core/helpers/utils.js +96 -0
- 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 +58 -31
- package/src/core/bootstrap/app.ts +80 -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 +136 -0
- package/src/core/cache/ViewCache.ts +1 -1
- package/src/core/cache/index.ts +1 -1
- package/src/core/contracts/ApplicationInterface.ts +13 -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/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
|
@@ -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.
|
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
*/
|
|
16
16
|
// ─── ActiveRoute ────────────────────────────────────────────────
|
|
17
17
|
export class ActiveRoute {
|
|
18
|
-
constructor(route, urlPath, params = {}, query = {}, fragment = '') {
|
|
18
|
+
constructor(route, urlPath, params = {}, query = {}, fragment = '', uri) {
|
|
19
19
|
this.$route = route;
|
|
20
20
|
this.$urlPath = urlPath;
|
|
21
|
+
this.$uri = uri ?? urlPath;
|
|
21
22
|
this.$params = params;
|
|
22
23
|
this.$query = query;
|
|
23
24
|
this.$fragment = fragment;
|
|
@@ -83,21 +84,57 @@ export class Router {
|
|
|
83
84
|
this.currentUri = '';
|
|
84
85
|
/** Navigation guards */
|
|
85
86
|
this._beforeEach = null;
|
|
86
|
-
|
|
87
|
+
/**
|
|
88
|
+
* NHIỀU hook, không phải một.
|
|
89
|
+
*
|
|
90
|
+
* Trước đây đây là một slot duy nhất, và mỗi layout muốn biết "đã điều hướng
|
|
91
|
+
* xong" đều phải giành lấy nó: layout mới đăng ký đè hook của layout cũ, rồi
|
|
92
|
+
* layout cũ destroy lại xoá hook của layout mới → không còn hook nào. Đúng
|
|
93
|
+
* chuỗi docs → demo → docs. Hook giờ là tập hợp, `afterEach()` trả về hàm huỷ.
|
|
94
|
+
*/
|
|
95
|
+
this.afterHooks = new Set();
|
|
87
96
|
/** Caches */
|
|
88
97
|
this.routeCache = new Map();
|
|
89
98
|
/** State */
|
|
90
99
|
this.isStarted = false;
|
|
91
100
|
this.isNavigating = false;
|
|
101
|
+
this.navigationSequence = 0;
|
|
102
|
+
this.activeNavigationUrl = null;
|
|
103
|
+
// ─── Internal: Route Handling ────────────────────────────────
|
|
104
|
+
/** Kiểu navigation nội bộ — quyết định thao tác history + nav type xuống ViewManager */
|
|
105
|
+
this.pendingNavigation = null;
|
|
106
|
+
/** Live region dùng lại giữa các lần điều hướng — tạo lười, chỉ 1 node. */
|
|
107
|
+
this.liveRegion = null;
|
|
92
108
|
this.App = app || null;
|
|
93
109
|
this.currentUri = typeof window !== 'undefined'
|
|
94
110
|
? window.location.pathname + window.location.search
|
|
95
111
|
: '/';
|
|
96
112
|
this._handlePopState = this.handlePopState.bind(this);
|
|
97
113
|
this._handleAutoNavigation = this.handleAutoNavigation.bind(this);
|
|
114
|
+
this._handleViewContextChange = this.handleViewContextChange.bind(this);
|
|
98
115
|
}
|
|
99
116
|
// ─── Configuration ──────────────────────────────────────────
|
|
117
|
+
/**
|
|
118
|
+
* init — nạp config và wire các dependency.
|
|
119
|
+
* Gọi bởi RouteServiceProvider.boot() sau khi tất cả providers đã boot.
|
|
120
|
+
*
|
|
121
|
+
* Thực hiện:
|
|
122
|
+
* 1. configure() — load routes, mode, guards từ config
|
|
123
|
+
* 2. Wire ViewManager — lấy từ App.View nếu chưa set thủ công
|
|
124
|
+
*/
|
|
100
125
|
init(config) {
|
|
126
|
+
if (config && Object.keys(config).length > 0) {
|
|
127
|
+
this.configure(config);
|
|
128
|
+
}
|
|
129
|
+
// Auto-wire ViewManager nếu App đã có View registered
|
|
130
|
+
if (this.App && !this.viewManager) {
|
|
131
|
+
try {
|
|
132
|
+
const vm = this.App.View ?? this.App.get?.('View') ?? null;
|
|
133
|
+
if (vm)
|
|
134
|
+
this.viewManager = vm;
|
|
135
|
+
}
|
|
136
|
+
catch (_) { /* View chưa sẵn sàng — sẽ fallback lúc handleRoute */ }
|
|
137
|
+
}
|
|
101
138
|
return this;
|
|
102
139
|
}
|
|
103
140
|
setApp(app) {
|
|
@@ -139,10 +176,63 @@ export class Router {
|
|
|
139
176
|
return this;
|
|
140
177
|
}
|
|
141
178
|
/**
|
|
142
|
-
*
|
|
179
|
+
* Nối path con vào path cha. Con bắt đầu bằng '/' là ĐƯỜNG TUYỆT ĐỐI —
|
|
180
|
+
* bỏ qua prefix cha (lối thoát cho route lệch khỏi cây, như Vue Router).
|
|
181
|
+
* Con rỗng ('') = index route, trùng đúng path cha.
|
|
182
|
+
*/
|
|
183
|
+
static joinRoutePath(parent, child) {
|
|
184
|
+
if (child.startsWith('/'))
|
|
185
|
+
return child;
|
|
186
|
+
const base = parent.replace(/\/+$/, '');
|
|
187
|
+
if (!child)
|
|
188
|
+
return base || '/';
|
|
189
|
+
return `${base}/${child}`.replace(/\/{2,}/g, '/');
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Trải cây route thành bảng phẳng — bảng phẳng là thứ `matchRoute()` duyệt,
|
|
193
|
+
* và nó khớp THEO THỨ TỰ (first match wins), nên thứ tự emit ở đây chính là
|
|
194
|
+
* độ ưu tiên: giữ nguyên thứ tự khai báo để `/users/profile` đứng trước
|
|
195
|
+
* `/users/{id}` đúng như người viết mong đợi.
|
|
196
|
+
*
|
|
197
|
+
* Cha có `children` mà KHÔNG có `component` = nhóm thuần tuý (gom prefix +
|
|
198
|
+
* meta dùng chung), không sinh route nào cho chính nó.
|
|
199
|
+
*
|
|
200
|
+
* Index child (`path: ''`) sinh ra đúng path của cha → khi đó bỏ route
|
|
201
|
+
* riêng của cha, vì khai báo con là chỉ định cụ thể hơn cho URL đó.
|
|
202
|
+
*/
|
|
203
|
+
flattenRouteTree(routes, parentPath = '', parentMeta = {}) {
|
|
204
|
+
const flat = [];
|
|
205
|
+
for (const route of routes) {
|
|
206
|
+
const { children, ...own } = route;
|
|
207
|
+
const path = Router.joinRoutePath(parentPath, route.path ?? '');
|
|
208
|
+
const meta = { ...parentMeta, ...(route.meta ?? {}) };
|
|
209
|
+
const flatChildren = children && children.length
|
|
210
|
+
? this.flattenRouteTree(children, path, meta)
|
|
211
|
+
: [];
|
|
212
|
+
const component = route.component || route.view || '';
|
|
213
|
+
const coveredByIndexChild = flatChildren.some(c => c.path === path);
|
|
214
|
+
if (component && !coveredByIndexChild) {
|
|
215
|
+
flat.push({ ...own, path, meta });
|
|
216
|
+
}
|
|
217
|
+
flat.push(...flatChildren);
|
|
218
|
+
}
|
|
219
|
+
return flat;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Add multiple routes at once. Chấp nhận cây lồng qua `children` — mọi
|
|
223
|
+
* đường vào (`configure`, `replaceRoutes`) đều đi qua đây nên chỉ cần
|
|
224
|
+
* flatten một chỗ.
|
|
143
225
|
*/
|
|
144
226
|
addRoutes(routes) {
|
|
227
|
+
// Route KHÔNG có component (API, redirect, fallback) vẫn cần TÊN để
|
|
228
|
+
// sinh URL — chỉ không vào bảng khớp path. flattenRouteTree lọc chúng
|
|
229
|
+
// ra khỏi nhánh khớp, nên đăng ký tên riêng ở đây.
|
|
145
230
|
for (const route of routes) {
|
|
231
|
+
if (route.name && !(route.component || route.view)) {
|
|
232
|
+
this.addRouteConfig(route);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
for (const route of this.flattenRouteTree(routes)) {
|
|
146
236
|
const component = route.component || route.view || '';
|
|
147
237
|
this.addRoute(route.path, component, route.meta || {});
|
|
148
238
|
if (route.name) {
|
|
@@ -151,6 +241,13 @@ export class Router {
|
|
|
151
241
|
}
|
|
152
242
|
return this;
|
|
153
243
|
}
|
|
244
|
+
/** Atomically replace the materialized route table for a new context revision. */
|
|
245
|
+
replaceRoutes(routes) {
|
|
246
|
+
this.routes = [];
|
|
247
|
+
this.routeConfigs = {};
|
|
248
|
+
this.routeCache.clear();
|
|
249
|
+
return this.addRoutes(routes);
|
|
250
|
+
}
|
|
154
251
|
/**
|
|
155
252
|
* Configure router from a config object.
|
|
156
253
|
*/
|
|
@@ -161,14 +258,17 @@ export class Router {
|
|
|
161
258
|
this.base = config.base;
|
|
162
259
|
if (config.defaultRoute)
|
|
163
260
|
this.defaultRoute = config.defaultRoute;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
261
|
+
// MỘT danh sách thôi. Server từng phát cả `routes` (chỉ route có
|
|
262
|
+
// component) lẫn `allRoutes` (đủ) — tập con của nhau, nên đăng ký cả
|
|
263
|
+
// hai là mỗi route vào bảng hai lần. `allRoutes` giữ lại làm bí danh
|
|
264
|
+
// cho payload cũ.
|
|
265
|
+
const routes = config.routes ?? config.allRoutes;
|
|
266
|
+
if (routes)
|
|
267
|
+
this.addRoutes(routes);
|
|
168
268
|
if (config.beforeEach)
|
|
169
269
|
this._beforeEach = config.beforeEach;
|
|
170
270
|
if (config.afterEach)
|
|
171
|
-
this.
|
|
271
|
+
this.afterEach(config.afterEach);
|
|
172
272
|
return this;
|
|
173
273
|
}
|
|
174
274
|
// ─── Guards ─────────────────────────────────────────────────
|
|
@@ -176,23 +276,19 @@ export class Router {
|
|
|
176
276
|
this._beforeEach = guard;
|
|
177
277
|
return this;
|
|
178
278
|
}
|
|
279
|
+
/** Đăng ký hook chạy sau mỗi lần điều hướng. Trả về hàm HUỶ đăng ký. */
|
|
179
280
|
afterEach(hook) {
|
|
180
|
-
this.
|
|
181
|
-
return this;
|
|
281
|
+
this.afterHooks.add(hook);
|
|
282
|
+
return () => { this.afterHooks.delete(hook); };
|
|
182
283
|
}
|
|
183
284
|
// ─── Navigation ─────────────────────────────────────────────
|
|
184
285
|
/**
|
|
185
286
|
* Navigate to a URL path.
|
|
287
|
+
* History chỉ được cập nhật SAU khi guard cho phép (trong handleRoute) —
|
|
288
|
+
* guard chặn thì URL không đổi, tránh desync URL ↔ view.
|
|
186
289
|
*/
|
|
187
290
|
navigate(path) {
|
|
188
|
-
|
|
189
|
-
window.history.pushState({}, '', path);
|
|
190
|
-
}
|
|
191
|
-
else {
|
|
192
|
-
window.location.hash = path;
|
|
193
|
-
}
|
|
194
|
-
this.handleRoute(path);
|
|
195
|
-
this.currentUri = path;
|
|
291
|
+
this.requestNavigation(path, 'push');
|
|
196
292
|
}
|
|
197
293
|
/** Alias for navigate */
|
|
198
294
|
push(path) {
|
|
@@ -214,14 +310,7 @@ export class Router {
|
|
|
214
310
|
* Replace current history entry without adding to stack.
|
|
215
311
|
*/
|
|
216
312
|
replace(path) {
|
|
217
|
-
|
|
218
|
-
window.history.replaceState({}, '', path);
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
window.location.hash = path;
|
|
222
|
-
}
|
|
223
|
-
this.handleRoute(path);
|
|
224
|
-
this.currentUri = path;
|
|
313
|
+
this.requestNavigation(path, 'replace');
|
|
225
314
|
}
|
|
226
315
|
/**
|
|
227
316
|
* Go back in history.
|
|
@@ -333,9 +422,10 @@ export class Router {
|
|
|
333
422
|
window.addEventListener('hashchange', this._handlePopState);
|
|
334
423
|
}
|
|
335
424
|
document.addEventListener('click', this._handleAutoNavigation);
|
|
425
|
+
window.addEventListener('saola:view-context', this._handleViewContextChange);
|
|
336
426
|
// Handle initial route
|
|
337
427
|
if (!skipInitial) {
|
|
338
|
-
this.
|
|
428
|
+
this.requestNavigation(initialPath, 'initial');
|
|
339
429
|
}
|
|
340
430
|
this.isStarted = true;
|
|
341
431
|
}
|
|
@@ -346,39 +436,150 @@ export class Router {
|
|
|
346
436
|
window.removeEventListener('popstate', this._handlePopState);
|
|
347
437
|
window.removeEventListener('hashchange', this._handlePopState);
|
|
348
438
|
document.removeEventListener('click', this._handleAutoNavigation);
|
|
439
|
+
window.removeEventListener('saola:view-context', this._handleViewContextChange);
|
|
349
440
|
this.isStarted = false;
|
|
350
441
|
}
|
|
442
|
+
handleViewContextChange(event) {
|
|
443
|
+
const state = event.detail;
|
|
444
|
+
if (!state || state.changed !== true)
|
|
445
|
+
return;
|
|
446
|
+
const vm = this.viewManager ?? this.App?.View;
|
|
447
|
+
const target = this.activeNavigationUrl || this.currentUri
|
|
448
|
+
|| (typeof window !== 'undefined'
|
|
449
|
+
? window.location.pathname + window.location.search + window.location.hash
|
|
450
|
+
: '/');
|
|
451
|
+
if (vm?.requiresReloadForViewContext?.(state)) {
|
|
452
|
+
vm.cancelNavigation?.();
|
|
453
|
+
this.reloadForViewContext(target, state);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const applied = vm?.applyViewContext?.(state) ?? false;
|
|
457
|
+
if (!applied)
|
|
458
|
+
return;
|
|
459
|
+
if (Array.isArray(state.routes)) {
|
|
460
|
+
this.replaceRoutes(state.routes);
|
|
461
|
+
}
|
|
462
|
+
// If a fetch discovered the change mid-navigation, requestNavigation
|
|
463
|
+
// queues the same target and invalidates the old render generation.
|
|
464
|
+
if (target)
|
|
465
|
+
this.requestNavigation(target, 'replace');
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Switch to a coherent server-rendered document when the view namespace
|
|
469
|
+
* changes. A short-lived token prevents a bad deployment from reloading the
|
|
470
|
+
* same URL/revision forever.
|
|
471
|
+
*/
|
|
472
|
+
reloadForViewContext(target, state) {
|
|
473
|
+
if (typeof window === 'undefined')
|
|
474
|
+
return;
|
|
475
|
+
const url = new URL(target || '/', window.location.href);
|
|
476
|
+
const revision = typeof state.revision === 'string' ? state.revision : 'unknown';
|
|
477
|
+
const token = `${revision}:${url.pathname}${url.search}`;
|
|
478
|
+
const storageKey = 'saola:view-context-reload';
|
|
479
|
+
const now = Date.now();
|
|
480
|
+
try {
|
|
481
|
+
const previousRaw = window.sessionStorage.getItem(storageKey);
|
|
482
|
+
const previous = previousRaw ? JSON.parse(previousRaw) : null;
|
|
483
|
+
if (previous?.token === token && typeof previous.at === 'number' && now - previous.at < 15000) {
|
|
484
|
+
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.';
|
|
485
|
+
console.error(`[Router] ${message}`, { revision, target: url.href });
|
|
486
|
+
(this.viewManager ?? this.App?.View)?.showError?.(message, {
|
|
487
|
+
revision,
|
|
488
|
+
target: url.href,
|
|
489
|
+
});
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
window.sessionStorage.setItem(storageKey, JSON.stringify({ token, at: now }));
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
// Storage có thể bị chặn; coherence vẫn quan trọng hơn loop guard.
|
|
496
|
+
}
|
|
497
|
+
window.dispatchEvent(new CustomEvent('saola:view-context-reload', {
|
|
498
|
+
detail: { ...state, target: url.href },
|
|
499
|
+
}));
|
|
500
|
+
window.location.replace(url.href);
|
|
501
|
+
}
|
|
351
502
|
/**
|
|
352
503
|
* Full destroy — cleanup everything.
|
|
353
504
|
*/
|
|
354
505
|
destroy() {
|
|
355
506
|
this.stop();
|
|
507
|
+
this.navigationSequence++;
|
|
508
|
+
this.pendingNavigation = null;
|
|
509
|
+
this.activeNavigationUrl = null;
|
|
510
|
+
this.viewManager?.cancelNavigation?.();
|
|
356
511
|
this.routes = [];
|
|
357
512
|
this.routeConfigs = {};
|
|
358
513
|
this.routeCache.clear();
|
|
359
514
|
this._beforeEach = null;
|
|
360
|
-
this.
|
|
515
|
+
this.afterHooks.clear();
|
|
361
516
|
this.currentRoute = null;
|
|
517
|
+
this.liveRegion?.remove();
|
|
518
|
+
this.liveRegion = null;
|
|
362
519
|
Router.activeRoute = null;
|
|
363
520
|
}
|
|
364
521
|
// ─── Helper ────────────────────────────────
|
|
365
522
|
getFullUrl() {
|
|
366
523
|
if (typeof window === 'undefined')
|
|
367
524
|
return '';
|
|
525
|
+
if (this.activeNavigationUrl) {
|
|
526
|
+
return new URL(this.activeNavigationUrl, window.location.href).href;
|
|
527
|
+
}
|
|
368
528
|
return window.location.href;
|
|
369
529
|
}
|
|
370
|
-
// ─── Internal: Route Handling ────────────────────────────────
|
|
371
530
|
/**
|
|
372
|
-
*
|
|
531
|
+
* Tách một URL string thành pathname / query string / fragment.
|
|
532
|
+
* PHẢI tách query + hash TRƯỚC khi match route — pattern chỉ match pathname.
|
|
533
|
+
*/
|
|
534
|
+
splitLocation(raw) {
|
|
535
|
+
let rest = raw ?? '';
|
|
536
|
+
let fragment = '';
|
|
537
|
+
const hashIdx = rest.indexOf('#');
|
|
538
|
+
if (hashIdx !== -1) {
|
|
539
|
+
fragment = rest.slice(hashIdx + 1);
|
|
540
|
+
rest = rest.slice(0, hashIdx);
|
|
541
|
+
}
|
|
542
|
+
let queryString = '';
|
|
543
|
+
const queryIdx = rest.indexOf('?');
|
|
544
|
+
if (queryIdx !== -1) {
|
|
545
|
+
queryString = rest.slice(queryIdx + 1);
|
|
546
|
+
rest = rest.slice(0, queryIdx);
|
|
547
|
+
}
|
|
548
|
+
return { pathname: rest, queryString, fragment };
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Điểm vào duy nhất cho mọi navigation (navigate/replace/popstate/initial).
|
|
552
|
+
* Đang navigate dở → ghi nhận request MỚI NHẤT, xử lý sau khi xong
|
|
553
|
+
* (không drop im lặng như trước).
|
|
373
554
|
*/
|
|
374
|
-
|
|
375
|
-
|
|
555
|
+
requestNavigation(path, type) {
|
|
556
|
+
const request = { id: ++this.navigationSequence, path, type };
|
|
557
|
+
if (this.isNavigating) {
|
|
558
|
+
this.pendingNavigation = request;
|
|
559
|
+
// Cho ViewManager thoát sớm khỏi fetch/render cũ. Request mới nhất
|
|
560
|
+
// sẽ được chạy trong finally, không commit DOM/history trung gian.
|
|
561
|
+
(this.viewManager ?? this.App?.View)?.cancelNavigation?.();
|
|
376
562
|
return;
|
|
563
|
+
}
|
|
564
|
+
void this.handleRoute(path, type, request.id);
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Core route handler — prepare/guard/render trước, chỉ commit history +
|
|
568
|
+
* active route sau khi ViewManager đã mount/hydrate thành công.
|
|
569
|
+
*/
|
|
570
|
+
async handleRoute(path, type = 'push', requestId = ++this.navigationSequence) {
|
|
377
571
|
this.isNavigating = true;
|
|
572
|
+
// Khai ngoài try: `finally` phải nhả được kể cả khi render ném giữa chừng.
|
|
573
|
+
let releaseHeight = () => { };
|
|
378
574
|
try {
|
|
379
|
-
const
|
|
380
|
-
const
|
|
381
|
-
|
|
575
|
+
const { pathname, queryString, fragment } = this.splitLocation(path);
|
|
576
|
+
const normalizedPath = this.normalizePath(pathname);
|
|
577
|
+
// Request URI = path + query, KHÔNG gồm hash — cache key của ViewManager
|
|
578
|
+
const uri = queryString ? `${normalizedPath}?${queryString}` : normalizedPath;
|
|
579
|
+
const query = this.parseQuery(queryString);
|
|
580
|
+
// popstate/hashchange echo về đúng URI đang đứng → bỏ qua
|
|
581
|
+
if (type === 'pop' && uri === this.currentUri)
|
|
582
|
+
return;
|
|
382
583
|
const match = this.matchRoute(normalizedPath);
|
|
383
584
|
if (!match) {
|
|
384
585
|
console.warn(`[Router] No route matched: ${path}`);
|
|
@@ -387,62 +588,227 @@ export class Router {
|
|
|
387
588
|
const { route, params } = match;
|
|
388
589
|
const from = this.currentRoute;
|
|
389
590
|
// Create ActiveRoute
|
|
390
|
-
const activeRoute = new ActiveRoute(route, normalizedPath, params, query, fragment);
|
|
391
|
-
// Before guard
|
|
591
|
+
const activeRoute = new ActiveRoute(route, normalizedPath, params, query, fragment, uri);
|
|
592
|
+
// Before guard — chạy TRƯỚC khi đụng vào history: guard chặn thì URL giữ nguyên
|
|
392
593
|
if (this._beforeEach) {
|
|
393
594
|
const allow = await this._beforeEach(route, from, normalizedPath);
|
|
394
595
|
if (allow === false)
|
|
395
596
|
return;
|
|
396
597
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
598
|
+
if (requestId !== this.navigationSequence)
|
|
599
|
+
return;
|
|
600
|
+
const historyTarget = fragment ? `${uri}#${fragment}` : uri;
|
|
601
|
+
this.activeNavigationUrl = historyTarget;
|
|
602
|
+
// Mount view via ViewManager.
|
|
603
|
+
// Route ĐẦU TIÊN sau SSR: nếu view khớp entry server đã render →
|
|
604
|
+
// hydrateView (claim DOM) thay vì mountView. consumeSSRViewId() chỉ
|
|
605
|
+
// trả id 1 lần → các navigate sau là CSR (SPA takeover).
|
|
401
606
|
const viewComponent = route.component || route.view;
|
|
607
|
+
let transitionSucceeded = true;
|
|
608
|
+
// View cũ bị gỡ trước khi cây mới dựng xong, nên tài liệu sập chiều
|
|
609
|
+
// cao trong vài frame — footer nhảy vọt lên, thanh cuộn biến mất,
|
|
610
|
+
// trình duyệt kẹp lại scrollY. Giữ chiều cao cũ tới khi thay xong.
|
|
611
|
+
releaseHeight = this.freezeDocumentHeight();
|
|
402
612
|
if (viewComponent) {
|
|
403
|
-
|
|
404
|
-
|
|
613
|
+
const vm = this.viewManager ?? this.App?.View;
|
|
614
|
+
if (vm) {
|
|
615
|
+
const ssrViewId = vm.consumeSSRViewId?.(viewComponent) ?? null;
|
|
616
|
+
let result;
|
|
617
|
+
if (ssrViewId) {
|
|
618
|
+
result = await vm.hydrateView(viewComponent, { __SSR_VIEW_ID__: ssrViewId, ...params }, activeRoute);
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
result = await vm.mountView(viewComponent, params, activeRoute, type === 'pop' ? 'pop' : 'push');
|
|
622
|
+
}
|
|
623
|
+
// mountView(null) có thể là duplicate no-op; xác nhận chain
|
|
624
|
+
// active đang thật sự thuộc URI đích trước khi commit Router.
|
|
625
|
+
transitionSucceeded = result != null
|
|
626
|
+
|| vm.getCurrentView?.()?.__ctrl__?.urlPath === uri;
|
|
405
627
|
}
|
|
406
|
-
else
|
|
407
|
-
|
|
408
|
-
await this.App.View.mountView(viewComponent, params, activeRoute);
|
|
628
|
+
else {
|
|
629
|
+
transitionSucceeded = false;
|
|
409
630
|
}
|
|
410
631
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
632
|
+
if (requestId !== this.navigationSequence)
|
|
633
|
+
return;
|
|
634
|
+
if (!transitionSucceeded) {
|
|
635
|
+
if (type === 'pop')
|
|
636
|
+
this.restoreUrlAfterFailedPop(from);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
// Commit point: từ đây URL, Router state và mounted view cùng
|
|
640
|
+
// đại diện cho một navigation duy nhất.
|
|
641
|
+
if (this.mode === 'history') {
|
|
642
|
+
if (type === 'push')
|
|
643
|
+
window.history.pushState({}, '', historyTarget);
|
|
644
|
+
else if (type === 'replace')
|
|
645
|
+
window.history.replaceState({}, '', historyTarget);
|
|
646
|
+
}
|
|
647
|
+
else if (type === 'push' || type === 'replace') {
|
|
648
|
+
window.location.hash = historyTarget;
|
|
649
|
+
}
|
|
650
|
+
Router.activeRoute = activeRoute;
|
|
651
|
+
this.currentRoute = activeRoute;
|
|
652
|
+
this.currentUri = uri;
|
|
653
|
+
this.applyScroll(type, fragment);
|
|
654
|
+
this.announceNavigation(type);
|
|
655
|
+
// After hook — chạy trên BẢN SAO: hook được phép tự huỷ đăng ký trong
|
|
656
|
+
// lúc chạy. Bọc try/catch từng hook: một hook hỏng (view đã destroy
|
|
657
|
+
// 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.
|
|
658
|
+
for (const hook of [...this.afterHooks]) {
|
|
659
|
+
try {
|
|
660
|
+
hook({ ...route, path: normalizedPath }, from);
|
|
661
|
+
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
console.error('[Router] afterEach hook lỗi:', error);
|
|
664
|
+
}
|
|
414
665
|
}
|
|
415
666
|
}
|
|
416
667
|
catch (error) {
|
|
417
668
|
console.error('[Router] Navigation error:', error);
|
|
418
669
|
}
|
|
419
670
|
finally {
|
|
671
|
+
releaseHeight();
|
|
420
672
|
this.isNavigating = false;
|
|
673
|
+
if (requestId === this.navigationSequence)
|
|
674
|
+
this.activeNavigationUrl = null;
|
|
675
|
+
// Có navigation đến trong lúc đang xử lý → chạy request mới nhất
|
|
676
|
+
const pending = this.pendingNavigation;
|
|
677
|
+
this.pendingNavigation = null;
|
|
678
|
+
if (pending) {
|
|
679
|
+
void this.handleRoute(pending.path, pending.type, pending.id);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/** Pop đã đổi address bar; render fail thì đưa URL về chain còn active. */
|
|
684
|
+
restoreUrlAfterFailedPop(from) {
|
|
685
|
+
if (this.mode !== 'history' || !from)
|
|
686
|
+
return;
|
|
687
|
+
const target = from.$fragment ? `${from.$uri}#${from.$fragment}` : from.$uri;
|
|
688
|
+
window.history.replaceState(window.history.state, '', target);
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Ghim chiều cao tài liệu bằng đúng chiều cao hiện tại, trả về hàm nhả.
|
|
692
|
+
* Chỉ chạm `min-height` của body nên không đụng layout khi đã nhả.
|
|
693
|
+
*/
|
|
694
|
+
freezeDocumentHeight() {
|
|
695
|
+
if (typeof document === 'undefined' || !document.body)
|
|
696
|
+
return () => { };
|
|
697
|
+
const body = document.body;
|
|
698
|
+
const previous = body.style.minHeight;
|
|
699
|
+
try {
|
|
700
|
+
body.style.minHeight = `${document.documentElement.scrollHeight}px`;
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
return () => { };
|
|
704
|
+
}
|
|
705
|
+
return () => { try {
|
|
706
|
+
body.style.minHeight = previous;
|
|
707
|
+
}
|
|
708
|
+
catch { /* noop */ } };
|
|
709
|
+
}
|
|
710
|
+
applyScroll(type, fragment) {
|
|
711
|
+
if (typeof window === 'undefined')
|
|
712
|
+
return;
|
|
713
|
+
if (fragment) {
|
|
714
|
+
let id = fragment;
|
|
715
|
+
try {
|
|
716
|
+
id = decodeURIComponent(fragment);
|
|
717
|
+
}
|
|
718
|
+
catch { /* malformed fragment */ }
|
|
719
|
+
const target = document.getElementById(id) || document.getElementsByName(id)[0];
|
|
720
|
+
target?.scrollIntoView?.();
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
// PageCache tự khôi phục pop position; initial giữ vị trí SSR/browser.
|
|
724
|
+
if (type === 'push' || type === 'replace') {
|
|
725
|
+
// scrollTo(0,0) tuân theo `html { scroll-behavior: smooth }` nếu trang
|
|
726
|
+
// có đặt — biến cú nhảy về đầu thành cuộn chậm ~0.4s chạy SAU khi nội
|
|
727
|
+
// dung đã thay xong, nhìn như treo. Đổi route phải tức thì; smooth để
|
|
728
|
+
// dành cho anchor trong trang (nhánh fragment ở trên).
|
|
729
|
+
try {
|
|
730
|
+
window.scrollTo({ top: 0, left: 0, behavior: 'instant' });
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
// Engine cũ chưa biết 'instant' sẽ ném khi validate enum.
|
|
734
|
+
try {
|
|
735
|
+
window.scrollTo(0, 0);
|
|
736
|
+
}
|
|
737
|
+
catch { /* non-browser/test runtime */ }
|
|
738
|
+
}
|
|
421
739
|
}
|
|
422
740
|
}
|
|
741
|
+
/**
|
|
742
|
+
* A11y sau khi điều hướng xong (SPA không tự làm được như full page load):
|
|
743
|
+
* 1. Đưa focus về container view — không làm thì bàn phím vẫn ở link cũ
|
|
744
|
+
* của trang TRƯỚC, Tab tiếp tục từ vị trí không còn tồn tại.
|
|
745
|
+
* 2. Đọc tên trang mới qua live region — screen reader không hề biết
|
|
746
|
+
* nội dung đã đổi vì document không reload.
|
|
747
|
+
*
|
|
748
|
+
* `preventScroll` để không phá `applyScroll()` vừa chạy. Bỏ qua `initial`:
|
|
749
|
+
* lần paint đầu/hydrate không được cướp focus khỏi thứ user đang thao tác.
|
|
750
|
+
*/
|
|
751
|
+
announceNavigation(type) {
|
|
752
|
+
if (typeof document === 'undefined' || type === 'initial')
|
|
753
|
+
return;
|
|
754
|
+
const container = (this.viewManager ?? this.App?.View)?.getContainer?.();
|
|
755
|
+
if (container instanceof HTMLElement) {
|
|
756
|
+
// tabindex=-1: focus được bằng script nhưng KHÔNG chen vào thứ tự Tab.
|
|
757
|
+
if (!container.hasAttribute('tabindex'))
|
|
758
|
+
container.setAttribute('tabindex', '-1');
|
|
759
|
+
try {
|
|
760
|
+
container.focus({ preventScroll: true });
|
|
761
|
+
}
|
|
762
|
+
catch { /* jsdom/older browsers */ }
|
|
763
|
+
}
|
|
764
|
+
if (!this.liveRegion || !this.liveRegion.isConnected) {
|
|
765
|
+
const region = document.createElement('div');
|
|
766
|
+
region.setAttribute('aria-live', 'polite');
|
|
767
|
+
region.setAttribute('aria-atomic', 'true');
|
|
768
|
+
// Ẩn khỏi mắt nhưng KHÔNG display:none / visibility:hidden —
|
|
769
|
+
// hai cái đó làm screen reader bỏ qua luôn.
|
|
770
|
+
region.style.cssText =
|
|
771
|
+
'position:absolute;width:1px;height:1px;margin:-1px;padding:0;'
|
|
772
|
+
+ 'overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0';
|
|
773
|
+
document.body.appendChild(region);
|
|
774
|
+
this.liveRegion = region;
|
|
775
|
+
}
|
|
776
|
+
// Ghi ở frame sau: nội dung mới phải khác nội dung cũ thì AT mới đọc lại.
|
|
777
|
+
const label = document.title || this.currentUri || '';
|
|
778
|
+
this.liveRegion.textContent = '';
|
|
779
|
+
requestAnimationFrame(() => {
|
|
780
|
+
if (this.liveRegion)
|
|
781
|
+
this.liveRegion.textContent = label;
|
|
782
|
+
});
|
|
783
|
+
}
|
|
423
784
|
/**
|
|
424
785
|
* Handle browser back/forward.
|
|
425
786
|
*/
|
|
426
787
|
handlePopState() {
|
|
427
788
|
const path = this.mode === 'history'
|
|
428
|
-
? window.location.pathname + window.location.search
|
|
789
|
+
? window.location.pathname + window.location.search + window.location.hash
|
|
429
790
|
: window.location.hash.slice(1) || this.defaultRoute;
|
|
430
|
-
this.
|
|
791
|
+
this.requestNavigation(path, 'pop');
|
|
431
792
|
}
|
|
432
793
|
/**
|
|
433
794
|
* Auto-navigation: intercept clicks on <a>, [data-nav-link], [data-navigate].
|
|
434
795
|
*/
|
|
435
796
|
handleAutoNavigation(e) {
|
|
797
|
+
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
|
|
798
|
+
return;
|
|
436
799
|
const target = e.target;
|
|
800
|
+
if (!target?.closest)
|
|
801
|
+
return;
|
|
437
802
|
// 1. [data-nav-link] — highest priority
|
|
438
803
|
const navLinkEl = target.closest('[data-nav-link]');
|
|
439
804
|
if (navLinkEl) {
|
|
440
805
|
if (navLinkEl.hasAttribute('data-nav-disabled'))
|
|
441
806
|
return;
|
|
442
807
|
const navPath = navLinkEl.getAttribute('data-nav-link');
|
|
443
|
-
if (navPath && navPath.trim() !== ''
|
|
808
|
+
if (navPath && navPath.trim() !== '') {
|
|
444
809
|
e.preventDefault();
|
|
445
|
-
this.
|
|
810
|
+
if (navPath !== this.currentUri)
|
|
811
|
+
this.navigate(navPath);
|
|
446
812
|
return;
|
|
447
813
|
}
|
|
448
814
|
}
|
|
@@ -452,9 +818,10 @@ export class Router {
|
|
|
452
818
|
if (navigateEl.hasAttribute('data-nav-disabled'))
|
|
453
819
|
return;
|
|
454
820
|
const navPath = navigateEl.getAttribute('data-navigate');
|
|
455
|
-
if (navPath && navPath.trim() !== ''
|
|
821
|
+
if (navPath && navPath.trim() !== '') {
|
|
456
822
|
e.preventDefault();
|
|
457
|
-
this.
|
|
823
|
+
if (navPath !== this.currentUri)
|
|
824
|
+
this.navigate(navPath);
|
|
458
825
|
return;
|
|
459
826
|
}
|
|
460
827
|
}
|
|
@@ -463,7 +830,9 @@ export class Router {
|
|
|
463
830
|
if (!link)
|
|
464
831
|
return;
|
|
465
832
|
// Skip: target="_blank", disabled, special protocols
|
|
466
|
-
if (link.target
|
|
833
|
+
if (link.target && link.target !== '_self')
|
|
834
|
+
return;
|
|
835
|
+
if (link.hasAttribute('download'))
|
|
467
836
|
return;
|
|
468
837
|
if (link.dataset.nav === 'disabled' || link.dataset.nav === 'false')
|
|
469
838
|
return;
|
|
@@ -476,25 +845,29 @@ export class Router {
|
|
|
476
845
|
const currentUrl = new URL(window.location.href);
|
|
477
846
|
if (linkUrl.host !== currentUrl.host)
|
|
478
847
|
return; // External
|
|
479
|
-
const path = linkUrl.pathname + linkUrl.search;
|
|
480
|
-
|
|
481
|
-
|
|
848
|
+
const path = linkUrl.pathname + linkUrl.search + linkUrl.hash;
|
|
849
|
+
// Link trỏ về đúng route đang đứng vẫn phải nuốt click. Thoát sớm mà
|
|
850
|
+
// không preventDefault là để browser tự điều hướng — reload cả tài
|
|
851
|
+
// liệu, mất sạch state SPA. Đây là logo và mục menu đang active.
|
|
482
852
|
e.preventDefault();
|
|
853
|
+
if (path === this.currentUri && !linkUrl.hash)
|
|
854
|
+
return; // Same page — no-op
|
|
483
855
|
this.navigate(path);
|
|
484
856
|
}
|
|
485
857
|
catch {
|
|
486
858
|
// Relative URL
|
|
487
859
|
if (href && !href.startsWith('http') && !href.startsWith('//')) {
|
|
488
|
-
if (href === this.currentUri)
|
|
489
|
-
return;
|
|
490
860
|
e.preventDefault();
|
|
861
|
+
if (href === this.currentUri)
|
|
862
|
+
return; // Same page — no-op
|
|
491
863
|
this.navigate(href);
|
|
492
864
|
}
|
|
493
865
|
}
|
|
494
866
|
}
|
|
495
867
|
// ─── Internal: Pattern Matching ─────────────────────────────
|
|
496
868
|
matchRoute(path) {
|
|
497
|
-
|
|
869
|
+
// Defensive: strip query/hash nếu caller truyền URI đầy đủ
|
|
870
|
+
const normalizedPath = this.normalizePath(this.splitLocation(path).pathname);
|
|
498
871
|
if (this.routeCache.has(normalizedPath)) {
|
|
499
872
|
return this.routeCache.get(normalizedPath);
|
|
500
873
|
}
|
|
@@ -550,7 +923,9 @@ export class Router {
|
|
|
550
923
|
const escaped = seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
551
924
|
regexParts.push('\\/' + escaped);
|
|
552
925
|
}
|
|
553
|
-
|
|
926
|
+
// Root path '/' → mọi segment rỗng → regexParts rỗng. Phải khớp '\/'
|
|
927
|
+
// (nếu để '^$' thì route '/' không bao giờ match — bug gốc).
|
|
928
|
+
const regex = new RegExp(`^${regexParts.join('') || '\\/'}$`);
|
|
554
929
|
const match = normalizedPath.match(regex);
|
|
555
930
|
if (!match)
|
|
556
931
|
return null;
|
|
@@ -578,12 +953,13 @@ export class Router {
|
|
|
578
953
|
return query;
|
|
579
954
|
}
|
|
580
955
|
setActiveRouteForPath(path) {
|
|
581
|
-
const
|
|
956
|
+
const { pathname, queryString, fragment } = this.splitLocation(path);
|
|
957
|
+
const normalizedPath = this.normalizePath(pathname);
|
|
582
958
|
const match = this.matchRoute(normalizedPath);
|
|
583
959
|
if (match) {
|
|
584
|
-
const
|
|
585
|
-
const
|
|
586
|
-
const activeRoute = new ActiveRoute(match.route, normalizedPath, match.params, query, fragment);
|
|
960
|
+
const uri = queryString ? `${normalizedPath}?${queryString}` : normalizedPath;
|
|
961
|
+
const query = this.parseQuery(queryString || window.location.search);
|
|
962
|
+
const activeRoute = new ActiveRoute(match.route, normalizedPath, match.params, query, fragment || window.location.hash.substring(1), uri);
|
|
587
963
|
Router.activeRoute = activeRoute;
|
|
588
964
|
this.currentRoute = activeRoute;
|
|
589
965
|
}
|