@shortfuse/materialdesignweb 0.7.6 → 0.9.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 (412) hide show
  1. package/README.md +87 -90
  2. package/bin/mdw-css.js +1 -1
  3. package/components/Badge.js +14 -7
  4. package/components/Body.js +3 -0
  5. package/components/BottomAppBar.js +4 -13
  6. package/components/BottomSheet.js +424 -0
  7. package/components/Box.js +20 -28
  8. package/components/Button.js +85 -79
  9. package/components/Button.md +9 -9
  10. package/components/Card.js +60 -72
  11. package/components/Checkbox.js +33 -42
  12. package/components/CheckboxIcon.js +11 -29
  13. package/components/Chip.js +13 -11
  14. package/components/Dialog.js +214 -394
  15. package/components/DialogActions.js +2 -2
  16. package/components/Display.js +55 -0
  17. package/components/Divider.js +3 -3
  18. package/components/Fab.js +83 -18
  19. package/components/FabContainer.js +48 -0
  20. package/components/FilterChip.js +35 -33
  21. package/components/Grid.js +176 -0
  22. package/components/Headline.js +5 -28
  23. package/components/Icon.js +61 -76
  24. package/components/IconButton.js +72 -126
  25. package/components/Input.js +859 -1
  26. package/components/InputChip.js +161 -0
  27. package/components/Label.js +3 -0
  28. package/components/List.js +4 -6
  29. package/components/ListItem.js +46 -30
  30. package/components/ListOption.js +86 -53
  31. package/components/Listbox.js +248 -0
  32. package/components/Menu.js +69 -528
  33. package/components/MenuItem.js +33 -36
  34. package/components/NavBar.js +49 -86
  35. package/components/NavDrawer.js +16 -15
  36. package/components/NavDrawerItem.js +4 -5
  37. package/components/NavItem.js +58 -41
  38. package/components/NavRail.js +30 -20
  39. package/components/NavRailItem.js +8 -3
  40. package/components/Page.js +105 -0
  41. package/components/Pane.js +11 -274
  42. package/components/Popup.js +29 -0
  43. package/components/Progress.js +24 -23
  44. package/components/Radio.js +40 -36
  45. package/components/RadioIcon.js +12 -16
  46. package/components/Ripple.js +13 -10
  47. package/components/Root.js +209 -0
  48. package/components/Scrim.js +87 -0
  49. package/components/Search.js +77 -0
  50. package/components/SegmentedButton.js +33 -45
  51. package/components/SegmentedButtonGroup.js +25 -13
  52. package/components/Select.js +10 -11
  53. package/components/Shape.js +5 -65
  54. package/components/SideSheet.js +308 -0
  55. package/components/Slider.js +108 -78
  56. package/components/Snackbar.js +26 -21
  57. package/components/SnackbarContainer.js +42 -0
  58. package/components/Surface.js +17 -12
  59. package/components/Switch.js +45 -24
  60. package/components/SwitchIcon.js +49 -39
  61. package/components/Tab.js +64 -43
  62. package/components/TabContent.js +5 -3
  63. package/components/TabList.js +62 -34
  64. package/components/TabPanel.js +11 -8
  65. package/components/Table.js +116 -0
  66. package/components/TextArea.js +54 -50
  67. package/components/Title.js +4 -9
  68. package/components/Tooltip.js +44 -22
  69. package/components/TopAppBar.js +68 -172
  70. package/constants/shapes.js +36 -0
  71. package/constants/typography.js +127 -0
  72. package/core/Composition.js +1164 -645
  73. package/core/CompositionAdapter.js +314 -0
  74. package/core/CustomElement.js +701 -286
  75. package/core/css.js +121 -15
  76. package/core/customTypes.js +157 -40
  77. package/core/dom.js +17 -11
  78. package/{utils → core}/jsonMergePatch.js +42 -18
  79. package/core/observe.js +343 -244
  80. package/core/optimizations.js +23 -0
  81. package/core/template.js +19 -56
  82. package/core/uid.js +13 -0
  83. package/dist/index.min.js +85 -184
  84. package/dist/index.min.js.map +4 -4
  85. package/dist/meta.json +1 -1
  86. package/dom/HTMLOptionsCollectionProxy.js +106 -0
  87. package/loaders/palette.js +13 -0
  88. package/loaders/theme.js +12 -0
  89. package/mixins/AriaReflectorMixin.js +53 -14
  90. package/mixins/AriaToolbarMixin.js +5 -3
  91. package/mixins/ControlMixin.js +92 -41
  92. package/mixins/DelegatesFocusMixin.js +54 -0
  93. package/mixins/DensityMixin.js +2 -3
  94. package/mixins/ElevationMixin.js +62 -0
  95. package/mixins/FlexableMixin.js +67 -39
  96. package/mixins/FormAssociatedMixin.js +71 -16
  97. package/mixins/HyperlinkMixin.js +66 -0
  98. package/mixins/InputMixin.js +205 -39
  99. package/mixins/KeyboardNavMixin.js +22 -7
  100. package/mixins/NavigationListenerMixin.js +33 -0
  101. package/mixins/PopupMixin.js +725 -0
  102. package/mixins/RTLObserverMixin.js +0 -1
  103. package/mixins/ResizeObserverMixin.js +16 -5
  104. package/mixins/RippleMixin.js +11 -10
  105. package/mixins/ScrollListenerMixin.js +42 -33
  106. package/mixins/SemiStickyMixin.js +97 -0
  107. package/mixins/ShapeMaskedMixin.js +117 -0
  108. package/mixins/ShapeMixin.js +17 -194
  109. package/mixins/StateMixin.js +80 -39
  110. package/mixins/TextFieldMixin.js +139 -183
  111. package/mixins/ThemableMixin.js +71 -161
  112. package/mixins/TooltipTriggerMixin.js +292 -366
  113. package/mixins/TouchTargetMixin.js +5 -4
  114. package/mixins/TypographyMixin.js +121 -0
  115. package/package.json +111 -71
  116. package/services/rtl.js +10 -0
  117. package/services/svgAlias.js +17 -0
  118. package/{theming/index.js → services/theme.js} +25 -175
  119. package/types/bin/mdw-css.d.ts +3 -0
  120. package/types/bin/mdw-css.d.ts.map +1 -0
  121. package/types/components/Badge.d.ts +39 -0
  122. package/types/components/Badge.d.ts.map +1 -0
  123. package/types/components/Body.d.ts +29 -0
  124. package/types/components/Body.d.ts.map +1 -0
  125. package/types/components/BottomAppBar.d.ts +73 -0
  126. package/types/components/BottomAppBar.d.ts.map +1 -0
  127. package/types/components/BottomSheet.d.ts +109 -0
  128. package/types/components/BottomSheet.d.ts.map +1 -0
  129. package/types/components/Box.d.ts +16 -0
  130. package/types/components/Box.d.ts.map +1 -0
  131. package/types/components/Button.d.ts +714 -0
  132. package/types/components/Button.d.ts.map +1 -0
  133. package/types/components/Card.d.ts +412 -0
  134. package/types/components/Card.d.ts.map +1 -0
  135. package/types/components/Checkbox.d.ts +205 -0
  136. package/types/components/Checkbox.d.ts.map +1 -0
  137. package/types/components/CheckboxIcon.d.ts +44 -0
  138. package/types/components/CheckboxIcon.d.ts.map +1 -0
  139. package/types/components/Chip.d.ts +1425 -0
  140. package/types/components/Chip.d.ts.map +1 -0
  141. package/types/components/Dialog.d.ts +286 -0
  142. package/types/components/Dialog.d.ts.map +1 -0
  143. package/types/components/DialogActions.d.ts +4 -0
  144. package/types/components/DialogActions.d.ts.map +1 -0
  145. package/types/components/Display.d.ts +45 -0
  146. package/types/components/Display.d.ts.map +1 -0
  147. package/types/components/Divider.d.ts +10 -0
  148. package/types/components/Divider.d.ts.map +1 -0
  149. package/types/components/Fab.d.ts +741 -0
  150. package/types/components/Fab.d.ts.map +1 -0
  151. package/types/components/FabContainer.d.ts +10 -0
  152. package/types/components/FabContainer.d.ts.map +1 -0
  153. package/types/components/FilterChip.d.ts +4283 -0
  154. package/types/components/FilterChip.d.ts.map +1 -0
  155. package/types/components/Grid.d.ts +37 -0
  156. package/types/components/Grid.d.ts.map +1 -0
  157. package/types/components/Headline.d.ts +47 -0
  158. package/types/components/Headline.d.ts.map +1 -0
  159. package/types/components/Icon.d.ts +103 -0
  160. package/types/components/Icon.d.ts.map +1 -0
  161. package/types/components/IconButton.d.ts +1486 -0
  162. package/types/components/IconButton.d.ts.map +1 -0
  163. package/types/components/Input.d.ts +51288 -0
  164. package/types/components/Input.d.ts.map +1 -0
  165. package/types/components/InputChip.d.ts +243 -0
  166. package/types/components/InputChip.d.ts.map +1 -0
  167. package/types/components/Label.d.ts +29 -0
  168. package/types/components/Label.d.ts.map +1 -0
  169. package/types/components/List.d.ts +31 -0
  170. package/types/components/List.d.ts.map +1 -0
  171. package/types/components/ListItem.d.ts +349 -0
  172. package/types/components/ListItem.d.ts.map +1 -0
  173. package/types/components/ListOption.d.ts +1493 -0
  174. package/types/components/ListOption.d.ts.map +1 -0
  175. package/types/components/Listbox.d.ts +12012 -0
  176. package/types/components/Listbox.d.ts.map +1 -0
  177. package/types/components/Menu.d.ts +119 -0
  178. package/types/components/Menu.d.ts.map +1 -0
  179. package/types/components/MenuItem.d.ts +3109 -0
  180. package/types/components/MenuItem.d.ts.map +1 -0
  181. package/types/components/NavBar.d.ts +18 -0
  182. package/types/components/NavBar.d.ts.map +1 -0
  183. package/types/components/NavBarItem.d.ts +186 -0
  184. package/types/components/NavBarItem.d.ts.map +1 -0
  185. package/types/components/NavDrawer.d.ts +108 -0
  186. package/types/components/NavDrawer.d.ts.map +1 -0
  187. package/types/components/NavDrawerItem.d.ts +186 -0
  188. package/types/components/NavDrawerItem.d.ts.map +1 -0
  189. package/types/components/NavItem.d.ts +190 -0
  190. package/types/components/NavItem.d.ts.map +1 -0
  191. package/types/components/NavRail.d.ts +109 -0
  192. package/types/components/NavRail.d.ts.map +1 -0
  193. package/types/components/NavRailItem.d.ts +186 -0
  194. package/types/components/NavRailItem.d.ts.map +1 -0
  195. package/types/components/Page.d.ts +24 -0
  196. package/types/components/Page.d.ts.map +1 -0
  197. package/types/components/Pane.d.ts +44 -0
  198. package/types/components/Pane.d.ts.map +1 -0
  199. package/types/components/Popup.d.ts +76 -0
  200. package/types/components/Popup.d.ts.map +1 -0
  201. package/types/components/Progress.d.ts +19 -0
  202. package/types/components/Progress.d.ts.map +1 -0
  203. package/types/components/Radio.d.ts +199 -0
  204. package/types/components/Radio.d.ts.map +1 -0
  205. package/types/components/RadioIcon.d.ts +46 -0
  206. package/types/components/RadioIcon.d.ts.map +1 -0
  207. package/types/components/Ripple.d.ts +34 -0
  208. package/types/components/Ripple.d.ts.map +1 -0
  209. package/types/components/Root.d.ts +68 -0
  210. package/types/components/Root.d.ts.map +1 -0
  211. package/types/components/Scrim.d.ts +6 -0
  212. package/types/components/Scrim.d.ts.map +1 -0
  213. package/types/components/Search.d.ts +16 -0
  214. package/types/components/Search.d.ts.map +1 -0
  215. package/types/components/SegmentedButton.d.ts +718 -0
  216. package/types/components/SegmentedButton.d.ts.map +1 -0
  217. package/types/components/SegmentedButtonGroup.d.ts +44 -0
  218. package/types/components/SegmentedButtonGroup.d.ts.map +1 -0
  219. package/types/components/Select.d.ts +1361 -0
  220. package/types/components/Select.d.ts.map +1 -0
  221. package/types/components/Shape.d.ts +10 -0
  222. package/types/components/Shape.d.ts.map +1 -0
  223. package/types/components/SideSheet.d.ts +106 -0
  224. package/types/components/SideSheet.d.ts.map +1 -0
  225. package/types/components/Slider.d.ts +382 -0
  226. package/types/components/Slider.d.ts.map +1 -0
  227. package/types/components/Snackbar.d.ts +65 -0
  228. package/types/components/Snackbar.d.ts.map +1 -0
  229. package/types/components/SnackbarContainer.d.ts +6 -0
  230. package/types/components/SnackbarContainer.d.ts.map +1 -0
  231. package/types/components/Surface.d.ts +45 -0
  232. package/types/components/Surface.d.ts.map +1 -0
  233. package/types/components/Switch.d.ts +183 -0
  234. package/types/components/Switch.d.ts.map +1 -0
  235. package/types/components/SwitchIcon.d.ts +169 -0
  236. package/types/components/SwitchIcon.d.ts.map +1 -0
  237. package/types/components/Tab.d.ts +879 -0
  238. package/types/components/Tab.d.ts.map +1 -0
  239. package/types/components/TabContent.d.ts +122 -0
  240. package/types/components/TabContent.d.ts.map +1 -0
  241. package/types/components/TabList.d.ts +6266 -0
  242. package/types/components/TabList.d.ts.map +1 -0
  243. package/types/components/TabPanel.d.ts +28 -0
  244. package/types/components/TabPanel.d.ts.map +1 -0
  245. package/types/components/Table.d.ts +2 -0
  246. package/types/components/Table.d.ts.map +1 -0
  247. package/types/components/TextArea.d.ts +1394 -0
  248. package/types/components/TextArea.d.ts.map +1 -0
  249. package/types/components/Title.d.ts +47 -0
  250. package/types/components/Title.d.ts.map +1 -0
  251. package/types/components/Tooltip.d.ts +49 -0
  252. package/types/components/Tooltip.d.ts.map +1 -0
  253. package/types/components/TopAppBar.d.ts +130 -0
  254. package/types/components/TopAppBar.d.ts.map +1 -0
  255. package/types/constants/colorKeywords.d.ts +2 -0
  256. package/types/constants/colorKeywords.d.ts.map +1 -0
  257. package/types/constants/shapes.d.ts +38 -0
  258. package/types/constants/shapes.d.ts.map +1 -0
  259. package/types/constants/typography.d.ts +212 -0
  260. package/types/constants/typography.d.ts.map +1 -0
  261. package/types/core/Composition.d.ts +252 -0
  262. package/types/core/Composition.d.ts.map +1 -0
  263. package/types/core/CompositionAdapter.d.ts +92 -0
  264. package/types/core/CompositionAdapter.d.ts.map +1 -0
  265. package/types/core/CustomElement.d.ts +302 -0
  266. package/types/core/CustomElement.d.ts.map +1 -0
  267. package/types/core/css.d.ts +44 -0
  268. package/types/core/css.d.ts.map +1 -0
  269. package/types/core/customTypes.d.ts +26 -0
  270. package/types/core/customTypes.d.ts.map +1 -0
  271. package/types/core/dom.d.ts +32 -0
  272. package/types/core/dom.d.ts.map +1 -0
  273. package/types/core/jsonMergePatch.d.ts +31 -0
  274. package/types/core/jsonMergePatch.d.ts.map +1 -0
  275. package/types/core/observe.d.ts +113 -0
  276. package/types/core/observe.d.ts.map +1 -0
  277. package/types/core/optimizations.d.ts +7 -0
  278. package/types/core/optimizations.d.ts.map +1 -0
  279. package/types/core/template.d.ts +47 -0
  280. package/types/core/template.d.ts.map +1 -0
  281. package/types/core/uid.d.ts +6 -0
  282. package/types/core/uid.d.ts.map +1 -0
  283. package/types/dom/HTMLOptionsCollectionProxy.d.ts +18 -0
  284. package/types/dom/HTMLOptionsCollectionProxy.d.ts.map +1 -0
  285. package/types/index.d.ts +88 -0
  286. package/types/index.d.ts.map +1 -0
  287. package/types/loaders/palette.d.ts +2 -0
  288. package/types/loaders/palette.d.ts.map +1 -0
  289. package/types/loaders/theme.d.ts +2 -0
  290. package/types/loaders/theme.d.ts.map +1 -0
  291. package/types/mixins/AriaReflectorMixin.d.ts +23 -0
  292. package/types/mixins/AriaReflectorMixin.d.ts.map +1 -0
  293. package/types/mixins/AriaToolbarMixin.d.ts +32 -0
  294. package/types/mixins/AriaToolbarMixin.d.ts.map +1 -0
  295. package/types/mixins/ControlMixin.d.ts +124 -0
  296. package/types/mixins/ControlMixin.d.ts.map +1 -0
  297. package/types/mixins/DelegatesFocusMixin.d.ts +5 -0
  298. package/types/mixins/DelegatesFocusMixin.d.ts.map +1 -0
  299. package/types/mixins/DensityMixin.d.ts +5 -0
  300. package/types/mixins/DensityMixin.d.ts.map +1 -0
  301. package/types/mixins/ElevationMixin.d.ts +33 -0
  302. package/types/mixins/ElevationMixin.d.ts.map +1 -0
  303. package/types/mixins/FlexableMixin.d.ts +13 -0
  304. package/types/mixins/FlexableMixin.d.ts.map +1 -0
  305. package/types/mixins/FormAssociatedMixin.d.ts +122 -0
  306. package/types/mixins/FormAssociatedMixin.d.ts.map +1 -0
  307. package/types/mixins/HyperlinkMixin.d.ts +22 -0
  308. package/types/mixins/HyperlinkMixin.d.ts.map +1 -0
  309. package/types/mixins/InputMixin.d.ts +179 -0
  310. package/types/mixins/InputMixin.d.ts.map +1 -0
  311. package/types/mixins/KeyboardNavMixin.d.ts +47 -0
  312. package/types/mixins/KeyboardNavMixin.d.ts.map +1 -0
  313. package/types/mixins/NavigationListenerMixin.d.ts +8 -0
  314. package/types/mixins/NavigationListenerMixin.d.ts.map +1 -0
  315. package/types/mixins/PopupMixin.d.ts +82 -0
  316. package/types/mixins/PopupMixin.d.ts.map +1 -0
  317. package/types/mixins/RTLObserverMixin.d.ts +7 -0
  318. package/types/mixins/RTLObserverMixin.d.ts.map +1 -0
  319. package/types/mixins/ResizeObserverMixin.d.ts +12 -0
  320. package/types/mixins/ResizeObserverMixin.d.ts.map +1 -0
  321. package/types/mixins/RippleMixin.d.ts +92 -0
  322. package/types/mixins/RippleMixin.d.ts.map +1 -0
  323. package/types/mixins/ScrollListenerMixin.d.ts +41 -0
  324. package/types/mixins/ScrollListenerMixin.d.ts.map +1 -0
  325. package/types/mixins/SemiStickyMixin.d.ts +50 -0
  326. package/types/mixins/SemiStickyMixin.d.ts.map +1 -0
  327. package/types/mixins/ShapeMaskedMixin.d.ts +9 -0
  328. package/types/mixins/ShapeMaskedMixin.d.ts.map +1 -0
  329. package/types/mixins/ShapeMixin.d.ts +38 -0
  330. package/types/mixins/ShapeMixin.d.ts.map +1 -0
  331. package/types/mixins/StateMixin.d.ts +27 -0
  332. package/types/mixins/StateMixin.d.ts.map +1 -0
  333. package/types/mixins/TextFieldMixin.d.ts +1354 -0
  334. package/types/mixins/TextFieldMixin.d.ts.map +1 -0
  335. package/types/mixins/ThemableMixin.d.ts +9 -0
  336. package/types/mixins/ThemableMixin.d.ts.map +1 -0
  337. package/types/mixins/TooltipTriggerMixin.d.ts +106 -0
  338. package/types/mixins/TooltipTriggerMixin.d.ts.map +1 -0
  339. package/types/mixins/TouchTargetMixin.d.ts +3 -0
  340. package/types/mixins/TouchTargetMixin.d.ts.map +1 -0
  341. package/types/mixins/TypographyMixin.d.ts +17 -0
  342. package/types/mixins/TypographyMixin.d.ts.map +1 -0
  343. package/types/services/rtl.d.ts +3 -0
  344. package/types/services/rtl.d.ts.map +1 -0
  345. package/types/services/svgAlias.d.ts +13 -0
  346. package/types/services/svgAlias.d.ts.map +1 -0
  347. package/types/services/theme.d.ts +45 -0
  348. package/types/services/theme.d.ts.map +1 -0
  349. package/types/utils/cli.d.ts +3 -0
  350. package/types/utils/cli.d.ts.map +1 -0
  351. package/types/utils/function.d.ts +3 -0
  352. package/types/utils/function.d.ts.map +1 -0
  353. package/types/utils/jsx-runtime.d.ts +20 -0
  354. package/types/utils/jsx-runtime.d.ts.map +1 -0
  355. package/types/utils/material-color/blend.d.ts +34 -0
  356. package/types/utils/material-color/blend.d.ts.map +1 -0
  357. package/types/utils/material-color/hct/Cam16.d.ts +142 -0
  358. package/types/utils/material-color/hct/Cam16.d.ts.map +1 -0
  359. package/types/utils/material-color/hct/Hct.d.ts +93 -0
  360. package/types/utils/material-color/hct/Hct.d.ts.map +1 -0
  361. package/types/utils/material-color/hct/ViewingConditions.d.ts +69 -0
  362. package/types/utils/material-color/hct/ViewingConditions.d.ts.map +1 -0
  363. package/types/utils/material-color/hct/hctSolver.d.ts +30 -0
  364. package/types/utils/material-color/hct/hctSolver.d.ts.map +1 -0
  365. package/types/utils/material-color/helper.d.ts +8 -0
  366. package/types/utils/material-color/helper.d.ts.map +1 -0
  367. package/types/utils/material-color/palettes/CorePalette.d.ts +75 -0
  368. package/types/utils/material-color/palettes/CorePalette.d.ts.map +1 -0
  369. package/types/utils/material-color/palettes/TonalPalette.d.ts +38 -0
  370. package/types/utils/material-color/palettes/TonalPalette.d.ts.map +1 -0
  371. package/types/utils/material-color/scheme/Scheme.d.ts +264 -0
  372. package/types/utils/material-color/scheme/Scheme.d.ts.map +1 -0
  373. package/types/utils/material-color/utils/color.d.ts +172 -0
  374. package/types/utils/material-color/utils/color.d.ts.map +1 -0
  375. package/types/utils/material-color/utils/math.d.ts +94 -0
  376. package/types/utils/material-color/utils/math.d.ts.map +1 -0
  377. package/types/utils/pixelmatch.d.ts +22 -0
  378. package/types/utils/pixelmatch.d.ts.map +1 -0
  379. package/types/utils/popup.d.ts +106 -0
  380. package/types/utils/popup.d.ts.map +1 -0
  381. package/types/utils/searchParams.d.ts +3 -0
  382. package/types/utils/searchParams.d.ts.map +1 -0
  383. package/types/utils/svg.d.ts +7 -0
  384. package/types/utils/svg.d.ts.map +1 -0
  385. package/utils/{hct → material-color}/blend.js +8 -10
  386. package/utils/{hct → material-color/hct}/Cam16.js +196 -69
  387. package/utils/{hct → material-color/hct}/Hct.js +61 -19
  388. package/utils/{hct → material-color/hct}/ViewingConditions.js +3 -3
  389. package/utils/{hct → material-color/hct}/hctSolver.js +9 -16
  390. package/utils/{hct → material-color}/helper.js +11 -18
  391. package/utils/{hct → material-color/palettes}/CorePalette.js +79 -19
  392. package/utils/{hct → material-color/palettes}/TonalPalette.js +12 -4
  393. package/utils/material-color/scheme/Scheme.js +376 -0
  394. package/utils/{hct/colorUtils.js → material-color/utils/color.js} +61 -1
  395. package/utils/pixelmatch.js +360 -0
  396. package/utils/popup.js +127 -30
  397. package/utils/searchParams.js +19 -0
  398. package/components/ExtendedFab.js +0 -36
  399. package/components/Layout.js +0 -35
  400. package/components/ListSelect.js +0 -220
  401. package/components/Nav.js +0 -40
  402. package/components/Option.js +0 -91
  403. package/core/ICustomElement.d.ts +0 -291
  404. package/core/ICustomElement.js +0 -1
  405. package/core/identify.js +0 -40
  406. package/core/typings.d.ts +0 -136
  407. package/core/typings.js +0 -1
  408. package/mixins/SurfaceMixin.js +0 -181
  409. package/theming/loader.js +0 -22
  410. package/utils/hct/Scheme.js +0 -587
  411. /package/{utils/color_keywords.js → constants/colorKeywords.js} +0 -0
  412. /package/utils/{hct/mathUtils.js → material-color/utils/math.js} +0 -0
