@shortfuse/materialdesignweb 0.4.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (452) hide show
  1. package/README.md +155 -79
  2. package/bin/generate-css.js +12 -0
  3. package/components/Badge.css +30 -0
  4. package/components/Badge.js +15 -0
  5. package/components/Body.css +14 -0
  6. package/components/Body.js +7 -0
  7. package/components/BottomAppBar.css +23 -0
  8. package/components/BottomAppBar.js +25 -0
  9. package/components/Box.css +31 -0
  10. package/components/Box.js +24 -0
  11. package/components/Button.css +146 -0
  12. package/components/Button.js +95 -0
  13. package/components/Button.md +61 -0
  14. package/components/Card.css +109 -0
  15. package/components/Card.js +82 -0
  16. package/components/Checkbox.css +77 -0
  17. package/components/Checkbox.js +59 -0
  18. package/components/CheckboxIcon.css +89 -0
  19. package/components/CheckboxIcon.js +41 -0
  20. package/components/Chip.css +35 -0
  21. package/components/Chip.js +22 -0
  22. package/components/Dialog.css +235 -0
  23. package/components/Dialog.js +327 -0
  24. package/components/DialogActions.js +13 -0
  25. package/components/Divider.css +41 -0
  26. package/components/Divider.js +13 -0
  27. package/components/ExtendedFab.css +24 -0
  28. package/components/ExtendedFab.js +11 -0
  29. package/components/Fab.css +23 -0
  30. package/components/Fab.js +26 -0
  31. package/components/FilterChip.css +80 -0
  32. package/components/FilterChip.js +51 -0
  33. package/components/Headline.css +14 -0
  34. package/components/Headline.js +33 -0
  35. package/components/Icon.css +76 -0
  36. package/components/Icon.js +174 -0
  37. package/components/IconButton.css +151 -0
  38. package/components/IconButton.js +65 -0
  39. package/components/Input.js +16 -0
  40. package/components/Label.css +14 -0
  41. package/components/Label.js +7 -0
  42. package/components/Layout.css +19 -0
  43. package/components/Layout.js +12 -0
  44. package/components/List.css +12 -0
  45. package/components/List.js +17 -0
  46. package/components/ListItem.css +224 -0
  47. package/components/ListItem.js +112 -0
  48. package/components/ListOption.css +34 -0
  49. package/components/ListOption.js +122 -0
  50. package/components/ListSelect.css +9 -0
  51. package/components/ListSelect.js +206 -0
  52. package/components/Menu.css +171 -0
  53. package/components/Menu.js +470 -0
  54. package/components/MenuItem.css +53 -0
  55. package/components/MenuItem.js +215 -0
  56. package/components/Nav.css +17 -0
  57. package/components/Nav.js +23 -0
  58. package/components/NavBar.css +34 -0
  59. package/components/NavBar.js +88 -0
  60. package/components/NavBarItem.css +41 -0
  61. package/components/NavBarItem.js +7 -0
  62. package/components/NavDrawer.css +31 -0
  63. package/components/NavDrawer.js +13 -0
  64. package/components/NavDrawerItem.css +42 -0
  65. package/components/NavDrawerItem.js +12 -0
  66. package/components/NavItem.css +181 -0
  67. package/components/NavItem.js +83 -0
  68. package/components/NavRail.css +47 -0
  69. package/components/NavRail.js +17 -0
  70. package/components/NavRailItem.css +25 -0
  71. package/components/NavRailItem.js +7 -0
  72. package/components/Option.js +91 -0
  73. package/components/Outline.css +138 -0
  74. package/components/Pane.css +261 -0
  75. package/components/Pane.js +21 -0
  76. package/components/Progress.css +74 -0
  77. package/components/Progress.js +67 -0
  78. package/components/ProgressCircle.css +226 -0
  79. package/components/ProgressLine.css +155 -0
  80. package/components/Radio.css +83 -0
  81. package/components/Radio.js +42 -0
  82. package/components/RadioIcon.css +73 -0
  83. package/components/RadioIcon.js +37 -0
  84. package/components/Ripple.css +74 -0
  85. package/components/Ripple.js +114 -0
  86. package/components/SegmentedButton.css +94 -0
  87. package/components/SegmentedButton.js +49 -0
  88. package/components/SegmentedButtonGroup.css +12 -0
  89. package/components/SegmentedButtonGroup.js +44 -0
  90. package/components/Select.css +52 -0
  91. package/components/Select.js +71 -0
  92. package/components/Shape.css +132 -0
  93. package/components/Shape.js +25 -0
  94. package/components/Slider.css +306 -0
  95. package/components/Slider.js +206 -0
  96. package/components/Snackbar.css +80 -0
  97. package/components/Snackbar.js +75 -0
  98. package/components/Surface.css +10 -0
  99. package/components/Surface.js +23 -0
  100. package/components/Switch.css +63 -0
  101. package/components/Switch.js +127 -0
  102. package/components/SwitchIcon.css +177 -0
  103. package/components/SwitchIcon.js +89 -0
  104. package/components/SwitchIconAnimations.css +89 -0
  105. package/components/Tab.css +85 -0
  106. package/components/Tab.js +103 -0
  107. package/components/TabContent.js +151 -0
  108. package/components/TabList.css +129 -0
  109. package/components/TabList.js +309 -0
  110. package/components/TabPanel.js +37 -0
  111. package/components/TextArea.css +93 -0
  112. package/components/TextArea.js +229 -0
  113. package/components/Title.css +14 -0
  114. package/components/Title.js +15 -0
  115. package/components/Tooltip.css +40 -0
  116. package/components/Tooltip.js +22 -0
  117. package/components/TopAppBar.css +209 -0
  118. package/components/TopAppBar.js +201 -0
  119. package/core/Composition.js +988 -0
  120. package/core/CustomElement.js +844 -0
  121. package/core/ICustomElement.d.ts +288 -0
  122. package/core/ICustomElement.js +1 -0
  123. package/core/css.js +51 -0
  124. package/core/customTypes.js +125 -0
  125. package/core/dom.js +56 -245
  126. package/core/identify.js +40 -0
  127. package/core/observe.js +410 -0
  128. package/core/template.js +121 -0
  129. package/core/typings.d.ts +135 -0
  130. package/core/typings.js +1 -0
  131. package/mixins/AriaReflectorMixin.js +42 -0
  132. package/mixins/AriaToolbarMixin.js +13 -0
  133. package/mixins/ControlMixin.css +57 -0
  134. package/mixins/ControlMixin.js +212 -0
  135. package/mixins/DensityMixin.css +40 -0
  136. package/mixins/DensityMixin.js +11 -0
  137. package/mixins/FlexableMixin.css +79 -0
  138. package/mixins/FlexableMixin.js +32 -0
  139. package/mixins/FormAssociatedMixin.js +170 -0
  140. package/mixins/InputMixin.js +335 -0
  141. package/mixins/KeyboardNavMixin.js +244 -0
  142. package/mixins/RTLObserverMixin.js +35 -0
  143. package/mixins/ResizeObserverMixin.js +38 -0
  144. package/mixins/RippleMixin.css +12 -0
  145. package/mixins/RippleMixin.js +115 -0
  146. package/mixins/ScrollListenerMixin.js +100 -0
  147. package/mixins/ShapeMixin.css +135 -0
  148. package/mixins/ShapeMixin.js +31 -0
  149. package/mixins/StateMixin.css +82 -0
  150. package/mixins/StateMixin.js +114 -0
  151. package/mixins/SurfaceMixin.css +150 -0
  152. package/mixins/SurfaceMixin.js +32 -0
  153. package/mixins/TextFieldMixin.css +657 -0
  154. package/mixins/TextFieldMixin.js +121 -0
  155. package/mixins/ThemableMixin.css +204 -0
  156. package/mixins/ThemableMixin.js +16 -0
  157. package/mixins/TooltipTriggerMixin.css +27 -0
  158. package/mixins/TooltipTriggerMixin.js +366 -0
  159. package/mixins/TouchTargetMixin.css +26 -0
  160. package/mixins/TouchTargetMixin.js +9 -0
  161. package/package.json +57 -41
  162. package/theming/index.js +594 -0
  163. package/theming/loader.js +24 -0
  164. package/utils/cli.js +11 -0
  165. package/utils/color_keywords.js +151 -0
  166. package/utils/hct/Cam16.js +298 -0
  167. package/utils/hct/CorePalette.js +84 -0
  168. package/utils/hct/Hct.js +172 -0
  169. package/utils/hct/Scheme.js +587 -0
  170. package/utils/hct/TonalPalette.js +68 -0
  171. package/utils/hct/ViewingConditions.js +136 -0
  172. package/utils/hct/blend.js +93 -0
  173. package/utils/hct/colorUtils.js +302 -0
  174. package/utils/hct/hctSolver.js +559 -0
  175. package/utils/hct/helper.js +182 -0
  176. package/utils/hct/mathUtils.js +153 -0
  177. package/utils/jsonMergePatch.js +100 -0
  178. package/utils/jsx-runtime.js +101 -0
  179. package/utils/popup.js +117 -0
  180. package/utils/svg.js +129 -0
  181. package/.browserslistrc +0 -3
  182. package/.eslintrc.json +0 -153
  183. package/.stylelintrc.json +0 -600
  184. package/.vscode/launch.json +0 -31
  185. package/.vscode/settings.json +0 -3
  186. package/.vscode/tasks.json +0 -32
  187. package/CHANGELOG.md +0 -24
  188. package/CODE_OF_CONDUCT.md +0 -46
  189. package/adapters/datatable/column.js +0 -203
  190. package/adapters/datatable/index.js +0 -972
  191. package/adapters/dom/index.js +0 -601
  192. package/adapters/list/index.js +0 -69
  193. package/adapters/search/index.js +0 -521
  194. package/components/appbar/_spec.scss +0 -225
  195. package/components/appbar/_theme.scss +0 -0
  196. package/components/appbar/index.scss +0 -2
  197. package/components/banner/_spec.scss +0 -118
  198. package/components/banner/_theme.scss +0 -0
  199. package/components/banner/index.scss +0 -2
  200. package/components/bottomnav/README.md +0 -85
  201. package/components/bottomnav/_spec.scss +0 -157
  202. package/components/bottomnav/_theme.scss +0 -0
  203. package/components/bottomnav/index.js +0 -122
  204. package/components/bottomnav/index.scss +0 -2
  205. package/components/bottomnav/item.js +0 -89
  206. package/components/button/README.md +0 -61
  207. package/components/button/_spec.scss +0 -161
  208. package/components/button/_theme.scss +0 -65
  209. package/components/button/index.eta +0 -32
  210. package/components/button/index.js +0 -43
  211. package/components/button/index.pug +0 -18
  212. package/components/button/index.scss +0 -2
  213. package/components/card/_spec.scss +0 -249
  214. package/components/card/_theme.scss +0 -0
  215. package/components/card/index.scss +0 -2
  216. package/components/chip/_spec.scss +0 -134
  217. package/components/chip/_theme.scss +0 -177
  218. package/components/chip/index.js +0 -21
  219. package/components/chip/index.scss +0 -2
  220. package/components/chip/item.js +0 -20
  221. package/components/datatable/_spec.scss +0 -288
  222. package/components/datatable/_theme.scss +0 -154
  223. package/components/datatable/cell.js +0 -45
  224. package/components/datatable/columnheader.js +0 -47
  225. package/components/datatable/index.js +0 -388
  226. package/components/datatable/index.scss +0 -2
  227. package/components/datatable/row.js +0 -49
  228. package/components/datatable/rowheader.js +0 -18
  229. package/components/dialog/_spec.scss +0 -213
  230. package/components/dialog/_theme.scss +0 -0
  231. package/components/dialog/index.js +0 -627
  232. package/components/dialog/index.scss +0 -2
  233. package/components/divider/_spec.scss +0 -13
  234. package/components/divider/_theme.scss +0 -0
  235. package/components/divider/index.scss +0 -2
  236. package/components/elevation/_spec.scss +0 -9
  237. package/components/elevation/_theme.scss +0 -0
  238. package/components/elevation/index.scss +0 -2
  239. package/components/fab/_spec.scss +0 -222
  240. package/components/fab/_theme.scss +0 -0
  241. package/components/fab/index.js +0 -103
  242. package/components/fab/index.scss +0 -2
  243. package/components/grid/_spec.scss +0 -312
  244. package/components/grid/_theme.scss +0 -0
  245. package/components/grid/index.scss +0 -2
  246. package/components/layout/_mixins.scss +0 -33
  247. package/components/layout/_spec.scss +0 -1012
  248. package/components/layout/_theme.scss +0 -44
  249. package/components/layout/index.js +0 -464
  250. package/components/layout/index.scss +0 -2
  251. package/components/list/_spec.scss +0 -397
  252. package/components/list/_theme.scss +0 -111
  253. package/components/list/content.js +0 -110
  254. package/components/list/index.js +0 -260
  255. package/components/list/index.scss +0 -2
  256. package/components/list/item.js +0 -170
  257. package/components/list/secondary.js +0 -46
  258. package/components/menu/_spec.scss +0 -362
  259. package/components/menu/_theme.scss +0 -0
  260. package/components/menu/index.js +0 -721
  261. package/components/menu/index.scss +0 -2
  262. package/components/menu/item.js +0 -239
  263. package/components/progress/_spec.scss +0 -147
  264. package/components/progress/_theme.scss +0 -0
  265. package/components/progress/index.js +0 -36
  266. package/components/progress/index.scss +0 -2
  267. package/components/selection/_spec.scss +0 -386
  268. package/components/selection/_theme.scss +0 -166
  269. package/components/selection/index.eta +0 -60
  270. package/components/selection/index.js +0 -76
  271. package/components/selection/index.pug +0 -30
  272. package/components/selection/index.scss +0 -2
  273. package/components/selection/input.js +0 -56
  274. package/components/selection/radiogroup.js +0 -47
  275. package/components/slider/_spec.scss +0 -64
  276. package/components/slider/_theme.scss +0 -0
  277. package/components/slider/index.scss +0 -2
  278. package/components/snackbar/_spec.scss +0 -195
  279. package/components/snackbar/_theme.scss +0 -0
  280. package/components/snackbar/index.js +0 -344
  281. package/components/snackbar/index.scss +0 -2
  282. package/components/tab/_spec.scss +0 -235
  283. package/components/tab/_theme.scss +0 -0
  284. package/components/tab/content.js +0 -205
  285. package/components/tab/index.js +0 -260
  286. package/components/tab/index.scss +0 -2
  287. package/components/tab/item.js +0 -89
  288. package/components/tab/list.js +0 -210
  289. package/components/tab/panel.js +0 -54
  290. package/components/template/_theme.scss +0 -27
  291. package/components/textfield/README.md +0 -179
  292. package/components/textfield/_mixins.scss +0 -52
  293. package/components/textfield/_spec.scss +0 -809
  294. package/components/textfield/_theme.scss +0 -299
  295. package/components/textfield/index.eta +0 -74
  296. package/components/textfield/index.js +0 -168
  297. package/components/textfield/index.pug +0 -30
  298. package/components/textfield/index.scss +0 -2
  299. package/components/tooltip/_spec.scss +0 -188
  300. package/components/tooltip/_theme.scss +0 -0
  301. package/components/tooltip/index.scss +0 -2
  302. package/components/type/_spec.scss +0 -224
  303. package/components/type/_theme.scss +0 -0
  304. package/components/type/index.scss +0 -2
  305. package/core/_breakpoint.scss +0 -189
  306. package/core/_elevation.scss +0 -38
  307. package/core/_length.scss +0 -9
  308. package/core/_motion.scss +0 -31
  309. package/core/_platform.scss +0 -34
  310. package/core/_type.scss +0 -127
  311. package/core/aria/attributes.js +0 -141
  312. package/core/aria/button.js +0 -50
  313. package/core/aria/keyboard.js +0 -93
  314. package/core/aria/rovingtabindex.js +0 -178
  315. package/core/aria/tab.js +0 -60
  316. package/core/color/_spec.scss +0 -0
  317. package/core/color/_theme.scss +0 -390
  318. package/core/color/index.scss +0 -2
  319. package/core/document/index.js +0 -39
  320. package/core/overlay/_spec.scss +0 -31
  321. package/core/overlay/_theme.scss +0 -171
  322. package/core/overlay/index.js +0 -108
  323. package/core/overlay/index.scss +0 -2
  324. package/core/ripple/_spec.scss +0 -197
  325. package/core/ripple/_theme.scss +0 -40
  326. package/core/ripple/index.js +0 -294
  327. package/core/ripple/index.scss +0 -2
  328. package/core/theme/_config.scss +0 -2
  329. package/core/theme/_mixins.scss +0 -172
  330. package/core/theme/_palettes.scss +0 -406
  331. package/core/theme/_variables.scss +0 -24
  332. package/core/theme/index.js +0 -50
  333. package/core/throttler.js +0 -42
  334. package/core/transition/index.js +0 -468
  335. package/docs/_flex.scss +0 -22
  336. package/docs/_menuoptions.js +0 -183
  337. package/docs/_mixins.pug +0 -155
  338. package/docs/_partials/_androidnavbar.eta +0 -5
  339. package/docs/_partials/_androidstatusbar.eta +0 -13
  340. package/docs/_partials/_appbar.eta +0 -29
  341. package/docs/_partials/_buttontest.eta +0 -31
  342. package/docs/_partials/_header.eta +0 -149
  343. package/docs/_partials/_navlistitem.eta +0 -16
  344. package/docs/_partials/_target.eta +0 -1
  345. package/docs/_sample-utils.js +0 -93
  346. package/docs/_storage.js +0 -33
  347. package/docs/docs.scss +0 -295
  348. package/docs/index.eta +0 -16
  349. package/docs/index.js +0 -0
  350. package/docs/pages/appbar.eta +0 -114
  351. package/docs/pages/appbar.js +0 -0
  352. package/docs/pages/appbar.pug +0 -78
  353. package/docs/pages/bottomnav.eta +0 -188
  354. package/docs/pages/bottomnav.js +0 -115
  355. package/docs/pages/bottomnav.pug +0 -137
  356. package/docs/pages/button.eta +0 -124
  357. package/docs/pages/button.js +0 -224
  358. package/docs/pages/button.pug +0 -121
  359. package/docs/pages/card.eta +0 -90
  360. package/docs/pages/card.js +0 -177
  361. package/docs/pages/card.pug +0 -74
  362. package/docs/pages/chip.eta +0 -122
  363. package/docs/pages/chip.js +0 -82
  364. package/docs/pages/chip.pug +0 -91
  365. package/docs/pages/color.eta +0 -143
  366. package/docs/pages/color.js +0 -262
  367. package/docs/pages/color.pug +0 -121
  368. package/docs/pages/datatable.eta +0 -323
  369. package/docs/pages/datatable.js +0 -164
  370. package/docs/pages/datatable.pug +0 -283
  371. package/docs/pages/dialog.eta +0 -186
  372. package/docs/pages/dialog.js +0 -177
  373. package/docs/pages/dialog.pug +0 -132
  374. package/docs/pages/dom.eta +0 -26
  375. package/docs/pages/dom.js +0 -143
  376. package/docs/pages/dom.pug +0 -22
  377. package/docs/pages/elevation.eta +0 -35
  378. package/docs/pages/elevation.js +0 -0
  379. package/docs/pages/elevation.pug +0 -25
  380. package/docs/pages/fab.eta +0 -99
  381. package/docs/pages/fab.js +0 -44
  382. package/docs/pages/fab.pug +0 -66
  383. package/docs/pages/grid.eta +0 -135
  384. package/docs/pages/grid.js +0 -128
  385. package/docs/pages/grid.pug +0 -95
  386. package/docs/pages/layout.eta +0 -8
  387. package/docs/pages/layout.js +0 -0
  388. package/docs/pages/layout.pug +0 -7
  389. package/docs/pages/list.eta +0 -465
  390. package/docs/pages/list.js +0 -9
  391. package/docs/pages/list.pug +0 -326
  392. package/docs/pages/menu.eta +0 -276
  393. package/docs/pages/menu.js +0 -217
  394. package/docs/pages/menu.pug +0 -205
  395. package/docs/pages/overlay.eta +0 -69
  396. package/docs/pages/overlay.js +0 -4
  397. package/docs/pages/overlay.pug +0 -55
  398. package/docs/pages/progress.eta +0 -23
  399. package/docs/pages/progress.js +0 -12
  400. package/docs/pages/progress.pug +0 -16
  401. package/docs/pages/ripple.eta +0 -27
  402. package/docs/pages/ripple.js +0 -4
  403. package/docs/pages/ripple.pug +0 -21
  404. package/docs/pages/search.eta +0 -246
  405. package/docs/pages/search.js +0 -243
  406. package/docs/pages/search.pug +0 -165
  407. package/docs/pages/selection.eta +0 -111
  408. package/docs/pages/selection.js +0 -13
  409. package/docs/pages/selection.pug +0 -74
  410. package/docs/pages/slider.eta +0 -23
  411. package/docs/pages/slider.js +0 -0
  412. package/docs/pages/slider.pug +0 -17
  413. package/docs/pages/snackbar.eta +0 -83
  414. package/docs/pages/snackbar.js +0 -158
  415. package/docs/pages/snackbar.pug +0 -60
  416. package/docs/pages/tab.eta +0 -421
  417. package/docs/pages/tab.js +0 -151
  418. package/docs/pages/tab.pug +0 -304
  419. package/docs/pages/textfield.eta +0 -486
  420. package/docs/pages/textfield.js +0 -254
  421. package/docs/pages/textfield.pug +0 -360
  422. package/docs/pages/tooltip.eta +0 -94
  423. package/docs/pages/tooltip.js +0 -0
  424. package/docs/pages/tooltip.pug +0 -78
  425. package/docs/pages/transition.eta +0 -117
  426. package/docs/pages/transition.js +0 -54
  427. package/docs/pages/transition.pug +0 -76
  428. package/docs/pages/type.eta +0 -31
  429. package/docs/pages/type.js +0 -0
  430. package/docs/pages/type.pug +0 -29
  431. package/docs/postrender.js +0 -39
  432. package/docs/prerender.js +0 -16
  433. package/docs/pwa/_dialogs.eta +0 -143
  434. package/docs/pwa/_dialogs.pug +0 -96
  435. package/docs/pwa/_menus.eta +0 -16
  436. package/docs/pwa/_menus.pug +0 -11
  437. package/docs/pwa/pwa-prerender.js +0 -3
  438. package/docs/pwa/pwa.eta +0 -480
  439. package/docs/pwa/pwa.js +0 -306
  440. package/docs/pwa/pwa.pug +0 -325
  441. package/docs/pwa/pwa.scss +0 -26
  442. package/docs/spec.scss +0 -26
  443. package/docs/themes/_component-themes.scss +0 -26
  444. package/docs/themes/theme-colored-fallbacks.scss +0 -17
  445. package/docs/themes/theme-colored.scss +0 -17
  446. package/docs/themes/theme-default-fallbacks.scss +0 -17
  447. package/docs/themes/theme-default.scss +0 -17
  448. package/jsconfig.json +0 -12
  449. package/scripts/deploy-docs.sh +0 -9
  450. package/templates/index.eta +0 -2
  451. package/templates/index.pug +0 -3
  452. package/webpack.config.cjs +0 -257
