@shortfuse/materialdesignweb 0.5.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 (418) hide show
  1. package/README.md +155 -77
  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 -154
  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 +54 -49
  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 -4
  182. package/.eslintrc.json +0 -204
  183. package/.stylelintrc.json +0 -645
  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 -36
  188. package/CODE_OF_CONDUCT.md +0 -46
  189. package/adapters/datatable/column.js +0 -176
  190. package/adapters/datatable/index.js +0 -960
  191. package/adapters/dom/index.js +0 -586
  192. package/adapters/list/index.js +0 -69
  193. package/adapters/search/index.js +0 -495
  194. package/components/appbar/_spec.scss +0 -165
  195. package/components/appbar/_theme.scss +0 -0
  196. package/components/appbar/index.scss +0 -2
  197. package/components/banner/_spec.scss +0 -83
  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 -149
  202. package/components/bottomnav/_theme.scss +0 -0
  203. package/components/bottomnav/index.js +0 -117
  204. package/components/bottomnav/index.scss +0 -2
  205. package/components/bottomnav/item.js +0 -88
  206. package/components/button/README.md +0 -61
  207. package/components/button/_spec.scss +0 -162
  208. package/components/button/_theme.scss +0 -42
  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 -241
  214. package/components/card/_theme.scss +0 -0
  215. package/components/card/index.scss +0 -2
  216. package/components/chip/_spec.scss +0 -111
  217. package/components/chip/_theme.scss +0 -105
  218. package/components/chip/index.js +0 -23
  219. package/components/chip/index.scss +0 -2
  220. package/components/chip/item.js +0 -20
  221. package/components/datatable/_spec.scss +0 -225
  222. package/components/datatable/_theme.scss +0 -128
  223. package/components/datatable/cell.js +0 -44
  224. package/components/datatable/columnheader.js +0 -46
  225. package/components/datatable/index.js +0 -374
  226. package/components/datatable/index.scss +0 -2
  227. package/components/datatable/row.js +0 -48
  228. package/components/datatable/rowheader.js +0 -18
  229. package/components/dialog/_spec.scss +0 -203
  230. package/components/dialog/_theme.scss +0 -7
  231. package/components/dialog/index.js +0 -601
  232. package/components/dialog/index.scss +0 -2
  233. package/components/divider/_spec.scss +0 -11
  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 -210
  240. package/components/fab/_theme.scss +0 -0
  241. package/components/fab/index.js +0 -99
  242. package/components/fab/index.scss +0 -2
  243. package/components/grid/_spec.scss +0 -169
  244. package/components/grid/_theme.scss +0 -0
  245. package/components/grid/index.scss +0 -2
  246. package/components/layout/_mixins.scss +0 -11
  247. package/components/layout/_spec.scss +0 -916
  248. package/components/layout/_theme.scss +0 -19
  249. package/components/layout/index.js +0 -454
  250. package/components/layout/index.scss +0 -2
  251. package/components/list/_spec.scss +0 -363
  252. package/components/list/_theme.scss +0 -102
  253. package/components/list/content.js +0 -106
  254. package/components/list/index.js +0 -256
  255. package/components/list/index.scss +0 -2
  256. package/components/list/item.js +0 -167
  257. package/components/list/secondary.js +0 -45
  258. package/components/menu/_spec.scss +0 -329
  259. package/components/menu/_theme.scss +0 -0
  260. package/components/menu/index.js +0 -705
  261. package/components/menu/index.scss +0 -2
  262. package/components/menu/item.js +0 -231
  263. package/components/progress/_spec.scss +0 -156
  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 -376
  268. package/components/selection/_theme.scss +0 -134
  269. package/components/selection/index.eta +0 -60
  270. package/components/selection/index.js +0 -70
  271. package/components/selection/index.pug +0 -30
  272. package/components/selection/index.scss +0 -2
  273. package/components/selection/input.js +0 -54
  274. package/components/selection/radiogroup.js +0 -40
  275. package/components/slider/_spec.scss +0 -59
  276. package/components/slider/_theme.scss +0 -0
  277. package/components/slider/index.scss +0 -2
  278. package/components/snackbar/_spec.scss +0 -150
  279. package/components/snackbar/_theme.scss +0 -0
  280. package/components/snackbar/index.js +0 -338
  281. package/components/snackbar/index.scss +0 -2
  282. package/components/tab/_spec.scss +0 -220
  283. package/components/tab/_theme.scss +0 -0
  284. package/components/tab/content.js +0 -210
  285. package/components/tab/index.js +0 -257
  286. package/components/tab/index.scss +0 -2
  287. package/components/tab/item.js +0 -88
  288. package/components/tab/list.js +0 -196
  289. package/components/tab/panel.js +0 -54
  290. package/components/textfield/README.md +0 -179
  291. package/components/textfield/_spec.scss +0 -763
  292. package/components/textfield/_theme.scss +0 -264
  293. package/components/textfield/index.eta +0 -74
  294. package/components/textfield/index.js +0 -160
  295. package/components/textfield/index.pug +0 -30
  296. package/components/textfield/index.scss +0 -2
  297. package/components/tooltip/_spec.scss +0 -185
  298. package/components/tooltip/_theme.scss +0 -0
  299. package/components/tooltip/index.scss +0 -2
  300. package/components/type/_spec.scss +0 -227
  301. package/components/type/_theme.scss +0 -0
  302. package/components/type/index.scss +0 -2
  303. package/core/_breakpoint.scss +0 -189
  304. package/core/_elevation.scss +0 -78
  305. package/core/_length.scss +0 -8
  306. package/core/_motion.scss +0 -31
  307. package/core/_platform.scss +0 -12
  308. package/core/_type.scss +0 -128
  309. package/core/aria/attributes.js +0 -141
  310. package/core/aria/button.js +0 -49
  311. package/core/aria/keyboard.js +0 -92
  312. package/core/aria/rovingtabindex.js +0 -175
  313. package/core/aria/tab.js +0 -59
  314. package/core/document/index.js +0 -39
  315. package/core/overlay/_spec.scss +0 -28
  316. package/core/overlay/_theme.scss +0 -147
  317. package/core/overlay/index.js +0 -95
  318. package/core/overlay/index.scss +0 -2
  319. package/core/ripple/_spec.scss +0 -196
  320. package/core/ripple/_theme.scss +0 -20
  321. package/core/ripple/index.js +0 -286
  322. package/core/ripple/index.scss +0 -2
  323. package/core/theme/_aliases.scss +0 -15
  324. package/core/theme/_config.scss +0 -8
  325. package/core/theme/_functions.scss +0 -22
  326. package/core/theme/_palettes.scss +0 -405
  327. package/core/theme/_spec.scss +0 -0
  328. package/core/theme/_theme.scss +0 -268
  329. package/core/theme/index.js +0 -50
  330. package/core/theme/index.scss +0 -4
  331. package/core/throttler.js +0 -42
  332. package/core/transition/index.js +0 -465
  333. package/docs/_flex.scss +0 -28
  334. package/docs/_menuoptions.js +0 -183
  335. package/docs/_partials/_androidnavbar.eta +0 -5
  336. package/docs/_partials/_androidstatusbar.eta +0 -13
  337. package/docs/_partials/_appbar.eta +0 -27
  338. package/docs/_partials/_buttontest.eta +0 -31
  339. package/docs/_partials/_header.eta +0 -146
  340. package/docs/_partials/_navlistitem.eta +0 -16
  341. package/docs/_partials/_target.eta +0 -1
  342. package/docs/_sample-utils.js +0 -88
  343. package/docs/_storage.js +0 -33
  344. package/docs/docs.scss +0 -331
  345. package/docs/framework.scss +0 -26
  346. package/docs/index.eta +0 -12
  347. package/docs/index.js +0 -7
  348. package/docs/pages/appbar.eta +0 -108
  349. package/docs/pages/appbar.js +0 -0
  350. package/docs/pages/bottomnav.eta +0 -188
  351. package/docs/pages/bottomnav.js +0 -118
  352. package/docs/pages/button.eta +0 -124
  353. package/docs/pages/button.js +0 -224
  354. package/docs/pages/card.eta +0 -90
  355. package/docs/pages/card.js +0 -175
  356. package/docs/pages/chip.eta +0 -122
  357. package/docs/pages/chip.js +0 -80
  358. package/docs/pages/color.eta +0 -143
  359. package/docs/pages/color.js +0 -261
  360. package/docs/pages/datatable.eta +0 -323
  361. package/docs/pages/datatable.js +0 -160
  362. package/docs/pages/dialog.eta +0 -184
  363. package/docs/pages/dialog.js +0 -174
  364. package/docs/pages/dom.eta +0 -26
  365. package/docs/pages/dom.js +0 -140
  366. package/docs/pages/elevation.eta +0 -35
  367. package/docs/pages/elevation.js +0 -0
  368. package/docs/pages/fab.eta +0 -99
  369. package/docs/pages/fab.js +0 -43
  370. package/docs/pages/grid.eta +0 -135
  371. package/docs/pages/grid.js +0 -128
  372. package/docs/pages/layout.eta +0 -8
  373. package/docs/pages/layout.js +0 -0
  374. package/docs/pages/list.eta +0 -465
  375. package/docs/pages/list.js +0 -8
  376. package/docs/pages/menu.eta +0 -274
  377. package/docs/pages/menu.js +0 -213
  378. package/docs/pages/overlay.eta +0 -69
  379. package/docs/pages/overlay.js +0 -3
  380. package/docs/pages/progress.eta +0 -23
  381. package/docs/pages/progress.js +0 -12
  382. package/docs/pages/ripple.eta +0 -27
  383. package/docs/pages/ripple.js +0 -3
  384. package/docs/pages/search.eta +0 -242
  385. package/docs/pages/search.js +0 -226
  386. package/docs/pages/selection.eta +0 -107
  387. package/docs/pages/selection.js +0 -12
  388. package/docs/pages/slider.eta +0 -23
  389. package/docs/pages/slider.js +0 -0
  390. package/docs/pages/snackbar.eta +0 -83
  391. package/docs/pages/snackbar.js +0 -157
  392. package/docs/pages/tab.eta +0 -407
  393. package/docs/pages/tab.js +0 -152
  394. package/docs/pages/textfield.eta +0 -487
  395. package/docs/pages/textfield.js +0 -257
  396. package/docs/pages/tooltip.eta +0 -92
  397. package/docs/pages/tooltip.js +0 -0
  398. package/docs/pages/transition.eta +0 -117
  399. package/docs/pages/transition.js +0 -52
  400. package/docs/pages/type.eta +0 -31
  401. package/docs/pages/type.js +0 -0
  402. package/docs/postrender.js +0 -41
  403. package/docs/prerender.js +0 -16
  404. package/docs/pwa/_dialogs.eta +0 -143
  405. package/docs/pwa/_menus.eta +0 -16
  406. package/docs/pwa/pwa-prerender.js +0 -3
  407. package/docs/pwa/pwa.eta +0 -478
  408. package/docs/pwa/pwa.js +0 -298
  409. package/docs/pwa/pwa.scss +0 -31
  410. package/docs/themes/theme-colored.scss +0 -15
  411. package/docs/themes/theme-default.scss +0 -3
  412. package/index.scss +0 -27
  413. package/jsconfig.json +0 -16
  414. package/scripts/deploy-docs.sh +0 -9
  415. package/templates/index.eta +0 -2
  416. package/templates/index.pug +0 -3
  417. package/tsconfig.json +0 -16
  418. package/webpack.config.js +0 -304
@@ -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
+ }