@@ -1,12 +1,15 @@
1
1
  /* eslint-disable sort-class-members/sort-class-members */
2
+
3
+ import CompositionAdapter from './CompositionAdapter.js';
2
4
  import { generateCSSStyleSheets, generateHTMLStyleElements } from './css.js';
3
- import { identifierFromElement } from './identify.js';
4
5
  import { observeFunction } from './observe.js';
6
+ import { createEmptyComment, createEmptyTextNode } from './optimizations.js';
5
7
  import { generateFragment, inlineFunctions } from './template.js';
8
+ import { generateUID } from './uid.js';
6
9
 
7
10
  /**
8
11
  * @template T
9
- * @typedef {Composition<?>|HTMLStyleElement|CSSStyleSheet|DocumentFragment|((this:T, changes:T) => any)|string} CompositionPart
12
+ * @typedef {Composition<?>|HTMLStyleElement|CSSStyleSheet|DocumentFragment|string} CompositionPart
10
13
  */
11
14
 
12
15
  /**
@@ -18,24 +21,363 @@ import { generateFragment, inlineFunctions } from './template.js';
18
21
 
19
22
  /**
20
23
  * @template T
21
- * @typedef {Object} WatcherBindEntry
22
- * @prop {Function} fn
23
- * @prop {Set<keyof T & string>} props
24
+ * @typedef {Object} RenderOptions
25
+ * @prop {T} [defaults] what
26
+ * @prop {T} [store] what
27
+ * @prop {DocumentFragment|HTMLElement|Element} [target] where
28
+ * @prop {ShadowRoot} [shadowRoot] where
29
+ * @prop {any} [context] `this` on callbacks/events
30
+ * @prop {any} [injections]
31
+ */
32
+
33
+ /** @typedef {HTMLElementEventMap & { input: InputEvent; } } HTMLElementEventMapFixed */
34
+
35
+ /**
36
+ * @typedef {(
37
+ * Pick<HTMLElementEventMapFixed,
38
+ * 'auxclick' |
39
+ * 'beforeinput' |
40
+ * 'click' |
41
+ * 'compositionstart' |
42
+ * 'contextmenu' |
43
+ * 'drag' |
44
+ * 'dragenter' |
45
+ * 'dragover' |
46
+ * 'dragstart' |
47
+ * 'drop' |
48
+ * 'invalid' |
49
+ * 'keydown' |
50
+ * 'keypress' |
51
+ * 'keyup' |
52
+ * 'mousedown' |
53
+ * 'mousemove' |
54
+ * 'mouseout' |
55
+ * 'mouseover' |
56
+ * 'mouseup' |
57
+ * 'pointerdown' |
58
+ * 'pointermove' |
59
+ * 'pointerout' |
60
+ * 'pointerover' |
61
+ * 'pointerup' |
62
+ * 'reset' |
63
+ * 'selectstart' |
64
+ * 'submit' |
65
+ * 'touchend' |
66
+ * 'touchmove' |
67
+ * 'touchstart' |
68
+ * 'wheel'
69
+ * >
70
+ * )} HTMLElementCancellableEventMap
71
+ */
72
+
73
+ /**
74
+ * @typedef {(
75
+ * HTMLElementEventMapFixed
76
+ * & {[P in keyof HTMLElementCancellableEventMap as `~${P}`]: HTMLElementCancellableEventMap[P]}
77
+ * & Record<string, Event|CustomEvent>
78
+ * )} CompositionEventMap
79
+ */
80
+
81
+ /**
82
+ * @template {any} T
83
+ * @template {keyof CompositionEventMap} [K = keyof CompositionEventMap]
84
+ * @typedef {{
85
+ * type?: K
86
+ * tag?: string,
87
+ * capture?: boolean;
88
+ * once?: boolean;
89
+ * passive?: boolean;
90
+ * signal?: AbortSignal;
91
+ * handleEvent?: (
92
+ * this: T,
93
+ * event: (K extends keyof CompositionEventMap ? CompositionEventMap[K] : Event) & {currentTarget:HTMLElement}
94
+ * ) => any;
95
+ * prop?: string;
96
+ * deepProp?: string[],
97
+ * }} CompositionEventListener
98
+ */
99
+
100
+ /**
101
+ * @template T
102
+ * @typedef {{
103
+ * [P in keyof CompositionEventMap]?: (keyof T & string)
104
+ * | ((this: T, event: CompositionEventMap[P] & {currentTarget:HTMLElement}) => any)
105
+ * | CompositionEventListener<T, P>
106
+ * }} CompositionEventListenerObject
24
107
  */