@@ -0,0 +1,988 @@
1
+ /* eslint-disable sort-class-members/sort-class-members */
2
+ import { generateCSSStyleSheets, generateHTMLStyleElements } from './css.js';
3
+ import { identifierFromElement } from './identify.js';
4
+ import { observeFunction } from './observe.js';
5
+ import { generateFragment, inlineFunctions } from './template.js';
6
+
7
+ /**
8
+ * @template T
9
+ * @typedef {Composition<?>|HTMLStyleElement|CSSStyleSheet|DocumentFragment|((this:T, changes:T) => any)|string} CompositionPart
10
+ */
11
+
12
+ /**
13
+ * @template {any} T
14
+ * @callback Compositor
15
+ * @param {...(CompositionPart<T>)} parts source for interpolation (not mutated)
16
+ * @return {Composition<T>}
17
+ */
18
+
19
+ /**
20
+ * @template T
21
+ * @typedef {Object} WatcherBindEntry
22
+ * @prop {Function} fn
23
+ * @prop {Set<keyof T & string>} props
24
+ */
25
+
26
+ /**
27
+ * @template {any} T
28
+ * @typedef {Object} NodeBindEntry
29
+ * @prop {string} id
30
+ * @prop {number} nodeType
31
+ * @prop {string} node
32
+ * @prop {boolean} [negate]
33
+ * @prop {boolean} [doubleNegate]
34
+ * @prop {Function} [fn]
35
+ * @prop {Set<keyof T & string>} props
36
+ * @prop {T} defaultValue
37
+ */
38
+
39
+ /** Splits: `{template}text{template}` as `['', 'template', 'text', 'template', '']` */
40
+ const STRING_INTERPOLATION_REGEX = /{([^}]*)}/g;
41
+
42
+ /**
43
+ * Returns event listener bound to shadow root host.
44
+ * Use this function to avoid generating extra closures
45
+ * @this {HTMLElement}
46
+ * @param {Function} fn
47
+ */
48
+ function buildShadowRootChildListener(fn) {
49
+ /** @param {Event & {currentTarget:{getRootNode: () => ShadowRoot}}} event */
50
+ return function onShadowRootChildEvent(event) {
51
+ const host = event.currentTarget.getRootNode().host;
52
+ fn.call(host, event);
53
+ };
54
+ }
55
+
56
+ /**
57
+ *
58
+ * @param {Object} object
59
+ * @param {'dot'|'bracket'} [syntax]
60
+ * @param {Object} [target]
61
+ * @param {string} [scope]
62
+ * @return {Object}
63
+ */
64
+ function flattenObject(object, syntax = 'dot', target = {}, scope = '') {
65
+ for (const [key, value] of Object.entries(object)) {
66
+ if (!key) continue; // Blank keys are not supported;
67
+ const scopedKey = scope ? `${scope}.${key}` : key;
68
+ target[scopedKey] = value;
69
+ if (value != null && typeof value === 'object') {
70
+ flattenObject(value, syntax, target, scopedKey);
71
+ }
72
+ }
73
+ if (Array.isArray(object)) {
74
+ const scopedKey = scope ? `${scope}.length` : 'length';
75
+ target[scopedKey] = object.length;
76
+ }
77
+ return target;
78
+ }
79
+
80
+ /**
81
+ * @example
82
+ * entryFromPropName(
83
+ * 'address.home.houseNumber',
84
+ * {
85
+ * address: {
86
+ * home: {
87
+ * houseNumber:35,
88
+ * },
89
+ * }
90
+ * }
91
+ * ) === {value:35}
92
+ * @param {string} prop
93
+ * @param {any} source
94
+ * @return {null|[string, any]}
95
+ */
96
+ function entryFromPropName(prop, source) {
97
+ let value = source;
98
+ let child;
99
+ for (child of prop.split('.')) {
100
+ if (!child) throw new Error(`Invalid property: ${prop}`);
101
+ if (child in value === false) return null;
102
+ // @ts-ignore Skip cast
103
+ value = value[child];
104
+ }
105
+ if (value === source) return null;
106
+ return [child, value];
107
+ }
108
+
109
+ /**
110
+ * @param {string} prop
111
+ * @param {any} source
112
+ * @return {any}
113
+ */
114
+ function valueFromPropName(prop, source) {
115
+ let value = source;
116
+ for (const child of prop.split('.')) {
117
+ if (!child) return null;
118
+ // @ts-ignore Skip cast
119
+ value = value[child];
120
+ if (value == null) return null;
121
+ }
122
+ if (value === source) return null;
123
+ return value;
124
+ }
125
+
126
+ /** @template T */
127
+ export default class Composition {
128
+ /**
129
+ * Collection of property bindings.
130
+ * @type {Map<keyof T & string, Set<NodeBindEntry<?>>>}
131
+ */
132
+ bindings = new Map();
133
+
134
+ /**
135
+ * Data of arrays used in templates
136
+ * Usage of a [_for] will create an ArrayLike expectation based on key
137
+ * Only store metadata, not actual data. Currently only needs length.
138
+ * TBD if more is needed later
139
+ * Referenced by property key (string)
140
+ * @type {Map<keyof T & string, ArrayMetadata<T>}
141
+ */
142
+ arrayMetadata = new Map();
143
+
144
+ /**
145
+ * Collection of events to bind.
146
+ * Indexed by ID
147
+ * @type {Map<string, Set<import('./typings.js').CompositionEventListener<any>>>}
148
+ */
149
+ events = new Map();
150
+
151
+ /**
152
+ * Snapshot of composition at initial state.
153
+ * This fragment can be cloned for first rendering, instead of calling
154
+ * of using `render()` to construct the initial DOM tree.
155
+ * @type {DocumentFragment}
156
+ */
157
+ cloneable;
158
+
159
+ /**
160
+ * Result of interpolation of the composition template.
161
+ * Includes all DOM elements, which is used to reference for adding and
162
+ * removing DOM elements during render.
163
+ * @type {DocumentFragment}
164
+ */
165
+ interpolation;
166
+
167
+ /** @type {(HTMLStyleElement|CSSStyleSheet)[]} */
168
+ styles = [];
169
+
170
+ /** @type {CSSStyleSheet[]} */
171
+ adoptedStyleSheets = [];
172
+
173
+ /** @type {DocumentFragment} */
174
+ stylesFragment;
175
+
176
+ /** @type {((this:T, changes:T) => any)[]} */
177
+ watchers = [];
178
+
179
+ /**
180
+ * Maintains a reference list of elements used by render target (root).
181
+ * When root is garbage collected, references are released.
182
+ * This includes disconnected elements.
183
+ * @type {WeakMap<Element|DocumentFragment, Map<string,HTMLElement>>}
184
+ */
185
+ referenceCache = new WeakMap();
186
+
187
+ /**
188
+ * Part of interpolation phase.
189
+ * Maintains a reference list of conditional elements that were removed from
190
+ * `cloneable` due to default state. Used to reconstruct conditional elements
191
+ * with conditional children in default state as well (unlike `interpolation`).
192
+ * @type {Map<string, {element: Element, id: string, parentId: string, commentCache: WeakMap<Element|DocumentFragment,Comment>}>}
193
+ */
194
+ conditionalElementMetadata = new Map();
195
+
196
+ /** Flag set when template and styles have been interpolated */
197
+ interpolated = false;
198
+
199
+ /**
200
+ * @param {(CompositionPart<T>)[]} parts
201
+ */
202
+ constructor(...parts) {
203
+ /**
204
+ * Template used to build interpolation and cloneable
205
+ */
206
+ this.template = generateFragment();
207
+ this.append(...parts);
208
+ }
209
+
210
+ * [Symbol.iterator]() {
211
+ for (const part of this.styles) {
212
+ yield part;
213
+ }
214
+ yield this.template;
215
+ for (const part of this.watchers) {
216
+ yield part;
217
+ }
218
+ }
219
+
220
+ /**
221
+ * @param {CompositionPart<T>[]} parts
222
+ */
223
+ append(...parts) {
224
+ for (const part of parts) {
225
+ if (typeof part === 'string') {
226
+ this.append(generateFragment(part.trim()));
227
+ } else if (typeof part === 'function') {
228
+ this.watchers.push(part);
229
+ } else if (part instanceof Composition) {
230
+ this.append(...part);
231
+ } else if (part instanceof DocumentFragment) {
232
+ this.template.append(part);
233
+ } else if (part instanceof CSSStyleSheet || part instanceof HTMLStyleElement) {
234
+ this.styles.push(part);
235
+ }
236
+ }
237
+ // Allow chaining
238
+ return this;
239
+ }
240
+
241
+ /** @param {import('./typings.js').CompositionEventListener<T>} listener */
242
+ addCompositionEventListener(listener) {
243
+ const key = listener.id ?? '';
244
+ let set = this.events.get(key);
245
+ if (!set) {
246
+ set = new Set();
247
+ this.events.set(key, set);
248
+ }
249
+ set.add(listener);
250
+ return this;
251
+ }
252
+
253
+ /**
254
+ * Updates component nodes based on data.
255
+ * Expects data in JSON Merge Patch format
256
+ * @see https://www.rfc-editor.org/rfc/rfc7386
257
+ * @param {DocumentFragment|ShadowRoot} root where
258
+ * @param {Partial<?>} changes what
259
+ * @param {any} [context] who
260
+ * @param {Partial<?>} [store] If needed, where to grab extra props
261
+ * @return {void}
262
+ */
263
+ render(root, changes, context, store) {
264
+ if (!this.initiallyRendered) this.initialRender(root, changes);
265
+
266
+ if (!changes) return;
267
+
268
+ const fnResults = new WeakMap();
269
+ /** @type {WeakMap<Element, Set<string>>} */
270
+ const modifiedNodes = new WeakMap();
271
+
272
+ // Iterate data instead of bindings.
273
+ // TODO: Avoid double iteration and flatten on-the-fly
274
+ const flattened = flattenObject(changes);
275
+
276
+ for (const [key, rawValue] of Object.entries(flattened)) {
277
+ const entries = this.bindings.get(key);
278
+ if (!entries) continue;
279
+ for (const { id, node, nodeType, fn, props, negate, doubleNegate } of entries) {
280
+ /* 1. Find Element */
281
+
282
+ // TODO: Avoid unnecessary element creation.
283
+ // If element can be fully reconstructed with internal properties,
284
+ // skip recreation of element unless it actually needs to added to DOM.
285
+ // Requires tracing of all properties used by conditional elements.
286
+ const ref = this.getElement(root, id);
287
+ if (!ref) {
288
+ console.warn('Non existent id', id);
289
+ continue;
290
+ }
291
+ if (!ref) continue;
292
+ if (modifiedNodes.get(ref)?.has(node)) {
293
+ // console.warn('Node already modified. Skipping', id, node);
294
+ continue;
295
+ }
296
+
297
+ // if (!ref.parentElement && node !== '_if') {
298
+ // if (ref.parentNode === root) {
299
+ // console.debug('Offscreen? root? rendering', ref, node, ref.id, root.host.outerHTML);
300
+ // } else {
301
+ // console.debug('Offscreen rendering', ref, node, ref.id, root.host.outerHTML);
302
+ // }
303
+ // }
304
+
305
+ /* 2. Compute value */
306
+ let value;
307
+ if (fn) {
308
+ if (fnResults.has(fn)) {
309
+ value = fnResults.get(fn);
310
+ } else {
311
+ const args = structuredClone(changes);
312
+ for (const prop of props) {
313
+ if (prop in flattened) continue;
314
+ let lastIndexOfDot = prop.lastIndexOf('.');
315
+ if (lastIndexOfDot === -1) {
316
+ // console.debug('injected shallow', prop);
317
+ args[prop] = store[prop];
318
+ } else {
319
+ // Relying on props being sorted...
320
+ console.debug('need deep', prop);
321
+ let entry;
322
+ let propSearchKey = prop;
323
+ let lastPropSearchKey = prop;
324
+ while (!entry) {
325
+ entry = entryFromPropName(propSearchKey, args);
326
+ if (entry) {
327
+ const propName = lastPropSearchKey.slice(propSearchKey.length + 1);
328
+ entry[1][propName] = valueFromPropName(lastPropSearchKey, store);
329
+ break;
330
+ }
331
+ if (lastIndexOfDot === -1) break;
332
+ lastPropSearchKey = prop;
333
+ propSearchKey = prop.slice(0, lastIndexOfDot);
334
+ lastIndexOfDot = propSearchKey.lastIndexOf(',');
335
+ }
336
+ if (!entry) {
337
+ console.warn('what do?');
338
+ }
339
+ }
340
+ }
341
+ value = fn.call(context, args);
342
+ fnResults.set(fn, value);
343
+ }
344
+ } else {
345
+ value = rawValue;
346
+ }
347
+
348
+ /* 3. Operate on value */
349
+ if (doubleNegate) {
350
+ value = !!value;
351
+ } else if (negate) {
352
+ value = !value;
353
+ }
354
+
355
+ /* 4. Find Target Node */
356
+ if (nodeType === Node.TEXT_NODE) {
357
+ const index = (node === '#text')
358
+ ? 0
359
+ : Number.parseInt(node.slice('#text'.length), 10);
360
+ let nodesFound = 0;
361
+ for (const childNode of ref.childNodes) {
362
+ if (childNode.nodeType !== Node.TEXT_NODE) continue;
363
+ if (index !== nodesFound++) continue;
364
+ childNode.nodeValue = value ?? '';
365
+ break;
366
+ }
367
+ if (index > nodesFound) {
368
+ console.warn('Node not found, adding?');
369
+ ref.append(value);
370
+ }
371
+ } else if (node === '_if') {
372
+ const attached = root.contains(ref);
373
+ const orphaned = ref.parentElement == null && ref.parentNode !== root;
374
+ const shouldShow = value !== null && value !== false;
375
+ if (orphaned && ref.parentNode) {
376
+ console.warn('Orphaned with parent node?', id, { attached, orphaned, shouldShow }, ref.parentNode);
377
+ }
378
+ if (attached !== !orphaned) {
379
+ console.warn('Conditional state', id, { attached, orphaned, shouldShow });
380
+ console.warn('Not attached and not orphaned. Should do nothing?', ref, ref.parentElement);
381
+ }
382
+ if (shouldShow) {
383
+ if (orphaned) {
384
+ const metadata = this.conditionalElementMetadata.get(id);
385
+ if (!metadata) {
386
+ console.error(id);
387
+ throw new Error('Could not find conditional element metadata');
388
+ }
389
+
390
+ let comment = metadata.commentCache.get(root);
391
+ if (!comment) {
392
+ console.debug('Composition: Comment not cached, building first time');
393
+ const parent = metadata.parentId
394
+ ? this.getElement(root, metadata.parentId)
395
+ : root;
396
+ if (!parent) {
397
+ console.error(id);
398
+ throw new Error('Could not find reference parent!');
399
+ }
400
+
401
+ const commentText = `{#${id}}`;
402
+ for (const child of parent.childNodes) {
403
+ if (child.nodeType !== Node.COMMENT_NODE) continue;
404
+ if ((/** @type {Comment} */child).nodeValue === commentText) {
405
+ comment = child;
406
+ break;
407
+ }
408
+ }
409
+ metadata.commentCache.set(this, comment);
410
+ }
411
+ if (comment) {
412
+ console.debug('Composition: Add', id, 'back', ref.outerHTML);
413
+ comment.replaceWith(ref);
414
+ } else {
415
+ console.warn('Could not add', id, 'back to parent');
416
+ }
417
+ }
418
+ } else if (!orphaned) {
419
+ const metadata = this.conditionalElementMetadata.get(id);
420
+ if (!metadata) {
421
+ console.error(id);
422
+ throw new Error(`Could not find conditional element metadata for ${id}`);
423
+ }
424
+ let comment = metadata.commentCache.get(root);
425
+ if (!comment) {
426
+ comment = new Comment(`{#${id}}`);
427
+ metadata.commentCache.set(this, comment);
428
+ }
429
+ console.debug('Composition: Remove', id, ref.outerHTML);
430
+ ref.replaceWith(comment);
431
+ }
432
+ } else if (value === false || value == null) {
433
+ ref.removeAttribute(node);
434
+ } else {
435
+ ref.setAttribute(node, value === true ? '' : value);
436
+ }
437
+
438
+ /* 5. Mark Node as modified */
439
+ let set = modifiedNodes.get(ref);
440
+ if (!set) {
441
+ set = new Set();
442
+ modifiedNodes.set(ref, set);
443
+ }
444
+ set.add(node);
445
+ }
446
+ }
447
+ }
448
+
449
+ /**
450
+ * @param {Attr|Text} node
451
+ * @param {Element} element
452
+ * @param {Object} [defaults]
453
+ * @param {string} [parsedValue]
454
+ * @return {boolean} Remove node
455
+ */
456
+ #interpolateNode(node, element, defaults, parsedValue) {
457
+ const { nodeValue, nodeName, nodeType } = node;
458
+
459
+ if (parsedValue == null) {
460
+ if (!nodeValue) return false;
461
+ const trimmed = nodeValue.trim();
462
+ if (!trimmed) return false;
463
+ if (nodeType === Node.ATTRIBUTE_NODE) {
464
+ if (trimmed[0] !== '{') return false;
465
+ const { length } = trimmed;
466
+ if (trimmed[length - 1] !== '}') return false;
467
+ parsedValue = trimmed.slice(1, -1);
468
+ } else {
469
+ // Split text node into segments
470
+ // TODO: Benchmark indexOf pre-check vs regex
471
+
472
+ const segments = trimmed.split(STRING_INTERPOLATION_REGEX);
473
+ if (segments.length < 3) return false;
474
+ if (segments.length === 3 && !segments[0] && !segments[2]) {
475
+ parsedValue = segments[1];
476
+ } else {
477
+ segments.forEach((segment, index) => {
478
+ // is even = is template string
479
+ if (index % 2) {
480
+ const newNode = new Text();
481
+ node.before(newNode);
482
+ this.#interpolateNode(newNode, element, defaults, segment);
483
+ } else {
484
+ if (!segment) return; // blank
485
+ node.before(segment);
486
+ }
487
+ });
488
+ // node.remove();
489
+ return true;
490
+ }
491
+ }
492
+ }
493
+
494
+ const negate = parsedValue[0] === '!';
495
+ let doubleNegate = false;
496
+ if (negate) {
497
+ parsedValue = parsedValue.slice(1);
498
+ doubleNegate = parsedValue[0] === '!';
499
+ if (doubleNegate) {
500
+ parsedValue = parsedValue.slice(1);
501
+ }
502
+ }
503
+
504
+ let isEvent;
505
+ let textNodeIndex;
506
+
507
+ if (nodeType === Node.TEXT_NODE) {
508
+ // eslint-disable-next-line unicorn/consistent-destructuring
509
+ if (element !== node.parentElement) {
510
+ console.warn('mismatch?');
511
+ element = node.parentElement;
512
+ }
513
+ textNodeIndex = 0;
514
+ let prev = node;
515
+ while ((prev = prev.previousSibling)) {
516
+ if (prev.nodeType === Node.TEXT_NODE) {
517
+ textNodeIndex++;
518
+ }
519
+ }
520
+ } else {
521
+ // @ts-ignore Skip cast
522
+ // eslint-disable-next-line unicorn/consistent-destructuring
523
+ if (element !== node.ownerElement) {
524
+ console.warn('mismatch?');
525
+ element = node.ownerElement;
526
+ }
527
+ if (nodeName.startsWith('on')) {
528
+ // Do not interpolate inline event listeners
529
+ if (nodeName[2] !== '-') return false;
530
+ isEvent = true;
531
+ }
532
+ }
533
+
534
+ const id = identifierFromElement(element, true);
535
+
536
+ if (isEvent) {
537
+ const eventType = nodeName.slice(3);
538
+ const [, flags, type] = eventType.match(/^([*1~]+)?(.*)$/);
539
+ const options = {
540
+ once: flags?.includes('1'),
541
+ passive: flags?.includes('~'),
542
+ capture: flags?.includes('*'),
543
+ };
544
+
545
+ element.removeAttribute(nodeName);
546
+
547
+ let set = this.events.get(id);
548
+ if (!set) {
549
+ set = new Set();
550
+ this.events.set(id, set);
551
+ }
552
+ if (parsedValue.startsWith('#')) {
553
+ set.add({ type, handleEvent: inlineFunctions.get(parsedValue).fn, ...options });
554
+ } else {
555
+ set.add({ type, prop: parsedValue, ...options });
556
+ }
557
+ return false;
558
+ }
559
+
560
+ /** @type {Function} */
561
+ let fn;
562
+ /** @type {Set<string>} */
563
+ let props;
564
+
565
+ /** @type {any} */
566
+ let defaultValue;
567
+ let inlineFunctionOptions;
568
+ // Is Inline Function?
569
+ if (parsedValue.startsWith('#')) {
570
+ inlineFunctionOptions = inlineFunctions.get(parsedValue);
571
+ if (!inlineFunctionOptions) {
572
+ console.warn(`Invalid interpolation value: ${parsedValue}`);
573
+ return false;
574
+ }
575
+ if (inlineFunctionOptions.props) {
576
+ console.log('This function has already been called. Reuse props', inlineFunctionOptions, this);
577
+ props = inlineFunctionOptions.props;
578
+ defaultValue = inlineFunctionOptions.defaultValue ?? null;
579
+ } else {
580
+ defaultValue = inlineFunctionOptions.fn;
581
+ }
582
+ } else {
583
+ defaultValue = valueFromPropName(parsedValue, defaults);
584
+ }
585
+
586
+ if (!props) {
587
+ if (typeof defaultValue === 'function') {
588
+ // Value must be reinterpolated and function observed
589
+ const observeResult = observeFunction.call(this, defaultValue, defaults);
590
+ fn = defaultValue;
591
+ defaultValue = observeResult.defaultValue;
592
+ props = observeResult.props;
593
+ // console.log(this.static.name, fn.name || parsedValue, combinedSet);
594
+ } else {
595
+ props = new Set([parsedValue]);
596
+ }
597
+ }
598
+
599
+ if (typeof defaultValue === 'symbol') {
600
+ console.warn(': Invalid binding:', parsedValue);
601
+ defaultValue = null;
602
+ }
603
+
604
+ if (doubleNegate) {
605
+ defaultValue = !!defaultValue;
606
+ } else if (negate) {
607
+ defaultValue = !defaultValue;
608
+ }
609
+
610
+ if (inlineFunctionOptions) {
611
+ inlineFunctionOptions.defaultValue = defaultValue;
612
+ inlineFunctionOptions.props = props;
613
+ }
614
+
615
+ // Bind
616
+ const parsedNodeName = textNodeIndex ? nodeName + textNodeIndex : nodeName;
617
+ const entry = { id, node: parsedNodeName, fn, props, nodeType, defaultValue, negate, doubleNegate };
618
+ for (const prop of props) {
619
+ let set = this.bindings.get(prop);
620
+ if (!set) {
621
+ set = new Set();
622
+ this.bindings.set(prop, set);
623
+ }
624
+ set.add(entry);
625
+ }
626
+
627
+ // Mutate
628
+
629
+ if (nodeType === Node.TEXT_NODE) {
630
+ node.nodeValue = defaultValue ?? '';
631
+ } else if (nodeName === '_if') {
632
+ element.removeAttribute(nodeName);
633
+ if (defaultValue == null || defaultValue === false) {
634
+ // If default state is removed, mark for removal
635
+ return true;
636
+ }
637
+ } else if (defaultValue == null || defaultValue === false) {
638
+ element.removeAttribute(nodeName);
639
+ } else {
640
+ element.setAttribute(nodeName, defaultValue === true ? '' : defaultValue);
641
+ }
642
+ return false;
643
+ }
644
+
645
+ /**
646
+ * @param {Object} [defaults]
647
+ */
648
+ interpolate(defaults) {
649
+ // console.log('Template', [...this.template.children].map((child) => child.outerHTML).join('\n'));
650
+
651
+ // Copy template before working on it
652
+ // Store into `cloneable` to split later into `interpolation`
653
+ this.cloneable = /** @type {DocumentFragment} */ (this.template.cloneNode(true));
654
+
655
+ /**
656
+ * Track elements to be removed before using for cloning
657
+ * @type {Element[]}
658
+ */
659
+ const removalList = [];
660
+
661
+ const TREE_WALKER_FILTER = 5; /* NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT */
662
+
663
+ const treeWalker = document.createTreeWalker(this.cloneable, TREE_WALKER_FILTER);
664
+ let node = treeWalker.nextNode();
665
+ while (node) {
666
+ /** @type {Element} */
667
+ let element = null;
668
+ let removeElement = false;
669
+ switch (node.nodeType) {
670
+ case Node.ELEMENT_NODE:
671
+ element = node;
672
+ if (element instanceof HTMLTemplateElement) {
673
+ node = treeWalker.nextSibling();
674
+ continue;
675
+ }
676
+ if (node instanceof HTMLStyleElement) {
677
+ // Move style elements out of cloneable
678
+ if (node.parentNode === this.cloneable) {
679
+ this.styles.push(node);
680
+ node.remove();
681
+ node = treeWalker.nextSibling();
682
+ continue;
683
+ }
684
+ console.warn('<style> element not moved');
685
+ }
686
+ if (node instanceof HTMLScriptElement) {
687
+ console.warn('<script> element found.');
688
+ node.remove();
689
+ node = treeWalker.nextSibling();
690
+ continue;
691
+ }
692
+ for (const attr of [...element.attributes].reverse()) {
693
+ if (attr.nodeName === '_if') {
694
+ // Ensure elements to be removed has identifiable parent
695
+ const id = identifierFromElement(element, true);
696
+ const parentId = element.parentElement
697
+ ? identifierFromElement(element.parentElement, true)
698
+ : null;
699
+ this.conditionalElementMetadata.set(id, {
700
+ element,
701
+ id,
702
+ parentId,
703
+ commentCache: new WeakMap(),
704
+ });
705
+ }
706
+ removeElement ||= this.#interpolateNode(attr, element, defaults);
707
+ }
708
+
709
+ break;
710
+ case Node.TEXT_NODE:
711
+ element = node.parentNode;
712
+ if (this.#interpolateNode(/** @type {Text} */ (node), element, defaults)) {
713
+ const nextNode = treeWalker.nextNode();
714
+ node.remove();
715
+ node = nextNode;
716
+ continue;
717
+ }
718
+
719
+ break;
720
+ default:
721
+ throw new Error(`Unexpected node type: ${node.nodeType}`);
722
+ }
723
+ if (removeElement) {
724
+ removalList.push(element);
725
+ }
726
+ node = treeWalker.nextNode();
727
+ }
728
+
729
+ // Split into `interpolation` before removing elements
730
+ /** @type {DocumentFragment} */
731
+ this.interpolation = /** @type {DocumentFragment} */ (this.cloneable.cloneNode(true));
732
+
733
+ // console.debug('Interpolated', [...this.interpolation.children].map((child) => child.outerHTML).join('\n'));
734
+
735
+ // Remove elements from `cloneable` and place comment placeholders
736
+ // Remove in reverse so conditionals within conditionals are properly isolated
737
+ for (const element of [...removalList].reverse()) {
738
+ const { id } = element;
739
+ element.replaceWith(new Comment(`{#${id}}`));
740
+ }
741
+
742
+ for (const watcher of this.watchers) {
743
+ this.bindWatcher(watcher, defaults);
744
+ }
745
+
746
+ if ('adoptedStyleSheets' in document) {
747
+ this.adoptedStyleSheets = [
748
+ ...generateCSSStyleSheets(this.styles),
749
+ ];
750
+ } else {
751
+ this.stylesFragment = generateFragment();
752
+ this.stylesFragment.append(
753
+ ...generateHTMLStyleElements(this.styles),
754
+ );
755
+ }
756
+
757
+ this.interpolated = true;
758
+
759
+ // console.log('Cloneable', [...this.cloneable.children].map((child) => child.outerHTML).join('\n'));
760
+ }
761
+
762
+ /**
763
+ * Updates component nodes based on data
764
+ * Expects data in JSON Merge Patch format
765
+ * @see https://www.rfc-editor.org/rfc/rfc7386
766
+ * @param {DocumentFragment|ShadowRoot} root where
767
+ * @param {Partial<?>} data what
768
+ * @return {void}
769
+ */
770
+ initialRender(root, data) {
771
+ if (!this.interpolated) this.interpolate(data);
772
+
773
+ if ('adoptedStyleSheets' in root) {
774
+ root.adoptedStyleSheets = [
775
+ ...root.adoptedStyleSheets,
776
+ ...this.adoptedStyleSheets,
777
+ ];
778
+ } else if (root instanceof ShadowRoot) {
779
+ root.append(this.stylesFragment.cloneNode(true));
780
+ } else {
781
+ // TODO: Support document styles
782
+ // console.warn('Cannot apply styles to singular element');
783
+ }
784
+
785
+ root.append(this.cloneable.cloneNode(true));
786
+
787
+ // console.log('Initial render', [...root.children].map((child) => child.outerHTML).join('\n'));
788
+
789
+ /** @type {EventTarget} */
790
+ const rootEventTarget = root instanceof ShadowRoot ? root.host : root;
791
+ // Bind events in reverse order to support stopImmediatePropagation
792
+ for (const [id, events] of [...this.events].reverse()) {
793
+ // Prepare all event listeners first
794
+ for (const entry of [...events].reverse()) {
795
+ let listener = entry.listener;
796
+ if (!listener) {
797
+ if (root instanceof ShadowRoot) {
798
+ listener = entry.handleEvent ?? valueFromPropName(entry.prop, data);
799
+ if (id) {
800
+ // Wrap to retarget this
801
+ listener = buildShadowRootChildListener(listener);
802
+ }
803
+ // Cache and reuse
804
+ entry.listener = listener;
805
+ // console.log('caching listener', entry);
806
+ } else {
807
+ throw new TypeError('Anonymous event listeners cannot be used in templates');
808
+ // console.warn('creating new listener', entry);
809
+ // listener = entry.handleEvent ?? ((event) => {
810
+ // valueFromPropName(entry.prop, data)(event);
811
+ // });
812
+ }
813
+ }
814
+ const eventTarget = id ? root.getElementById(id) : rootEventTarget;
815
+ if (!eventTarget) {
816
+ // Element is not available yet. Bind on reference
817
+ console.debug('Composition: Skip bind events for', id);
818
+ continue;
819
+ }
820
+ eventTarget.addEventListener(entry.type, listener, entry);
821
+ }
822
+ }
823
+
824
+ this.initiallyRendered = true;
825
+ }
826
+
827
+ /**
828
+ * @param {string} id
829
+ * @param {Element} element
830
+ */
831
+ attachEventListeners(id, element) {
832
+ const events = this.events.get(id);
833
+ if (events) {
834
+ console.debug('attaching events for', id);
835
+ } else {
836
+ // console.log('no events for', id);
837
+ return;
838
+ }
839
+ for (const entry of [...this.events.get(id)].reverse()) {
840
+ const { listener } = entry;
841
+ if (!listener) {
842
+ throw new Error('Template must be interpolated before attaching events');
843
+ }
844
+ element.addEventListener(entry.type, listener, entry);
845
+ }
846
+ }
847
+
848
+ /**
849
+ * @param {Element|DocumentFragment} root
850
+ * @return {Map<string,Element>}
851
+ */
852
+ getReferences(root) {
853
+ let references = this.referenceCache.get(root);
854
+ if (!references) {
855
+ references = new Map();
856
+ this.referenceCache.set(root, references);
857
+ }
858
+ return references;
859
+ }
860
+
861
+ /**
862
+ * @param {ShadowRoot|DocumentFragment} root
863
+ * @param {string} id
864
+ * @return {Element}
865
+ */
866
+ getElement(root, id) {
867
+ const references = this.getReferences(root);
868
+ let element = references.get(id);
869
+ if (element) {
870
+ // console.log('Returning from cache', id);
871
+ return element;
872
+ }
873
+ if (element === null) return null; // Cached null response
874
+
875
+ // Undefined
876
+
877
+ // console.log('Search in DOM', id);
878
+ element = root.getElementById(id);
879
+
880
+ if (element) {
881
+ // console.log('Found in DOM', id);
882
+ references.set(id, element);
883
+ return element;
884
+ }
885
+
886
+ // Element not in DOM means child of conditional element
887
+ /** @type {Element} */
888
+ let anchorElement;
889
+
890
+ // Check if element is conditional
891
+ let cloneTarget = this.conditionalElementMetadata.get(id)?.element;
892
+
893
+ if (!cloneTarget) {
894
+ // Check if element even exists in interpolation
895
+ // Check interpolation (full-tree) first
896
+ const interpolatedElement = this.interpolation.getElementById(id);
897
+ if (!interpolatedElement) {
898
+ console.warn('Not in full-tree', id);
899
+ // Cache not in full composition
900
+ references.set(id, null);
901
+ return null;
902
+ }
903
+ // Iterate backgrounds until closest conditional element
904
+ // const anchorElementId = this.template.getElementById(id).closest('[_if]').id;
905
+ // anchorElement = this.references.get(anchorElementId) this.interpolation.getElementById(anchorElementId).cloneNode(true);
906
+ let parentElement = interpolatedElement;
907
+ while ((parentElement = parentElement.parentElement) != null) {
908
+ const parentId = parentElement.id;
909
+ if (!parentId) {
910
+ console.warn('Parent does not have ID!');
911
+ cloneTarget = parentElement;
912
+ continue;
913
+ }
914
+
915
+ // Parent already referenced
916
+ const referencedParent = references.get(parentId);
917
+ if (referencedParent) {
918
+ // Element may have been removed without ever tree-walking
919
+ console.debug('Parent already referenced', parentId, '>', id);
920
+ anchorElement = referencedParent;
921
+ break;
922
+ }
923
+
924
+ const liveElement = root.getElementById(parentId);
925
+ if (liveElement) {
926
+ console.warn('Parent in DOM and not referenced', parentId, '>', id);
927
+ // Parent already in DOM. Cache reference
928
+ references.set(parentId, liveElement);
929
+ anchorElement = referencedParent;
930
+ break;
931
+ }
932
+
933
+ const conditionalParent = this.conditionalElementMetadata.get(parentId)?.element;
934
+ if (conditionalParent) {
935
+ console.debug('Found parent conditional element', parentId, '>', id);
936
+ cloneTarget = conditionalParent;
937
+ break;
938
+ }
939
+
940
+ cloneTarget = parentElement;
941
+ }
942
+ }
943
+
944
+ anchorElement ??= /** @type {Element} */ (cloneTarget.cloneNode(true));
945
+
946
+ // Iterate downwards and cache all references
947
+ let node = anchorElement;
948
+ const iterator = document.createTreeWalker(anchorElement, NodeFilter.SHOW_ELEMENT);
949
+ do {
950
+ const nodeIdentifier = node.id;
951
+ if (!element && nodeIdentifier === id) {
952
+ element = node;
953
+ }
954
+
955
+ if (nodeIdentifier) {
956
+ // console.debug('Caching element', nodeIdentifier);
957
+ references.set(nodeIdentifier, node);
958
+ if (cloneTarget) {
959
+ // Attach events regardless of DOM state.
960
+ // EventTargets should still fire even if not part of live document
961
+ this.attachEventListeners(id, element);
962
+ }
963
+ } else {
964
+ console.warn('Could not cache node', node);
965
+ }
966
+ } while ((node = iterator.nextNode()));
967
+ return element;
968
+ }
969
+
970
+ /**
971
+ * @param {*} fn
972
+ * @param {any} defaults
973
+ * @return {boolean} reusable
974
+ */
975
+ bindWatcher(fn, defaults) {
976
+ const { props, defaultValue, reusable } = observeFunction(fn, defaults);
977
+ const entry = { fn, props, defaultValue };
978
+ for (const prop of props) {
979
+ let set = this.bindings.get(prop);
980
+ if (!set) {
981
+ set = new Set();
982
+ this.bindings.set(prop, set);
983
+ }
984
+ set.add(entry);
985
+ }
986
+ return reusable;
987
+ }
988
+ }