@kubex/zinc 1.1.185 → 2.0.1

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 (358) hide show
  1. package/.browserslistrc +2 -0
  2. package/.github/workflows/js_build_and_deploy.yaml +54 -1
  3. package/.github/workflows/test.yaml +27 -0
  4. package/dist/chunks/zn.UWJLIXDT.js +1 -0
  5. package/dist/custom-elements.json +10319 -2666
  6. package/dist/vscode.html-custom-data.json +1012 -57
  7. package/dist/web-types.json +2097 -145
  8. package/dist/zn.d.ts +2294 -416
  9. package/dist/zn.min.css +1 -1
  10. package/dist/zn.min.js +2310 -1637
  11. package/docs/_includes/default.njk +4 -1
  12. package/docs/assets/scripts/docs.js +22 -6
  13. package/docs/assets/styles/docs.css +9 -2
  14. package/docs/data/report-table-day-compare.json +380 -0
  15. package/docs/data/report-table.json +314 -0
  16. package/docs/pages/components/avatar-group.md +57 -0
  17. package/docs/pages/components/badge.md +67 -0
  18. package/docs/pages/components/button.md +41 -1
  19. package/docs/pages/components/chat-message.md +16 -0
  20. package/docs/pages/components/checkbox.md +1 -1
  21. package/docs/pages/components/chip.md +25 -13
  22. package/docs/pages/components/color-picker.md +37 -0
  23. package/docs/pages/components/copy-button.md +2 -2
  24. package/docs/pages/components/data-table-filter.md +33 -50
  25. package/docs/pages/components/data-table-search.md +4 -4
  26. package/docs/pages/components/data-table.md +27 -3
  27. package/docs/pages/components/date-range.md +116 -0
  28. package/docs/pages/components/dialog.md +29 -37
  29. package/docs/pages/components/diff-row.md +28 -0
  30. package/docs/pages/components/diff.md +46 -0
  31. package/docs/pages/components/editor.md +55 -0
  32. package/docs/pages/components/icon.md +46 -1
  33. package/docs/pages/components/input.md +30 -11
  34. package/docs/pages/components/item.md +35 -0
  35. package/docs/pages/components/key-value.md +25 -0
  36. package/docs/pages/components/menu.md +74 -7
  37. package/docs/pages/components/nav-item.md +15 -0
  38. package/docs/pages/components/nav-list.md +132 -0
  39. package/docs/pages/components/navbar.md +100 -1
  40. package/docs/pages/components/page.md +242 -74
  41. package/docs/pages/components/priority-list.md +43 -5
  42. package/docs/pages/components/query-builder.md +23 -0
  43. package/docs/pages/components/radio-group.md +33 -1
  44. package/docs/pages/components/radio.md +1 -1
  45. package/docs/pages/components/report-table.md +202 -0
  46. package/docs/pages/components/stat.md +30 -0
  47. package/docs/pages/components/tile.md +11 -0
  48. package/docs/pages/components/tooltip.md +1 -1
  49. package/docs/pages/components/translation-grid.md +86 -0
  50. package/docs/pages/components/vertical-stepper.md +108 -64
  51. package/package.json +2 -1
  52. package/scss/_global-spacing.scss +2 -2
  53. package/scss/_root.scss +11 -171
  54. package/scss/boot.scss +14 -0
  55. package/scss/mixins/_helper-mixins.scss +24 -2
  56. package/scss/mixins/_motion-mixins.scss +126 -0
  57. package/scss/mixins/_spacing-mixins.scss +4 -3
  58. package/scss/mixins/_tab-indicator-mixins.scss +12 -0
  59. package/scss/mixins/_typography-mixins.scss +1 -1
  60. package/scss/mixins/index.scss +14 -0
  61. package/scss/shared/_base.scss +32 -1
  62. package/scss/shared/_button-mixin.scss +7 -5
  63. package/scss/shared/_table.scss +35 -4
  64. package/scss/shared/nav.scss +4 -0
  65. package/scss/shared/shimmer.scss +1 -1
  66. package/scss/table-controls.scss +206 -0
  67. package/scss/themes/_dark.scss +1 -3
  68. package/scss/themes/_index.scss +1 -1
  69. package/scss/themes/_light.scss +46 -6
  70. package/scss/themes/_modes.scss +11 -11
  71. package/scss/themes/_openbyte.scss +944 -0
  72. package/scss/themes/_typography.scss +4 -2
  73. package/scss/variables/index.scss +0 -9
  74. package/src/components/alert/alert.component.ts +14 -4
  75. package/src/components/alert/alert.scss +55 -69
  76. package/src/components/alert/alert.test.ts +8 -0
  77. package/src/components/animated-button/animated-button.component.ts +3 -2
  78. package/src/components/animated-button/animated-button.scss +6 -23
  79. package/src/components/animated-button/animated-button.test.ts +4 -3
  80. package/src/components/avatar-group/avatar-group.component.ts +84 -0
  81. package/src/components/avatar-group/avatar-group.scss +54 -0
  82. package/src/components/avatar-group/avatar-group.test.ts +59 -0
  83. package/src/components/avatar-group/index.ts +12 -0
  84. package/src/components/badge/badge.component.ts +49 -0
  85. package/src/components/badge/badge.scss +80 -0
  86. package/src/components/badge/badge.test.ts +34 -0
  87. package/src/components/badge/index.ts +12 -0
  88. package/src/components/bulk-actions/bulk-actions.component.ts +14 -2
  89. package/src/components/button/button.component.ts +228 -8
  90. package/src/components/button/button.scss +115 -32
  91. package/src/components/button/button.test.ts +122 -0
  92. package/src/components/button-menu/button-menu.component.ts +32 -11
  93. package/src/components/button-menu/button-menu.scss +5 -1
  94. package/src/components/channel-tile/channel-tile.component.ts +34 -27
  95. package/src/components/channel-tile/channel-tile.scss +76 -43
  96. package/src/components/chart/builders.test.ts +46 -0
  97. package/src/components/chart/builders.ts +27 -3
  98. package/src/components/chart/chart-live.test.ts +4 -6
  99. package/src/components/chart/chart.component.ts +125 -5
  100. package/src/components/chart/chart.scss +121 -0
  101. package/src/components/chart/chart.test.ts +81 -0
  102. package/src/components/chat-message/chat-message.component.ts +21 -1
  103. package/src/components/chat-message/chat-message.scss +13 -2
  104. package/src/components/chat-message/chat-message.test.ts +9 -0
  105. package/src/components/checkbox/checkbox.component.ts +61 -27
  106. package/src/components/checkbox/checkbox.scss +58 -25
  107. package/src/components/checkbox-group/checkbox-group.component.ts +7 -1
  108. package/src/components/chip/chip.component.ts +11 -1
  109. package/src/components/chip/chip.scss +91 -26
  110. package/src/components/chip/chip.test.ts +17 -0
  111. package/src/components/collapsible/collapsible.component.ts +17 -3
  112. package/src/components/collapsible/collapsible.scss +19 -10
  113. package/src/components/color-picker/color-picker.component.ts +277 -0
  114. package/src/components/color-picker/color-picker.scss +141 -0
  115. package/src/components/color-picker/color-picker.test.ts +57 -0
  116. package/src/components/color-picker/index.ts +12 -0
  117. package/src/components/cols/cols.component.ts +10 -0
  118. package/src/components/cols/cols.test.ts +17 -0
  119. package/src/components/confirm/confirm.component.ts +20 -6
  120. package/src/components/confirm/confirm.test.ts +52 -0
  121. package/src/components/content-block/content-block.component.ts +38 -9
  122. package/src/components/content-block/content-block.scss +11 -5
  123. package/src/components/content-block/content-block.test.ts +26 -0
  124. package/src/components/copy-button/copy-button.component.ts +2 -2
  125. package/src/components/copy-button/copy-button.scss +17 -4
  126. package/src/components/data-select/data-select.component.ts +3 -1
  127. package/src/components/data-table/data-table.component.ts +362 -140
  128. package/src/components/data-table/data-table.scss +136 -60
  129. package/src/components/data-table/data-table.test.ts +356 -4
  130. package/src/components/data-table-filter/data-table-filter.component.ts +426 -406
  131. package/src/components/data-table-filter/data-table-filter.scss +176 -148
  132. package/src/components/data-table-filter/data-table-filter.test.ts +271 -273
  133. package/src/components/data-table-search/data-table-search.component.ts +4 -2
  134. package/src/components/data-table-search/data-table-search.scss +8 -1
  135. package/src/components/data-table-search/data-table-search.test.ts +29 -0
  136. package/src/components/date-range/date-range-math.test.ts +78 -0
  137. package/src/components/date-range/date-range-math.ts +105 -0
  138. package/src/components/date-range/date-range.component.ts +375 -0
  139. package/src/components/date-range/date-range.scss +272 -0
  140. package/src/components/date-range/date-range.test.ts +247 -0
  141. package/src/components/date-range/index.ts +12 -0
  142. package/src/components/datepicker/datepicker.component.ts +44 -7
  143. package/src/components/datepicker/datepicker.scss +9 -0
  144. package/src/components/datepicker/datepicker.test.ts +2 -2
  145. package/src/components/dialog/dialog.component.ts +38 -19
  146. package/src/components/dialog/dialog.scss +65 -36
  147. package/src/components/dialog/dialog.test.ts +14 -0
  148. package/src/components/diff/diff.component.ts +55 -0
  149. package/src/components/diff/diff.scss +46 -0
  150. package/src/components/diff/diff.test.ts +15 -0
  151. package/src/components/diff/index.ts +12 -0
  152. package/src/components/diff-row/diff-row.component.ts +75 -0
  153. package/src/components/diff-row/diff-row.scss +70 -0
  154. package/src/components/diff-row/diff-row.test.ts +37 -0
  155. package/src/components/diff-row/index.ts +12 -0
  156. package/src/components/dropdown/dropdown.component.ts +12 -10
  157. package/src/components/dropdown/dropdown.scss +2 -0
  158. package/src/components/editor/editor.component.ts +159 -11
  159. package/src/components/editor/editor.scss +250 -1
  160. package/src/components/editor/editor.test.ts +31 -3
  161. package/src/components/editor/modules/ai/panel/ai-panel.scss +0 -1
  162. package/src/components/editor/modules/attachment/attachment.ts +53 -31
  163. package/src/components/editor/modules/context-menu/context-menu.ts +17 -0
  164. package/src/components/editor/modules/dialog/dialog.component.ts +23 -1
  165. package/src/components/editor/modules/dialog/dialog.test.ts +28 -0
  166. package/src/components/editor/modules/emoji/emoji.ts +4 -5
  167. package/src/components/editor/modules/merge-field/merge-field.ts +83 -0
  168. package/src/components/editor/modules/toolbar/toolbar.component.ts +121 -68
  169. package/src/components/editor/modules/toolbar/toolbar.scss +38 -0
  170. package/src/components/editor/modules/toolbar/toolbar.ts +25 -1
  171. package/src/components/empty-state/empty-state.component.ts +6 -1
  172. package/src/components/empty-state/empty-state.scss +21 -4
  173. package/src/components/expanding-action/expanding-action.component.ts +22 -34
  174. package/src/components/expanding-action/expanding-action.scss +3 -45
  175. package/src/components/expanding-action/expanding-action.test.ts +2 -1
  176. package/src/components/file/file.component.ts +30 -2
  177. package/src/components/file/file.scss +6 -0
  178. package/src/components/flow-builder/flow-builder.component.ts +36 -3
  179. package/src/components/flow-builder/flow-builder.scss +0 -6
  180. package/src/components/flow-builder/modules/flow-canvas/flow-canvas.component.ts +8 -0
  181. package/src/components/flow-builder/modules/flow-canvas/flow-canvas.scss +10 -5
  182. package/src/components/flow-builder/modules/flow-node/flow-node.component.ts +17 -0
  183. package/src/components/flow-builder/modules/flow-node/flow-node.scss +10 -0
  184. package/src/components/form-actions/form-actions.component.ts +48 -2
  185. package/src/components/form-actions/form-actions.scss +1 -1
  186. package/src/components/form-group/form-group.component.ts +10 -3
  187. package/src/components/form-group/form-group.scss +37 -4
  188. package/src/components/form-group/form-group.test.ts +10 -0
  189. package/src/components/header/header.component.ts +11 -4
  190. package/src/components/header/header.scss +47 -5
  191. package/src/components/hover-container/hover-container.component.ts +4 -5
  192. package/src/components/icon/icon.component.ts +95 -9
  193. package/src/components/icon/icon.scss +70 -6
  194. package/src/components/icon/icon.test.ts +55 -0
  195. package/src/components/icon-picker/icon-picker.component.ts +35 -1
  196. package/src/components/inline-edit/inline-edit.component.ts +38 -8
  197. package/src/components/inline-edit/inline-edit.scss +128 -19
  198. package/src/components/input/input.component.ts +173 -297
  199. package/src/components/input/input.scss +65 -43
  200. package/src/components/input/input.test.ts +86 -4
  201. package/src/components/item/item.component.ts +12 -0
  202. package/src/components/item/item.scss +102 -3
  203. package/src/components/item/item.test.ts +23 -0
  204. package/src/components/key/key.component.ts +2 -2
  205. package/src/components/key/key.scss +11 -0
  206. package/src/components/key-value/index.ts +12 -0
  207. package/src/components/key-value/key-value.component.ts +160 -0
  208. package/src/components/key-value/key-value.scss +20 -0
  209. package/src/components/key-value/key-value.test.ts +22 -0
  210. package/src/components/markdown-editor/markdown-editor.component.ts +8 -1
  211. package/src/components/menu/menu.component.ts +21 -2
  212. package/src/components/menu/menu.scss +43 -23
  213. package/src/components/menu/menu.test.ts +33 -0
  214. package/src/components/menu-item/menu-item.component.ts +13 -2
  215. package/src/components/menu-item/menu-item.scss +56 -23
  216. package/src/components/menu-item/menu-item.test.ts +31 -0
  217. package/src/components/menu-item/submenu-controller.ts +25 -15
  218. package/src/components/menu-label/index.ts +12 -0
  219. package/src/components/menu-label/menu-label.component.ts +30 -0
  220. package/src/components/menu-label/menu-label.scss +18 -0
  221. package/src/components/nav-item/index.ts +12 -0
  222. package/src/components/nav-item/nav-item.component.ts +83 -0
  223. package/src/components/nav-item/nav-item.scss +128 -0
  224. package/src/components/nav-list/index.ts +12 -0
  225. package/src/components/nav-list/nav-list.component.ts +313 -0
  226. package/src/components/nav-list/nav-list.scss +87 -0
  227. package/src/components/nav-list/nav-list.test.ts +250 -0
  228. package/src/components/navbar/navbar.component.ts +235 -171
  229. package/src/components/navbar/navbar.scss +333 -169
  230. package/src/components/navbar/navbar.test.ts +63 -143
  231. package/src/components/page/page.component.ts +114 -29
  232. package/src/components/page/page.scss +223 -95
  233. package/src/components/page/page.test.ts +214 -3
  234. package/src/components/page-builder/page-builder.component.ts +51 -5
  235. package/src/components/page-builder/page-builder.scss +6 -1
  236. package/src/components/page-nav/page-nav.component.ts +25 -3
  237. package/src/components/page-nav/page-nav.scss +10 -2
  238. package/src/components/pagination/pagination.scss +1 -1
  239. package/src/components/panel/panel.component.ts +14 -13
  240. package/src/components/panel/panel.scss +14 -10
  241. package/src/components/panel/panel.test.ts +42 -0
  242. package/src/components/popup/popup.component.ts +20 -13
  243. package/src/components/popup/popup.test.ts +21 -1
  244. package/src/components/priority-list/priority-list.component.ts +157 -172
  245. package/src/components/priority-list/priority-list.scss +113 -17
  246. package/src/components/priority-list/priority-list.test.ts +301 -3
  247. package/src/components/progress-bar/progress-bar.component.ts +1 -1
  248. package/src/components/progress-bar/progress-bar.scss +16 -4
  249. package/src/components/progress-tile/progress-tile.scss +1 -1
  250. package/src/components/query-builder/query-builder.component.ts +85 -10
  251. package/src/components/query-builder/query-builder.scss +7 -1
  252. package/src/components/query-builder/query-builder.test.ts +40 -0
  253. package/src/components/radio/radio.component.ts +57 -18
  254. package/src/components/radio/radio.scss +101 -26
  255. package/src/components/radio-group/radio-group.component.ts +45 -7
  256. package/src/components/radio-group/radio-group.scss +17 -0
  257. package/src/components/radio-group/radio-group.test.ts +63 -2
  258. package/src/components/rating/rating.component.ts +23 -0
  259. package/src/components/rating/rating.scss +1 -1
  260. package/src/components/remarkd-editor/remarkd-editor.component.ts +60 -5
  261. package/src/components/remarkd-editor/remarkd-editor.scss +12 -4
  262. package/src/components/report-table/index.ts +12 -0
  263. package/src/components/report-table/report-format.test.ts +86 -0
  264. package/src/components/report-table/report-format.ts +72 -0
  265. package/src/components/report-table/report-math.test.ts +74 -0
  266. package/src/components/report-table/report-math.ts +252 -0
  267. package/src/components/report-table/report-table.component.ts +681 -0
  268. package/src/components/report-table/report-table.scss +328 -0
  269. package/src/components/report-table/report-table.test.ts +702 -0
  270. package/src/components/reveal/reveal.component.ts +15 -1
  271. package/src/components/reveal/reveal.scss +5 -0
  272. package/src/components/schedule-builder/schedule-builder.component.ts +23 -8
  273. package/src/components/scroll-container/scroll-container.component.ts +19 -1
  274. package/src/components/scroll-container/scroll-container.scss +6 -1
  275. package/src/components/select/select.component.ts +52 -36
  276. package/src/components/select/select.scss +42 -25
  277. package/src/components/select/select.test.ts +14 -2
  278. package/src/components/settings-container/settings-container.scss +18 -5
  279. package/src/components/sidebar/sidebar.component.ts +8 -1
  280. package/src/components/sidebar/sidebar.scss +6 -1
  281. package/src/components/skeleton/skeleton.component.ts +3 -3
  282. package/src/components/skeleton/skeleton.scss +6 -6
  283. package/src/components/skeleton/skeleton.test.ts +9 -0
  284. package/src/components/slash-menu/slash-menu.scss +1 -1
  285. package/src/components/slideout/slideout.component.ts +20 -3
  286. package/src/components/slideout/slideout.scss +13 -6
  287. package/src/components/slideout/slideout.test.ts +19 -0
  288. package/src/components/split-button/split-button.component.ts +1 -1
  289. package/src/components/split-pane/split-pane.component.ts +71 -8
  290. package/src/components/split-pane/split-pane.scss +145 -41
  291. package/src/components/split-pane/split-pane.test.ts +23 -3
  292. package/src/components/stat/stat.component.ts +209 -14
  293. package/src/components/stat/stat.scss +305 -5
  294. package/src/components/stat/stat.test.ts +143 -2
  295. package/src/components/status-indicator/status-indicator.scss +22 -21
  296. package/src/components/stepper/stepper.scss +3 -3
  297. package/src/components/tab/tab.component.ts +1 -0
  298. package/src/components/table/table.component.ts +12 -1
  299. package/src/components/table/table.scss +21 -13
  300. package/src/components/table/table.test.ts +17 -0
  301. package/src/components/tabs/tabs.component.ts +150 -37
  302. package/src/components/tabs/tabs.scss +48 -7
  303. package/src/components/tabs/tabs.test.ts +169 -0
  304. package/src/components/textarea/textarea.component.ts +25 -3
  305. package/src/components/textarea/textarea.scss +10 -0
  306. package/src/components/textarea/textarea.test.ts +8 -4
  307. package/src/components/theme-editor/theme-editor.scss +0 -6
  308. package/src/components/tile/tile.component.ts +32 -2
  309. package/src/components/tile/tile.scss +40 -7
  310. package/src/components/tile/tile.test.ts +10 -0
  311. package/src/components/toggle/toggle.component.ts +23 -7
  312. package/src/components/toggle/toggle.scss +36 -29
  313. package/src/components/toggle/toggle.test.ts +17 -0
  314. package/src/components/tooltip/tooltip.component.ts +3 -0
  315. package/src/components/tooltip/tooltip.scss +11 -6
  316. package/src/components/tooltip/tooltip.test.ts +12 -0
  317. package/src/components/translation-field/index.ts +12 -0
  318. package/src/components/translation-field/translation-field.component.ts +55 -0
  319. package/src/components/translation-field/translation-field.scss +101 -0
  320. package/src/components/translation-fields/index.ts +12 -0
  321. package/src/components/translation-fields/translation-fields.component.ts +80 -0
  322. package/src/components/translation-fields/translation-fields.scss +60 -0
  323. package/src/components/translation-grid/index.ts +12 -0
  324. package/src/components/translation-grid/translation-grid.component.ts +157 -0
  325. package/src/components/translation-grid/translation-grid.scss +138 -0
  326. package/src/components/translation-grid/translation-grid.test.ts +28 -0
  327. package/src/components/vertical-stepper/vertical-stepper.component.ts +35 -9
  328. package/src/components/vertical-stepper/vertical-stepper.scss +110 -39
  329. package/src/components/vertical-stepper/vertical-stepper.test.ts +35 -0
  330. package/src/components/well/well.scss +2 -2
  331. package/src/events/events.ts +2 -0
  332. package/src/events/zn-chart-click.ts +13 -0
  333. package/src/events/zn-report-change.ts +7 -0
  334. package/src/form-control.scss +36 -3
  335. package/src/internal/anchored-popover.ts +175 -0
  336. package/src/internal/animate.ts +19 -5
  337. package/src/internal/cosmic-border.ts +15 -0
  338. package/src/internal/form.ts +1 -1
  339. package/src/internal/hover-intent.ts +15 -0
  340. package/src/internal/input-modality.ts +15 -0
  341. package/src/internal/pointer-intent.ts +37 -0
  342. package/src/internal/reorder.ts +632 -0
  343. package/src/internal/scroll.ts +51 -3
  344. package/src/internal/selection-card.ts +1 -1
  345. package/src/internal/tab-indicator.ts +92 -0
  346. package/src/internal/table-sort.ts +25 -0
  347. package/src/internal/table-tabs.ts +108 -0
  348. package/src/internal/test-drag.ts +35 -0
  349. package/src/internal/theme-openbyte.test.ts +126 -0
  350. package/src/internal/theme.ts +4 -4
  351. package/src/internal/zinc-element.ts +2 -2
  352. package/src/selection-card.scss +25 -0
  353. package/src/types/web-test-runner-commands.d.ts +1 -0
  354. package/src/utilities/color.test.ts +43 -0
  355. package/src/utilities/color.ts +189 -0
  356. package/src/zinc.ts +14 -0
  357. package/.github/workflows/eleventy_build.yml +0 -42
  358. package/scss/themes/_aura.scss +0 -114