25
108
 
26
109
  /**
27
110
  * @template {any} T
28
111
  * @typedef {Object} NodeBindEntry
29
- * @prop {string} id
30
- * @prop {number} nodeType
31
- * @prop {string} node
112
+ * @prop {string} [key]
113
+ * @prop {number} [index]
114
+ * @prop {string} tag
115
+ * @prop {string|number} subnode Index of childNode or attrName
116
+ * @prop {string[]} props
117
+ * @prop {string[][]} deepProps
32
118
  * @prop {boolean} [negate]
33
119
  * @prop {boolean} [doubleNegate]
34
- * @prop {Function} [fn]
35
- * @prop {Set<keyof T & string>} props
120
+ * @prop {Function} [expression]
121
+ * @prop {(options: RenderOptions<?>, element: Element, changes:any, data:any) => any} [render] custom render function
122
+ * @prop {CompositionEventListener<T>[]} [listeners]
123
+ * @prop {Composition<any>} [composition] // Sub composition templating (eg: array)
36
124
  * @prop {T} defaultValue
37
125
  */
38
126
 
127
+ /** @typedef {any[]} RenderState */
128
+
129
+ /**
130
+ * @typedef RenderGraphSearch
131
+ * @prop {(state:InitializationState, changes:any, data:any) => any} invocation
132
+ * @prop {number} cacheIndex
133
+ * @prop {number} searchIndex
134
+ * @prop {string | Function | string[]} query
135
+ * @prop {Function} [expression]
136
+ * @prop {string} prop
137
+ * @prop {string[]} deepProp
138
+ * @prop {string[]} propsUsed
139
+ * @prop {string[][]} deepPropsUsed
140
+ * @prop {any} defaultValue
141
+ * @prop {RenderGraphSearch} [subSearch]
142
+ */
143
+
144
+ /**
145
+ * @typedef RenderGraphAction
146
+ * @prop {(state:InitializationState, value:any, changes: any, data:any) => any} invocation
147
+ * @prop {number} [commentIndex]
148
+ * @prop {number} [nodeIndex]
149
+ * @prop {number} [cacheIndex]
150
+ * @prop {string} [attrName]
151
+ * @prop {any} [defaultValue]
152
+ * @prop {RenderGraphSearch} search
153
+ */
154
+
155
+ /**
156
+ * @type {RenderGraphAction['invocation']}
157
+ * @this {RenderGraphAction}
158
+ */
159
+ function writeDOMAttribute({ nodes }, value) {
160
+ const { nodeIndex, attrName } = this;
161
+ /** @type {Element} */
162
+ const element = nodes[nodeIndex];
163
+ switch (value) {
164
+ case undefined:
165
+ case null:
166
+ case false:
167
+ element.removeAttribute(attrName);
168
+ return false;
169
+ case true:
170
+ element.setAttribute(attrName, '');
171
+ return '';
172
+ default:
173
+ element.setAttribute(attrName, value);
174
+ return value;
175
+ }
176
+ }
177
+
178
+ /**
179
+ * @type {RenderGraphAction['invocation']}
180
+ * @this {RenderGraphAction}
181
+ */
182
+ function writeDynamicNode({ nodeStates, comments, nodes }, value) {
183
+ const { commentIndex, nodeIndex } = this;
184
+ const nodeState = nodeStates[nodeIndex];
185
+ // eslint-disable-next-line no-bitwise
186
+ const hidden = nodeState & 0b0001;
187
+ const show = value != null && value !== false;
188
+ if (!show) {
189
+ // Should be hidden
190
+ if (hidden) return;
191
+ // Replace whatever node is there with the comment
192
+ let comment = comments[commentIndex];
193
+ if (!comment) {
194
+ comment = createEmptyComment();
195
+ comments[commentIndex] = comment;
196
+ }
197
+ nodes[nodeIndex].replaceWith(comment);
198
+ // eslint-disable-next-line no-bitwise
199
+ nodeStates[nodeIndex] |= 0b0001;
200
+ return;
201
+ }
202
+ // Must be shown
203
+ // Update node first (offscreen rendering)
204
+ const node = nodes[nodeIndex];
205
+ // eslint-disable-next-line no-bitwise
206
+ const isDynamicNode = nodeState & 0b0010;
207
+
208
+ if (typeof value === 'object') {
209
+ // Not string data, need to replace
210
+ console.warn('Dynamic nodes not supported yet');
211
+ } else if (isDynamicNode) {
212
+ const textNode = new Text(value);
213
+ node.replaceWith(textNode);
214
+ nodes[nodeIndex] = textNode;
215
+ // eslint-disable-next-line no-bitwise
216
+ nodeStates[nodeIndex] &= ~0b0010;
217
+ } else {
218
+ node.data = value;
219
+ }
220
+
221
+ // Updated, now set hidden state
222
+
223
+ if (hidden) {
224
+ const comment = comments[commentIndex];
225
+ comment.replaceWith(node);
226
+ // eslint-disable-next-line no-bitwise
227
+ nodeStates[nodeIndex] &= ~0b0001;
228
+ }
229
+ // Done
230
+ }
231
+
232
+ /**
233
+ * @type {RenderGraphAction['invocation']}
234
+ * @this {RenderGraphAction}
235
+ */
236
+ function writeDOMElementAttachedState({ nodeStates, nodes, comments }, value) {
237
+ const { commentIndex, nodeIndex } = this;
238
+ // eslint-disable-next-line no-bitwise
239
+ const hidden = nodeStates[nodeIndex] & 1;
240
+ const show = value != null && value !== false;
241
+ if (show === !hidden) return;
242
+
243
+ const element = nodes[nodeIndex];
244
+ let comment = comments[commentIndex];
245
+ if (!comment) {
246
+ comment = createEmptyComment();
247
+ comments[commentIndex] = comment;
248
+ }
249
+ if (show) {
250
+ comment.replaceWith(element);
251
+ // eslint-disable-next-line no-bitwise
252
+ nodeStates[nodeIndex] &= ~0b0001;
253
+ } else {
254
+ element.replaceWith(comment);
255
+ // eslint-disable-next-line no-bitwise
256
+ nodeStates[nodeIndex] |= 0b0001;
257
+ }
258
+ }
259
+
260
+ /**
261
+ * @type {RenderGraphAction['invocation']}
262
+ * @this {RenderGraphAction}
263
+ */
264
+ function writeDOMHideNodeOnInit({ comments, nodeStates, nodes }) {
265
+ const { commentIndex, nodeIndex } = this;
266
+
267
+ const comment = createEmptyComment();
268
+ comments[commentIndex] = comment;
269
+ // eslint-disable-next-line no-bitwise
270
+ nodeStates[nodeIndex] |= 1;
271
+
272
+ nodes[nodeIndex].replaceWith(comment);
273
+ }
274
+
275
+ /**
276
+ * @param {RenderGraphSearch} search
277
+ * @param {Parameters<RenderGraphSearch['invocation']>} args
278
+ */
279
+ function executeSearch(search, ...args) {
280
+ const [{ caches, searchStates }] = args;
281
+ const { cacheIndex, searchIndex, subSearch, invocation } = search;
282
+ const cachedValue = caches[cacheIndex];
283
+ const searchState = searchStates[searchIndex];
284
+
285
+ // Ran = 0b0001
286
+ // Dirty = 0b0010
287
+ // eslint-disable-next-line no-bitwise
288
+ if (searchState & 0b0001) {
289
+ // Return last result
290
+ return {
291
+ value: cachedValue,
292
+ // eslint-disable-next-line no-bitwise
293
+ dirty: ((searchState & 0b0010) === 0b0010),
294
+ };
295
+ }
296
+
297
+ // eslint-disable-next-line no-bitwise
298
+ searchStates[searchIndex] |= 0b0001;
299
+ let result;
300
+ if (invocation) {
301
+ if (subSearch) {
302
+ const subResult = executeSearch(subSearch, ...args);
303
+ // Use last cached value (if any)
304
+ if (!subResult.dirty && cachedValue !== undefined) {
305
+ // eslint-disable-next-line no-bitwise
306
+ searchStates[searchIndex] &= ~0b0010;
307
+ return { value: cachedValue, dirty: false };
308
+ }
309
+ // Pass from subquery
310
+ result = search.invocation(subResult.value);
311
+ } else {
312
+ result = search.invocation(...args);
313
+ }
314
+ if ((result === undefined) || (cachedValue === result)) {
315
+ // Return from cache
316
+ return { value: result, dirty: false };
317
+ }
318
+ }
319
+
320
+ // Overwrite cache and flag as dirty
321
+ caches[cacheIndex] = result;
322
+ // eslint-disable-next-line no-bitwise
323
+ searchStates[searchIndex] |= 0b0010;
324
+ return { value: result, dirty: true };
325
+ }
326
+
327
+ /**
328
+ * @type {RenderGraphSearch['invocation']}
329
+ * @this {RenderGraphSearch}
330
+ */
331
+ function searchWithExpression({ options: { context, store, injections } }, changes, data) {
332
+ return this.expression.call(
333
+ context,
334
+ store ?? data,
335
+ injections,
336
+ );
337
+ }
338
+
339
+ /**
340
+ * @type {RenderGraphSearch['invocation']}
341
+ * @this {RenderGraphSearch}
342
+ */
343
+ function searchWithProp(state, changes) {
344
+ return changes[this.prop];
345
+ }
346
+
347
+ /**
348
+ * @type {RenderGraphSearch['invocation']}
349
+ * @this {RenderGraphSearch}
350
+ */
351
+ function searchWithDeepProp(state, changes, data) {
352
+ let scope = changes;
353
+ for (const prop of this.deepProp) {
354
+ if (scope === null) return null;
355
+ if (prop in scope === false) return undefined;
356
+ scope = scope[prop];
357
+ }
358
+ return scope;
359
+ }
360
+
361
+ /**
362
+ * @typedef InterpolateOptions
363
+ * @prop {Record<string,any>} [defaults] Default values to use for interpolation
364
+ * @prop {{iterable:string} & Record<string,any>} [injections] Context-specific injected properties. (Experimental)
365
+ */
366
+
367
+ /**
368
+ * @typedef InitializationState
369
+ * @prop {Element} lastElement
370
+ * @prop {ChildNode} lastChildNode
371
+ * @prop {(Element|Text)[]} nodes
372
+ * @prop {any[]} caches
373
+ * @prop {Comment[]} comments
374
+ * @prop {Uint8Array} nodeStates
375
+ * @prop {Uint8Array} searchStates
376
+ * @prop {Element[]} refs
377
+ * @prop {number} lastChildNodeIndex
378
+ * @prop {RenderOptions<?>} options
379
+ */
380
+
39
381
  /** Splits: `{template}text{template}` as `['', 'template', 'text', 'template', '']` */
