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