@@ -49,8 +49,7 @@ describe('<zn-editor>', () => {
49
49
  document.dispatchEvent(new Event('zn-editor-update'));
50
50
  await el.updateComplete;
51
51
 
52
- // Quill's getSemanticHTML encodes spaces as &nbsp;
53
- expect(sessionStorage.getItem(KEY)).to.contain('Typed&nbsp;reply');
52
+ expect(sessionStorage.getItem(KEY)).to.contain('Typed reply');
54
53
  });
55
54
 
56
55
  it('should clear cached content on form submit', async () => {
@@ -62,10 +61,39 @@ describe('<zn-editor>', () => {
62
61
 
63
62
  document.dispatchEvent(new Event('zn-editor-update'));
64
63
  await el.updateComplete;
65
- expect(sessionStorage.getItem(KEY)).to.contain('Typed&nbsp;reply');
64
+ expect(sessionStorage.getItem(KEY)).to.contain('Typed reply');
66
65
 
67
66
  form.requestSubmit();
68
67
  expect(sessionStorage.getItem(KEY)).to.equal(null);
69
68
  });
70
69
  });
70
+
71
+ describe('merge fields', () => {
72
+ it('should show tokens as labelled chips and keep the raw token in the value', async () => {
73
+ const el = await fixture<ZnEditor>(html`
74
+ <zn-editor merge-fields .value=${'<p>Hi {{ recipient.firstName }}</p>'}>
75
+ <zn-editor-quick-action slot="context-items" icon="braces@lu" label="First name"
76
+ content="{{recipient.firstName}}"></zn-editor-quick-action>
77
+ </zn-editor>`);
78
+
79
+ const chip = el.shadowRoot!.querySelector<HTMLElement>('.ql-merge-field')!;
80
+ expect(chip.textContent).to.contain('First name');
81
+
82
+ (el as unknown as { quillElement: { insertText: (i: number, t: string, s: string) => void } })
83
+ .quillElement.insertText(0, 'Oh ', 'user');
84
+ expect(el.value).to.equal('<p>Oh Hi {{recipient.firstName}}</p>');
85
+ });
86
+ });
87
+
88
+ describe('document mode', () => {
89
+ it('should render designed HTML in place and keep it as written', async () => {
90
+ const markup = '<table width="600"><tbody><tr><td style="color:red">Hi</td></tr></tbody></table>';
91
+ const el = await fixture<ZnEditor>(html`<zn-editor document-mode .value=${markup}></zn-editor>`);
92
+
93
+ const frame = el.shadowRoot!.querySelector<HTMLIFrameElement>('.editor__document')!;
94
+ await new Promise(resolve => frame.addEventListener('load', resolve, {once: true}));
95
+ expect(frame.contentDocument!.querySelector('td')!.getAttribute('style')).to.equal('color:red');
96
+ expect(el.value).to.equal(markup);
97
+ });
98
+ });
71
99
  });