40
382
  const STRING_INTERPOLATION_REGEX = /{([^}]*)}/g;
41
383
 
@@ -54,33 +396,23 @@ function buildShadowRootChildListener(fn) {
54
396
  }
55
397
 
56
398
  /**
57
- *
58
- * @param {Object} object
59
- * @param {'dot'|'bracket'} [syntax]
60
- * @param {Object} [target]
61
- * @param {string} [scope]
62
- * @return {Object}
399
+ * @example
400
+ * propFromObject('foo', {foo:'bar'}) == ['foo', 'bar'];
401
+ * @param {string} prop
402
+ * @param {any} source
403
+ * @return {any}
63
404
  */
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;
405
+ function propFromObject(prop, source) {
406
+ if (source) {
407
+ return source[prop];
76
408
  }
77
- return target;
409
+ return undefined;
78
410
  }
79
411
 
80
412
  /**
81
413
  * @example
82
- * entryFromPropName(
83
- * 'address.home.houseNumber',
414
+ * deepPropFromObject(
415
+ * ['address', 'home, 'houseNumber'],
84
416
  * {
85
417
  * address: {
86
418
  * home: {
@@ -88,22 +420,25 @@ function flattenObject(object, syntax = 'dot', target = {}, scope = '') {
88
420
  * },
89
421
  * }
90
422
  * }
91
- * ) === {value:35}
92
- * @param {string} prop
423
+ * ) == [houseNumber, 35]
424
+ * @param {string[]} nameArray
93
425
  * @param {any} source
94
- * @return {null|[string, any]}
426
+ * @return {any}
95
427
  */
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];
428
+ function deepPropFromObject(nameArray, source) {
429
+ if (!source) return undefined;
430
+ let scope = source;
431
+ let prop;
432
+ for (prop of nameArray) {
433
+ if (typeof scope === 'object') {
434
+ if (scope === null) return null;
435
+ if (!(prop in scope)) return undefined;
436
+ scope = scope[prop];
437
+ } else {
438
+ return scope[prop];
439
+ }
104
440
  }
105
- if (value === source) return null;
106
- return [child, value];
441
+ return scope;
107
442
  }
108
443
 
109
444
  /**
@@ -123,30 +458,76 @@ function valueFromPropName(prop, source) {
123
458
  return value;
124
459
  }
125
460
 
461
+ const compositionCache = new Map();
462
+
126
463
  /** @template T */
