@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
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PageCache — bfcache-style cache cho trang đã ghé (ROUTE_RENDER_FLOW.md §8).
|
|
3
|
+
*
|
|
4
|
+
* Key = `${viewName}::${requestUri}` — requestUri gồm path + query, KHÔNG gồm
|
|
5
|
+
* hash (ViewManager.cacheKey). Value = view chain (instance còn sống, state
|
|
6
|
+
* nguyên vẹn) + DOM đã detach + scroll position.
|
|
7
|
+
*
|
|
8
|
+
* Chính sách:
|
|
9
|
+
* - LRU max `maxEntries` (mặc định 10)
|
|
10
|
+
* - TTL mặc định 10 phút — entry quá hạn bị destroy thật + xoá khỏi cache
|
|
11
|
+
* - Hết hạn kiểm tra 3 tầng: lazy khi take(), sweep() khi navigate,
|
|
12
|
+
* sweep() khi tab visible lại (đăng ký ở bootstrap)
|
|
13
|
+
* - Trang đang active KHÔNG nằm trong cache (take() chuyển ownership ra ngoài)
|
|
14
|
+
* - Page thuộc layout: DOM được detach THEO TỪNG OUTLET (outletContents);
|
|
15
|
+
* chỉ restore được khi layout đang mount trùng `layoutPath`.
|
|
16
|
+
*/
|
|
17
|
+
import type { ViewInterface } from "../contracts/ViewInterface.js";
|
|
18
|
+
|
|
19
|
+
/** Nội dung một outlet đã detach (page thuộc layout) */
|
|
20
|
+
export interface OutletContent {
|
|
21
|
+
fragment: DocumentFragment;
|
|
22
|
+
/** Saola elements (block content) — để re-track lifecycle khi restore */
|
|
23
|
+
children: any[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PageCacheEntry {
|
|
27
|
+
urlPath: string;
|
|
28
|
+
/** View chain (page + layouts không reuse), instance sống — state giữ nguyên */
|
|
29
|
+
views: ViewInterface[];
|
|
30
|
+
/** DOM đã detach (toàn bộ vùng wrapper, gồm cả comment markers) — standalone page */
|
|
31
|
+
fragment: DocumentFragment;
|
|
32
|
+
/** Page thuộc layout: path của layout lúc cache — restore đòi hỏi layout trùng */
|
|
33
|
+
layoutPath: string | null;
|
|
34
|
+
/** Page thuộc layout: DOM + children đã detach theo tên outlet */
|
|
35
|
+
outletContents: Map<string, OutletContent> | null;
|
|
36
|
+
scroll: { x: number; y: number };
|
|
37
|
+
savedAt: number;
|
|
38
|
+
ttl: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PageCacheSetOptions {
|
|
42
|
+
views: ViewInterface[];
|
|
43
|
+
fragment?: DocumentFragment;
|
|
44
|
+
layoutPath?: string | null;
|
|
45
|
+
outletContents?: Map<string, OutletContent> | null;
|
|
46
|
+
scroll?: { x: number; y: number };
|
|
47
|
+
/** Override TTL cho entry này (ms). 0 = không cache. */
|
|
48
|
+
ttl?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class PageCacheService {
|
|
52
|
+
/** Map giữ insertion order → dùng làm LRU */
|
|
53
|
+
private entries = new Map<string, PageCacheEntry>();
|
|
54
|
+
|
|
55
|
+
public maxEntries = 10;
|
|
56
|
+
/** 10 phút — trong khoảng thiết kế 5–15 phút; override per-view qua cache:{ttl} */
|
|
57
|
+
public defaultTTL = 10 * 60 * 1000;
|
|
58
|
+
|
|
59
|
+
/** Hook khi entry bị destroy (evict/expire/invalidate) — ViewManager dọn store tại đây */
|
|
60
|
+
public onEvict: ((entry: PageCacheEntry) => void) | null = null;
|
|
61
|
+
|
|
62
|
+
/** Injectable cho test */
|
|
63
|
+
public now: () => number = () => Date.now();
|
|
64
|
+
|
|
65
|
+
set(urlPath: string, options: PageCacheSetOptions): void {
|
|
66
|
+
const ttl = options.ttl ?? this.defaultTTL;
|
|
67
|
+
if (ttl <= 0) {
|
|
68
|
+
// Không cache — destroy luôn
|
|
69
|
+
this.destroyViews(options.views);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Đã có entry cùng path → destroy bản cũ trước
|
|
74
|
+
const existing = this.entries.get(urlPath);
|
|
75
|
+
if (existing) {
|
|
76
|
+
this.entries.delete(urlPath);
|
|
77
|
+
this.destroyEntry(existing);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.entries.set(urlPath, {
|
|
81
|
+
urlPath,
|
|
82
|
+
views: options.views,
|
|
83
|
+
fragment: options.fragment ?? document.createDocumentFragment(),
|
|
84
|
+
layoutPath: options.layoutPath ?? null,
|
|
85
|
+
outletContents: options.outletContents ?? null,
|
|
86
|
+
scroll: options.scroll ?? { x: 0, y: 0 },
|
|
87
|
+
savedAt: this.now(),
|
|
88
|
+
ttl,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// LRU evict
|
|
92
|
+
while (this.entries.size > this.maxEntries) {
|
|
93
|
+
const oldestKey = this.entries.keys().next().value as string;
|
|
94
|
+
const oldest = this.entries.get(oldestKey)!;
|
|
95
|
+
this.entries.delete(oldestKey);
|
|
96
|
+
this.destroyEntry(oldest);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Trả một entry ĐÃ take() về lại cache — dùng khi restore thất bại nhưng
|
|
102
|
+
* entry còn giá trị (vd layout của page chưa mount lại; lần điều hướng sau
|
|
103
|
+
* có thể restore được). GIỮ savedAt gốc — TTL KHÔNG được reset.
|
|
104
|
+
*/
|
|
105
|
+
putBack(entry: PageCacheEntry): void {
|
|
106
|
+
if (this.isExpired(entry)) {
|
|
107
|
+
this.destroyEntry(entry);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const existing = this.entries.get(entry.urlPath);
|
|
111
|
+
if (existing && existing !== entry) {
|
|
112
|
+
this.entries.delete(entry.urlPath);
|
|
113
|
+
this.destroyEntry(existing);
|
|
114
|
+
}
|
|
115
|
+
this.entries.set(entry.urlPath, entry);
|
|
116
|
+
|
|
117
|
+
while (this.entries.size > this.maxEntries) {
|
|
118
|
+
const oldestKey = this.entries.keys().next().value as string;
|
|
119
|
+
const oldest = this.entries.get(oldestKey)!;
|
|
120
|
+
this.entries.delete(oldestKey);
|
|
121
|
+
this.destroyEntry(oldest);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Lấy entry RA KHỎI cache (chuyển ownership cho caller để restore).
|
|
127
|
+
* Entry quá hạn → destroy, trả null.
|
|
128
|
+
*/
|
|
129
|
+
take(urlPath: string): PageCacheEntry | null {
|
|
130
|
+
const entry = this.entries.get(urlPath);
|
|
131
|
+
if (!entry) return null;
|
|
132
|
+
|
|
133
|
+
this.entries.delete(urlPath);
|
|
134
|
+
|
|
135
|
+
if (this.isExpired(entry)) {
|
|
136
|
+
this.destroyEntry(entry);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return entry;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
has(urlPath: string): boolean {
|
|
143
|
+
const entry = this.entries.get(urlPath);
|
|
144
|
+
return !!entry && !this.isExpired(entry);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Xoá + destroy entry (vd: push navigation cần data tươi, hoặc sau mutation) */
|
|
148
|
+
invalidate(urlPath: string): boolean {
|
|
149
|
+
const entry = this.entries.get(urlPath);
|
|
150
|
+
if (!entry) return false;
|
|
151
|
+
this.entries.delete(urlPath);
|
|
152
|
+
this.destroyEntry(entry);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Quét destroy mọi entry quá hạn. Gọi sau mỗi navigation + khi tab visible lại. */
|
|
157
|
+
sweep(): number {
|
|
158
|
+
let removed = 0;
|
|
159
|
+
for (const [key, entry] of this.entries) {
|
|
160
|
+
if (this.isExpired(entry)) {
|
|
161
|
+
this.entries.delete(key);
|
|
162
|
+
this.destroyEntry(entry);
|
|
163
|
+
removed++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return removed;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
clear(): void {
|
|
170
|
+
for (const entry of this.entries.values()) {
|
|
171
|
+
this.destroyEntry(entry);
|
|
172
|
+
}
|
|
173
|
+
this.entries.clear();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
get size(): number {
|
|
177
|
+
return this.entries.size;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─── Private ────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
private isExpired(entry: PageCacheEntry): boolean {
|
|
183
|
+
return this.now() - entry.savedAt > entry.ttl;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private destroyEntry(entry: PageCacheEntry): void {
|
|
187
|
+
this.destroyTakenEntry(entry);
|
|
188
|
+
try { this.onEvict?.(entry); } catch (e) { console.error('[PageCache] onEvict error:', e); }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Destroy một entry ĐÃ take() ra khỏi cache nhưng không restore được
|
|
193
|
+
* (vd layout hiện tại không khớp layoutPath). Public cho ViewManager.
|
|
194
|
+
*/
|
|
195
|
+
destroyTakenEntry(entry: PageCacheEntry): void {
|
|
196
|
+
// Block content đã detach — ViewController.destroy() không đi qua chúng
|
|
197
|
+
// (không nằm trong _rootTree) nên phải destroy tường minh trước.
|
|
198
|
+
if (entry.outletContents) {
|
|
199
|
+
for (const [, content] of entry.outletContents) {
|
|
200
|
+
for (const child of content.children) {
|
|
201
|
+
try { child?.destroy?.(); }
|
|
202
|
+
catch (e) { console.error('[PageCache] destroy block child error:', e); }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
this.destroyViews(entry.views);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private destroyViews(views: ViewInterface[]): void {
|
|
210
|
+
for (const view of views) {
|
|
211
|
+
try { view.__ctrl__?.destroy(); }
|
|
212
|
+
catch (e) { console.error('[PageCache] destroy view error:', e); }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Detach toàn bộ vùng DOM của một wrapper (từ openTag đến closeTag, inclusive)
|
|
219
|
+
* vào DocumentFragment — node KHÔNG bị destroy, chỉ rời khỏi document.
|
|
220
|
+
*/
|
|
221
|
+
export function detachWrapperDOM(wrapper: { openTag: Comment; closeTag: Comment }): DocumentFragment {
|
|
222
|
+
const fragment = document.createDocumentFragment();
|
|
223
|
+
const start = wrapper.openTag;
|
|
224
|
+
const end = wrapper.closeTag;
|
|
225
|
+
if (!start.parentNode) return fragment;
|
|
226
|
+
|
|
227
|
+
let current: Node | null = start;
|
|
228
|
+
while (current) {
|
|
229
|
+
const isEnd = current === end;
|
|
230
|
+
const next: Node | null = current.nextSibling;
|
|
231
|
+
fragment.appendChild(current); // appendChild tự remove khỏi vị trí cũ
|
|
232
|
+
if (isEnd) break;
|
|
233
|
+
current = next;
|
|
234
|
+
}
|
|
235
|
+
return fragment;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export const PageCache = new PageCacheService();
|
|
239
|
+
export default PageCache;
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import type { SectionInterface, SectionManagerInterface } from "../contracts/SectionInterface.js";
|
|
2
|
+
import type { YieldInterface } from "../contracts/ElementInterface.js";
|
|
3
|
+
import HeadService from "./HeadService.js";
|
|
4
|
+
|
|
5
|
+
const HEAD_SECTION_PREFIX = 'meta:';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* SectionManager — connects `@section(name, ...)` declarations to `@yield(name, ...)`
|
|
9
|
+
* markers, cross-controller (a page declares a section, a layout — or nothing —
|
|
10
|
+
* yields it), mirroring how BlockManager connects Block ↔ BlockOutlet.
|
|
11
|
+
*
|
|
12
|
+
* Two content shapes (RUNTIME_CONTRACT):
|
|
13
|
+
* - contentType 'text' (short `@section(name, value)`) — resolves to a plain
|
|
14
|
+
* string. Used as: (a) a single text node between the yield's markers,
|
|
15
|
+
* (b) the `.value` of a `<textarea>` when the yield's immediate DOM parent
|
|
16
|
+
* is a textarea (mutating child text nodes of a live textarea does not
|
|
17
|
+
* reliably update what's displayed once the dirty-value flag is set — every
|
|
18
|
+
* framework sets `.value` directly instead), (c) resolved synchronously via
|
|
19
|
+
* `resolve()` for `this.yieldContent()` used inside an attribute/prop factory.
|
|
20
|
+
* - contentType 'html' (long `@section(name) ... @endsection`) — a children
|
|
21
|
+
* array, mounted between the yield's markers the same way BlockManager
|
|
22
|
+
* mounts block content into an outlet.
|
|
23
|
+
*
|
|
24
|
+
* Head metadata (`meta:title`, `meta:description`, ...): the app shell's
|
|
25
|
+
* `<head>` is plain Laravel Blade (native `@yield`), entirely outside the
|
|
26
|
+
* client's mounted tree — SSR already gets it right for free. SPA navigation
|
|
27
|
+
* bypasses Blade though, so `syncHeadSections()` pushes any `meta:*` section
|
|
28
|
+
* straight to `document.title` / `<meta>` on every mount and on every reactive
|
|
29
|
+
* update, keeping the tab title and metadata "live" across client-side routes.
|
|
30
|
+
*/
|
|
31
|
+
export class SectionManagerService implements SectionManagerInterface {
|
|
32
|
+
sections: Map<string, SectionInterface> = new Map(); // key: name + viewId
|
|
33
|
+
activeSections: Map<string, SectionInterface> = new Map(); // key: name
|
|
34
|
+
yields: Map<string, YieldInterface> = new Map(); // key: yield element id
|
|
35
|
+
subscribers: Map<string, ((section: SectionInterface) => void)[]> = new Map();
|
|
36
|
+
|
|
37
|
+
/** name+viewId -> unsubscribe fn (only for reactive sections, active while started) */
|
|
38
|
+
private unsubscribers: Map<string, () => void> = new Map();
|
|
39
|
+
/** yield id -> tracked nodes/elements mounted between its markers (for clear/start/stop) */
|
|
40
|
+
private mounted: Map<string, any[]> = new Map();
|
|
41
|
+
/** Owners in the current route chain; cached pages retain their sections. */
|
|
42
|
+
private headViewIds = new Set<string>();
|
|
43
|
+
|
|
44
|
+
add(section: SectionInterface): void {
|
|
45
|
+
const key = section.name + (section.viewId ?? '');
|
|
46
|
+
if (!this.sections.has(key)) {
|
|
47
|
+
this.sections.set(key, section);
|
|
48
|
+
}
|
|
49
|
+
this.active(section.name, section.viewId ?? '');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
active(name: string, viewId: string): void {
|
|
53
|
+
const key = name + viewId;
|
|
54
|
+
const section = this.sections.get(key);
|
|
55
|
+
if (!section) return;
|
|
56
|
+
this.activeSections.set(name, section);
|
|
57
|
+
this.notify(section);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
subscribe(name: string, callback: (section: SectionInterface) => void): () => void {
|
|
61
|
+
if (!this.subscribers.has(name)) this.subscribers.set(name, []);
|
|
62
|
+
this.subscribers.get(name)!.push(callback);
|
|
63
|
+
return () => this.unsubscribe(name, callback);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
unsubscribe(name: string, callback?: (section: SectionInterface) => void): void {
|
|
67
|
+
if (!this.subscribers.has(name)) return;
|
|
68
|
+
if (!callback) {
|
|
69
|
+
this.subscribers.delete(name);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const listeners = this.subscribers.get(name)!.filter(fn => fn !== callback);
|
|
73
|
+
if (listeners.length === 0) this.subscribers.delete(name);
|
|
74
|
+
else this.subscribers.set(name, listeners);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
addYield(id: string, yieldEl: YieldInterface): void {
|
|
78
|
+
this.yields.set(id, yieldEl);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** XOÁ THẬT yields của một view bị destroy — destroy rồi mới rời Map. */
|
|
82
|
+
removeYieldsOfView(viewId: string): void {
|
|
83
|
+
for (const [id, y] of Array.from(this.yields)) {
|
|
84
|
+
if ((y as any).ctx?.viewId === viewId) {
|
|
85
|
+
this.clearYield(y);
|
|
86
|
+
(y as any).destroy?.(); // idempotent — gỡ khỏi ctrl.elements
|
|
87
|
+
this.yields.delete(id);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Resolve the current value of a named section — for `this.yieldContent()` in attrs/props. */
|
|
93
|
+
resolve(name: string, defaultValue: any = null): any {
|
|
94
|
+
const section = this.activeSections.get(name);
|
|
95
|
+
if (!section?.renderFactory) return defaultValue;
|
|
96
|
+
const value = section.renderFactory(null);
|
|
97
|
+
return value === undefined || value === null ? defaultValue : value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Mount every yield owned by one controller (page or layout — matches the
|
|
102
|
+
* controller Router/ViewManager is currently activating).
|
|
103
|
+
*
|
|
104
|
+
* Render happens for the whole chain before any mounting does (page
|
|
105
|
+
* registers its sections while building its tree, THEN returns
|
|
106
|
+
* `extendView(layout)`, whose own render builds the yields) — so by mount
|
|
107
|
+
* time `activeSections` already holds every section in the chain
|
|
108
|
+
* regardless of call order. Only the yield side needs to be scoped to
|
|
109
|
+
* `viewId`, so each controller's own markers get touched exactly once,
|
|
110
|
+
* mirroring BlockManagerService.mountViewBlocks()'s per-owner pass.
|
|
111
|
+
*/
|
|
112
|
+
mountViewSections(viewId: string): void {
|
|
113
|
+
this.activateHeadSections(viewId);
|
|
114
|
+
for (const [, yieldEl] of this.yields) {
|
|
115
|
+
if ((yieldEl as any).ctx?.viewId !== viewId) continue;
|
|
116
|
+
this.mountSectionIntoYield(this.activeSections.get(yieldEl.name) ?? null, yieldEl);
|
|
117
|
+
}
|
|
118
|
+
this.syncHeadSections();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Hydrate counterpart: trust server-rendered content, only let nested elements self-claim. */
|
|
122
|
+
hydrateViewSections(viewId: string): void {
|
|
123
|
+
this.activateHeadSections(viewId);
|
|
124
|
+
for (const [, yieldEl] of this.yields) {
|
|
125
|
+
if ((yieldEl as any).ctx?.viewId !== viewId) continue;
|
|
126
|
+
const section = this.activeSections.get(yieldEl.name) ?? null;
|
|
127
|
+
if (section) this.hydrateApplyToYield(section, yieldEl);
|
|
128
|
+
}
|
|
129
|
+
this.syncHeadSections();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private findYieldsByName(name: string): YieldInterface[] {
|
|
133
|
+
const found: YieldInterface[] = [];
|
|
134
|
+
for (const [, y] of this.yields) if (y.name === name) found.push(y);
|
|
135
|
+
return found;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Re-apply every yield bound to `name` — used after a reactive section change. */
|
|
139
|
+
private applyByName(name: string): void {
|
|
140
|
+
const section = this.activeSections.get(name) ?? null;
|
|
141
|
+
for (const yieldEl of this.findYieldsByName(name)) {
|
|
142
|
+
this.mountSectionIntoYield(section, yieldEl);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private mountSectionIntoYield(section: SectionInterface | null, yieldEl: any): void {
|
|
147
|
+
this.clearYield(yieldEl);
|
|
148
|
+
if (!yieldEl.openTag?.parentNode) return; // outlet not in DOM yet
|
|
149
|
+
|
|
150
|
+
const isTextarea = yieldEl.parent?.element?.tagName === 'TEXTAREA';
|
|
151
|
+
|
|
152
|
+
if (!section) {
|
|
153
|
+
this.applyText(yieldEl, isTextarea, String(yieldEl.defaultValue ?? ''));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (section.contentType === 'html') {
|
|
157
|
+
this.mountHtml(section, yieldEl);
|
|
158
|
+
} else {
|
|
159
|
+
const raw = section.renderFactory ? section.renderFactory(null) : undefined;
|
|
160
|
+
const text = raw === undefined || raw === null ? String(yieldEl.defaultValue ?? '') : String(raw);
|
|
161
|
+
this.applyText(yieldEl, isTextarea, text);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private applyText(yieldEl: any, isTextarea: boolean, text: string): void {
|
|
166
|
+
if (isTextarea) {
|
|
167
|
+
// Mutating a live textarea's child text nodes does not reliably repaint
|
|
168
|
+
// its displayed value once the browser's dirty-value flag is set — set
|
|
169
|
+
// `.value` directly instead, the same way every DOM framework does.
|
|
170
|
+
(yieldEl.parent!.element as HTMLTextAreaElement).value = text;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!text) return;
|
|
174
|
+
const node = document.createTextNode(text);
|
|
175
|
+
yieldEl.closeTag.parentNode?.insertBefore(node, yieldEl.closeTag);
|
|
176
|
+
this.mounted.set(yieldEl.id, [node]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Same insertion pattern as BlockManagerService.mountBlockIntoOutlet(). */
|
|
180
|
+
private mountHtml(section: SectionInterface, yieldEl: any): void {
|
|
181
|
+
const insertBeforeClose = (node: Node) => yieldEl.closeTag.parentNode?.insertBefore(node, yieldEl.closeTag);
|
|
182
|
+
const content = section.renderFactory!(yieldEl.parent);
|
|
183
|
+
if (!Array.isArray(content)) return;
|
|
184
|
+
|
|
185
|
+
const children: any[] = [];
|
|
186
|
+
for (const child of content) {
|
|
187
|
+
if (child === null || child === undefined) continue;
|
|
188
|
+
if (typeof child === 'string' || typeof child === 'number') {
|
|
189
|
+
insertBeforeClose(document.createTextNode(String(child)));
|
|
190
|
+
} else if (child instanceof Node) {
|
|
191
|
+
insertBeforeClose(child);
|
|
192
|
+
} else if (typeof child === 'object') {
|
|
193
|
+
if ('element' in child && (child as any).element) {
|
|
194
|
+
insertBeforeClose((child as any).element);
|
|
195
|
+
children.push(child);
|
|
196
|
+
child.render();
|
|
197
|
+
} else if ('openTag' in child) {
|
|
198
|
+
if ('parent' in child) (child as any).parent = yieldEl.parent;
|
|
199
|
+
if ('parentElement' in child) (child as any).parentElement = yieldEl.parent;
|
|
200
|
+
insertBeforeClose((child as any).openTag);
|
|
201
|
+
insertBeforeClose((child as any).closeTag);
|
|
202
|
+
children.push(child);
|
|
203
|
+
child.render();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
this.mounted.set(yieldEl.id, children);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Hydrate: claim only — DOM already there from SSR, never insert/clear. */
|
|
211
|
+
private hydrateApplyToYield(section: SectionInterface, yieldEl: YieldInterface): void {
|
|
212
|
+
if (section.contentType !== 'html') return; // text: leave server's text node as-is
|
|
213
|
+
const content = section.renderFactory ? section.renderFactory((yieldEl as any).parent) : null;
|
|
214
|
+
if (!Array.isArray(content)) return;
|
|
215
|
+
const children: any[] = [];
|
|
216
|
+
for (const child of content) {
|
|
217
|
+
if (child === null || child === undefined) continue;
|
|
218
|
+
if (typeof child === 'string' || typeof child === 'number') continue; // text: server already has it
|
|
219
|
+
if (child instanceof Node) continue;
|
|
220
|
+
if (typeof child === 'object') {
|
|
221
|
+
if ('element' in child && (child as any).element) {
|
|
222
|
+
children.push(child);
|
|
223
|
+
(child as any).render();
|
|
224
|
+
} else if ('openTag' in child) {
|
|
225
|
+
if ('parent' in child) (child as any).parent = (yieldEl as any).parent;
|
|
226
|
+
if ('parentElement' in child) (child as any).parentElement = (yieldEl as any).parent;
|
|
227
|
+
children.push(child);
|
|
228
|
+
(child as any).render();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
this.mounted.set((yieldEl as any).id, children);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private clearYield(yieldEl: any): void {
|
|
236
|
+
const children = this.mounted.get(yieldEl.id) ?? [];
|
|
237
|
+
for (const child of children) {
|
|
238
|
+
if (child && typeof child.destroy === 'function') child.destroy();
|
|
239
|
+
}
|
|
240
|
+
this.mounted.delete(yieldEl.id);
|
|
241
|
+
|
|
242
|
+
if (yieldEl.openTag?.parentNode) {
|
|
243
|
+
let current = yieldEl.openTag.nextSibling;
|
|
244
|
+
while (current && current !== yieldEl.closeTag) {
|
|
245
|
+
const next = current.nextSibling;
|
|
246
|
+
current.remove();
|
|
247
|
+
current = next;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Activate reactive sections (subscribe) + start any mounted html-content children. Call after mount/hydrate. */
|
|
253
|
+
startAll(): void {
|
|
254
|
+
for (const section of this.activeSections.values()) {
|
|
255
|
+
this.subscribeIfReactive(section);
|
|
256
|
+
}
|
|
257
|
+
for (const children of this.mounted.values()) {
|
|
258
|
+
for (const child of children) {
|
|
259
|
+
if (child && typeof child.start === 'function') child.start();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Deactivate — unsubscribe + stop mounted children. */
|
|
265
|
+
stopAll(): void {
|
|
266
|
+
for (const unsub of this.unsubscribers.values()) unsub();
|
|
267
|
+
this.unsubscribers.clear();
|
|
268
|
+
for (const children of this.mounted.values()) {
|
|
269
|
+
for (const child of children) {
|
|
270
|
+
if (child && typeof child.stop === 'function') child.stop();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private subscribeIfReactive(section: SectionInterface): void {
|
|
276
|
+
const key = section.name + (section.viewId ?? '');
|
|
277
|
+
if (this.unsubscribers.has(key)) return;
|
|
278
|
+
if ((section.type === 'reactive' || section.type === 'dynamic')
|
|
279
|
+
&& section.stateKeys?.length && section.ctx) {
|
|
280
|
+
const unsub = section.ctx.states.__.subscribe(section.stateKeys, () => {
|
|
281
|
+
this.applyByName(section.name);
|
|
282
|
+
this.syncHeadSections();
|
|
283
|
+
this.notify(section);
|
|
284
|
+
});
|
|
285
|
+
this.unsubscribers.set(key, unsub);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Notify `subscribe(name, cb)` listeners — both when a different section becomes active
|
|
290
|
+
* (see `active()`) and when the currently active section's own value changes. Consumers:
|
|
291
|
+
* `Html.ts` attribute/prop bindings built from `@yield(...)` (no stateKeys of their own —
|
|
292
|
+
* the section they depend on is only known at runtime), subscribed via `yieldName`. */
|
|
293
|
+
private notify(section: SectionInterface): void {
|
|
294
|
+
const listeners = this.subscribers.get(section.name);
|
|
295
|
+
if (listeners) listeners.forEach(fn => fn(section));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
unmountView(viewId: string): void {
|
|
299
|
+
this.headViewIds.delete(viewId);
|
|
300
|
+
for (const [name, section] of Array.from(this.activeSections)) {
|
|
301
|
+
if (section.viewId === viewId) this.activeSections.delete(name);
|
|
302
|
+
}
|
|
303
|
+
for (const [key, section] of Array.from(this.sections)) {
|
|
304
|
+
if (section.viewId !== viewId) continue;
|
|
305
|
+
const unsub = this.unsubscribers.get(key);
|
|
306
|
+
if (unsub) { unsub(); this.unsubscribers.delete(key); }
|
|
307
|
+
this.sections.delete(key);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Push `meta:*` sections to HeadService — SPA navigation never runs Blade,
|
|
313
|
+
* so nothing else keeps `<title>`/`<meta>` (outside the client's mounted
|
|
314
|
+
* tree entirely) in sync with the active page after the first load. This
|
|
315
|
+
* is the declarative convenience layer over HeadService: a page can
|
|
316
|
+
* either call `@section('meta:title', ...)` in its template, or reach
|
|
317
|
+
* `app('Head')` directly (e.g. from an async data callback) — both end up
|
|
318
|
+
* writing through the same service, so they never fight each other.
|
|
319
|
+
*/
|
|
320
|
+
private activateHeadSections(viewId: string): void {
|
|
321
|
+
this.headViewIds.add(viewId);
|
|
322
|
+
for (const section of this.sections.values()) {
|
|
323
|
+
if (section.viewId === viewId && section.name.startsWith(HEAD_SECTION_PREFIX)) {
|
|
324
|
+
this.activeSections.set(section.name, section);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private syncHeadSections(): void {
|
|
330
|
+
for (const [name, section] of this.activeSections) {
|
|
331
|
+
if (!name.startsWith(HEAD_SECTION_PREFIX) || !section.renderFactory) continue;
|
|
332
|
+
if (!this.headViewIds.has(section.viewId ?? '')) continue;
|
|
333
|
+
const raw = section.renderFactory(null);
|
|
334
|
+
if (raw === undefined || raw === null) continue;
|
|
335
|
+
const value = String(raw);
|
|
336
|
+
const key = name.slice(HEAD_SECTION_PREFIX.length);
|
|
337
|
+
if (key === 'title') {
|
|
338
|
+
HeadService.setTitle(value);
|
|
339
|
+
HeadService.setMetaProperty('og:title', value);
|
|
340
|
+
HeadService.setMeta('twitter:title', value);
|
|
341
|
+
} else if (key === 'description') {
|
|
342
|
+
HeadService.setMeta('description', value);
|
|
343
|
+
HeadService.setMetaProperty('og:description', value);
|
|
344
|
+
HeadService.setMeta('twitter:description', value);
|
|
345
|
+
} else if (key.startsWith('og:')) {
|
|
346
|
+
HeadService.setMetaProperty(key, value);
|
|
347
|
+
} else if (key.startsWith('twitter:')) {
|
|
348
|
+
HeadService.setMeta(key, value);
|
|
349
|
+
} else if (key === 'canonical') {
|
|
350
|
+
HeadService.setLink('canonical', value);
|
|
351
|
+
} else if (key.startsWith('jsonld:')) {
|
|
352
|
+
if (!value.trim()) continue; // Async page data has not arrived yet.
|
|
353
|
+
try {
|
|
354
|
+
const data = JSON.parse(value);
|
|
355
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
356
|
+
HeadService.setJsonLd(key.slice('jsonld:'.length), data);
|
|
357
|
+
}
|
|
358
|
+
} catch {
|
|
359
|
+
console.warn(`[SectionManager] Invalid JSON-LD in section "${name}".`);
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
HeadService.setMeta(key, value);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Revert every page-scoped head tag before mounting a new page's chain. Call once per navigation. */
|
|
368
|
+
resetPageHead(): void {
|
|
369
|
+
HeadService.resetPage();
|
|
370
|
+
this.headViewIds.clear();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
destroy(): void {
|
|
374
|
+
this.headViewIds.clear();
|
|
375
|
+
for (const unsub of this.unsubscribers.values()) unsub();
|
|
376
|
+
this.unsubscribers.clear();
|
|
377
|
+
for (const [, y] of this.yields) this.clearYield(y);
|
|
378
|
+
this.sections.clear();
|
|
379
|
+
this.activeSections.clear();
|
|
380
|
+
this.yields.clear();
|
|
381
|
+
this.subscribers.clear();
|
|
382
|
+
this.mounted.clear();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export const SectionManager = new SectionManagerService();
|
|
387
|
+
export default SectionManager;
|
|
@@ -47,7 +47,7 @@ export class StorageService {
|
|
|
47
47
|
private listeners = new Map<string, StorageEventCallback[]>();
|
|
48
48
|
private supported: boolean;
|
|
49
49
|
|
|
50
|
-
constructor(storageKey: string = '
|
|
50
|
+
constructor(storageKey: string = 'saoview_storage') {
|
|
51
51
|
this.storageKey = storageKey;
|
|
52
52
|
this.supported = typeof localStorage !== 'undefined';
|
|
53
53
|
if (this.supported) this.load();
|