@shortfuse/materialdesignweb 0.5.0 → 0.7.1-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 (419) hide show
  1. package/README.md +155 -77
  2. package/bin/generate-css.js +12 -0
  3. package/components/Badge.css +38 -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 +147 -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 +89 -0
  17. package/components/Checkbox.js +59 -0
  18. package/components/CheckboxIcon.css +90 -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 +150 -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 +75 -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 +95 -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 +307 -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 +64 -0
  101. package/components/Switch.js +127 -0
  102. package/components/SwitchIcon.css +178 -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/index.js +77 -0
  132. package/mixins/AriaReflectorMixin.js +42 -0
  133. package/mixins/AriaToolbarMixin.js +13 -0
  134. package/mixins/ControlMixin.css +57 -0
  135. package/mixins/ControlMixin.js +212 -0
  136. package/mixins/DensityMixin.css +40 -0
  137. package/mixins/DensityMixin.js +11 -0
  138. package/mixins/FlexableMixin.css +79 -0
  139. package/mixins/FlexableMixin.js +32 -0
  140. package/mixins/FormAssociatedMixin.js +170 -0
  141. package/mixins/InputMixin.js +335 -0
  142. package/mixins/KeyboardNavMixin.js +244 -0
  143. package/mixins/RTLObserverMixin.js +35 -0
  144. package/mixins/ResizeObserverMixin.js +38 -0
  145. package/mixins/RippleMixin.css +12 -0
  146. package/mixins/RippleMixin.js +115 -0
  147. package/mixins/ScrollListenerMixin.js +100 -0
  148. package/mixins/ShapeMixin.css +135 -0
  149. package/mixins/ShapeMixin.js +31 -0
  150. package/mixins/StateMixin.css +82 -0
  151. package/mixins/StateMixin.js +114 -0
  152. package/mixins/SurfaceMixin.css +150 -0
  153. package/mixins/SurfaceMixin.js +32 -0
  154. package/mixins/TextFieldMixin.css +657 -0
  155. package/mixins/TextFieldMixin.js +121 -0
  156. package/mixins/ThemableMixin.css +204 -0
  157. package/mixins/ThemableMixin.js +16 -0
  158. package/mixins/TooltipTriggerMixin.css +27 -0
  159. package/mixins/TooltipTriggerMixin.js +366 -0
  160. package/mixins/TouchTargetMixin.css +26 -0
  161. package/mixins/TouchTargetMixin.js +9 -0
  162. package/package.json +55 -49
  163. package/theming/index.js +473 -0
  164. package/theming/loader.js +24 -0
  165. package/utils/cli.js +11 -0
  166. package/utils/color_keywords.js +151 -0
  167. package/utils/hct/Cam16.js +298 -0
  168. package/utils/hct/CorePalette.js +84 -0
  169. package/utils/hct/Hct.js +172 -0
  170. package/utils/hct/Scheme.js +587 -0
  171. package/utils/hct/TonalPalette.js +68 -0
  172. package/utils/hct/ViewingConditions.js +136 -0
  173. package/utils/hct/blend.js +93 -0
  174. package/utils/hct/colorUtils.js +302 -0
  175. package/utils/hct/hctSolver.js +559 -0
  176. package/utils/hct/helper.js +182 -0
  177. package/utils/hct/mathUtils.js +153 -0
  178. package/utils/jsonMergePatch.js +100 -0
  179. package/utils/jsx-runtime.js +101 -0
  180. package/utils/popup.js +117 -0
  181. package/utils/svg.js +12 -0
  182. package/.browserslistrc +0 -4
  183. package/.eslintrc.json +0 -204
  184. package/.stylelintrc.json +0 -645
  185. package/.vscode/launch.json +0 -31
  186. package/.vscode/settings.json +0 -3
  187. package/.vscode/tasks.json +0 -32
  188. package/CHANGELOG.md +0 -36
  189. package/CODE_OF_CONDUCT.md +0 -46
  190. package/adapters/datatable/column.js +0 -176
  191. package/adapters/datatable/index.js +0 -960
  192. package/adapters/dom/index.js +0 -586
  193. package/adapters/list/index.js +0 -69
  194. package/adapters/search/index.js +0 -495
  195. package/components/appbar/_spec.scss +0 -165
  196. package/components/appbar/_theme.scss +0 -0
  197. package/components/appbar/index.scss +0 -2
  198. package/components/banner/_spec.scss +0 -83
  199. package/components/banner/_theme.scss +0 -0
  200. package/components/banner/index.scss +0 -2
  201. package/components/bottomnav/README.md +0 -85
  202. package/components/bottomnav/_spec.scss +0 -149
  203. package/components/bottomnav/_theme.scss +0 -0
  204. package/components/bottomnav/index.js +0 -117
  205. package/components/bottomnav/index.scss +0 -2
  206. package/components/bottomnav/item.js +0 -88
  207. package/components/button/README.md +0 -61
  208. package/components/button/_spec.scss +0 -162
  209. package/components/button/_theme.scss +0 -42
  210. package/components/button/index.eta +0 -32
  211. package/components/button/index.js +0 -43
  212. package/components/button/index.pug +0 -18
  213. package/components/button/index.scss +0 -2
  214. package/components/card/_spec.scss +0 -241
  215. package/components/card/_theme.scss +0 -0
  216. package/components/card/index.scss +0 -2
  217. package/components/chip/_spec.scss +0 -111
  218. package/components/chip/_theme.scss +0 -105
  219. package/components/chip/index.js +0 -23
  220. package/components/chip/index.scss +0 -2
  221. package/components/chip/item.js +0 -20
  222. package/components/datatable/_spec.scss +0 -225
  223. package/components/datatable/_theme.scss +0 -128
  224. package/components/datatable/cell.js +0 -44
  225. package/components/datatable/columnheader.js +0 -46
  226. package/components/datatable/index.js +0 -374
  227. package/components/datatable/index.scss +0 -2
  228. package/components/datatable/row.js +0 -48
  229. package/components/datatable/rowheader.js +0 -18
  230. package/components/dialog/_spec.scss +0 -203
  231. package/components/dialog/_theme.scss +0 -7
  232. package/components/dialog/index.js +0 -601
  233. package/components/dialog/index.scss +0 -2
  234. package/components/divider/_spec.scss +0 -11
  235. package/components/divider/_theme.scss +0 -0
  236. package/components/divider/index.scss +0 -2
  237. package/components/elevation/_spec.scss +0 -9
  238. package/components/elevation/_theme.scss +0 -0
  239. package/components/elevation/index.scss +0 -2
  240. package/components/fab/_spec.scss +0 -210
  241. package/components/fab/_theme.scss +0 -0
  242. package/components/fab/index.js +0 -99
  243. package/components/fab/index.scss +0 -2
  244. package/components/grid/_spec.scss +0 -169
  245. package/components/grid/_theme.scss +0 -0
  246. package/components/grid/index.scss +0 -2
  247. package/components/layout/_mixins.scss +0 -11
  248. package/components/layout/_spec.scss +0 -916
  249. package/components/layout/_theme.scss +0 -19
  250. package/components/layout/index.js +0 -454
  251. package/components/layout/index.scss +0 -2
  252. package/components/list/_spec.scss +0 -363
  253. package/components/list/_theme.scss +0 -102
  254. package/components/list/content.js +0 -106
  255. package/components/list/index.js +0 -256
  256. package/components/list/index.scss +0 -2
  257. package/components/list/item.js +0 -167
  258. package/components/list/secondary.js +0 -45
  259. package/components/menu/_spec.scss +0 -329
  260. package/components/menu/_theme.scss +0 -0
  261. package/components/menu/index.js +0 -705
  262. package/components/menu/index.scss +0 -2
  263. package/components/menu/item.js +0 -231
  264. package/components/progress/_spec.scss +0 -156
  265. package/components/progress/_theme.scss +0 -0
  266. package/components/progress/index.js +0 -36
  267. package/components/progress/index.scss +0 -2
  268. package/components/selection/_spec.scss +0 -376
  269. package/components/selection/_theme.scss +0 -134
  270. package/components/selection/index.eta +0 -60
  271. package/components/selection/index.js +0 -70
  272. package/components/selection/index.pug +0 -30
  273. package/components/selection/index.scss +0 -2
  274. package/components/selection/input.js +0 -54
  275. package/components/selection/radiogroup.js +0 -40
  276. package/components/slider/_spec.scss +0 -59
  277. package/components/slider/_theme.scss +0 -0
  278. package/components/slider/index.scss +0 -2
  279. package/components/snackbar/_spec.scss +0 -150
  280. package/components/snackbar/_theme.scss +0 -0
  281. package/components/snackbar/index.js +0 -338
  282. package/components/snackbar/index.scss +0 -2
  283. package/components/tab/_spec.scss +0 -220
  284. package/components/tab/_theme.scss +0 -0
  285. package/components/tab/content.js +0 -210
  286. package/components/tab/index.js +0 -257
  287. package/components/tab/index.scss +0 -2
  288. package/components/tab/item.js +0 -88
  289. package/components/tab/list.js +0 -196
  290. package/components/tab/panel.js +0 -54
  291. package/components/textfield/README.md +0 -179
  292. package/components/textfield/_spec.scss +0 -763
  293. package/components/textfield/_theme.scss +0 -264
  294. package/components/textfield/index.eta +0 -74
  295. package/components/textfield/index.js +0 -160
  296. package/components/textfield/index.pug +0 -30
  297. package/components/textfield/index.scss +0 -2
  298. package/components/tooltip/_spec.scss +0 -185
  299. package/components/tooltip/_theme.scss +0 -0
  300. package/components/tooltip/index.scss +0 -2
  301. package/components/type/_spec.scss +0 -227
  302. package/components/type/_theme.scss +0 -0
  303. package/components/type/index.scss +0 -2
  304. package/core/_breakpoint.scss +0 -189
  305. package/core/_elevation.scss +0 -78
  306. package/core/_length.scss +0 -8
  307. package/core/_motion.scss +0 -31
  308. package/core/_platform.scss +0 -12
  309. package/core/_type.scss +0 -128
  310. package/core/aria/attributes.js +0 -141
  311. package/core/aria/button.js +0 -49
  312. package/core/aria/keyboard.js +0 -92
  313. package/core/aria/rovingtabindex.js +0 -175
  314. package/core/aria/tab.js +0 -59
  315. package/core/document/index.js +0 -39
  316. package/core/overlay/_spec.scss +0 -28
  317. package/core/overlay/_theme.scss +0 -147
  318. package/core/overlay/index.js +0 -95
  319. package/core/overlay/index.scss +0 -2
  320. package/core/ripple/_spec.scss +0 -196
  321. package/core/ripple/_theme.scss +0 -20
  322. package/core/ripple/index.js +0 -286
  323. package/core/ripple/index.scss +0 -2
  324. package/core/theme/_aliases.scss +0 -15
  325. package/core/theme/_config.scss +0 -8
  326. package/core/theme/_functions.scss +0 -22
  327. package/core/theme/_palettes.scss +0 -405
  328. package/core/theme/_spec.scss +0 -0
  329. package/core/theme/_theme.scss +0 -268
  330. package/core/theme/index.js +0 -50
  331. package/core/theme/index.scss +0 -4
  332. package/core/throttler.js +0 -42
  333. package/core/transition/index.js +0 -465
  334. package/docs/_flex.scss +0 -28
  335. package/docs/_menuoptions.js +0 -183
  336. package/docs/_partials/_androidnavbar.eta +0 -5
  337. package/docs/_partials/_androidstatusbar.eta +0 -13
  338. package/docs/_partials/_appbar.eta +0 -27
  339. package/docs/_partials/_buttontest.eta +0 -31
  340. package/docs/_partials/_header.eta +0 -146
  341. package/docs/_partials/_navlistitem.eta +0 -16
  342. package/docs/_partials/_target.eta +0 -1
  343. package/docs/_sample-utils.js +0 -88
  344. package/docs/_storage.js +0 -33
  345. package/docs/docs.scss +0 -331
  346. package/docs/framework.scss +0 -26
  347. package/docs/index.eta +0 -12
  348. package/docs/index.js +0 -7
  349. package/docs/pages/appbar.eta +0 -108
  350. package/docs/pages/appbar.js +0 -0
  351. package/docs/pages/bottomnav.eta +0 -188
  352. package/docs/pages/bottomnav.js +0 -118
  353. package/docs/pages/button.eta +0 -124
  354. package/docs/pages/button.js +0 -224
  355. package/docs/pages/card.eta +0 -90
  356. package/docs/pages/card.js +0 -175
  357. package/docs/pages/chip.eta +0 -122
  358. package/docs/pages/chip.js +0 -80
  359. package/docs/pages/color.eta +0 -143
  360. package/docs/pages/color.js +0 -261
  361. package/docs/pages/datatable.eta +0 -323
  362. package/docs/pages/datatable.js +0 -160
  363. package/docs/pages/dialog.eta +0 -184
  364. package/docs/pages/dialog.js +0 -174
  365. package/docs/pages/dom.eta +0 -26
  366. package/docs/pages/dom.js +0 -140
  367. package/docs/pages/elevation.eta +0 -35
  368. package/docs/pages/elevation.js +0 -0
  369. package/docs/pages/fab.eta +0 -99
  370. package/docs/pages/fab.js +0 -43
  371. package/docs/pages/grid.eta +0 -135
  372. package/docs/pages/grid.js +0 -128
  373. package/docs/pages/layout.eta +0 -8
  374. package/docs/pages/layout.js +0 -0
  375. package/docs/pages/list.eta +0 -465
  376. package/docs/pages/list.js +0 -8
  377. package/docs/pages/menu.eta +0 -274
  378. package/docs/pages/menu.js +0 -213
  379. package/docs/pages/overlay.eta +0 -69
  380. package/docs/pages/overlay.js +0 -3
  381. package/docs/pages/progress.eta +0 -23
  382. package/docs/pages/progress.js +0 -12
  383. package/docs/pages/ripple.eta +0 -27
  384. package/docs/pages/ripple.js +0 -3
  385. package/docs/pages/search.eta +0 -242
  386. package/docs/pages/search.js +0 -226
  387. package/docs/pages/selection.eta +0 -107
  388. package/docs/pages/selection.js +0 -12
  389. package/docs/pages/slider.eta +0 -23
  390. package/docs/pages/slider.js +0 -0
  391. package/docs/pages/snackbar.eta +0 -83
  392. package/docs/pages/snackbar.js +0 -157
  393. package/docs/pages/tab.eta +0 -407
  394. package/docs/pages/tab.js +0 -152
  395. package/docs/pages/textfield.eta +0 -487
  396. package/docs/pages/textfield.js +0 -257
  397. package/docs/pages/tooltip.eta +0 -92
  398. package/docs/pages/tooltip.js +0 -0
  399. package/docs/pages/transition.eta +0 -117
  400. package/docs/pages/transition.js +0 -52
  401. package/docs/pages/type.eta +0 -31
  402. package/docs/pages/type.js +0 -0
  403. package/docs/postrender.js +0 -41
  404. package/docs/prerender.js +0 -16
  405. package/docs/pwa/_dialogs.eta +0 -143
  406. package/docs/pwa/_menus.eta +0 -16
  407. package/docs/pwa/pwa-prerender.js +0 -3
  408. package/docs/pwa/pwa.eta +0 -478
  409. package/docs/pwa/pwa.js +0 -298
  410. package/docs/pwa/pwa.scss +0 -31
  411. package/docs/themes/theme-colored.scss +0 -15
  412. package/docs/themes/theme-default.scss +0 -3
  413. package/index.scss +0 -27
  414. package/jsconfig.json +0 -16
  415. package/scripts/deploy-docs.sh +0 -9
  416. package/templates/index.eta +0 -2
  417. package/templates/index.pug +0 -3
  418. package/tsconfig.json +0 -16
  419. 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
+ }