127
464
  export default class Composition {
465
+ static EVENT_PREFIX_REGEX = /^([*1~]+)?(.*)$/;
466
+
467
+ _interpolationState = {
468
+ nodeIndex: -1,
469
+ searchIndex: 0,
470
+ cacheIndex: 0,
471
+ commentIndex: 0,
472
+ /** @type {this['nodesToBind'][0]} */
473
+ nodeEntry: null,
474
+ };
475
+
476
+ // eslint-disable-next-line symbol-description
477
+ static shadowRootTag = Symbol();
478
+
479
+ /** @type {{tag:string, textNodes: number[]}[]} */
480
+ nodesToBind = [];
481
+
482
+ /** @type {string[]} */
483
+ props = [];
484
+
485
+ /** @type {RenderGraphSearch[]} */
486
+ searches = [];
487
+
488
+ /** @type {any[]} */
489
+ initCache = [];
490
+
128
491
  /**
129
- * Collection of property bindings.
130
- * @type {Map<keyof T & string, Set<NodeBindEntry<?>>>}
492
+ * Index of searches by query (dotted notation for deep props)
493
+ * @type {Map<Function|string, RenderGraphSearch>}
131
494
  */
132
- bindings = new Map();
495
+ searchByQuery;
496
+
497
+ /**
498
+ * Index of searches by query (dotted notation for deep props)
499
+ * @type {Map<string, RenderGraphAction[]>}
500
+ */
501
+ actionsByPropsUsed;
502
+
503
+ /** @type {RenderGraphAction[]} */
504
+ postInitActions = [];
505
+
506
+ /** @type {Set<string>} */
507
+ tagsWithBindings;
508
+
509
+ /**
510
+ * Array of element tags
511
+ * @type {string[]}
512
+ */
513
+ tags = [];
133
514
 
134
515
  /**
135
516
  * Data of arrays used in templates
136
- * Usage of a [_for] will create an ArrayLike expectation based on key
517
+ * Usage of a [mdw-for] will create an ArrayLike expectation based on key
137
518
  * Only store metadata, not actual data. Currently only needs length.
138
519
  * TBD if more is needed later
139
520
  * Referenced by property key (string)
140
- * @type {Map<keyof T & string, ArrayMetadata<T>}
521
+ * @type {CompositionAdapter}
141
522
  */
142
- arrayMetadata = new Map();
523
+ adapter;
143
524
 
144
525
  /**
145
526
  * Collection of events to bind.
146
527
  * Indexed by ID
147
- * @type {Map<string, Set<import('./typings.js').CompositionEventListener<any>>>}
528
+ * @type {Map<string|symbol, CompositionEventListener<any>[]>}
148
529
  */
149
- events = new Map();
530
+ events;
150
531
 
151
532
  /**
152
533
  * Snapshot of composition at initial state.
@@ -156,14 +537,6 @@ export default class Composition {
156
537
  */
157
538
  cloneable;
158
539
 
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
540
  /** @type {(HTMLStyleElement|CSSStyleSheet)[]} */
168
541
  styles = [];
169
542
 
@@ -173,25 +546,19 @@ export default class Composition {
173
546
  /** @type {DocumentFragment} */
174
547
  stylesFragment;
175
548
 
176
- /** @type {((this:T, changes:T) => any)[]} */
177
- watchers = [];
178
-
179
549
  /**
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>>}
550
+ * List of IDs used by template elements
551
+ * May be needed to be removed when adding to non-DocumentFragment
552
+ * @type {string[]}
184
553
  */
185
- referenceCache = new WeakMap();
554
+ allIds = [];
186
555
 
187
556
  /**
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>}>}
557
+ * Collection of IDs used for referencing elements
558
+ * Not meant for live DOM. Removed before attaching to document
193
559
  */
194
- conditionalElementMetadata = new Map();
560
+ /** @type {Set<string>} */
561
+ temporaryIds;
195
562
 
196
563
  /** Flag set when template and styles have been interpolated */
197
564
  interpolated = false;
@@ -212,9 +579,24 @@ export default class Composition {
212
579
  yield part;
213
580
  }
214
581
  yield this.template;
215
- for (const part of this.watchers) {
216
- yield part;
582
+ }
583
+
584
+ /**
585
+ * @template T
586
+ * @param {ConstructorParameters<typeof Composition<T>>} parts
587
+ * @return {Composition<T>}
588
+ */
589
+ static compose(...parts) {
590
+ for (const [cache, comp] of compositionCache) {
591
+ if (cache.length !== parts.length) continue;
592
+ if (parts.every((part, index) => part === cache[index])) {
593
+ return comp;
594
+ }
217
595
  }
596
+
597
+ const composition = new Composition(...parts);
598
+ compositionCache.set(parts, composition);
599
+ return composition;
218
600
  }
219
601
 
220
602
  /**
@@ -224,8 +606,6 @@ export default class Composition {
224
606
  for (const part of parts) {
225
607
  if (typeof part === 'string') {
226
608
  this.append(generateFragment(part.trim()));
227
- } else if (typeof part === 'function') {
228
- this.watchers.push(part);
229
609
  } else if (part instanceof Composition) {
230
610
  this.append(...part);
231
611
  } else if (part instanceof DocumentFragment) {
@@ -238,259 +618,272 @@ export default class Composition {
238
618
  return this;
239
619
  }
240
620
 
241
- /** @param {import('./typings.js').CompositionEventListener<T>} listener */
621
+ /** @param {CompositionEventListener<T>} listener */
242
622
  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);
623
+ const key = listener.tag ?? '';
624
+ // eslint-disable-next-line no-multi-assign
625
+ const events = (this.events ??= new Map());
626
+ if (events.has(key)) {
627
+ events.get(key).push(listener);
628
+ } else {
629
+ events.set(key, [listener]);
248
630
  }
249
- set.add(listener);
250
631
  return this;
251
632
  }
252
633
 
253
634
  /**
635
+ * @param {string|symbol} tag
636
+ * @param {EventTarget} target
637
+ * @param {any} [context]
638
+ * @return {void}
639
+ */
640
+ #bindCompositionEventListeners(tag, target, context) {
641
+ if (!this.events?.has(tag)) return;
642
+ for (const event of this.events.get(tag)) {
643
+ let listener;
644
+ if (event.handleEvent) {
645
+ listener = event.handleEvent;
646
+ } else if (event.deepProp.length) {
647
+ listener = deepPropFromObject(event.deepProp, this.interpolateOptions.defaults);
648
+ } else {
649
+ listener = propFromObject(event.prop, this.interpolateOptions.defaults);
650
+ }
651
+ target.addEventListener(event.type, context ? listener.bind(context) : listener, event);
652
+ }
653
+ }
654
+
655
+ /**
656
+ * TODO: Add types and clean up closure leak
254
657
  * Updates component nodes based on data.
255
658
  * Expects data in JSON Merge Patch format
256
659
  * @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}
660
+ * @template {Object} T
661
+ * @param {Partial<T>} changes what specifically
662
+ * @param {T} [data]
663
+ * @param {RenderOptions<T>} [options]
664
+ * @return {Function & {target:Element}} anchor
262
665
  */
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);
666
+ render(changes, data, options = {}) {
667
+ // console.log('render', changes, options);
668
+ if (!this.interpolated) {
669
+ this.interpolate({
670
+ defaults: data ?? changes,
671
+ ...options,
672
+ });
673
+ }
674
+
675
+ const instanceFragment = /** @type {DocumentFragment} */ (this.cloneable.cloneNode(true));
676
+
677
+ const shadowRoot = options.shadowRoot;
678
+ const target = shadowRoot ?? options.target ?? instanceFragment.firstElementChild;
679
+
680
+ /** @type {InitializationState} */
681
+ const initState = {
682
+ lastChildNode: null,
683
+ lastChildNodeIndex: 0,
684
+ lastElement: null,
685
+ nodeStates: new Uint8Array(this._interpolationState.nodeIndex + 1),
686
+ searchStates: new Uint8Array(this._interpolationState.searchIndex),
687
+ comments: [],
688
+ nodes: [],
689
+ caches: this.initCache.slice(),
690
+ refs: [],
691
+ options,
692
+ };
693
+
694
+ const { nodes, refs, searchStates, caches } = initState;
695
+ for (const { tag, textNodes } of this.nodesToBind) {
696
+ /** @type {Text} */
697
+ let textNode;
698
+ if (tag === '') {
699
+ if (!textNodes.length) {
700
+ console.warn('why was root tagged?');
289
701
  continue;
290
702
  }
291
- if (!ref) continue;
292
- if (modifiedNodes.get(ref)?.has(node)) {
293
- // console.warn('Node already modified. Skipping', id, node);
294
- continue;
703
+ console.warn('found empty tag??');
704
+ refs.push(null);
705
+ nodes.push(null);
706
+ textNode = instanceFragment.firstChild;
707
+ } else {
708
+ const element = instanceFragment.getElementById(tag);
709
+ refs.push(element);
710
+ nodes.push(element);
711
+ this.#bindCompositionEventListeners(tag, element, options.context);
712
+ if (!textNodes.length) continue;
713
+ textNode = element.firstChild;
714
+ }
715
+
716
+ let currentIndex = 0;
717
+ for (const index of textNodes) {
718
+ while (index !== currentIndex) {
719
+ textNode = textNode.nextSibling;
720
+ currentIndex++;
295
721
  }
722
+ nodes.push(textNode);
723
+ }
724
+ }
725
+ this.#bindCompositionEventListeners('', options.context);
726
+ this.#bindCompositionEventListeners(Composition.shadowRootTag, options.context.shadowRoot, options.context);
296
727
 
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);
728
+ for (const action of this.postInitActions) {
729
+ action.invocation(initState);
730
+ }
731
+
732
+ /**
733
+ * @param {Partial<T>} changes
734
+ * @param {T} data
735
+ */
736
+ const draw = (changes, data) => {
737
+ let ranSearch = false;
738
+ for (const prop of this.props) {
739
+ if (!this.actionsByPropsUsed?.has(prop)) continue;
740
+ if (!(prop in changes)) continue;
741
+ const actions = this.actionsByPropsUsed.get(prop);
742
+ for (const action of actions) {
743
+ ranSearch = true;
744
+ const { dirty, value } = executeSearch(action.search, initState, changes, data);
745
+ if (dirty) {
746
+ // console.log('dirty, updating from batch', initState.nodes[action.nodeIndex], 'with', value);
747
+ action.invocation(initState, value, changes, data);
343
748
  }
344
- } else {
345
- value = rawValue;
346
749
  }
347
-
348
- /* 3. Operate on value */
349
- if (doubleNegate) {
350
- value = !!value;
351
- } else if (negate) {
352
- value = !value;
750
+ }
751
+ if (!ranSearch) return;
752
+ searchStates.fill(0);
753
+ };
754
+
755
+ if (shadowRoot) {
756
+ options.context ??= shadowRoot.host;
757
+ if ('adoptedStyleSheets' in shadowRoot) {
758
+ if (this.adoptedStyleSheets.length) {
759
+ shadowRoot.adoptedStyleSheets = [
760
+ ...shadowRoot.adoptedStyleSheets,
761
+ ...this.adoptedStyleSheets,
762
+ ];
353
763
  }
764
+ } else if (this.stylesFragment.hasChildNodes()) {
765
+ instanceFragment.prepend(this.stylesFragment.cloneNode(true));
766
+ }
767
+ } else {
768
+ options.context ??= target;
769
+ }
354
770
 
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
- }
771
+ if (changes !== this.interpolateOptions.defaults) {
772
+ // Not default, overwrite nodes
773
+ draw(changes, data);
774
+ }
389
775
 
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);
776
+ if (shadowRoot) {
777
+ shadowRoot.append(instanceFragment);
778
+ customElements.upgrade(shadowRoot);
779
+ }
780
+
781
+ draw.target = target;
782
+
783
+ /**
784
+ * @param {keyof T & string} prop
785
+ * @param {any} value
786
+ * @param {Partial<T>} [data]
787
+ */
788
+ draw.byProp = (prop, value, data) => {
789
+ if (!this.actionsByPropsUsed?.has(prop)) return;
790
+ let ranSearch = false;
791
+
792
+ // Update search
793
+ if (this.searchByQuery?.has(prop)) {
794
+ ranSearch = true;
795
+ const search = this.searchByQuery.get(prop);
796
+ const cachedValue = caches[search.cacheIndex];
797
+ if (cachedValue === value) {
798
+ return;
436
799
  }
800
+ caches[search.cacheIndex] = value;
801
+ searchStates[search.searchIndex] = 0b0011;
802
+ }
437
803
 
438
- /* 5. Mark Node as modified */
439
- let set = modifiedNodes.get(ref);
440
- if (!set) {
441
- set = new Set();
442
- modifiedNodes.set(ref, set);
804
+ let changes;
805
+ const actions = this.actionsByPropsUsed.get(prop);
806
+ for (const action of actions) {
807
+ if (action.search.query === prop) {
808
+ action.invocation(initState, value);
809
+ } else {
810
+ changes ??= { [prop]: value };
811
+ data ??= changes;
812
+ ranSearch = true;
813
+ const result = executeSearch(action.search, initState, changes, data);
814
+ if (result.dirty) {
815
+ // console.debug('dirty, updating by prop', prop, initState.nodes[action.nodeIndex], 'with', result.value);
816
+ action.invocation(initState, result.value, changes, data);
817
+ }
443
818
  }
444
- set.add(node);
445
819
  }
446
- }
820
+
821
+ if (!ranSearch) return;
822
+ searchStates.fill(0);
823
+ };
824
+ draw.state = initState;
825
+ return draw;
447
826
  }