@@ -20,7 +20,6 @@
20
20
  position: relative;
21
21
  min-width: 240px;
22
22
  width: 240px;
23
- transition: width 0.8s ease, height 0.8s ease;
24
23
 
25
24
  &::part(base) {
26
25
  margin-bottom: 0;
@@ -3,15 +3,18 @@ import type Toolbar from "../toolbar/toolbar";
3
3
 
4
4
  interface AttachmentOptions {
5
5
  upload: (file: File) => Promise<{ path: any; url: any; filename: any }>;
6
- onFileUploaded?: (node: HTMLElement, {url}: { url: string }) => void;
6
+ // node is null when the chip was removed (or reset) during the upload
7
+ onFileUploaded?: (node: HTMLElement | null, {url}: { url: string }) => void;
8
+ onFileUploadFailed?: (error: Error) => void;
7
9
  attachmentInput?: HTMLInputElement;
8
10
  }
9
11
 
10
- const generateId = () => {
11
- const name = 'attachment';
12
- const id = new Date().getTime();
13
- return `${name}_${id}`;
14
- };
12
+ // Mirrors the server's upload body-size limit.
13
+ const MAX_ATTACHMENT_BYTES = 30 * 1024 * 1024;
14
+
15
+ let nextAttachmentId = 0;
16
+
17
+ const generateId = () => `attachment_${++nextAttachmentId}`;
15
18
 
16
19
  export default class Attachment {
17
20
  private _quill: Quill;
@@ -46,38 +49,33 @@ export default class Attachment {
46
49
  }
47
50
 
48
51
  private _fileChanged() {
49
- if (!this._fileHolder?.files || this._fileHolder.files.length < 0) {
50
- return;
51
- }
52
+ const file = this._fileHolder?.files?.[0];
53
+ if (!file) return;
52
54
 
53
- const file = this._fileHolder.files[0];
54
55
  this.addAttachment(file);
55
56
  }
56
57
 
57
58
  public addAttachment(file: File, dataUrl?: string) {
58
59
  const attachmentId = generateId();
59
- const insertWithDataUrl = (base64: string) => {
60
- this._insertAttachment({dataUrl: base64, file, id: attachmentId});
61
- };
60
+ this._insertAttachment({href: dataUrl || URL.createObjectURL(file), file, id: attachmentId});
62
61
 
63
- if (dataUrl) {
64
- insertWithDataUrl(dataUrl);
65
- } else {
66
- const fileReader = new FileReader();
67
- fileReader.addEventListener('load', () => {
68
- const base64Content = fileReader.result as string;
69
- insertWithDataUrl(base64Content);
70
- }, false);
71
- fileReader.readAsDataURL(file);
62
+ if (file.size > MAX_ATTACHMENT_BYTES) {
63
+ this._markAttachmentFailed(attachmentId, 'File too large (max 30 MB)');
64
+ return;
72
65
  }
73
66
 
74
67
  if (typeof this._options.upload === 'function') {
68
+ // the rejection handler covers only the upload, so each upload settles exactly once
75
69
  this._options.upload(file).then(async ({path, url}: { path: string; url: string }) => {
76
70
  await this._uploadAttachment(file, url);
77
- this._updateAttachment(attachmentId, url, path);
78
- }).catch((err: { message: string }) => {
71
+ return {path, url};
72
+ }).then(({path, url}) => this._updateAttachment(attachmentId, url, path), (err: Error) => {
79
73
  console.warn(err.message);
74
+ this._markAttachmentFailed(attachmentId, 'Upload failed');
75
+ this._options.onFileUploadFailed?.(err);
80
76
  });
77
+ } else {
78
+ this._markAttachmentFailed(attachmentId, 'Upload failed');
81
79
  }
82
80
  }
83
81
 
@@ -98,14 +96,36 @@ export default class Attachment {
98
96
  this._quill.container.appendChild(attachmentContainer);
99
97
  }
100
98
 
101
- private _insertAttachment({dataUrl, file, id}: { dataUrl: string; file: File; id: string }) {
102
- this._attachmentContainer.appendChild(this._createAttachment(dataUrl, file, id));
99
+ private _insertAttachment({href, file, id}: { href: string; file: File; id: string }) {
100
+ this._attachmentContainer.appendChild(this._createAttachment(href, file, id));
103
101
  }
104
102
 
105
- private _updateAttachment(id: string, url: string, filename: string) {
106
- const element = this._quill.container.querySelector(`#${id}`);
103
+ private _markAttachmentFailed(id: string, message: string) {
104
+ const element = this._quill.container.querySelector<HTMLElement>(`#${id}`);
107
105
  if (!element) return;
108
106
 
107
+ const attachmentName = element.querySelector('.attachment-name');
108
+ if (attachmentName) {
109
+ attachmentName.textContent = message;
110
+ }
111
+ element.style.borderColor = '#d93025';
112
+ }
113
+
114
+ private _revokeObjectUrl(element: Element) {
115
+ const href = element.getAttribute('href');
116
+ if (href?.startsWith('blob:')) {
117
+ URL.revokeObjectURL(href);
118
+ }
119
+ }
120
+
121
+ private _updateAttachment(id: string, url: string, filename: string) {
122
+ const element = this._quill.container.querySelector<HTMLElement>(`#${id}`);
123
+ if (!element) {
124
+ this._options.onFileUploaded?.(null, {url});
125
+ return;
126
+ }
127
+
128
+ this._revokeObjectUrl(element);
109
129
  element.setAttribute('href', url);
110
130
  const attachmentName = element.querySelector('.attachment-name');
111
131
  if (attachmentName) {
@@ -117,7 +137,7 @@ export default class Attachment {
117
137
  }
118
138
 
119
139
  if (this._options.onFileUploaded) {
120
- this._options.onFileUploaded(element as HTMLElement, {url});
140
+ this._options.onFileUploaded(element, {url});
121
141
  }
122
142
 
123
143
  // add the url to the hidden input
@@ -131,9 +151,9 @@ export default class Attachment {
131
151
  }
132
152
  }
133
153
 
134
- private _createAttachment(dataUrl: string, file: File, id: string) {
154
+ private _createAttachment(href: string, file: File, id: string) {
135
155
  const attachment = document.createElement('a');
136
- attachment.setAttribute('href', dataUrl);
156
+ attachment.setAttribute('href', href);
137
157
  attachment.setAttribute('id', id);
138
158
  attachment.setAttribute('download', file.name);
139
159
  attachment.style.padding = '10px';
@@ -165,6 +185,7 @@ export default class Attachment {
165
185
 
166
186
  /** Clear every staged attachment preview and reset the backing form input. */
167
187
  public reset() {
188
+ Array.from(this._attachmentContainer.children).forEach(chip => this._revokeObjectUrl(chip));
168
189
  this._attachmentContainer.replaceChildren();
169
190
  const attachments = this._options.attachmentInput;
170
191
  if (attachments) {
@@ -175,6 +196,7 @@ export default class Attachment {
175
196
  private _removeAttachment(id: string) {
176
197
  const attachment = this._quill.container.querySelector(`#${id}`);
177
198
  if (attachment) {
199
+ this._revokeObjectUrl(attachment);
178
200
  attachment.remove();
179
201
  }
180
202
 
@@ -29,6 +29,10 @@ class ContextMenu {
29
29
  private _dismissedIndex = -1;
30
30
  private _keydownHandler = (e: KeyboardEvent) => this.onKeydown(e);
31
31
  private _docClickHandler = (e: MouseEvent) => this.onDocumentClick(e);
32
+ private _pointerHeld = false;
33
+ private _pointerReleaseHandler = () => {
34
+ this._pointerHeld = false;
35
+ };
32
36
  private _featureConfig: EditorFeatureConfig = {};
33
37
  private readonly _caretAnchor: VirtualElement = {
34
38
  getBoundingClientRect: () => this.caretRect()
@@ -57,6 +61,10 @@ class ContextMenu {
57
61
  this._quill.on(Quill.events.EDITOR_CHANGE, () => this.updateFromEditor());
58
62
  this._quill.root.addEventListener('keydown', this._keydownHandler);
59
63
  this._menu.addEventListener(SLASH_ITEM_SELECT, (e: Event) => this.onItemSelect(e as CustomEvent<{ item: ContextMenuItem }>));
64
+ this._menu.addEventListener('pointerdown', () => {
65
+ this._pointerHeld = true;
66
+ this._quill.container.ownerDocument.addEventListener('pointerup', this._pointerReleaseHandler, {once: true, capture: true});
67
+ });
60
68
  this._quill.focus();
61
69
  }
62
70
 
@@ -202,6 +210,15 @@ class ContextMenu {
202
210
  return;
203
211
  }
204
212
 
213
+ if (key === 'dialog' && this._pointerHeld) {
214
+ // Items select on mousedown; a modal opened mid-press is light-dismissed by the pointerup landing on its backdrop
215
+ this._quill.container.ownerDocument.addEventListener('pointerup', () => {
216
+ setTimeout(() => this._toolbarModule.callFormat(key, value));
217
+ }, {once: true, capture: true});
218
+ this.hide();
219
+ return;
220
+ }
221
+
205
222
  this._toolbarModule.callFormat(key, value);
206
223
  this._quill.focus();
207
224
  this.hide();
@@ -54,13 +54,35 @@ export default class DialogComponent extends ZincElement {
54
54
  private handleContentCloserClick = (e: Event) => {
55
55
  for (const node of e.composedPath()) {
56
56
  if (node === this) return;
57
- if (node instanceof HTMLElement && node.hasAttribute('dialog-closer')) {
57
+ if (!(node instanceof HTMLElement)) continue;
58
+ if (node.hasAttribute('dialog-closer')) {
58
59
  this.dialogEl.close();
59
60
  return;
60
61
  }
62
+ const mode = node.getAttribute('editor-mode');
63
+ if (mode === 'insert' || mode === 'replace') {
64
+ this.insertContent(node, mode);
65
+ return;
66
+ }
61
67
  }
62
68
  };
63
69
 
70
+ /** Inserts the element named by the control's `editor-content-id`: a `<template>` as HTML, anything else as text.
71
+ * The control's form is submitted first, as inserting closes the dialog and detaches it. */
72
+ private insertContent(control: HTMLElement, mode: 'insert' | 'replace') {
73
+ const id = control.getAttribute('editor-content-id');
74
+ const source = id ? (control.getRootNode() as Document | ShadowRoot).getElementById(id) : null;
75
+ if (!source) return;
76
+
77
+ const content = source instanceof HTMLTemplateElement
78
+ ? {html: source.innerHTML.trim()}
79
+ : {text: ((source as HTMLInputElement).value ?? source.textContent ?? '').trim()};
80
+ if (!content.html && !content.text) return;
81
+
82
+ control.closest('form')?.requestSubmit();
83
+ this.emit('zn-editor-insert', {detail: {mode, ...content}});
84
+ }
85
+
64
86
  setContent(content: string) {
65
87
  this.innerHTML = content;
66
88
  }
@@ -0,0 +1,28 @@
1
+ import '../../../../../dist/zn.min.js';
2
+ import {expect, fixture, html, oneEvent} from '@open-wc/testing';
3
+ import type DialogComponent from './dialog.component';
4
+ import type {ZnEditorInsertEvent} from '../events/zn-editor-insert';
5
+
6
+ describe('<zn-editor-dialog> content inserts', () => {
7
+ const dialog = () => fixture<DialogComponent>(html`
8
+ <zn-editor-dialog>
9
+ <template id="reply"><p>Hi <b>there</b></p></template>
10
+ <span id="plain">Plain reply</span>
11
+ <button id="html" editor-mode="replace" editor-content-id="reply">Replace</button>
12
+ <button id="text" editor-mode="insert" editor-content-id="plain">Insert</button>
13
+ </zn-editor-dialog>`);
14
+
15
+ it('inserts a template as HTML', async () => {
16
+ const el = await dialog();
17
+ setTimeout(() => el.querySelector<HTMLElement>('#html')!.click());
18
+ const event = await oneEvent(el, 'zn-editor-insert') as ZnEditorInsertEvent;
19
+ expect(event.detail).to.deep.equal({mode: 'replace', html: '<p>Hi <b>there</b></p>'});
20
+ });
21
+
22
+ it('inserts any other element as text', async () => {
23
+ const el = await dialog();
24
+ setTimeout(() => el.querySelector<HTMLElement>('#text')!.click());
25
+ const event = await oneEvent(el, 'zn-editor-insert') as ZnEditorInsertEvent;
26
+ expect(event.detail).to.deep.equal({mode: 'insert', text: 'Plain reply'});
27
+ });
28
+ });
@@ -20,7 +20,7 @@ class Emoji {
20
20
  const host = this.getHostEditor();
21
21
  if (host) {
22
22
  this._mo = new MutationObserver(() => this.initPicker());
23
- this._mo.observe(host, {attributes: true, attributeFilter: ['t']});
23
+ this._mo.observe(host, {attributes: true, attributeFilter: ['t', 'm']});
24
24
  }
25
25
  }
26
26
 
@@ -45,11 +45,10 @@ class Emoji {
45
45
  }
46
46
  }
47
47
 
48
+ // `m` is the rendered mode the editor mirrors from the page; older pages set `t="dark"` instead
48
49
  private getTheme(): 'light' | 'dark' {
49
- const host: any = this.getHostEditor();
50
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
51
- const t = host?.t ?? host?.getAttribute?.('t');
52
- return t === 'dark' ? 'dark' : 'light';
50
+ const host = this.getHostEditor();
51
+ return host?.getAttribute('m') === 'dark' || host?.getAttribute('t') === 'dark' ? 'dark' : 'light';
53
52
  }
54
53
 
55
54
  public initPicker() {
@@ -0,0 +1,83 @@
1
+ import Delta from "quill-delta";
2
+ import Embed from "quill/blots/embed";
3
+ import Quill from "quill";
4
+ import type {Op} from "quill-delta";
5
+
6
+ export interface MergeFieldValue {
7
+ token: string;
8
+ label: string;
9
+ }
10
+
11
+ const TOKEN = /\{\{\s*[^{}]+?\s*\}\}/g;
12
+
13
+ /** A `{{token}}` replacement shown as a labelled chip, saved back out as the raw token. */
14
+ export class MergeFieldBlot extends Embed {
15
+ static blotName = 'mergeField';
16
+ static tagName = 'SPAN';
17
+ static className = 'ql-merge-field';
18
+
19
+ static create(value: MergeFieldValue): HTMLElement {
20
+ const node = super.create() as HTMLElement;
21
+ node.dataset.token = value.token;
22
+ node.dataset.label = value.label;
23
+ node.title = value.token;
24
+ node.textContent = value.label;
25
+ return node;
26
+ }
27
+
28
+ static value(node: HTMLElement): MergeFieldValue {
29
+ return {token: node.dataset.token ?? '', label: node.dataset.label ?? ''};
30
+ }
31
+
32
+ html(): string {
33
+ return ((this.domNode as HTMLElement).dataset.token ?? '')
34
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
35
+ }
36
+ }
37
+
38
+ export const normaliseToken = (token: string) => token.replace(/\s+/g, '');
39
+
40
+ class MergeField {
41
+ constructor(quill: Quill, options: { labels?: Record<string, string> }) {
42
+ const labels = options.labels ?? {};
43
+
44
+ // The browser types a space after an embed into its caret guard and leaves the caret before it
45
+ quill.keyboard.addBinding({key: ' '}, (range: { index: number; length: number }) => {
46
+ if (range.length || !range.index) return true;
47
+ const before = quill.getContents(range.index - 1, 1).ops[0]?.insert;
48
+ if (typeof before !== 'object' || !('mergeField' in before)) return true;
49
+
50
+ quill.insertText(range.index, ' ', Quill.sources.USER);
51
+ quill.setSelection(range.index + 1, 0, Quill.sources.SILENT);
52
+ return false;
53
+ });
54
+ // Ahead of Quill's own space handlers, such as list autofill
55
+ const spaceBindings = (quill.keyboard as unknown as { bindings: Record<string, unknown[]> }).bindings[' '];
56
+ spaceBindings?.unshift(spaceBindings.pop());
57
+
58
+ // Text nodes only, so tokens inside attributes such as href stay as they are
59
+ quill.clipboard.addMatcher(Node.TEXT_NODE, (_node: Node, delta: Delta) => {
60
+ return new Delta(delta.ops.flatMap((op: Op) => {
61
+ if (typeof op.insert !== 'string' || !op.insert.includes('{{')) return [op];
62
+
63
+ const attrs = op.attributes ? {attributes: op.attributes} : {};
64
+ const ops: Op[] = [];
65
+ let last = 0;
66
+ for (const match of op.insert.matchAll(TOKEN)) {
67
+ if (match.index! > last) {
68
+ ops.push({insert: op.insert.slice(last, match.index), ...attrs});
69
+ }
70
+ const token = normaliseToken(match[0]);
71
+ ops.push({insert: {mergeField: {token, label: labels[token] ?? token.slice(2, -2)}}, ...attrs});
72
+ last = match.index! + match[0].length;
73
+ }
74
+ if (last < op.insert.length) {
75
+ ops.push({insert: op.insert.slice(last), ...attrs});
76
+ }
77
+ return ops;
78
+ }));
79
+ });
80
+ }
81
+ }
82
+
83
+ export default MergeField;