@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,52 +1,181 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { reconcileChildren, reuseStaticText } from './reconcileChildren.js';
|
|
2
|
+
import { InitModes } from "../contracts/common.js";
|
|
3
|
+
import { hydrateElementList } from "../helpers/view.js";
|
|
4
|
+
import { TextElement } from "./TextElement.js";
|
|
5
|
+
import SectionManager from "../services/SectionManager.js";
|
|
6
|
+
import { runEnter, runLeave } from "../helpers/transition.js";
|
|
7
|
+
/**
|
|
8
|
+
* Escape một chuỗi để dùng làm CSS class/id selector. Class hydrate dạng
|
|
9
|
+
* "{viewId}-{id}" có viewId là hex (uniqid) CÓ THỂ bắt đầu bằng chữ số, làm
|
|
10
|
+
* ".6a3a..." trở thành selector không hợp lệ → querySelector ném SyntaxError.
|
|
11
|
+
* Dùng CSS.escape khi có; fallback escape thủ công ký tự đầu là số + ký tự đặc biệt.
|
|
12
|
+
*/
|
|
13
|
+
function cssEscape(value) {
|
|
14
|
+
const g = typeof globalThis !== 'undefined' ? globalThis : {};
|
|
15
|
+
if (g.CSS && typeof g.CSS.escape === 'function') {
|
|
16
|
+
return g.CSS.escape(value);
|
|
17
|
+
}
|
|
18
|
+
// Fallback tối thiểu: escape chữ số đầu (\3N ) và ký tự không phải [-_a-zA-Z0-9].
|
|
19
|
+
return value
|
|
20
|
+
.replace(/^[0-9]/, ch => `\\3${ch} `)
|
|
21
|
+
.replace(/[^a-zA-Z0-9_-]/g, ch => `\\${ch}`);
|
|
22
|
+
}
|
|
23
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
24
|
+
/**
|
|
25
|
+
* `document.createElement('svg')` ra HTMLUnknownElement — SVG dựng bằng CSR
|
|
26
|
+
* không vẽ gì. Phải dùng createElementNS.
|
|
27
|
+
*
|
|
28
|
+
* Chỉ cần nhận ra `<svg>`: mọi thứ bên trong kế thừa namespace từ cha, kể cả
|
|
29
|
+
* tag không liệt kê được hết (`filter`, cả họ `fe*`, `textPath`…). Danh sách
|
|
30
|
+
* tag SVG là thừa và luôn thiếu.
|
|
31
|
+
*
|
|
32
|
+
* `foreignObject` CẮT chuỗi kế thừa: con của nó là HTML thật (đó là toàn bộ lý
|
|
33
|
+
* do nó tồn tại), parser của trình duyệt cũng làm đúng như vậy khi đọc markup
|
|
34
|
+
* SSR — không cắt ở đây thì SSR và CSR ra hai cây khác nhau.
|
|
35
|
+
*
|
|
36
|
+
* Tên tag phải giữ đúng hoa/thường: createElementNS KHÔNG có bảng điều chỉnh
|
|
37
|
+
* như parser HTML, `'clippath'` ra SVGElement trơ. Compiler đã trả về đúng
|
|
38
|
+
* `clipPath` (xem Parser::SVG_TAG_ADJUST).
|
|
39
|
+
*/
|
|
40
|
+
function createDomElement(tagName, parentElement) {
|
|
41
|
+
const parent = parentElement?.element;
|
|
42
|
+
const isSvg = tagName === 'svg'
|
|
43
|
+
|| (parent?.namespaceURI === SVG_NS && parent.tagName !== 'foreignObject');
|
|
44
|
+
return isSvg
|
|
45
|
+
? document.createElementNS(SVG_NS, tagName)
|
|
46
|
+
: document.createElement(tagName);
|
|
47
|
+
}
|
|
48
|
+
/** SVG phân biệt hoa thường — hạ chữ thường sẽ hỏng `clipPath`, `feGaussianBlur`… */
|
|
49
|
+
function domTagName(el) {
|
|
50
|
+
return el.namespaceURI === SVG_NS ? el.tagName : el.tagName.toLowerCase();
|
|
51
|
+
}
|
|
3
52
|
export class Html {
|
|
4
53
|
constructor({ ctx, id = null, parentElement = null, tagName = 'div', element = null, config = {}, childrenFactory = null, initMode = InitModes.CREATE, }) {
|
|
5
|
-
this.
|
|
54
|
+
this.saoType = 'Html';
|
|
6
55
|
this.children = [];
|
|
7
56
|
this.domChildren = []; // For compatibility with HtmlInterface; Html itself doesn't have a single root element
|
|
8
57
|
this.childrenFactory = null;
|
|
9
58
|
this.abortController = new AbortController();
|
|
10
59
|
/** All state subscriptions for reactive bindings — cleanup on destroy */
|
|
11
60
|
this.bindingUnsubscribes = [];
|
|
61
|
+
/** Invalidates deferred/stale binding callbacks after a config reconciliation. */
|
|
62
|
+
this.bindingGeneration = 0;
|
|
63
|
+
this.contentUnsubscribe = null;
|
|
64
|
+
/** DOM state owned by this Html config, used for exact cleanup before reuse. */
|
|
65
|
+
this.managedAttributeNames = new Set();
|
|
66
|
+
this.managedClassNames = new Set();
|
|
67
|
+
this.managedStyleNames = new Set();
|
|
68
|
+
this.managedPropertyNames = new Set();
|
|
69
|
+
/** Events actually registered through ViewController, independent of current config. */
|
|
70
|
+
this.registeredEventNames = new Set();
|
|
12
71
|
this.initMode = InitModes.CREATE;
|
|
72
|
+
this.adoptedSSR = false;
|
|
73
|
+
this.contentRendered = false;
|
|
74
|
+
this.bindingInitialized = false;
|
|
75
|
+
this.composing = false;
|
|
76
|
+
this.isStarted = false;
|
|
77
|
+
/** Đã chạy enter rồi — re-render không được chạy lại. */
|
|
78
|
+
this._entered = false;
|
|
79
|
+
/** Registry guard — element đã destroy không được reuse (xem RUNTIME_CONTRACT.md §2) */
|
|
80
|
+
this.__destroyed__ = false;
|
|
13
81
|
this.ctx = ctx;
|
|
14
82
|
this.parent = parentElement;
|
|
15
83
|
this.config = config;
|
|
16
84
|
this.tagName = tagName;
|
|
17
85
|
this.initMode = initMode;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
86
|
+
// ── Ưu tiên element trực tiếp (ViewManager rootElement / test) ─────────
|
|
87
|
+
// config.element hoặc tham số element cho phép caller truyền HTMLElement sẵn có
|
|
88
|
+
// mà không cần lookup DOM.
|
|
89
|
+
const directElement = element instanceof HTMLElement
|
|
90
|
+
? element
|
|
91
|
+
: (config.element instanceof HTMLElement ? config.element : null);
|
|
92
|
+
// ── Class hydrate = "{viewId}-{id}" ────────────────────────────────────
|
|
93
|
+
// Blade emit `$__VIEW_ID__ . '-' . $id` cho MỌI element, nên CSR phải gắn
|
|
94
|
+
// đúng class đó. Trước đây nhánh create dùng id THÔ ('e1') → cùng một view
|
|
95
|
+
// render bởi server và bởi client ra hai DOM khác nhau, và mọi element CSR
|
|
96
|
+
// dùng chung vài chục tên class ('e1', 'e13'...) trên toàn tài liệu.
|
|
97
|
+
const viewId = ctx.viewId ?? null;
|
|
98
|
+
const hydrateClass = id ? (viewId ? `${viewId}-${id}` : id) : null;
|
|
99
|
+
if (directElement) {
|
|
100
|
+
this.element = directElement;
|
|
101
|
+
this.tagName = domTagName(this.element);
|
|
102
|
+
this.adoptedSSR = initMode === InitModes.HYDRATE;
|
|
103
|
+
}
|
|
104
|
+
else if (initMode === InitModes.HYDRATE) {
|
|
105
|
+
// ── SSR Hydration: claim server-rendered DOM node bằng class ID ──────
|
|
106
|
+
//
|
|
107
|
+
// Blade compiler emit class: $__VIEW_ID__ . '-{id}' (e.g. "v12345-af0882bc-0-1").
|
|
108
|
+
// Tham chiếu: COMPILER_CONTRACT.md §hydration, docs/FOREACH_RECONCILIATION_DESIGN.md
|
|
109
|
+
//
|
|
110
|
+
// Thuật toán (top-down):
|
|
111
|
+
// 1. Tìm trong parentElement.element trước (để tránh cross-view collision)
|
|
112
|
+
// 2. Fallback: document.querySelector nếu không có parentElement
|
|
113
|
+
// 3. Không tìm thấy → tạo element mới (partial hydration)
|
|
114
|
+
let found = null;
|
|
115
|
+
if (hydrateClass) {
|
|
116
|
+
// viewId (server uniqid) là hex CÓ THỂ bắt đầu bằng chữ số → class
|
|
117
|
+
// "6a3a...-32a9c14a" làm selector ".6a3a..." KHÔNG hợp lệ
|
|
118
|
+
// (querySelector ném SyntaxError). CSS.escape() escape ký tự đầu.
|
|
119
|
+
const selector = `${tagName}.${cssEscape(hydrateClass)}`;
|
|
120
|
+
const searchScope = parentElement?.element ?? null;
|
|
121
|
+
if (searchScope) {
|
|
122
|
+
// CÓ parent → chỉ tìm trong đó. KHÔNG hạ xuống quét cả tài
|
|
123
|
+
// liệu: element không nằm dưới parent của nó thì cái tìm
|
|
124
|
+
// thấy ở nơi khác là của cây khác — claim vào rồi mount sẽ
|
|
125
|
+
// BỨNG node đó khỏi chỗ đúng, hỏng cả hai cây, không một
|
|
126
|
+
// tiếng động. Server không render vùng này thì đường đúng là
|
|
127
|
+
// partial hydration: tạo element mới ở dưới.
|
|
128
|
+
found = searchScope.querySelector(selector);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// Không có parent để giới hạn (element cấp gốc) — chỉ khi ấy
|
|
132
|
+
// quét cả tài liệu mới là cách duy nhất.
|
|
133
|
+
found = document.querySelector(selector);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (found) {
|
|
27
137
|
this.element = found;
|
|
28
|
-
this.tagName = found
|
|
138
|
+
this.tagName = domTagName(found);
|
|
139
|
+
this.adoptedSSR = true;
|
|
29
140
|
}
|
|
30
141
|
else {
|
|
31
|
-
|
|
32
|
-
this.element
|
|
33
|
-
|
|
142
|
+
// Partial hydration fallback: element không có trong SSR output
|
|
143
|
+
this.element = createDomElement(tagName, parentElement);
|
|
144
|
+
if (hydrateClass)
|
|
145
|
+
this.element.classList.add(hydrateClass);
|
|
34
146
|
}
|
|
35
147
|
}
|
|
36
148
|
else {
|
|
37
|
-
|
|
38
|
-
this.element
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
}
|
|
149
|
+
// ── CSR (create mode): tạo element mới ───────────────────────────────
|
|
150
|
+
this.element = createDomElement(this.tagName, parentElement);
|
|
151
|
+
if (hydrateClass)
|
|
152
|
+
this.element.classList.add(hydrateClass);
|
|
42
153
|
}
|
|
43
154
|
this.childrenFactory = childrenFactory;
|
|
44
155
|
this.initialize();
|
|
45
156
|
}
|
|
46
157
|
updateConfig(newConfig) {
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
158
|
+
if (this.__destroyed__)
|
|
159
|
+
return;
|
|
160
|
+
// Reuse must behave like a fresh initialization without replacing the DOM
|
|
161
|
+
// node: remove every resource owned by the previous config first, then make
|
|
162
|
+
// the new managed sections authoritative. This prevents stale attrs,
|
|
163
|
+
// duplicate events, and state subscriptions retaining old closures.
|
|
164
|
+
this.removeEventListeners();
|
|
165
|
+
this.cleanupBindingResources();
|
|
166
|
+
this.clearManagedDomState(newConfig);
|
|
167
|
+
this.config = {
|
|
168
|
+
...this.config,
|
|
169
|
+
...newConfig,
|
|
170
|
+
attrs: newConfig.attrs,
|
|
171
|
+
props: newConfig.props,
|
|
172
|
+
events: newConfig.events,
|
|
173
|
+
classes: newConfig.classes,
|
|
174
|
+
styles: newConfig.styles,
|
|
175
|
+
bind: newConfig.bind,
|
|
176
|
+
content: newConfig.content,
|
|
177
|
+
};
|
|
178
|
+
this.initialize();
|
|
50
179
|
}
|
|
51
180
|
initialize() {
|
|
52
181
|
this.initializeAttributes();
|
|
@@ -54,45 +183,223 @@ export class Html {
|
|
|
54
183
|
this.initializeStyles();
|
|
55
184
|
this.initializeEvents();
|
|
56
185
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
186
|
+
isBindingCurrent(generation) {
|
|
187
|
+
return !this.__destroyed__ && generation === this.bindingGeneration;
|
|
188
|
+
}
|
|
189
|
+
cleanupBindingResources(renewAbortController = true) {
|
|
190
|
+
this.bindingGeneration++;
|
|
191
|
+
this.contentUnsubscribe?.();
|
|
192
|
+
this.contentUnsubscribe = null;
|
|
193
|
+
this.abortController.abort();
|
|
194
|
+
if (renewAbortController) {
|
|
195
|
+
this.abortController = new AbortController();
|
|
196
|
+
}
|
|
197
|
+
for (const unsubscribe of this.bindingUnsubscribes) {
|
|
198
|
+
try {
|
|
199
|
+
unsubscribe();
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
console.error('[Html] Failed to unsubscribe a reactive binding:', error);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
this.bindingUnsubscribes = [];
|
|
206
|
+
}
|
|
207
|
+
clearManagedDomState(next = {}) {
|
|
208
|
+
const attrs = new Set(Object.keys(next.attrs ?? {}).map(name => this.normalizeAttrName(name)));
|
|
209
|
+
const classes = new Set();
|
|
210
|
+
if (Array.isArray(next.classes)) {
|
|
211
|
+
for (const item of next.classes)
|
|
212
|
+
if (item && item.type !== 'dynamic' && item.value)
|
|
213
|
+
classes.add(String(item.value));
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
for (const [name, item] of Object.entries(next.classes ?? {})) {
|
|
217
|
+
if (item.type === 'binding' || item.value)
|
|
218
|
+
classes.add(name);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const attrName of this.managedAttributeNames) {
|
|
222
|
+
if (!attrs.has(attrName))
|
|
223
|
+
this.element.removeAttribute(attrName);
|
|
224
|
+
}
|
|
225
|
+
this.managedAttributeNames.clear();
|
|
226
|
+
for (const className of this.managedClassNames) {
|
|
227
|
+
if (!classes.has(className))
|
|
228
|
+
this.removeClass(className);
|
|
229
|
+
}
|
|
230
|
+
this.managedClassNames.clear();
|
|
231
|
+
for (const prop of this.managedStyleNames) {
|
|
232
|
+
if (!(prop in (next.styles ?? {})))
|
|
233
|
+
this.element.style.removeProperty(prop);
|
|
234
|
+
}
|
|
235
|
+
this.managedStyleNames.clear();
|
|
236
|
+
const removedProps = [...this.managedPropertyNames].filter(name => !(name in (next.props ?? {})));
|
|
237
|
+
const defaults = removedProps.length ? createDomElement(this.tagName, this.parent) : {};
|
|
238
|
+
const target = this.element;
|
|
239
|
+
for (const propName of removedProps) {
|
|
240
|
+
try {
|
|
241
|
+
if (propName in defaults) {
|
|
242
|
+
target[propName] = defaults[propName];
|
|
62
243
|
}
|
|
63
|
-
else
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
244
|
+
else {
|
|
245
|
+
delete target[propName];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// Some host properties are readonly. Removing an own property is
|
|
250
|
+
// still safe and avoids retaining user/config objects where possible.
|
|
251
|
+
try {
|
|
252
|
+
delete target[propName];
|
|
253
|
+
}
|
|
254
|
+
catch { /* no-op */ }
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
this.managedPropertyNames.clear();
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Chuẩn hóa tên attr từ camelCase → kebab-case cho data-* và aria-* attrs.
|
|
261
|
+
*
|
|
262
|
+
* Compiler emit camelCase: "dataCount" → client phải set "data-count" trên DOM.
|
|
263
|
+
* Tham chiếu: COMPILER_CONTRACT.md §3 — camelCase attrs.
|
|
264
|
+
*
|
|
265
|
+
* @example normalizeAttrName('dataCount') === 'data-count'
|
|
266
|
+
* @example normalizeAttrName('ariaLabel') === 'aria-label'
|
|
267
|
+
* @example normalizeAttrName('id') === 'id' (không đổi)
|
|
268
|
+
*/
|
|
269
|
+
normalizeAttrName(name) {
|
|
270
|
+
if (/^data[A-Z]/.test(name)) {
|
|
271
|
+
return 'data-' + name[4].toLowerCase() + name.slice(5).replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
272
|
+
}
|
|
273
|
+
if (/^aria[A-Z]/.test(name)) {
|
|
274
|
+
return 'aria-' + name[4].toLowerCase() + name.slice(5).replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
275
|
+
}
|
|
276
|
+
return name;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Thiết lập two-way data binding (v-model-like) theo compiler pattern:
|
|
280
|
+
*
|
|
281
|
+
* config.bind = { key: "<stateKey>" } — own bucket, sibling of attrs/props/events.
|
|
282
|
+
*
|
|
283
|
+
* Hành vi:
|
|
284
|
+
* 1. Khởi tạo: set element.value = state hiện tại
|
|
285
|
+
* 2. input event → update state
|
|
286
|
+
* 3. state change → update element.value
|
|
287
|
+
*
|
|
288
|
+
* Tham chiếu: COMPILER_CONTRACT.md §5 — @bind directive.
|
|
289
|
+
*/
|
|
290
|
+
setupTwoWayBinding(stateKey) {
|
|
291
|
+
const manager = this.ctx.states.__;
|
|
292
|
+
const el = this.element;
|
|
293
|
+
const generation = this.bindingGeneration;
|
|
294
|
+
const isSelect = el.tagName === 'SELECT';
|
|
295
|
+
const isMultiple = isSelect && el.multiple;
|
|
296
|
+
const isCheckbox = el.type === 'checkbox';
|
|
297
|
+
const isRadio = el.type === 'radio';
|
|
298
|
+
const isNumber = el.type === 'number' || el.type === 'range';
|
|
299
|
+
const readValue = () => {
|
|
300
|
+
if (isMultiple)
|
|
301
|
+
return Array.from(el.selectedOptions).map(option => option.value);
|
|
302
|
+
if (isCheckbox)
|
|
303
|
+
return el.checked;
|
|
304
|
+
if (isNumber)
|
|
305
|
+
return Number.isNaN(el.valueAsNumber) ? el.value : el.valueAsNumber;
|
|
306
|
+
return el.value;
|
|
307
|
+
};
|
|
308
|
+
const commitInput = () => {
|
|
309
|
+
if (!this.isBindingCurrent(generation) || (isRadio && !el.checked))
|
|
310
|
+
return;
|
|
311
|
+
const setter = manager.setters[stateKey];
|
|
312
|
+
if (typeof setter === 'function')
|
|
313
|
+
setter(readValue());
|
|
314
|
+
else
|
|
315
|
+
manager.updateStateByKey(stateKey, readValue());
|
|
316
|
+
};
|
|
317
|
+
const applyState = (value) => {
|
|
318
|
+
if (!this.isBindingCurrent(generation) || this.composing)
|
|
319
|
+
return;
|
|
320
|
+
if (isMultiple) {
|
|
321
|
+
const selected = new Set(Array.isArray(value) ? value.map(String) : []);
|
|
322
|
+
for (const option of el.options) {
|
|
323
|
+
const next = selected.has(option.value);
|
|
324
|
+
if (option.selected !== next)
|
|
325
|
+
option.selected = next;
|
|
84
326
|
}
|
|
85
327
|
}
|
|
328
|
+
else if (isCheckbox) {
|
|
329
|
+
if (el.checked !== !!value)
|
|
330
|
+
el.checked = !!value;
|
|
331
|
+
}
|
|
332
|
+
else if (isRadio) {
|
|
333
|
+
const checked = value != null && String(value) === el.value;
|
|
334
|
+
if (el.checked !== checked)
|
|
335
|
+
el.checked = checked;
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
const next = value == null ? '' : String(value);
|
|
339
|
+
if (el.value !== next)
|
|
340
|
+
el.value = next; // preserve caret on no-op patches
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
const initial = manager.getStateByKey(stateKey);
|
|
344
|
+
// Adopt user edits made while SSR HTML was visible before JS arrived.
|
|
345
|
+
const editedSSR = !this.bindingInitialized && this.adoptedSSR && (isSelect
|
|
346
|
+
? Array.from(el.options).some(option => option.selected !== option.defaultSelected)
|
|
347
|
+
: isCheckbox || isRadio ? el.checked !== el.defaultChecked
|
|
348
|
+
: el.value !== el.defaultValue);
|
|
349
|
+
this.bindingInitialized = true;
|
|
350
|
+
if (editedSSR)
|
|
351
|
+
commitInput();
|
|
352
|
+
else if (initial != null) {
|
|
353
|
+
if (isSelect)
|
|
354
|
+
queueMicrotask(() => applyState(manager.getStateByKey(stateKey)));
|
|
355
|
+
else
|
|
356
|
+
applyState(initial);
|
|
357
|
+
}
|
|
358
|
+
const signal = this.abortController.signal;
|
|
359
|
+
const inputHandler = (event) => {
|
|
360
|
+
if (this.composing || event.isComposing)
|
|
361
|
+
return;
|
|
362
|
+
commitInput();
|
|
363
|
+
};
|
|
364
|
+
const eventType = isCheckbox || isRadio || isSelect ? 'change' : 'input';
|
|
365
|
+
this.element.addEventListener(eventType, inputHandler, { signal });
|
|
366
|
+
if (!isSelect && !isCheckbox && !isRadio) {
|
|
367
|
+
this.element.addEventListener('compositionstart', () => { this.composing = true; }, { signal });
|
|
368
|
+
this.element.addEventListener('compositionend', () => {
|
|
369
|
+
this.composing = false;
|
|
370
|
+
commitInput();
|
|
371
|
+
}, { signal });
|
|
86
372
|
}
|
|
373
|
+
this.bindingUnsubscribes.push(manager.subscribe([stateKey], () => applyState(manager.getStateByKey(stateKey))));
|
|
374
|
+
}
|
|
375
|
+
initializeAttributes() {
|
|
376
|
+
const attrs = this.config.attrs;
|
|
377
|
+
if (attrs) {
|
|
378
|
+
for (const [attrName, attrConfig] of Object.entries(attrs)) {
|
|
379
|
+
this._applyAttr(attrName, attrConfig);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// @bind/@val — own top-level config bucket (sibling of attrs/props/events,
|
|
383
|
+
// same shape/spirit as events — see ElementInterface.ts). Applied AFTER
|
|
384
|
+
// attrs: setupTwoWayBinding needs el.type ('checkbox', 'radio', 'number'...)
|
|
385
|
+
// already in place.
|
|
386
|
+
if (this.config.bind?.key) {
|
|
387
|
+
this.setupTwoWayBinding(this.config.bind.key);
|
|
388
|
+
}
|
|
389
|
+
// props độc lập với attrs/bind — element chỉ có props vẫn phải chạy
|
|
87
390
|
if (this.config.props) {
|
|
88
391
|
for (const [propName, propConfig] of Object.entries(this.config.props)) {
|
|
89
|
-
|
|
90
|
-
|
|
392
|
+
this.managedPropertyNames.add(propName);
|
|
393
|
+
if (propConfig.type === 'static' || propConfig.type === 'value') {
|
|
394
|
+
if (this.element[propName] !== propConfig.value)
|
|
395
|
+
this.element[propName] = propConfig.value;
|
|
91
396
|
}
|
|
92
397
|
else if (propConfig.type === 'binding') {
|
|
398
|
+
const generation = this.bindingGeneration;
|
|
93
399
|
const value = propConfig.factory ? propConfig.factory() : '';
|
|
94
400
|
if (value !== undefined && value !== null && value !== false) {
|
|
95
|
-
this.element[propName]
|
|
401
|
+
if (this.element[propName] !== value)
|
|
402
|
+
this.element[propName] = value;
|
|
96
403
|
}
|
|
97
404
|
else {
|
|
98
405
|
this.element[propName] = false;
|
|
@@ -101,9 +408,12 @@ export class Html {
|
|
|
101
408
|
// Reactive binding for properties
|
|
102
409
|
if (propConfig.stateKeys?.length) {
|
|
103
410
|
const unsubscribe = this.ctx.states.__.subscribe(propConfig.stateKeys, () => {
|
|
411
|
+
if (!this.isBindingCurrent(generation))
|
|
412
|
+
return;
|
|
104
413
|
const newValue = propConfig.factory ? propConfig.factory() : '';
|
|
105
414
|
if (newValue !== undefined && newValue !== null && newValue !== false) {
|
|
106
|
-
this.element[propName]
|
|
415
|
+
if (this.element[propName] !== newValue)
|
|
416
|
+
this.element[propName] = newValue;
|
|
107
417
|
}
|
|
108
418
|
else {
|
|
109
419
|
this.element[propName] = false;
|
|
@@ -116,26 +426,109 @@ export class Html {
|
|
|
116
426
|
}
|
|
117
427
|
}
|
|
118
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* Apply một attr vào element, bao gồm:
|
|
431
|
+
* - Chuẩn hóa tên (camelCase → kebab-case cho data-* / aria-*)
|
|
432
|
+
* - Xử lý reactive binding
|
|
433
|
+
*/
|
|
434
|
+
_applyAttr(attrName, attrConfig) {
|
|
435
|
+
// FIX(Phase4): chuẩn hóa tên — dataCount → data-count
|
|
436
|
+
const normalizedName = this.normalizeAttrName(attrName);
|
|
437
|
+
this.managedAttributeNames.add(normalizedName);
|
|
438
|
+
// FIX(baseline#1): contract chuẩn là 'static' (compiler emit); 'value' giữ làm legacy alias
|
|
439
|
+
if (attrConfig.type === 'static' || attrConfig.type === 'value') {
|
|
440
|
+
if (attrConfig.value !== undefined && attrConfig.value !== null && attrConfig.value !== false) {
|
|
441
|
+
if (this.element.getAttribute(normalizedName) !== String(attrConfig.value))
|
|
442
|
+
this.element.setAttribute(normalizedName, String(attrConfig.value));
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
this.element.removeAttribute(normalizedName);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
else if (attrConfig.type === 'binding') {
|
|
449
|
+
const generation = this.bindingGeneration;
|
|
450
|
+
const applyValue = () => {
|
|
451
|
+
const newValue = attrConfig.factory ? attrConfig.factory() : '';
|
|
452
|
+
if (newValue !== undefined && newValue !== null && newValue !== false) {
|
|
453
|
+
if (this.element.getAttribute(normalizedName) !== String(newValue))
|
|
454
|
+
this.element.setAttribute(normalizedName, String(newValue));
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
this.element.removeAttribute(normalizedName);
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
applyValue();
|
|
461
|
+
if (attrConfig.stateKeys?.length) {
|
|
462
|
+
const unsubscribe = this.ctx.states.__.subscribe(attrConfig.stateKeys, () => { if (this.isBindingCurrent(generation))
|
|
463
|
+
applyValue(); });
|
|
464
|
+
this.bindingUnsubscribes.push(unsubscribe);
|
|
465
|
+
}
|
|
466
|
+
// `@yield(name, ...)` — no static stateKeys (the section it resolves to is
|
|
467
|
+
// only known at runtime); subscribe to SectionManager by name instead, fires
|
|
468
|
+
// both when a different section becomes active and when its value changes.
|
|
469
|
+
if (attrConfig.yieldName) {
|
|
470
|
+
const unsubscribe = SectionManager.subscribe(attrConfig.yieldName, () => { if (this.isBindingCurrent(generation))
|
|
471
|
+
applyValue(); });
|
|
472
|
+
this.bindingUnsubscribes.push(unsubscribe);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
119
476
|
initializeClasses() {
|
|
120
477
|
if (!this.config.classes)
|
|
121
478
|
return;
|
|
122
479
|
// New simplified format: classes: [{ type, value, factory?, stateKeys? }]
|
|
123
480
|
if (Array.isArray(this.config.classes)) {
|
|
124
481
|
for (const classConfig of this.config.classes) {
|
|
125
|
-
if (!classConfig
|
|
482
|
+
if (!classConfig)
|
|
483
|
+
continue;
|
|
484
|
+
// `class="language-{{ lang }}"` — factory trả về TÊN class chứ không
|
|
485
|
+
// phải boolean, nên không có `value` tĩnh để bật/tắt. Phải nhớ lại tên
|
|
486
|
+
// đã gắn để gỡ đúng chúng khi state đổi ('language-php' → 'language-js');
|
|
487
|
+
// tên còn lại trên element có thể do nơi khác quản lý.
|
|
488
|
+
if (classConfig.type === 'dynamic') {
|
|
489
|
+
const generation = this.bindingGeneration;
|
|
490
|
+
let applied = [];
|
|
491
|
+
const applyDynamic = () => {
|
|
492
|
+
const next = String(classConfig.factory ? classConfig.factory() : '')
|
|
493
|
+
.split(/\s+/).filter(Boolean);
|
|
494
|
+
for (const prev of applied) {
|
|
495
|
+
if (next.indexOf(prev) === -1) {
|
|
496
|
+
this.removeClass(prev);
|
|
497
|
+
this.managedClassNames.delete(prev);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
for (const name of next) {
|
|
501
|
+
this.addClass(name);
|
|
502
|
+
this.managedClassNames.add(name);
|
|
503
|
+
}
|
|
504
|
+
applied = next;
|
|
505
|
+
};
|
|
506
|
+
applyDynamic();
|
|
507
|
+
if (classConfig.stateKeys?.length) {
|
|
508
|
+
const unsubscribe = this.ctx.states.__.subscribe(classConfig.stateKeys, () => { if (this.isBindingCurrent(generation))
|
|
509
|
+
applyDynamic(); });
|
|
510
|
+
this.bindingUnsubscribes.push(unsubscribe);
|
|
511
|
+
}
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
if (!classConfig.value)
|
|
126
515
|
continue;
|
|
127
516
|
const className = classConfig.value;
|
|
517
|
+
this.managedClassNames.add(className);
|
|
128
518
|
if (classConfig.type === 'static') {
|
|
129
|
-
this.
|
|
519
|
+
this.addClass(className);
|
|
130
520
|
continue;
|
|
131
521
|
}
|
|
132
522
|
if (classConfig.type === 'binding') {
|
|
523
|
+
const generation = this.bindingGeneration;
|
|
133
524
|
const initialValue = classConfig.factory ? classConfig.factory() : false;
|
|
134
|
-
this.
|
|
525
|
+
this.toggleClass(className, !!initialValue);
|
|
135
526
|
if (classConfig.stateKeys?.length) {
|
|
136
527
|
const unsubscribe = this.ctx.states.__.subscribe(classConfig.stateKeys, () => {
|
|
528
|
+
if (!this.isBindingCurrent(generation))
|
|
529
|
+
return;
|
|
137
530
|
const newValue = classConfig.factory ? classConfig.factory() : false;
|
|
138
|
-
this.
|
|
531
|
+
this.toggleClass(className, !!newValue);
|
|
139
532
|
});
|
|
140
533
|
this.bindingUnsubscribes.push(unsubscribe);
|
|
141
534
|
}
|
|
@@ -144,42 +537,100 @@ export class Html {
|
|
|
144
537
|
return;
|
|
145
538
|
}
|
|
146
539
|
for (const [className, classConfig] of Object.entries(this.config.classes)) {
|
|
540
|
+
this.managedClassNames.add(className);
|
|
147
541
|
if (classConfig.type === 'static') {
|
|
148
542
|
if (classConfig.value) {
|
|
149
|
-
this.
|
|
543
|
+
this.addClass(className);
|
|
150
544
|
}
|
|
151
545
|
}
|
|
152
546
|
else if (classConfig.type === 'binding') {
|
|
547
|
+
const generation = this.bindingGeneration;
|
|
153
548
|
// Initial value
|
|
154
549
|
const initialValue = classConfig.factory ? classConfig.factory() : !!classConfig.value;
|
|
155
|
-
this.
|
|
550
|
+
this.toggleClass(className, !!initialValue);
|
|
156
551
|
// Subscribe for reactive updates
|
|
157
552
|
if (classConfig.stateKeys?.length) {
|
|
158
553
|
const unsubscribe = this.ctx.states.__.subscribe(classConfig.stateKeys, () => {
|
|
554
|
+
if (!this.isBindingCurrent(generation))
|
|
555
|
+
return;
|
|
159
556
|
const newValue = classConfig.factory ? classConfig.factory() : false;
|
|
160
|
-
this.
|
|
557
|
+
this.toggleClass(className, !!newValue);
|
|
161
558
|
});
|
|
162
559
|
this.bindingUnsubscribes.push(unsubscribe);
|
|
163
560
|
}
|
|
164
561
|
}
|
|
165
562
|
}
|
|
166
563
|
}
|
|
564
|
+
addClass(className) {
|
|
565
|
+
if (!className)
|
|
566
|
+
return;
|
|
567
|
+
if (className.includes(' ')) {
|
|
568
|
+
const tokens = className.trim().split(/\s+/);
|
|
569
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
570
|
+
if (tokens[i])
|
|
571
|
+
if (!this.element.classList.contains(tokens[i]))
|
|
572
|
+
this.element.classList.add(tokens[i]);
|
|
573
|
+
}
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (!this.element.classList.contains(className))
|
|
577
|
+
this.element.classList.add(className);
|
|
578
|
+
}
|
|
579
|
+
removeClass(className) {
|
|
580
|
+
if (!className)
|
|
581
|
+
return;
|
|
582
|
+
if (className.includes(' ')) {
|
|
583
|
+
const tokens = className.trim().split(/\s+/);
|
|
584
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
585
|
+
if (tokens[i])
|
|
586
|
+
if (this.element.classList.contains(tokens[i]))
|
|
587
|
+
this.element.classList.remove(tokens[i]);
|
|
588
|
+
}
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (this.element.classList.contains(className))
|
|
592
|
+
this.element.classList.remove(className);
|
|
593
|
+
}
|
|
594
|
+
toggleClass(className, force) {
|
|
595
|
+
if (!className)
|
|
596
|
+
return;
|
|
597
|
+
if (className.includes(' ')) {
|
|
598
|
+
const tokens = className.trim().split(/\s+/);
|
|
599
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
600
|
+
if (tokens[i])
|
|
601
|
+
if (this.element.classList.contains(tokens[i]) !== force)
|
|
602
|
+
this.element.classList.toggle(tokens[i], force);
|
|
603
|
+
}
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (this.element.classList.contains(className) !== force)
|
|
607
|
+
this.element.classList.toggle(className, force);
|
|
608
|
+
}
|
|
609
|
+
applyStyle(prop, value) {
|
|
610
|
+
const text = String(value ?? '');
|
|
611
|
+
if (this.element.style.getPropertyValue(prop) !== text)
|
|
612
|
+
this.element.style.setProperty(prop, text);
|
|
613
|
+
}
|
|
167
614
|
initializeStyles() {
|
|
168
615
|
if (!this.config.styles)
|
|
169
616
|
return;
|
|
170
617
|
for (const [prop, styleConfig] of Object.entries(this.config.styles)) {
|
|
171
|
-
|
|
172
|
-
|
|
618
|
+
this.managedStyleNames.add(prop);
|
|
619
|
+
if (styleConfig.type === 'static' || styleConfig.type === 'value') {
|
|
620
|
+
this.applyStyle(prop, styleConfig.value ?? '');
|
|
173
621
|
}
|
|
174
622
|
else if (styleConfig.type === 'binding') {
|
|
623
|
+
const generation = this.bindingGeneration;
|
|
175
624
|
// Initial value
|
|
176
625
|
const initialValue = styleConfig.factory ? styleConfig.factory() : (styleConfig.value ?? '');
|
|
177
|
-
this.
|
|
626
|
+
this.applyStyle(prop, initialValue);
|
|
178
627
|
// Subscribe for reactive updates
|
|
179
628
|
if (styleConfig.stateKeys?.length) {
|
|
180
629
|
const unsubscribe = this.ctx.states.__.subscribe(styleConfig.stateKeys, () => {
|
|
630
|
+
if (!this.isBindingCurrent(generation))
|
|
631
|
+
return;
|
|
181
632
|
const newValue = styleConfig.factory ? styleConfig.factory() : '';
|
|
182
|
-
this.
|
|
633
|
+
this.applyStyle(prop, newValue);
|
|
183
634
|
});
|
|
184
635
|
this.bindingUnsubscribes.push(unsubscribe);
|
|
185
636
|
}
|
|
@@ -187,12 +638,27 @@ export class Html {
|
|
|
187
638
|
}
|
|
188
639
|
}
|
|
189
640
|
initializeEvents() {
|
|
641
|
+
this.addEventListeners();
|
|
642
|
+
}
|
|
643
|
+
addEventListeners() {
|
|
190
644
|
if (this.config.events) {
|
|
191
645
|
for (const [eventName, handlers] of Object.entries(this.config.events)) {
|
|
192
|
-
this.ctx.addEventListener(this.element, eventName, handlers);
|
|
646
|
+
this.ctx.addEventListener(this.element, eventName, handlers, this.config.eventModifiers?.[eventName]);
|
|
647
|
+
this.registeredEventNames.add(eventName);
|
|
193
648
|
}
|
|
194
649
|
}
|
|
195
650
|
}
|
|
651
|
+
removeEventListeners() {
|
|
652
|
+
for (const eventName of this.registeredEventNames) {
|
|
653
|
+
try {
|
|
654
|
+
this.ctx.removeEventListener(this.element, eventName);
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
console.error(`[Html] Failed to remove "${eventName}" listener:`, error);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
this.registeredEventNames.clear();
|
|
661
|
+
}
|
|
196
662
|
setParentElement(parent) {
|
|
197
663
|
this.parent = parent;
|
|
198
664
|
}
|
|
@@ -205,47 +671,138 @@ export class Html {
|
|
|
205
671
|
isSingleElement() {
|
|
206
672
|
return ['input', 'img', 'br', 'hr', 'meta', 'link'].includes(this.tagName.toLowerCase());
|
|
207
673
|
}
|
|
674
|
+
getElement() {
|
|
675
|
+
return this.element;
|
|
676
|
+
}
|
|
677
|
+
renderChildren() {
|
|
678
|
+
const children = this.childrenFactory ? this.childrenFactory(this) : [];
|
|
679
|
+
this.children = [];
|
|
680
|
+
this.children = children
|
|
681
|
+
.filter((child) => child !== null && child !== undefined)
|
|
682
|
+
.map((child) => {
|
|
683
|
+
if (typeof child === 'string' || typeof child === 'number') {
|
|
684
|
+
return new TextElement({ ctx: this.ctx, parent: this.parent, stateKeys: [], generateText: () => String(child) });
|
|
685
|
+
}
|
|
686
|
+
return child;
|
|
687
|
+
});
|
|
688
|
+
return this.children;
|
|
689
|
+
}
|
|
208
690
|
render() {
|
|
691
|
+
const previous = this.children;
|
|
209
692
|
if (this.isSingleElement()) {
|
|
210
693
|
return this.element;
|
|
211
694
|
}
|
|
695
|
+
if (this.config.content) {
|
|
696
|
+
this.renderTextContent();
|
|
697
|
+
return this.element;
|
|
698
|
+
}
|
|
212
699
|
let children = [];
|
|
213
700
|
if (this.childrenFactory) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
});
|
|
701
|
+
children = this.renderChildren();
|
|
702
|
+
}
|
|
703
|
+
children = this.children = reuseStaticText(previous, children);
|
|
704
|
+
if (this.initMode === InitModes.HYDRATE) {
|
|
705
|
+
// ── Hydrate mode: DOM đã có từ server ────────────────────────
|
|
706
|
+
// renderChildren() đã tạo JS objects (Html claim DOM, Output claim markers).
|
|
707
|
+
// hydrateElementList gọi render() đệ quy để children cũng claim DOM,
|
|
708
|
+
// nhưng KHÔNG appendChild — giữ nguyên server-rendered DOM.
|
|
709
|
+
if (children && children.length > 0) {
|
|
710
|
+
hydrateElementList(this, children);
|
|
711
|
+
}
|
|
712
|
+
this.initMode = InitModes.CREATE;
|
|
713
|
+
return this.element;
|
|
714
|
+
}
|
|
715
|
+
reconcileChildren(this.element, previous, children);
|
|
716
|
+
// Drop unmanaged nodes left by previous renders, retaining marker ranges.
|
|
717
|
+
const keep = new Set();
|
|
718
|
+
for (const child of children) {
|
|
719
|
+
const first = child.element ?? child.openTag;
|
|
720
|
+
const last = child.element ?? child.closeTag;
|
|
721
|
+
let current = first;
|
|
722
|
+
while (current) {
|
|
723
|
+
keep.add(current);
|
|
724
|
+
if (current === last)
|
|
725
|
+
break;
|
|
726
|
+
current = current.nextSibling ?? undefined;
|
|
727
|
+
}
|
|
244
728
|
}
|
|
729
|
+
for (const node of Array.from(this.element.childNodes))
|
|
730
|
+
if (!keep.has(node))
|
|
731
|
+
node.remove();
|
|
732
|
+
if (this.isStarted)
|
|
733
|
+
for (const child of children)
|
|
734
|
+
child.start?.();
|
|
735
|
+
this.maybeRunEnter();
|
|
245
736
|
return this.element;
|
|
246
737
|
}
|
|
738
|
+
renderTextContent() {
|
|
739
|
+
this.contentUnsubscribe?.();
|
|
740
|
+
this.contentUnsubscribe = null;
|
|
741
|
+
const content = this.config.content;
|
|
742
|
+
const isTextarea = this.tagName.toLowerCase() === 'textarea';
|
|
743
|
+
const generation = this.bindingGeneration;
|
|
744
|
+
const apply = () => {
|
|
745
|
+
if (!this.isBindingCurrent(generation))
|
|
746
|
+
return;
|
|
747
|
+
const value = String(content.factory() ?? '');
|
|
748
|
+
if (isTextarea) {
|
|
749
|
+
const textarea = this.element;
|
|
750
|
+
// @bind owns the live value when both forms are declared.
|
|
751
|
+
if (!this.config.bind && textarea.value !== value)
|
|
752
|
+
textarea.value = value;
|
|
753
|
+
}
|
|
754
|
+
else if (this.element.textContent !== value) {
|
|
755
|
+
this.element.textContent = value;
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
if (this.contentRendered)
|
|
759
|
+
apply();
|
|
760
|
+
else if (!this.adoptedSSR) {
|
|
761
|
+
const value = String(content.factory() ?? '');
|
|
762
|
+
if (isTextarea) {
|
|
763
|
+
const textarea = this.element;
|
|
764
|
+
// Match HTML parsing: one initial LF after <textarea> is ignored.
|
|
765
|
+
textarea.defaultValue = value.replace(/^\n/, '');
|
|
766
|
+
if (!this.config.bind)
|
|
767
|
+
textarea.value = textarea.defaultValue;
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
this.element.textContent = value;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
this.contentRendered = true;
|
|
774
|
+
// Hydration adopts SSR content, including input typed before JS loaded.
|
|
775
|
+
// Future state changes patch the property, preserving the node itself.
|
|
776
|
+
if (content.stateKeys?.length && !(isTextarea && this.config.bind)) {
|
|
777
|
+
this.contentUnsubscribe = this.ctx.states.__.subscribe(content.stateKeys, apply);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Enter chạy MỘT lần, khi element vừa được tạo và đã nằm trong DOM.
|
|
782
|
+
* Bỏ qua ở HYDRATE: DOM đó do server render, animate lại là nháy vô cớ
|
|
783
|
+
* (tương đương `appear = false` mặc định của Vue).
|
|
784
|
+
*/
|
|
785
|
+
maybeRunEnter() {
|
|
786
|
+
if (this._entered)
|
|
787
|
+
return;
|
|
788
|
+
const name = this.config.transition?.name;
|
|
789
|
+
if (!name)
|
|
790
|
+
return;
|
|
791
|
+
if (this.initMode === InitModes.HYDRATE) {
|
|
792
|
+
this._entered = true;
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
if (!this.element.isConnected)
|
|
796
|
+
return; // caller chưa chèn — thử lại lần render sau
|
|
797
|
+
this._entered = true;
|
|
798
|
+
void runEnter(this.element, name);
|
|
799
|
+
}
|
|
800
|
+
appendElement(element) {
|
|
801
|
+
this.element.appendChild(element);
|
|
802
|
+
}
|
|
247
803
|
/** Start reactive bindings + children (Phase 2 lifecycle) */
|
|
248
804
|
start() {
|
|
805
|
+
this.isStarted = true;
|
|
249
806
|
for (const child of this.children) {
|
|
250
807
|
if ('start' in child && typeof child.start === 'function') {
|
|
251
808
|
child.start();
|
|
@@ -254,25 +811,69 @@ export class Html {
|
|
|
254
811
|
}
|
|
255
812
|
/** Stop reactive bindings + children */
|
|
256
813
|
stop() {
|
|
814
|
+
this.isStarted = false;
|
|
257
815
|
for (const child of this.children) {
|
|
258
816
|
if ('stop' in child && typeof child.stop === 'function') {
|
|
259
817
|
child.stop();
|
|
260
818
|
}
|
|
261
819
|
}
|
|
262
820
|
}
|
|
821
|
+
clearHTML() {
|
|
822
|
+
this.element.innerHTML = '';
|
|
823
|
+
}
|
|
263
824
|
remove() {
|
|
264
825
|
this.element.remove();
|
|
265
826
|
}
|
|
266
827
|
destroy() {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
this.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
828
|
+
if (this.__destroyed__)
|
|
829
|
+
return;
|
|
830
|
+
this.__destroyed__ = true;
|
|
831
|
+
this.ctx.releaseElement?.(this);
|
|
832
|
+
this.removeEventListeners();
|
|
833
|
+
// Abort @bind listeners and unsubscribe every reactive attr/prop/class/style.
|
|
834
|
+
this.cleanupBindingResources(false);
|
|
835
|
+
const transitionName = this.config.transition?.name;
|
|
836
|
+
if (transitionName && this.element.isConnected) {
|
|
837
|
+
// HOÃN teardown cây con: destroy() của child Html gỡ luôn DOM của nó,
|
|
838
|
+
// nên dọn ngay sẽ làm element bay ra trong trạng thái RỖNG. Element
|
|
839
|
+
// đã inert (listener gỡ, binding huỷ) nên giữ lại chỉ là phần nhìn.
|
|
840
|
+
// runLeave() tự gỡ node khi xong — không remove() ở đây.
|
|
841
|
+
//
|
|
842
|
+
// NHƯNG phải nhả id của CẢ CÂY CON khỏi registry NGAY. Một element
|
|
843
|
+
// đang leave là element đã chết về mặt logic; nếu id của nó còn
|
|
844
|
+
// trong `ctx.elements` thì pass render kế tiếp sẽ `aliveFromRegistry`
|
|
845
|
+
// trúng nó (chưa `__destroyed__` vì teardown bị hoãn) và tái dùng —
|
|
846
|
+
// Component thì `_childMounted` vẫn true nên `mountChild()` return
|
|
847
|
+
// ngay ⇒ vùng @include/@if của hàng mới RỖNG. Đây đúng là lớp lỗi
|
|
848
|
+
// §2.10, quay lại qua đường transition.
|
|
849
|
+
this.releaseSubtreeFromRegistry();
|
|
850
|
+
void runLeave(this.element, transitionName).then(() => this.teardownSubtree());
|
|
851
|
+
return;
|
|
273
852
|
}
|
|
274
|
-
this.
|
|
275
|
-
//
|
|
853
|
+
this.teardownSubtree();
|
|
854
|
+
// Gỡ element khỏi DOM — destroy là vĩnh viễn
|
|
855
|
+
this.element.remove();
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Nhả id của element này và toàn bộ cây con khỏi `ctx.elements`, KHÔNG
|
|
859
|
+
* destroy. Dùng khi leave transition giữ DOM lại: registry phải sạch ngay
|
|
860
|
+
* để pass sau tạo element mới thay vì tái dùng xác sắp chết.
|
|
861
|
+
*/
|
|
862
|
+
releaseSubtreeFromRegistry() {
|
|
863
|
+
const release = (node) => {
|
|
864
|
+
if (!node || typeof node !== 'object')
|
|
865
|
+
return;
|
|
866
|
+
this.ctx.releaseElement?.(node);
|
|
867
|
+
const kids = node.children;
|
|
868
|
+
if (Array.isArray(kids))
|
|
869
|
+
for (const k of kids)
|
|
870
|
+
release(k);
|
|
871
|
+
};
|
|
872
|
+
for (const child of this.children)
|
|
873
|
+
release(child);
|
|
874
|
+
}
|
|
875
|
+
/** Destroy children + dọn nội dung. Tách riêng để leave hoãn được. */
|
|
876
|
+
teardownSubtree() {
|
|
276
877
|
this.children.forEach(child => {
|
|
277
878
|
if ('destroy' in child && typeof child.destroy === 'function') {
|
|
278
879
|
child.destroy();
|
|
@@ -283,10 +884,10 @@ export class Html {
|
|
|
283
884
|
this.element.innerHTML = '';
|
|
284
885
|
}
|
|
285
886
|
}
|
|
286
|
-
get
|
|
887
|
+
get isSaoElement() {
|
|
287
888
|
return true;
|
|
288
889
|
}
|
|
289
|
-
set
|
|
890
|
+
set isSaoElement(value) {
|
|
290
891
|
// No-op setter to satisfy the Interface; this property is always true for Html elements
|
|
291
892
|
}
|
|
292
893
|
get isOneHtml() {
|