448
827
 
449
828
  /**
450
829
  * @param {Attr|Text} node
451
- * @param {Element} element
452
- * @param {Object} [defaults]
830
+ * @param {Element|null} [element]
831
+ * @param {InterpolateOptions} [options]
453
832
  * @param {string} [parsedValue]
454
- * @return {boolean} Remove node
833
+ * @return {true|undefined} remove node
455
834
  */
456
- #interpolateNode(node, element, defaults, parsedValue) {
835
+ #interpolateNode(node, element, options, parsedValue) {
457
836
  const { nodeValue, nodeName, nodeType } = node;
458
837
 
838
+ /** @type {Attr} */
839
+ let attr;
840
+ /** @type {Text} */
841
+ let text;
842
+ if (nodeType === Node.ATTRIBUTE_NODE) {
843
+ attr = /** @type {Attr} */ (node);
844
+ } else {
845
+ text = /** @type {Text} */ (node);
846
+ }
847
+
848
+ // Get template strings(s) in node if not passed
459
849
  if (parsedValue == null) {
460
- if (!nodeValue) return false;
850
+ if (!nodeValue) return;
461
851
  const trimmed = nodeValue.trim();
462
- if (!trimmed) return false;
463
- if (nodeType === Node.ATTRIBUTE_NODE) {
464
- if (trimmed[0] !== '{') return false;
852
+ if (!trimmed) return;
853
+ if (attr || element?.tagName === 'STYLE') {
854
+ if (trimmed[0] !== '{') return;
465
855
  const { length } = trimmed;
466
- if (trimmed[length - 1] !== '}') return false;
856
+ if (trimmed[length - 1] !== '}') return;
467
857
  parsedValue = trimmed.slice(1, -1);
858
+ // TODO: Support segmented attribute values
468
859
  } else {
469
860
  // Split text node into segments
470
861
  // TODO: Benchmark indexOf pre-check vs regex
471
862
 
472
863
  const segments = trimmed.split(STRING_INTERPOLATION_REGEX);
473
- if (segments.length < 3) return false;
864
+ if (segments.length < 3) return;
474
865
  if (segments.length === 3 && !segments[0] && !segments[2]) {
475
866
  parsedValue = segments[1];
476
867
  } else {
477
- segments.forEach((segment, index) => {
868
+ for (const [index, segment] of segments.entries()) {
478
869
  // is even = is template string
479
870
  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);
871
+ const newNode = createEmptyTextNode();
872
+ text.before(newNode);
873
+ this.#interpolateNode(newNode, element, options, segment);
874
+ } else if (segment) {
875
+ text.before(segment);
486
876
  }
487
- });
488
- // node.remove();
877
+ }
878
+ // eslint-disable-next-line consistent-return
489
879
  return true;
490
880
  }
491
881
  }
492
882
  }
493
883
 
884
+ // Check mutations
885
+
886
+ const query = parsedValue;
494
887
  const negate = parsedValue[0] === '!';
495
888
  let doubleNegate = false;
496
889
  if (negate) {
@@ -504,245 +897,552 @@ export default class Composition {
504
897
  let isEvent;
505
898
  let textNodeIndex;
506
899
 
507
- if (nodeType === Node.TEXT_NODE) {
900
+ if (text) {
508
901
  // eslint-disable-next-line unicorn/consistent-destructuring
509
- if (element !== node.parentElement) {
902
+ if (element !== text.parentElement) {
510
903
  console.warn('mismatch?');
511
- element = node.parentElement;
904
+ element = text.parentElement;
512
905
  }
513
906
  textNodeIndex = 0;
514
- let prev = node;
907
+ /** @type {ChildNode} */
908
+ let prev = text;
515
909
  while ((prev = prev.previousSibling)) {
516
- if (prev.nodeType === Node.TEXT_NODE) {
517
- textNodeIndex++;
518
- }
910
+ textNodeIndex++;
519
911
  }
520
912
  } else {
521
913
  // @ts-ignore Skip cast
522
914
  // eslint-disable-next-line unicorn/consistent-destructuring
523
- if (element !== node.ownerElement) {
915
+ if (element !== attr.ownerElement) {
524
916
  console.warn('mismatch?');
525
- element = node.ownerElement;
917
+ element = attr.ownerElement;
526
918
  }
527
919
  if (nodeName.startsWith('on')) {
528
920
  // Do not interpolate inline event listeners
529
- if (nodeName[2] !== '-') return false;
530
- isEvent = true;
921
+ const hyphenIndex = nodeName.indexOf('-');
922
+ if (hyphenIndex === -1) return;
923
+ isEvent = hyphenIndex === 2;
531
924
  }
532
925
  }
533
926
 
534
- const id = identifierFromElement(element, true);
535
-
536
927
  if (isEvent) {
928
+ element.removeAttribute(nodeName);
929
+ const tag = this.#tagElement(element);
537
930
  const eventType = nodeName.slice(3);
538
931
  const [, flags, type] = eventType.match(/^([*1~]+)?(.*)$/);
539
- const options = {
932
+
933
+ let handleEvent;
934
+ /** @type {string} */
935
+ let prop;
936
+ /** @type {string[]} */
937
+ let deepProp = [];
938
+ if (parsedValue.startsWith('#')) {
939
+ handleEvent = inlineFunctions.get(parsedValue).fn;
940
+ } else {
941
+ const parsedProps = parsedValue.split('.');
942
+ if (parsedProps.length === 1) {
943
+ prop = parsedValue;
944
+ deepProp = [];
945
+ } else {
946
+ prop = parsedProps[0];
947
+ deepProp = parsedProps;
948
+ }
949
+ }
950
+
951
+ this.addCompositionEventListener({
952
+ tag,
953
+ type,
954
+ handleEvent,
955
+ prop,
956
+ deepProp,
540
957
  once: flags?.includes('1'),
541
958
  passive: flags?.includes('~'),
542
959
  capture: flags?.includes('*'),
543
- };
960
+ });
544
961
 
545
- element.removeAttribute(nodeName);
962
+ return;
963
+ }
546
964
 
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 });
965
+ /** @type {RenderGraphSearch} */
966
+ let search;
967
+
968
+ if (this.searchByQuery?.has(query)) {
969
+ search = this.searchByQuery.get(query);
970
+ } else {
971
+ // Has subquery?
972
+ const subquery = parsedValue;
973
+ const isSubquery = subquery !== query;
974
+ /** @type {RenderGraphSearch} */
975
+ let subSearch;
976
+ if (isSubquery && this.searchByQuery?.has(subquery)) {
977
+ subSearch = this.searchByQuery.get(subquery);
554
978
  } else {
555
- set.add({ type, prop: parsedValue, ...options });
556
- }
557
- return false;
558
- }
979
+ // Construct subsearch, even is not subquery.
980
+ /** @type {Function} */
981
+ let expression;
982
+ /** @type {string[]} */
983
+ let propsUsed;
984
+ /** @type {string[][]} */
985
+ let deepPropsUsed;
986
+ let defaultValue;
987
+ let prop;
988
+ let deepProp;
989
+ let invocation;
990
+
991
+ let inlineFunctionOptions;
992
+ // Is Inline Function?
993
+ if (parsedValue.startsWith('#')) {
994
+ inlineFunctionOptions = inlineFunctions.get(parsedValue);
995
+ if (!inlineFunctionOptions) {
996
+ console.warn(`Invalid interpolation value: ${parsedValue}`);
997
+ return;
998
+ }
999
+ expression = inlineFunctionOptions.fn;
1000
+ invocation = searchWithExpression;
1001
+ if (inlineFunctionOptions.props) {
1002
+ // console.log('This function has already been called. Reuse props', inlineFunctionOptions, this);
1003
+ propsUsed = inlineFunctionOptions.props;
1004
+ deepPropsUsed = inlineFunctionOptions.deepProps;
1005
+ defaultValue = inlineFunctionOptions.defaultValue ?? null;
1006
+ } else {
1007
+ defaultValue = inlineFunctionOptions.fn;
1008
+ }
1009
+ } else {
1010
+ defaultValue = null;
1011
+ if (options?.defaults) {
1012
+ defaultValue = deepPropFromObject(parsedValue.split('.'), options.defaults) ?? null;
1013
+ }
1014
+ if (defaultValue == null && options?.injections) {
1015
+ defaultValue = valueFromPropName(parsedValue, options.injections);
1016
+ // console.log('default value from injection', parsedValue, { defaultValue });
1017
+ }
1018
+ }
1019
+
1020
+ if (!propsUsed) {
1021
+ if (typeof defaultValue === 'function') {
1022
+ // Value must be reinterpolated and function observed
1023
+ const observeResult = observeFunction.call(this, defaultValue, options?.defaults, options?.injections);
1024
+ const uniqueProps = new Set([
1025
+ ...observeResult.props.this,
1026
+ ...observeResult.props.args[0],
1027
+ ...observeResult.props.args[1],
1028
+ ]);
1029
+ const uniqueDeepProps = new Set([
1030
+ ...observeResult.deepPropStrings.this,
1031
+ ...observeResult.deepPropStrings.args[0],
1032
+ ]);
1033
+ expression = defaultValue;
1034
+ defaultValue = observeResult.defaultValue;
1035
+ propsUsed = [...uniqueProps];
1036
+ deepPropsUsed = [...uniqueDeepProps].map((deepPropString) => deepPropString.split('.'));
1037
+ invocation = searchWithExpression;
1038
+ // console.log(this.static.name, fn.name || parsedValue, combinedSet);
1039
+ } else {
1040
+ // property binding
1041
+ const parsedProps = parsedValue.split('.');
1042
+ if (parsedProps.length === 1) {
1043
+ prop = parsedValue;
1044
+ propsUsed = [prop];
1045
+ invocation = searchWithProp;
1046
+ } else {
1047
+ propsUsed = [parsedProps[0]];
1048
+ deepProp = parsedProps;
1049
+ deepPropsUsed = [parsedProps];
1050
+ invocation = searchWithDeepProp;
1051
+ }
559
1052
 
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;
1053
+ // TODO: Rewrite property as deep with array index?
1054
+ }
1055
+ }
1056
+
1057
+ if (inlineFunctionOptions) {
1058
+ inlineFunctionOptions.defaultValue = defaultValue;
1059
+ inlineFunctionOptions.props = propsUsed;
1060
+ inlineFunctionOptions.deepProps = deepPropsUsed;
1061
+ }
1062
+ subSearch = {
1063
+ cacheIndex: this._interpolationState.cacheIndex++,
1064
+ searchIndex: this._interpolationState.searchIndex++,
1065
+ query: subquery,
1066
+ defaultValue,
1067
+ subSearch: null,
1068
+ prop,
1069
+ propsUsed,
1070
+ deepProp,
1071
+ deepPropsUsed,
1072
+ invocation,
1073
+ expression,
1074
+ };
1075
+ this.addSearch(subSearch);
574
1076
  }
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;
1077
+ if (isSubquery) {
1078
+ search = {
1079
+ cacheIndex: this._interpolationState.cacheIndex++,
1080
+ searchIndex: this._interpolationState.searchIndex++,
1081
+ query,
1082
+ subSearch,
1083
+ negate,
1084
+ doubleNegate,
1085
+ prop: subSearch.prop,
1086
+ deepProp: subSearch.deepProp,
1087
+ propsUsed: subSearch.propsUsed,
1088
+ deepPropsUsed: subSearch.deepPropsUsed,
1089
+ defaultValue: doubleNegate ? !!subSearch.defaultValue
1090
+ : (negate ? !subSearch.defaultValue : subSearch.defaultValue),
1091
+ invocation(value) {
1092
+ if (this.doubleNegate) return !!value;
1093
+ if (this.negate) return !value;
1094
+ console.warn('Unknown query mutation', this.query);
1095
+ return value;
1096
+ },
1097
+ };
1098
+ this.addSearch(search);
579
1099
  } else {
580
- defaultValue = inlineFunctionOptions.fn;
1100
+ // Store as search instead
1101
+ search = subSearch;
581
1102
  }
582
- } else {
583
- defaultValue = valueFromPropName(parsedValue, defaults);
584
1103
  }
585
1104
 
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);
1105
+ // Tag
1106
+ let tag;
1107
+ let subnode = null;
1108
+ let defaultValue = search.defaultValue;
1109
+ if (text) {
1110
+ subnode = textNodeIndex;
1111
+ } else if (nodeName === 'mdw-if') {
1112
+ tag = this.#tagElement(element);
1113
+ element.removeAttribute(nodeName);
1114
+ defaultValue = defaultValue != null && defaultValue !== false;
1115
+ } else {
1116
+ subnode = nodeName;
1117
+ if (nodeName === 'id' || defaultValue == null || defaultValue === false) {
1118
+ element.removeAttribute(nodeName);
594
1119
  } else {
595
- props = new Set([parsedValue]);
1120
+ element.setAttribute(nodeName, defaultValue === true ? '' : defaultValue);
596
1121
  }
597
1122
  }
598
1123
 
599
- if (typeof defaultValue === 'symbol') {
600
- console.warn(': Invalid binding:', parsedValue);
601
- defaultValue = null;
602
- }
1124
+ tag ??= this.#tagElement(element);
603
1125
 
604
- if (doubleNegate) {
605
- defaultValue = !!defaultValue;
606
- } else if (negate) {
607
- defaultValue = !defaultValue;
1126
+ // Node entry
1127
+ let nodeEntry = this._interpolationState.nodeEntry;
1128
+ if (!nodeEntry || nodeEntry.tag !== tag) {
1129
+ nodeEntry = {
1130
+ tag,
1131
+ textNodes: [],
1132
+ };
1133
+ this._interpolationState.nodeEntry = nodeEntry;
1134
+ this.nodesToBind.push(nodeEntry);
1135
+ this._interpolationState.nodeIndex++;
608
1136
  }
609
1137
 
610
- if (inlineFunctionOptions) {
611
- inlineFunctionOptions.defaultValue = defaultValue;
612
- inlineFunctionOptions.props = props;
613
- }
1138
+ /** @type {RenderGraphAction} */
1139
+ let action;
1140
+
1141
+ // Node Action
1142
+ if (text) {
1143
+ nodeEntry.textNodes.push(textNodeIndex);
614
1144
 
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);
1145
+ this._interpolationState.nodeIndex++;
1146
+ action = {
1147
+ nodeIndex: this._interpolationState.nodeIndex,
1148
+ commentIndex: this._interpolationState.commentIndex++,
1149
+ invocation: writeDynamicNode,
1150
+ defaultValue,
1151
+ search,
1152
+ };
1153
+ text.data = typeof defaultValue === 'string' ? defaultValue : '';
1154
+ } else if (subnode) {
1155
+ action = {
1156
+ nodeIndex: this._interpolationState.nodeIndex,
1157
+ attrName: subnode,
1158
+ defaultValue,
1159
+ invocation: writeDOMAttribute,
1160
+ search,
1161
+ };
1162
+ } else {
1163
+ action = {
1164
+ nodeIndex: this._interpolationState.nodeIndex,
1165
+ commentIndex: this._interpolationState.commentIndex++,
1166
+ defaultValue,
1167
+ invocation: writeDOMElementAttachedState,
1168
+ search,
1169
+ };
1170
+ if (!defaultValue) {
1171
+ this.postInitActions.push({
1172
+ ...action,
1173
+ invocation: writeDOMHideNodeOnInit,
1174
+ });
623
1175
  }
624
- set.add(entry);
625
1176
  }
626
1177
 
627
- // Mutate
1178
+ this.addAction(action);
1179
+ // eslint-disable-next-line no-multi-assign
1180
+ const tagsWithBindings = (this.tagsWithBindings ??= new Set());
1181
+ tagsWithBindings.add(tag);
1182
+ }
628
1183
 
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;
1184
+ /**
1185
+ * @param {Element} element
1186
+ * @return {string}
1187
+ */
1188
+ #tagElement(element) {
1189
+ if (!element) return '';
1190
+ let id = element.id;
1191
+ if (id) {
1192
+ if (!this.allIds.includes(id)) {
1193
+ this.allIds.push(id);
636
1194
  }
637
- } else if (defaultValue == null || defaultValue === false) {
638
- element.removeAttribute(nodeName);
639
1195
  } else {
640
- element.setAttribute(nodeName, defaultValue === true ? '' : defaultValue);
1196
+ id = generateUID();
1197
+ // eslint-disable-next-line no-multi-assign
1198
+ const temporaryIds = (this.temporaryIds ??= new Set());
1199
+ temporaryIds.add(id);
1200
+ this.allIds.push(id);
1201
+ element.id = id;
1202
+ }
1203
+ return id;
1204
+ }
1205
+
1206
+ /**
1207
+ * TODO: Subtemplating lacks optimization, though functional.
1208
+ * - Would benefit from custom type handler for arrays
1209
+ * to avoid multi-iteration change-detection.
1210
+ * - Could benefit from debounced/throttled render
1211
+ * - Consider remap of {item.prop} as {array[index].prop}
1212
+ * @param {Element} element
1213
+ * @param {InterpolateOptions} options
1214
+ * @return {?Composition<?>}
1215
+ */
1216
+ #interpolateIterable(element, options) {
1217
+ // TODO: Microbenchmark element.attributes
1218
+ const forAttr = element.getAttribute('mdw-for');
1219
+ const trimmed = forAttr?.trim();
1220
+ if (!trimmed) {
1221
+ console.warn('Malformed mdw-for found at', element);
1222
+ return null;
641
1223
  }
642
- return false;
1224
+
1225
+ if (trimmed[0] !== '{') {
1226
+ console.warn('Malformed mdw-for found at', element);
1227
+ return null;
1228
+ }
1229
+ const { length } = trimmed;
1230
+ if (trimmed[length - 1] !== '}') {
1231
+ console.warn('Malformed mdw-for found at', element);
1232
+ return null;
1233
+ }
1234
+ const parsedValue = trimmed.slice(1, -1);
1235
+ const [valueName, iterableName] = parsedValue.split(/\s+of\s+/);
1236
+ element.removeAttribute('mdw-for');
1237
+ // Create a new composition targetting element as root
1238
+
1239
+ const elementAnchor = element.ownerDocument.createElement('template');
1240
+ element.replaceWith(elementAnchor);
1241
+ const tag = this.#tagElement(elementAnchor);
1242
+ // console.log('tagging placeholder element with', elementAnchor, tag);
1243
+
1244
+ let nodeEntry = this._interpolationState.nodeEntry;
1245
+ if (!nodeEntry || nodeEntry.tag !== tag) {
1246
+ nodeEntry = {
1247
+ tag,
1248
+ textNodes: [],
1249
+ };
1250
+ this._interpolationState.nodeEntry = nodeEntry;
1251
+ this.nodesToBind.push(nodeEntry);
1252
+ this._interpolationState.nodeIndex++;
1253
+ }
1254
+
1255
+ const newComposition = new Composition();
1256
+ newComposition.template.append(element);
1257
+ // Move uninterpolated element to new composition template.
1258
+ const injections = {
1259
+ ...options.injections,
1260
+ [valueName]: null,
1261
+ index: null,
1262
+ };
1263
+
1264
+ const propsUsed = [iterableName];
1265
+ /** @type {RenderGraphSearch} */
1266
+ const search = {
1267
+ cacheIndex: this._interpolationState.cacheIndex++,
1268
+ searchIndex: this._interpolationState.searchIndex++,
1269
+ propsUsed,
1270
+ deepPropsUsed: [[iterableName]],
1271
+ defaultValue: {},
1272
+ invocation: null,
1273
+ };
1274
+
1275
+ /** @type {RenderGraphAction} */
1276
+ const action = {
1277
+ defaultValue: null,
1278
+ nodeIndex: this._interpolationState.nodeIndex,
1279
+ search,
1280
+ commentIndex: this._interpolationState.commentIndex++,
1281
+ injections,
1282
+ invocation(state, value, changes, data) {
1283
+ if (!newComposition.adapter) {
1284
+ // console.log({ state.options });
1285
+ const instanceAnchorElement = state.nodes[this.nodeIndex];
1286
+ const anchorNode = createEmptyComment();
1287
+ // Avoid leak
1288
+ state.comments[this.commentIndex] = anchorNode;
1289
+ instanceAnchorElement.replaceWith(anchorNode);
1290
+ newComposition.adapter = new CompositionAdapter({
1291
+ anchorNode,
1292
+ composition: newComposition,
1293
+ renderOptions: {
1294
+ target: null,
1295
+ context: state.options.context,
1296
+ store: state.options.store,
1297
+ injections: this.injections,
1298
+ },
1299
+ });
1300
+ }
1301
+ const { adapter } = newComposition;
1302
+ const iterable = (data ?? state.options.store)[iterableName];
1303
+ // Remove oversized
1304
+ if (!iterable || iterable.length === 0) {
1305
+ adapter.removeEntries();
1306
+ return;
1307
+ }
1308
+ const changeList = changes[iterableName];
1309
+ const innerChanges = { ...changes };
1310
+ const needTargetAll = newComposition.props.some((prop) => prop !== iterableName && prop in changes);
1311
+
1312
+ adapter.startBatch();
1313
+ let isArray;
1314
+ if (!needTargetAll && !(isArray = Array.isArray(changeList))) {
1315
+ const iterator = isArray ? changeList.entries() : Object.entries(changeList);
1316
+ // console.log('changeList render', iterator);
1317
+ for (const [key, change] of iterator) {
1318
+ if (key === 'length') continue;
1319
+ if (change === null) {
1320
+ // console.warn('null?', 'remove?', key);
1321
+ continue;
1322
+ }
1323
+ const index = (+key);
1324
+ const resource = iterable[index];
1325
+ innerChanges[valueName] = change;
1326
+ this.injections[valueName] = resource;
1327
+ this.injections.index = index;
1328
+
1329
+ adapter.renderData(index, innerChanges, data, resource, change);
1330
+ }
1331
+ } else {
1332
+ if (!changeList) {
1333
+ delete innerChanges[valueName];
1334
+ }
1335
+ // console.log('full array render', iterable);
1336
+ for (const [index, resource] of iterable.entries()) {
1337
+ let change;
1338
+ if (changeList) {
1339
+ // console.warn('full array render has changeList?', changeList);
1340
+ if (!needTargetAll && !(index in changeList)) {
1341
+ console.warn('huh?');
1342
+ continue;
1343
+ }
1344
+ change = changeList[index];
1345
+ if (change === null) {
1346
+ // console.warn('remove?');
1347
+ continue;
1348
+ }
1349
+ innerChanges[valueName] = change;
1350
+ }
1351
+ this.injections[valueName] = resource;
1352
+ this.injections.index = index;
1353
+
1354
+ adapter.renderData(index, innerChanges, data, resource, change);
1355
+ // adapter.renderIndex(index, innerChanges, data, resource);
1356
+ }
1357
+ }
1358
+ adapter.stopBatch();
1359
+
1360
+ adapter.removeEntries(iterable.length);
1361
+ },
1362
+
1363
+ };
1364
+
1365
+ newComposition.interpolate({
1366
+ defaults: options.defaults,
1367
+ injections,
1368
+ });
1369
+
1370
+ propsUsed.push(...newComposition.props);
1371
+ this.addSearch(search);
1372
+ this.addAction(action);
1373
+ // eslint-disable-next-line no-multi-assign
1374
+ const tagsWithBindings = (this.tagsWithBindings ??= new Set());
1375
+ tagsWithBindings.add(tag);
1376
+ // console.log('adding', iterable, 'bind to', this);
1377
+ // this.addBinding(iterable, entry);
1378
+ return newComposition;
643
1379
  }
644
1380
 
645
1381
  /**
646
- * @param {Object} [defaults]
1382
+ * @param {InterpolateOptions} [options]
647
1383
  */
648
- interpolate(defaults) {
1384
+ interpolate(options) {
1385
+ this.interpolateOptions = options;
649
1386
  // console.log('Template', [...this.template.children].map((child) => child.outerHTML).join('\n'));
650
1387
 
651
1388
  // Copy template before working on it
652
1389
  // Store into `cloneable` to split later into `interpolation`
653
1390
  this.cloneable = /** @type {DocumentFragment} */ (this.template.cloneNode(true));
654
1391
 
655
- /**
656
- * Track elements to be removed before using for cloning
657
- * @type {Element[]}
658
- */
659
- const removalList = [];
660
-
661
1392
  const TREE_WALKER_FILTER = 5; /* NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT */
662
1393
 
663
1394
  const treeWalker = document.createTreeWalker(this.cloneable, TREE_WALKER_FILTER);
1395
+ /** Note: `node` and treeWalker.currentNode may deviate */
664
1396
  let node = treeWalker.nextNode();
665
1397
  while (node) {
666
1398
  /** @type {Element} */
667
1399
  let element = null;
668
- let removeElement = false;
669
1400
  switch (node.nodeType) {
670
1401
  case Node.ELEMENT_NODE:
671
- element = node;
672
- if (element instanceof HTMLTemplateElement) {
673
- node = treeWalker.nextSibling();
1402
+ element = /** @type {Element} */ (node);
1403
+ if (element.tagName === 'TEMPLATE') {
1404
+ while (element.contains(node = treeWalker.nextNode()));
674
1405
  continue;
675
1406
  }
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) {
1407
+
1408
+ if (element.tagName === 'SCRIPT') {
687
1409
  console.warn('<script> element found.');
688
- node.remove();
689
- node = treeWalker.nextSibling();
1410
+ while (element.contains(node = treeWalker.nextNode()));
690
1411
  continue;
691
1412
  }
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
- });
1413
+
1414
+ if (element.hasAttribute('mdw-for')) {
1415
+ while (element.contains(node = treeWalker.nextNode()));
1416
+ this.#interpolateIterable(element, options);
1417
+ continue;
1418
+ } else {
1419
+ const idAttr = element.attributes.id;
1420
+ if (idAttr) {
1421
+ this.#interpolateNode(idAttr, element, options);
1422
+ this.#tagElement(element);
1423
+ }
1424
+ for (const attr of [...element.attributes].reverse()) {
1425
+ if (attr.nodeName === 'id') continue; // Already handled
1426
+ this.#interpolateNode(attr, element, options);
705
1427
  }
706
- removeElement ||= this.#interpolateNode(attr, element, defaults);
707
1428
  }
708
1429
 
709
1430
  break;
710
1431
  case Node.TEXT_NODE:
711
- element = node.parentNode;
712
- if (this.#interpolateNode(/** @type {Text} */ (node), element, defaults)) {
1432
+ element = node.parentElement;
1433
+ if (this.#interpolateNode(/** @type {Text} */ (node), element, options)) {
713
1434
  const nextNode = treeWalker.nextNode();
714
1435
  node.remove();
715
1436
  node = nextNode;
716
1437
  continue;
717
1438
  }
718
-
719
1439
  break;
720
1440
  default:
721
1441
  throw new Error(`Unexpected node type: ${node.nodeType}`);
722
1442
  }
723
- if (removeElement) {
724
- removalList.push(element);
725
- }
726
1443
  node = treeWalker.nextNode();
727
1444
  }
728
1445
 
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
1446
  if ('adoptedStyleSheets' in document) {
747
1447
  this.adoptedStyleSheets = [
748
1448
  ...generateCSSStyleSheets(this.styles),
@@ -754,235 +1454,54 @@ export default class Composition {
754
1454
  );
755
1455
  }
756
1456
 
757
- this.interpolated = true;
1457
+ this.props = this.actionsByPropsUsed
1458
+ ? [...this.actionsByPropsUsed.keys()]
1459
+ : [];
758
1460
 
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);
1461
+ for (const id of this.allIds) {
1462
+ if (!this.tagsWithBindings?.has(id)) {
1463
+ this.nodesToBind.push({
1464
+ tag: id,
1465
+ textNodes: [],
1466
+ });
821
1467
  }
822
1468
  }
823
1469
 
824
- this.initiallyRendered = true;
825
- }
1470
+ this.tags = this.nodesToBind.map((n) => n.tag);
1471
+ this.interpolated = true;
826
1472
 
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
- }
1473
+ // console.log('Cloneable', [...this.cloneable.children].map((child) => child.outerHTML).join('\n'));
846
1474
  }
847
1475
 
848
1476
  /**
849
- * @param {Element|DocumentFragment} root
850
- * @return {Map<string,Element>}
1477
+ * @param {RenderGraphSearch} search
1478
+ * @return {RenderGraphSearch}
851
1479
  */
852
- getReferences(root) {
853
- let references = this.referenceCache.get(root);
854
- if (!references) {
855
- references = new Map();
856
- this.referenceCache.set(root, references);
1480
+ addSearch(search) {
1481
+ this.searches.push(search);
1482
+ if (search.query) {
1483
+ // eslint-disable-next-line no-multi-assign
1484
+ const searchByQuery = (this.searchByQuery ??= new Map());
1485
+ searchByQuery.set(search.query, search);
1486
+ this.initCache[search.cacheIndex] = search.defaultValue;
857
1487
  }
858
- return references;
1488
+ return search;
859
1489
  }
860
1490
 
861
1491
  /**
862
- * @param {ShadowRoot|DocumentFragment} root
863
- * @param {string} id
864
- * @return {Element}
1492
+ * @param {RenderGraphAction} action
1493
+ * @return {RenderGraphAction}
865
1494
  */
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
- }
1495
+ addAction(action) {
1496
+ // eslint-disable-next-line no-multi-assign
1497
+ const actionsByPropsUsed = (this.actionsByPropsUsed ??= new Map());
1498
+ for (const prop of action.search.propsUsed) {
1499
+ if (actionsByPropsUsed.has(prop)) {
1500
+ actionsByPropsUsed.get(prop).push(action);
963
1501
  } 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);
1502
+ actionsByPropsUsed.set(prop, [action]);
983
1503
  }
984
- set.add(entry);
985
1504
  }
986
- return reusable;
1505
+ return action;
987
1506
  }
988
1507
  }