ngwr 14.7.0 → 14.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 (482) hide show
  1. package/README.md +66 -332
  2. package/alert/styles/_index.scss +10 -0
  3. package/avatar/styles/_index.scss +18 -7
  4. package/badge/styles/_index.scss +7 -0
  5. package/badge/styles/_tag.scss +7 -1
  6. package/bar-chart/styles/_index.scss +1 -0
  7. package/button/styles/_index.scss +6 -0
  8. package/calendar-heatmap/styles/_index.scss +1 -0
  9. package/cascader/styles/_rules.scss +87 -15
  10. package/checkbox/styles/_index.scss +4 -0
  11. package/command-palette/styles/_index.scss +1 -1
  12. package/date-picker/styles/_index.scss +30 -0
  13. package/density/styles/_index.scss +4 -2
  14. package/divider/styles/_index.scss +2 -0
  15. package/donut-chart/styles/_index.scss +10 -0
  16. package/dropdown/styles/_index.scss +67 -5
  17. package/editor/styles/_index.scss +441 -0
  18. package/event-calendar/styles/_index.scss +209 -35
  19. package/fesm2022/ngwr-action-sheet-testing.mjs.map +1 -1
  20. package/fesm2022/ngwr-action-sheet.mjs +3 -3
  21. package/fesm2022/ngwr-action-sheet.mjs.map +1 -1
  22. package/fesm2022/ngwr-affix.mjs +3 -3
  23. package/fesm2022/ngwr-affix.mjs.map +1 -1
  24. package/fesm2022/ngwr-alert.mjs +3 -3
  25. package/fesm2022/ngwr-alert.mjs.map +1 -1
  26. package/fesm2022/ngwr-anchor.mjs +3 -3
  27. package/fesm2022/ngwr-anchor.mjs.map +1 -1
  28. package/fesm2022/ngwr-aurora.mjs +3 -3
  29. package/fesm2022/ngwr-aurora.mjs.map +1 -1
  30. package/fesm2022/ngwr-avatar.mjs +3 -3
  31. package/fesm2022/ngwr-avatar.mjs.map +1 -1
  32. package/fesm2022/ngwr-back-top.mjs +3 -3
  33. package/fesm2022/ngwr-back-top.mjs.map +1 -1
  34. package/fesm2022/ngwr-badge.mjs +28 -13
  35. package/fesm2022/ngwr-badge.mjs.map +1 -1
  36. package/fesm2022/ngwr-bar-chart-testing.mjs.map +1 -1
  37. package/fesm2022/ngwr-bar-chart.mjs +31 -6
  38. package/fesm2022/ngwr-bar-chart.mjs.map +1 -1
  39. package/fesm2022/ngwr-blur-text-testing.mjs.map +1 -1
  40. package/fesm2022/ngwr-blur-text.mjs +3 -3
  41. package/fesm2022/ngwr-blur-text.mjs.map +1 -1
  42. package/fesm2022/ngwr-border-glow-testing.mjs.map +1 -1
  43. package/fesm2022/ngwr-border-glow.mjs +3 -3
  44. package/fesm2022/ngwr-border-glow.mjs.map +1 -1
  45. package/fesm2022/ngwr-breadcrumbs.mjs +6 -6
  46. package/fesm2022/ngwr-breadcrumbs.mjs.map +1 -1
  47. package/fesm2022/ngwr-burger.mjs +3 -3
  48. package/fesm2022/ngwr-burger.mjs.map +1 -1
  49. package/fesm2022/ngwr-button-testing.mjs.map +1 -1
  50. package/fesm2022/ngwr-button.mjs +6 -6
  51. package/fesm2022/ngwr-button.mjs.map +1 -1
  52. package/fesm2022/ngwr-calendar-heatmap-testing.mjs.map +1 -1
  53. package/fesm2022/ngwr-calendar-heatmap.mjs +61 -6
  54. package/fesm2022/ngwr-calendar-heatmap.mjs.map +1 -1
  55. package/fesm2022/ngwr-calendar-testing.mjs.map +1 -1
  56. package/fesm2022/ngwr-calendar.mjs +3 -3
  57. package/fesm2022/ngwr-calendar.mjs.map +1 -1
  58. package/fesm2022/ngwr-card.mjs +9 -9
  59. package/fesm2022/ngwr-card.mjs.map +1 -1
  60. package/fesm2022/ngwr-carousel-testing.mjs.map +1 -1
  61. package/fesm2022/ngwr-carousel.mjs +6 -6
  62. package/fesm2022/ngwr-carousel.mjs.map +1 -1
  63. package/fesm2022/ngwr-cascader-testing.mjs.map +1 -1
  64. package/fesm2022/ngwr-cascader.mjs +4 -4
  65. package/fesm2022/ngwr-cascader.mjs.map +1 -1
  66. package/fesm2022/ngwr-checkbox-testing.mjs.map +1 -1
  67. package/fesm2022/ngwr-checkbox.mjs +18 -8
  68. package/fesm2022/ngwr-checkbox.mjs.map +1 -1
  69. package/fesm2022/ngwr-circular-text-testing.mjs.map +1 -1
  70. package/fesm2022/ngwr-circular-text.mjs +3 -3
  71. package/fesm2022/ngwr-circular-text.mjs.map +1 -1
  72. package/fesm2022/ngwr-click-spark.mjs +3 -3
  73. package/fesm2022/ngwr-click-spark.mjs.map +1 -1
  74. package/fesm2022/ngwr-clipboard.mjs +3 -3
  75. package/fesm2022/ngwr-clipboard.mjs.map +1 -1
  76. package/fesm2022/ngwr-collapse-testing.mjs.map +1 -1
  77. package/fesm2022/ngwr-collapse.mjs +9 -9
  78. package/fesm2022/ngwr-collapse.mjs.map +1 -1
  79. package/fesm2022/ngwr-color-picker-testing.mjs.map +1 -1
  80. package/fesm2022/ngwr-color-picker.mjs +6 -6
  81. package/fesm2022/ngwr-color-picker.mjs.map +1 -1
  82. package/fesm2022/ngwr-command-palette-testing.mjs.map +1 -1
  83. package/fesm2022/ngwr-command-palette.mjs +3 -3
  84. package/fesm2022/ngwr-command-palette.mjs.map +1 -1
  85. package/fesm2022/ngwr-compare.mjs +3 -3
  86. package/fesm2022/ngwr-compare.mjs.map +1 -1
  87. package/fesm2022/ngwr-confetti.mjs +3 -3
  88. package/fesm2022/ngwr-confetti.mjs.map +1 -1
  89. package/fesm2022/ngwr-config.mjs.map +1 -1
  90. package/fesm2022/ngwr-context-menu-testing.mjs.map +1 -1
  91. package/fesm2022/ngwr-context-menu.mjs +12 -12
  92. package/fesm2022/ngwr-context-menu.mjs.map +1 -1
  93. package/fesm2022/ngwr-cookie.mjs +3 -3
  94. package/fesm2022/ngwr-cookie.mjs.map +1 -1
  95. package/fesm2022/ngwr-counter.mjs +6 -6
  96. package/fesm2022/ngwr-counter.mjs.map +1 -1
  97. package/fesm2022/ngwr-date-adapters-fns.mjs +3 -3
  98. package/fesm2022/ngwr-date-adapters-fns.mjs.map +1 -1
  99. package/fesm2022/ngwr-date-adapters-luxon.mjs +3 -3
  100. package/fesm2022/ngwr-date-adapters-luxon.mjs.map +1 -1
  101. package/fesm2022/ngwr-date-picker-testing.mjs.map +1 -1
  102. package/fesm2022/ngwr-date-picker.mjs +721 -62
  103. package/fesm2022/ngwr-date-picker.mjs.map +1 -1
  104. package/fesm2022/ngwr-date.mjs +3 -3
  105. package/fesm2022/ngwr-date.mjs.map +1 -1
  106. package/fesm2022/ngwr-decrypt-text-testing.mjs.map +1 -1
  107. package/fesm2022/ngwr-decrypt-text.mjs +3 -3
  108. package/fesm2022/ngwr-decrypt-text.mjs.map +1 -1
  109. package/fesm2022/ngwr-density.mjs +6 -6
  110. package/fesm2022/ngwr-density.mjs.map +1 -1
  111. package/fesm2022/ngwr-descriptions.mjs +6 -6
  112. package/fesm2022/ngwr-descriptions.mjs.map +1 -1
  113. package/fesm2022/ngwr-dialog-testing.mjs.map +1 -1
  114. package/fesm2022/ngwr-dialog.mjs +15 -15
  115. package/fesm2022/ngwr-dialog.mjs.map +1 -1
  116. package/fesm2022/ngwr-directives.mjs +12 -12
  117. package/fesm2022/ngwr-directives.mjs.map +1 -1
  118. package/fesm2022/ngwr-divider.mjs +3 -3
  119. package/fesm2022/ngwr-divider.mjs.map +1 -1
  120. package/fesm2022/ngwr-donut-chart-testing.mjs.map +1 -1
  121. package/fesm2022/ngwr-donut-chart.mjs +48 -6
  122. package/fesm2022/ngwr-donut-chart.mjs.map +1 -1
  123. package/fesm2022/ngwr-drag-drop.mjs +6 -6
  124. package/fesm2022/ngwr-drag-drop.mjs.map +1 -1
  125. package/fesm2022/ngwr-drawer-testing.mjs.map +1 -1
  126. package/fesm2022/ngwr-drawer.mjs +18 -18
  127. package/fesm2022/ngwr-drawer.mjs.map +1 -1
  128. package/fesm2022/ngwr-dropdown-testing.mjs.map +1 -1
  129. package/fesm2022/ngwr-dropdown.mjs +174 -25
  130. package/fesm2022/ngwr-dropdown.mjs.map +1 -1
  131. package/fesm2022/ngwr-editor-testing.mjs +568 -0
  132. package/fesm2022/ngwr-editor-testing.mjs.map +1 -0
  133. package/fesm2022/ngwr-editor.mjs +2601 -0
  134. package/fesm2022/ngwr-editor.mjs.map +1 -0
  135. package/fesm2022/ngwr-empty.mjs +3 -3
  136. package/fesm2022/ngwr-empty.mjs.map +1 -1
  137. package/fesm2022/ngwr-event-calendar-testing.mjs.map +1 -1
  138. package/fesm2022/ngwr-event-calendar.mjs +14 -7
  139. package/fesm2022/ngwr-event-calendar.mjs.map +1 -1
  140. package/fesm2022/ngwr-falling-text-testing.mjs.map +1 -1
  141. package/fesm2022/ngwr-falling-text.mjs +3 -3
  142. package/fesm2022/ngwr-falling-text.mjs.map +1 -1
  143. package/fesm2022/ngwr-file-upload-testing.mjs.map +1 -1
  144. package/fesm2022/ngwr-file-upload.mjs +3 -3
  145. package/fesm2022/ngwr-file-upload.mjs.map +1 -1
  146. package/fesm2022/ngwr-form-testing.mjs.map +1 -1
  147. package/fesm2022/ngwr-form.mjs +61 -10
  148. package/fesm2022/ngwr-form.mjs.map +1 -1
  149. package/fesm2022/ngwr-fuzzy-text-testing.mjs.map +1 -1
  150. package/fesm2022/ngwr-fuzzy-text.mjs +3 -3
  151. package/fesm2022/ngwr-fuzzy-text.mjs.map +1 -1
  152. package/fesm2022/ngwr-gauge-testing.mjs.map +1 -1
  153. package/fesm2022/ngwr-gauge.mjs +35 -6
  154. package/fesm2022/ngwr-gauge.mjs.map +1 -1
  155. package/fesm2022/ngwr-glitch-text-testing.mjs.map +1 -1
  156. package/fesm2022/ngwr-glitch-text.mjs +3 -3
  157. package/fesm2022/ngwr-glitch-text.mjs.map +1 -1
  158. package/fesm2022/ngwr-gradient-text-testing.mjs.map +1 -1
  159. package/fesm2022/ngwr-gradient-text.mjs +3 -3
  160. package/fesm2022/ngwr-gradient-text.mjs.map +1 -1
  161. package/fesm2022/ngwr-graph-testing.mjs.map +1 -1
  162. package/fesm2022/ngwr-graph.mjs +6 -6
  163. package/fesm2022/ngwr-graph.mjs.map +1 -1
  164. package/fesm2022/ngwr-hotkey.mjs +6 -6
  165. package/fesm2022/ngwr-hotkey.mjs.map +1 -1
  166. package/fesm2022/ngwr-i18n-ar.mjs +31 -0
  167. package/fesm2022/ngwr-i18n-ar.mjs.map +1 -1
  168. package/fesm2022/ngwr-i18n-cs.mjs +31 -0
  169. package/fesm2022/ngwr-i18n-cs.mjs.map +1 -1
  170. package/fesm2022/ngwr-i18n-de.mjs +31 -0
  171. package/fesm2022/ngwr-i18n-de.mjs.map +1 -1
  172. package/fesm2022/ngwr-i18n-en.mjs +39 -0
  173. package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
  174. package/fesm2022/ngwr-i18n-es.mjs +31 -0
  175. package/fesm2022/ngwr-i18n-es.mjs.map +1 -1
  176. package/fesm2022/ngwr-i18n-fr.mjs +31 -0
  177. package/fesm2022/ngwr-i18n-fr.mjs.map +1 -1
  178. package/fesm2022/ngwr-i18n-he.mjs +31 -0
  179. package/fesm2022/ngwr-i18n-he.mjs.map +1 -1
  180. package/fesm2022/ngwr-i18n-hi.mjs +31 -0
  181. package/fesm2022/ngwr-i18n-hi.mjs.map +1 -1
  182. package/fesm2022/ngwr-i18n-id.mjs +31 -0
  183. package/fesm2022/ngwr-i18n-id.mjs.map +1 -1
  184. package/fesm2022/ngwr-i18n-it.mjs +31 -0
  185. package/fesm2022/ngwr-i18n-it.mjs.map +1 -1
  186. package/fesm2022/ngwr-i18n-ja.mjs +31 -0
  187. package/fesm2022/ngwr-i18n-ja.mjs.map +1 -1
  188. package/fesm2022/ngwr-i18n-ko.mjs +31 -0
  189. package/fesm2022/ngwr-i18n-ko.mjs.map +1 -1
  190. package/fesm2022/ngwr-i18n-nl.mjs +31 -0
  191. package/fesm2022/ngwr-i18n-nl.mjs.map +1 -1
  192. package/fesm2022/ngwr-i18n-pl.mjs +31 -0
  193. package/fesm2022/ngwr-i18n-pl.mjs.map +1 -1
  194. package/fesm2022/ngwr-i18n-pt.mjs +31 -0
  195. package/fesm2022/ngwr-i18n-pt.mjs.map +1 -1
  196. package/fesm2022/ngwr-i18n-ru.mjs +31 -0
  197. package/fesm2022/ngwr-i18n-ru.mjs.map +1 -1
  198. package/fesm2022/ngwr-i18n-sv.mjs +31 -0
  199. package/fesm2022/ngwr-i18n-sv.mjs.map +1 -1
  200. package/fesm2022/ngwr-i18n-tr.mjs +31 -0
  201. package/fesm2022/ngwr-i18n-tr.mjs.map +1 -1
  202. package/fesm2022/ngwr-i18n-uk.mjs +31 -0
  203. package/fesm2022/ngwr-i18n-uk.mjs.map +1 -1
  204. package/fesm2022/ngwr-i18n-vi.mjs +31 -0
  205. package/fesm2022/ngwr-i18n-vi.mjs.map +1 -1
  206. package/fesm2022/ngwr-i18n-zh-TW.mjs +31 -0
  207. package/fesm2022/ngwr-i18n-zh-TW.mjs.map +1 -1
  208. package/fesm2022/ngwr-i18n-zh.mjs +31 -0
  209. package/fesm2022/ngwr-i18n-zh.mjs.map +1 -1
  210. package/fesm2022/ngwr-i18n.mjs +9 -9
  211. package/fesm2022/ngwr-i18n.mjs.map +1 -1
  212. package/fesm2022/ngwr-icon-adapters-feather.mjs.map +1 -1
  213. package/fesm2022/ngwr-icon-adapters-lucide.mjs.map +1 -1
  214. package/fesm2022/ngwr-icon.mjs +6 -6
  215. package/fesm2022/ngwr-icon.mjs.map +1 -1
  216. package/fesm2022/ngwr-image-cropper-testing.mjs.map +1 -1
  217. package/fesm2022/ngwr-image-cropper.mjs +3 -3
  218. package/fesm2022/ngwr-image-cropper.mjs.map +1 -1
  219. package/fesm2022/ngwr-input-number-testing.mjs.map +1 -1
  220. package/fesm2022/ngwr-input-number.mjs +15 -3
  221. package/fesm2022/ngwr-input-number.mjs.map +1 -1
  222. package/fesm2022/ngwr-input-otp-testing.mjs.map +1 -1
  223. package/fesm2022/ngwr-input-otp.mjs +3 -3
  224. package/fesm2022/ngwr-input-otp.mjs.map +1 -1
  225. package/fesm2022/ngwr-input-testing.mjs.map +1 -1
  226. package/fesm2022/ngwr-input.mjs +15 -15
  227. package/fesm2022/ngwr-input.mjs.map +1 -1
  228. package/fesm2022/ngwr-keyboard.mjs +3 -3
  229. package/fesm2022/ngwr-keyboard.mjs.map +1 -1
  230. package/fesm2022/ngwr-knob-testing.mjs.map +1 -1
  231. package/fesm2022/ngwr-knob.mjs +3 -3
  232. package/fesm2022/ngwr-knob.mjs.map +1 -1
  233. package/fesm2022/ngwr-layout.mjs +15 -15
  234. package/fesm2022/ngwr-layout.mjs.map +1 -1
  235. package/fesm2022/ngwr-lightbox-testing.mjs.map +1 -1
  236. package/fesm2022/ngwr-lightbox.mjs +3 -3
  237. package/fesm2022/ngwr-lightbox.mjs.map +1 -1
  238. package/fesm2022/ngwr-line-chart-testing.mjs.map +1 -1
  239. package/fesm2022/ngwr-line-chart.mjs +18 -7
  240. package/fesm2022/ngwr-line-chart.mjs.map +1 -1
  241. package/fesm2022/ngwr-list.mjs +6 -6
  242. package/fesm2022/ngwr-list.mjs.map +1 -1
  243. package/fesm2022/ngwr-loading-bar-router.mjs.map +1 -1
  244. package/fesm2022/ngwr-loading-bar.mjs +6 -6
  245. package/fesm2022/ngwr-loading-bar.mjs.map +1 -1
  246. package/fesm2022/ngwr-markdown-testing.mjs.map +1 -1
  247. package/fesm2022/ngwr-markdown.mjs +798 -17
  248. package/fesm2022/ngwr-markdown.mjs.map +1 -1
  249. package/fesm2022/ngwr-marquee-testing.mjs.map +1 -1
  250. package/fesm2022/ngwr-marquee.mjs +3 -3
  251. package/fesm2022/ngwr-marquee.mjs.map +1 -1
  252. package/fesm2022/ngwr-media.mjs +3 -3
  253. package/fesm2022/ngwr-media.mjs.map +1 -1
  254. package/fesm2022/ngwr-mention-testing.mjs.map +1 -1
  255. package/fesm2022/ngwr-mention.mjs +6 -6
  256. package/fesm2022/ngwr-mention.mjs.map +1 -1
  257. package/fesm2022/ngwr-meta.mjs +6 -6
  258. package/fesm2022/ngwr-meta.mjs.map +1 -1
  259. package/fesm2022/ngwr-meter-group-testing.mjs +7 -3
  260. package/fesm2022/ngwr-meter-group-testing.mjs.map +1 -1
  261. package/fesm2022/ngwr-meter-group.mjs +26 -6
  262. package/fesm2022/ngwr-meter-group.mjs.map +1 -1
  263. package/fesm2022/ngwr-overlay.mjs +6 -6
  264. package/fesm2022/ngwr-overlay.mjs.map +1 -1
  265. package/fesm2022/ngwr-page-header.mjs +13 -5
  266. package/fesm2022/ngwr-page-header.mjs.map +1 -1
  267. package/fesm2022/ngwr-pagination-testing.mjs.map +1 -1
  268. package/fesm2022/ngwr-pagination.mjs +3 -3
  269. package/fesm2022/ngwr-pagination.mjs.map +1 -1
  270. package/fesm2022/ngwr-pipes.mjs +21 -21
  271. package/fesm2022/ngwr-pipes.mjs.map +1 -1
  272. package/fesm2022/ngwr-platform.mjs +9 -9
  273. package/fesm2022/ngwr-platform.mjs.map +1 -1
  274. package/fesm2022/ngwr-popconfirm-testing.mjs.map +1 -1
  275. package/fesm2022/ngwr-popconfirm.mjs +6 -6
  276. package/fesm2022/ngwr-popconfirm.mjs.map +1 -1
  277. package/fesm2022/ngwr-popover-testing.mjs.map +1 -1
  278. package/fesm2022/ngwr-popover.mjs +578 -16
  279. package/fesm2022/ngwr-popover.mjs.map +1 -1
  280. package/fesm2022/ngwr-progress.mjs +3 -3
  281. package/fesm2022/ngwr-progress.mjs.map +1 -1
  282. package/fesm2022/ngwr-pull-to-refresh.mjs +3 -3
  283. package/fesm2022/ngwr-pull-to-refresh.mjs.map +1 -1
  284. package/fesm2022/ngwr-qr.mjs +3 -3
  285. package/fesm2022/ngwr-qr.mjs.map +1 -1
  286. package/fesm2022/ngwr-radio-testing.mjs.map +1 -1
  287. package/fesm2022/ngwr-radio.mjs +6 -6
  288. package/fesm2022/ngwr-radio.mjs.map +1 -1
  289. package/fesm2022/ngwr-rating-testing.mjs.map +1 -1
  290. package/fesm2022/ngwr-rating.mjs +3 -3
  291. package/fesm2022/ngwr-rating.mjs.map +1 -1
  292. package/fesm2022/ngwr-result.mjs +12 -12
  293. package/fesm2022/ngwr-result.mjs.map +1 -1
  294. package/fesm2022/ngwr-rotating-text-testing.mjs.map +1 -1
  295. package/fesm2022/ngwr-rotating-text.mjs +3 -3
  296. package/fesm2022/ngwr-rotating-text.mjs.map +1 -1
  297. package/fesm2022/ngwr-schema-form.mjs +3 -3
  298. package/fesm2022/ngwr-schema-form.mjs.map +1 -1
  299. package/fesm2022/ngwr-scroll.mjs +3 -3
  300. package/fesm2022/ngwr-scroll.mjs.map +1 -1
  301. package/fesm2022/ngwr-segmented-testing.mjs.map +1 -1
  302. package/fesm2022/ngwr-segmented.mjs +3 -3
  303. package/fesm2022/ngwr-segmented.mjs.map +1 -1
  304. package/fesm2022/ngwr-select-testing.mjs.map +1 -1
  305. package/fesm2022/ngwr-select.mjs +12 -12
  306. package/fesm2022/ngwr-select.mjs.map +1 -1
  307. package/fesm2022/ngwr-shiny-text-testing.mjs.map +1 -1
  308. package/fesm2022/ngwr-shiny-text.mjs +6 -6
  309. package/fesm2022/ngwr-shiny-text.mjs.map +1 -1
  310. package/fesm2022/ngwr-sidebar.mjs +10 -3
  311. package/fesm2022/ngwr-sidebar.mjs.map +1 -1
  312. package/fesm2022/ngwr-skeleton.mjs +3 -3
  313. package/fesm2022/ngwr-skeleton.mjs.map +1 -1
  314. package/fesm2022/ngwr-slider-testing.mjs.map +1 -1
  315. package/fesm2022/ngwr-slider.mjs +3 -3
  316. package/fesm2022/ngwr-slider.mjs.map +1 -1
  317. package/fesm2022/ngwr-sparkline-testing.mjs.map +1 -1
  318. package/fesm2022/ngwr-sparkline.mjs +84 -10
  319. package/fesm2022/ngwr-sparkline.mjs.map +1 -1
  320. package/fesm2022/ngwr-speed-dial-testing.mjs.map +1 -1
  321. package/fesm2022/ngwr-speed-dial.mjs +3 -3
  322. package/fesm2022/ngwr-speed-dial.mjs.map +1 -1
  323. package/fesm2022/ngwr-spinner.mjs +3 -3
  324. package/fesm2022/ngwr-spinner.mjs.map +1 -1
  325. package/fesm2022/ngwr-splash-cursor-testing.mjs.map +1 -1
  326. package/fesm2022/ngwr-splash-cursor.mjs +3 -3
  327. package/fesm2022/ngwr-splash-cursor.mjs.map +1 -1
  328. package/fesm2022/ngwr-split-text-testing.mjs.map +1 -1
  329. package/fesm2022/ngwr-split-text.mjs +3 -3
  330. package/fesm2022/ngwr-split-text.mjs.map +1 -1
  331. package/fesm2022/ngwr-splitter-testing.mjs.map +1 -1
  332. package/fesm2022/ngwr-splitter.mjs +3 -3
  333. package/fesm2022/ngwr-splitter.mjs.map +1 -1
  334. package/fesm2022/ngwr-spotlight-card-testing.mjs.map +1 -1
  335. package/fesm2022/ngwr-spotlight-card.mjs +6 -6
  336. package/fesm2022/ngwr-spotlight-card.mjs.map +1 -1
  337. package/fesm2022/ngwr-squircle.mjs +6 -6
  338. package/fesm2022/ngwr-squircle.mjs.map +1 -1
  339. package/fesm2022/ngwr-star-border-testing.mjs.map +1 -1
  340. package/fesm2022/ngwr-star-border.mjs +3 -3
  341. package/fesm2022/ngwr-star-border.mjs.map +1 -1
  342. package/fesm2022/ngwr-statistic.mjs +9 -9
  343. package/fesm2022/ngwr-statistic.mjs.map +1 -1
  344. package/fesm2022/ngwr-stepper-testing.mjs.map +1 -1
  345. package/fesm2022/ngwr-stepper.mjs +6 -6
  346. package/fesm2022/ngwr-stepper.mjs.map +1 -1
  347. package/fesm2022/ngwr-storage.mjs +3 -3
  348. package/fesm2022/ngwr-storage.mjs.map +1 -1
  349. package/fesm2022/ngwr-switch-testing.mjs.map +1 -1
  350. package/fesm2022/ngwr-switch.mjs +3 -3
  351. package/fesm2022/ngwr-switch.mjs.map +1 -1
  352. package/fesm2022/ngwr-table-testing.mjs.map +1 -1
  353. package/fesm2022/ngwr-table.mjs +18 -18
  354. package/fesm2022/ngwr-table.mjs.map +1 -1
  355. package/fesm2022/ngwr-tabs-router.mjs +3 -3
  356. package/fesm2022/ngwr-tabs-router.mjs.map +1 -1
  357. package/fesm2022/ngwr-tabs-testing.mjs.map +1 -1
  358. package/fesm2022/ngwr-tabs.mjs +6 -6
  359. package/fesm2022/ngwr-tabs.mjs.map +1 -1
  360. package/fesm2022/ngwr-textarea-testing.mjs.map +1 -1
  361. package/fesm2022/ngwr-textarea.mjs +3 -3
  362. package/fesm2022/ngwr-textarea.mjs.map +1 -1
  363. package/fesm2022/ngwr-theme.mjs +3 -3
  364. package/fesm2022/ngwr-theme.mjs.map +1 -1
  365. package/fesm2022/ngwr-tilt-card-testing.mjs.map +1 -1
  366. package/fesm2022/ngwr-tilt-card.mjs +6 -6
  367. package/fesm2022/ngwr-tilt-card.mjs.map +1 -1
  368. package/fesm2022/ngwr-timeline.mjs +6 -6
  369. package/fesm2022/ngwr-timeline.mjs.map +1 -1
  370. package/fesm2022/ngwr-toast-testing.mjs.map +1 -1
  371. package/fesm2022/ngwr-toast.mjs +9 -9
  372. package/fesm2022/ngwr-toast.mjs.map +1 -1
  373. package/fesm2022/ngwr-toolbar.mjs +10 -3
  374. package/fesm2022/ngwr-toolbar.mjs.map +1 -1
  375. package/fesm2022/ngwr-tour-testing.mjs.map +1 -1
  376. package/fesm2022/ngwr-tour.mjs +6 -6
  377. package/fesm2022/ngwr-tour.mjs.map +1 -1
  378. package/fesm2022/ngwr-transfer-testing.mjs.map +1 -1
  379. package/fesm2022/ngwr-transfer.mjs +3 -3
  380. package/fesm2022/ngwr-transfer.mjs.map +1 -1
  381. package/fesm2022/ngwr-tree-testing.mjs.map +1 -1
  382. package/fesm2022/ngwr-tree.mjs +3 -3
  383. package/fesm2022/ngwr-tree.mjs.map +1 -1
  384. package/fesm2022/ngwr-typewriter-testing.mjs.map +1 -1
  385. package/fesm2022/ngwr-typewriter.mjs +3 -3
  386. package/fesm2022/ngwr-typewriter.mjs.map +1 -1
  387. package/fesm2022/ngwr-typography.mjs +7 -3
  388. package/fesm2022/ngwr-typography.mjs.map +1 -1
  389. package/fesm2022/ngwr-utils.mjs.map +1 -1
  390. package/fesm2022/ngwr-validators.mjs.map +1 -1
  391. package/fesm2022/ngwr-version.mjs +1 -1
  392. package/fesm2022/ngwr-version.mjs.map +1 -1
  393. package/fesm2022/ngwr-virtual-scroll.mjs +3 -3
  394. package/fesm2022/ngwr-virtual-scroll.mjs.map +1 -1
  395. package/fesm2022/ngwr-waves-testing.mjs.map +1 -1
  396. package/fesm2022/ngwr-waves.mjs +3 -3
  397. package/fesm2022/ngwr-waves.mjs.map +1 -1
  398. package/fesm2022/ngwr-window-testing.mjs.map +1 -1
  399. package/fesm2022/ngwr-window.mjs +12 -12
  400. package/fesm2022/ngwr-window.mjs.map +1 -1
  401. package/form/styles/_index.scss +16 -1
  402. package/gauge/styles/_index.scss +1 -0
  403. package/i18n/ar.json +32 -1
  404. package/i18n/cs.json +32 -1
  405. package/i18n/de.json +32 -1
  406. package/i18n/en.json +32 -1
  407. package/i18n/es.json +32 -1
  408. package/i18n/fr.json +32 -1
  409. package/i18n/he.json +32 -1
  410. package/i18n/hi.json +32 -1
  411. package/i18n/id.json +32 -1
  412. package/i18n/it.json +32 -1
  413. package/i18n/ja.json +32 -1
  414. package/i18n/ko.json +32 -1
  415. package/i18n/nl.json +32 -1
  416. package/i18n/pl.json +32 -1
  417. package/i18n/pt.json +32 -1
  418. package/i18n/ru.json +32 -1
  419. package/i18n/sv.json +32 -1
  420. package/i18n/tr.json +32 -1
  421. package/i18n/uk.json +32 -1
  422. package/i18n/vi.json +32 -1
  423. package/i18n/zh-TW.json +32 -1
  424. package/i18n/zh.json +32 -1
  425. package/input/styles/_index.scss +24 -1
  426. package/input-number/styles/_index.scss +69 -0
  427. package/input-otp/styles/_index.scss +5 -5
  428. package/line-chart/styles/_index.scss +30 -2
  429. package/llms-full.txt +14 -4
  430. package/llms.txt +188 -132
  431. package/mcp/providers.js +2 -2
  432. package/mcp/server.js +1 -1
  433. package/meter-group/styles/_index.scss +1 -0
  434. package/overlay/styles/_arrow.scss +30 -2
  435. package/package.json +43 -2
  436. package/page-header/styles/_index.scss +37 -1
  437. package/pagination/styles/_index.scss +9 -5
  438. package/popover/styles/_index.scss +73 -5
  439. package/progress/styles/_index.scss +4 -0
  440. package/radio/styles/_index.scss +4 -1
  441. package/schematics/use/symbol-map.json +1 -0
  442. package/select/styles/_index.scss +51 -12
  443. package/sidebar/styles/_index.scss +44 -2
  444. package/skeleton/styles/_index.scss +3 -0
  445. package/skills/ngwr/SKILL.md +5 -5
  446. package/skills/ngwr/references/catalog.md +2 -0
  447. package/skills/ngwr/references/setup.md +3 -3
  448. package/sparkline/styles/_index.scss +19 -0
  449. package/styles.scss +1 -0
  450. package/table/styles/_index.scss +7 -0
  451. package/textarea/styles/_index.scss +12 -3
  452. package/theme/styles/_colors.scss +39 -13
  453. package/theme/styles/_contrast.scss +9 -7
  454. package/toast/styles/_index.scss +4 -0
  455. package/toolbar/styles/_index.scss +35 -1
  456. package/tree/styles/_index.scss +46 -7
  457. package/types/ngwr-badge.d.ts +22 -7
  458. package/types/ngwr-bar-chart.d.ts +19 -1
  459. package/types/ngwr-calendar-heatmap.d.ts +29 -1
  460. package/types/ngwr-checkbox.d.ts +2 -0
  461. package/types/ngwr-config.d.ts +12 -0
  462. package/types/ngwr-date-picker.d.ts +308 -43
  463. package/types/ngwr-donut-chart.d.ts +24 -1
  464. package/types/ngwr-dropdown.d.ts +22 -2
  465. package/types/ngwr-editor-testing.d.ts +378 -0
  466. package/types/ngwr-editor.d.ts +449 -0
  467. package/types/ngwr-event-calendar.d.ts +8 -0
  468. package/types/ngwr-form.d.ts +35 -5
  469. package/types/ngwr-gauge.d.ts +20 -1
  470. package/types/ngwr-input-number.d.ts +7 -0
  471. package/types/ngwr-line-chart.d.ts +9 -1
  472. package/types/ngwr-markdown.d.ts +61 -2
  473. package/types/ngwr-meter-group-testing.d.ts +3 -2
  474. package/types/ngwr-meter-group.d.ts +14 -1
  475. package/types/ngwr-page-header.d.ts +10 -2
  476. package/types/ngwr-popover.d.ts +154 -3
  477. package/types/ngwr-sidebar.d.ts +7 -0
  478. package/types/ngwr-sparkline.d.ts +43 -1
  479. package/types/ngwr-toolbar.d.ts +7 -0
  480. package/types/ngwr-typography.d.ts +4 -0
  481. package/types/ngwr-version.d.ts +1 -1
  482. package/typography/styles/_index.scss +16 -0
@@ -0,0 +1,2601 @@
1
+ import { Directionality } from '@angular/cdk/bidi';
2
+ import { coerceBooleanProperty } from '@angular/cdk/coercion';
3
+ import { DOCUMENT, isPlatformBrowser, NgTemplateOutlet } from '@angular/common';
4
+ import * as i0 from '@angular/core';
5
+ import { signal, computed, model, output, input, inject, Injector, DestroyRef, ElementRef, PLATFORM_ID, viewChild, viewChildren, isDevMode, effect, untracked, afterRenderEffect, afterNextRender, ViewEncapsulation, Component } from '@angular/core';
6
+ import { WrButton } from 'ngwr/button';
7
+ import { useConfigValue } from 'ngwr/config';
8
+ import { useFormFieldAria, WR_FORM_FIELD } from 'ngwr/form';
9
+ import { useI18nText, readI18nText, useI18nFormatter } from 'ngwr/i18n';
10
+ import { WrInput } from 'ngwr/input';
11
+ import { parseMarkdown, serializeMarkdown, safeMarkdownUrl } from 'ngwr/markdown';
12
+ import { WrPlatform } from 'ngwr/platform';
13
+ import { WrPopover } from 'ngwr/popover';
14
+ import { isComposing } from 'ngwr/utils';
15
+ import { TextSelection, Plugin, EditorState } from 'prosemirror-state';
16
+ import { EditorView, DecorationSet, Decoration } from 'prosemirror-view';
17
+ import { DOMParser, DOMSerializer, Fragment, Schema } from 'prosemirror-model';
18
+ import { toggleMark, setBlockType, wrapIn, chainCommands, exitCode, baseKeymap } from 'prosemirror-commands';
19
+ import { redo, undo, history } from 'prosemirror-history';
20
+ import { undoInputRule, inputRules, textblockTypeInputRule, wrappingInputRule } from 'prosemirror-inputrules';
21
+ import { keymap } from 'prosemirror-keymap';
22
+ import { wrapInList, liftListItem, splitListItem, sinkListItem } from 'prosemirror-schema-list';
23
+
24
+ /**
25
+ * @license
26
+ *
27
+ * Use of this source code is governed by an MIT-style license that can be
28
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
29
+ */
30
+ /** Past this many nested elements the value is refused unread — see {@link readHtml}. */
31
+ const MAX_DOM_DEPTH = 512;
32
+ /** What the HTML parser collapses and strips, written where a space has to survive it. */
33
+ const NBSP = '\u00a0';
34
+ /** One answer per window, `null` where there is no policy to use. */
35
+ const policies = new WeakMap();
36
+ /**
37
+ * An HTML string wrapped for a `trusted-types` page, decided once per window and
38
+ * only where the API exists.
39
+ *
40
+ * A template's `innerHTML` is a Trusted Types sink like any other, and under
41
+ * `require-trusted-types-for 'script'` a plain string throws. Pass-through is
42
+ * sound HERE and nowhere else: the string lands in an inert template, and what
43
+ * leaves it is rebuilt node by node from the schema's allowlist — the same
44
+ * argument ProseMirror makes for its own clipboard policy.
45
+ *
46
+ * The page's default policy comes first, as it does in ProseMirror's clipboard
47
+ * code; then one named `ngwr-editor`. Creating that one THROWS on a page whose
48
+ * `trusted-types` allowlist does not name it, and uncaught that refused every
49
+ * HTML value on the page — so a refusal falls back to the plain string, which
50
+ * such a page refuses at the sink instead, as a value the editor could not read.
51
+ * An app with an allowlist and no default policy adds `ngwr-editor` (and
52
+ * ProseMirror's own `ProseMirrorClipboard`, for paste).
53
+ */
54
+ function trusted(html, view) {
55
+ const types = view?.trustedTypes;
56
+ if (!view || !types)
57
+ return html;
58
+ let policy = policies.get(view);
59
+ if (policy === undefined) {
60
+ policy = types.defaultPolicy ?? null;
61
+ if (!policy) {
62
+ try {
63
+ policy = types.createPolicy('ngwr-editor', { createHTML: input => input });
64
+ }
65
+ catch {
66
+ policy = null;
67
+ }
68
+ }
69
+ policies.set(view, policy);
70
+ }
71
+ return (policy ? policy.createHTML(html) : html);
72
+ }
73
+ /**
74
+ * An untrusted HTML string as a checked document, or a thrown `RangeError`.
75
+ *
76
+ * Parsed into a `<template>` of the INJECTED document, never into a live node:
77
+ * a template's content has no browsing context, so a `<script>` does not run, an
78
+ * `<img onerror>` never loads and no handler fires. It is also the one inert
79
+ * parser the server has — domino ships no `DOMParser`. From there the schema's
80
+ * parse rules rebuild the document from an allowlist, so an attribute they do
81
+ * not name never reaches it, and a link or image the URL policy refuses keeps its
82
+ * text and loses the URL.
83
+ *
84
+ * Depth is checked before ProseMirror sees the tree: its parser recurses, a
85
+ * browser's HTML parser caps nesting on its own and neither jsdom nor domino
86
+ * does.
87
+ */
88
+ function readHtml(html, document, schema) {
89
+ const template = document.createElement('template');
90
+ template.innerHTML = trusted(html, document.defaultView);
91
+ if (depthOf(template.content) > MAX_DOM_DEPTH)
92
+ throw new RangeError('<wr-editor>: the HTML value nests too deeply');
93
+ const doc = DOMParser.fromSchema(schema).parse(template.content);
94
+ doc.check();
95
+ return doc;
96
+ }
97
+ function depthOf(root) {
98
+ let deepest = 0;
99
+ const stack = [];
100
+ for (let child = root.firstElementChild; child; child = child.nextElementSibling)
101
+ stack.push([child, 1]);
102
+ while (stack.length) {
103
+ const [element, depth] = stack.pop();
104
+ if (depth > deepest)
105
+ deepest = depth;
106
+ if (deepest > MAX_DOM_DEPTH)
107
+ return deepest;
108
+ for (let child = element.firstElementChild; child; child = child.nextElementSibling)
109
+ stack.push([child, depth + 1]);
110
+ }
111
+ return deepest;
112
+ }
113
+ /**
114
+ * A document as HTML, serialized in a document of its own.
115
+ *
116
+ * Never the live one: an `<img>` created by the page's document starts loading
117
+ * the moment its `src` is set, attached or not. On the server the injected
118
+ * document is domino's and loads nothing, which is also why the fallback is safe
119
+ * where `createHTMLDocument` is missing.
120
+ *
121
+ * Spaces are written the way {@link keepSpaces} says, so the editor's own output
122
+ * reads back as the text it was written from.
123
+ */
124
+ function writeHtml(doc, document) {
125
+ const off = document.implementation?.createHTMLDocument?.('') ?? document;
126
+ const container = off.createElement('div');
127
+ const content = withKeptSpaces(doc).content;
128
+ container.appendChild(DOMSerializer.fromSchema(doc.type.schema).serializeFragment(content, { document: off }));
129
+ return container.innerHTML;
130
+ }
131
+ function withKeptSpaces(node) {
132
+ if (node.isTextblock)
133
+ return keepSpaces(node);
134
+ if (node.isLeaf)
135
+ return node;
136
+ const children = [];
137
+ let changed = false;
138
+ node.forEach(child => {
139
+ const next = withKeptSpaces(child);
140
+ changed ||= next !== child;
141
+ children.push(next);
142
+ });
143
+ return changed ? node.copy(Fragment.from(children)) : node;
144
+ }
145
+ /**
146
+ * A textblock whose spaces survive the HTML parser: every space it would collapse
147
+ * or strip is written as a no-break space.
148
+ *
149
+ * The surface draws spaces as typed (`white-space: break-spaces`), but the parser
150
+ * that reads a value back — ProseMirror's, and a browser's rendering of the same
151
+ * HTML — collapses a run into one and drops one at the start of a block, after a
152
+ * line break and at the end. So a space stays a space only where a character
153
+ * other than a space comes before it and anything but the block's end comes
154
+ * after; the rest alternate as no-break spaces, the way CKEditor writes them. A
155
+ * run that crosses a mark boundary is still one run. A code block is read with
156
+ * its whitespace kept, and left alone.
157
+ */
158
+ function keepSpaces(block) {
159
+ if (block.type.spec.code || !block.textContent.includes(' '))
160
+ return block;
161
+ const children = [];
162
+ let before = 'start';
163
+ let changed = false;
164
+ block.forEach((child, _offset, index) => {
165
+ if (!child.isText) {
166
+ children.push(child);
167
+ before = child.type.name === 'hard_break' ? 'start' : 'other';
168
+ return;
169
+ }
170
+ const text = child.text;
171
+ const last = index === block.childCount - 1;
172
+ let out = '';
173
+ for (let i = 0; i < text.length; i++) {
174
+ const char = text[i];
175
+ if (char !== ' ') {
176
+ out += char;
177
+ before = 'other';
178
+ }
179
+ else if (before === 'other' && !(last && i === text.length - 1)) {
180
+ out += ' ';
181
+ before = 'space';
182
+ }
183
+ else {
184
+ out += NBSP;
185
+ before = 'other';
186
+ }
187
+ }
188
+ changed ||= out !== text;
189
+ children.push(out === text ? child : block.type.schema.text(out, child.marks));
190
+ });
191
+ return changed ? block.copy(Fragment.from(children)) : block;
192
+ }
193
+
194
+ /**
195
+ * @license
196
+ *
197
+ * Use of this source code is governed by an MIT-style license that can be
198
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
199
+ */
200
+ /**
201
+ * How deep a JSON value may nest before it is refused unread.
202
+ *
203
+ * Not the editing limit — that is `MAX_NESTING`, counted in quotes and list
204
+ * items. This one is the stack's: `Node.fromJSON` and `check()` both recurse,
205
+ * and about three thousand nested blocks overflow them. A document the editor
206
+ * itself can produce is nowhere near it (eleven levels of list is under forty
207
+ * JSON levels), so anything past it was not written by an editor.
208
+ */
209
+ const MAX_JSON_DEPTH = 256;
210
+ /**
211
+ * An untrusted JSON value as a checked document, or a thrown `RangeError`.
212
+ *
213
+ * Both steps are needed. `nodeFromJSON` refuses an unknown node or mark, an
214
+ * empty text node and an attribute its validator rejects — the URL policy
215
+ * among them — but does NOT check content expressions; `check()` does, and
216
+ * re-runs the validators besides. The shape pass in front keeps a malformed or
217
+ * hostile value from reaching either: a non-object, a `text` that is not a
218
+ * string, or nesting past {@link MAX_JSON_DEPTH}.
219
+ */
220
+ function readJson(value, schema) {
221
+ assertShape(value, 0);
222
+ const doc = schema.nodeFromJSON(value);
223
+ if (doc.type !== schema.topNodeType)
224
+ throw new RangeError('<wr-editor>: a JSON value is a `doc` node');
225
+ doc.check();
226
+ return doc;
227
+ }
228
+ function assertShape(value, depth) {
229
+ if (depth > MAX_JSON_DEPTH)
230
+ throw new RangeError('<wr-editor>: the JSON value nests too deeply');
231
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
232
+ throw new RangeError('<wr-editor>: every JSON node is an object');
233
+ }
234
+ const node = value;
235
+ if (typeof node['type'] !== 'string')
236
+ throw new RangeError('<wr-editor>: every JSON node has a string `type`');
237
+ if (node['text'] !== undefined && typeof node['text'] !== 'string') {
238
+ throw new RangeError('<wr-editor>: `text` is a string');
239
+ }
240
+ for (const key of ['attrs', 'marks', 'content']) {
241
+ const child = node[key];
242
+ if (child === undefined)
243
+ continue;
244
+ if (key === 'attrs') {
245
+ if (typeof child !== 'object' || child === null || Array.isArray(child)) {
246
+ throw new RangeError('<wr-editor>: `attrs` is an object');
247
+ }
248
+ continue;
249
+ }
250
+ if (!Array.isArray(child))
251
+ throw new RangeError(`<wr-editor>: \`${key}\` is an array`);
252
+ for (const item of child)
253
+ assertShape(item, depth + 1);
254
+ }
255
+ }
256
+
257
+ /**
258
+ * @license
259
+ *
260
+ * Use of this source code is governed by an MIT-style license that can be
261
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
262
+ */
263
+ /**
264
+ * Markdown to and from the editor's document JSON — both walkers pure, with no
265
+ * ProseMirror import, so they run under SSR and in a spec with no DOM.
266
+ *
267
+ * The reading half never passes `streaming: true`: that mode withholds a
268
+ * trailing `**` or `[` as a half-typed fragment, which in an editor would delete
269
+ * what the user actually wrote.
270
+ */
271
+ // ---------------------------------------------------------------------------
272
+ // Markdown → document
273
+ // ---------------------------------------------------------------------------
274
+ /** A markdown string as the editor's document JSON. */
275
+ function markdownToJson(source) {
276
+ const content = blocksToJson(parseMarkdown(source));
277
+ return { type: 'doc', content: content.length ? content : [{ type: 'paragraph' }] };
278
+ }
279
+ function blocksToJson(blocks) {
280
+ return blocks.map(blockToJson);
281
+ }
282
+ function blockToJson(block) {
283
+ switch (block.kind) {
284
+ case 'paragraph':
285
+ return textblock('paragraph', undefined, block.inlines);
286
+ case 'heading':
287
+ // The schema stops at three; h4–h6 keep being headings rather than turning
288
+ // into paragraphs, the same clamp the HTML path applies.
289
+ return textblock('heading', { level: Math.min(block.level, 3) }, block.inlines);
290
+ case 'code':
291
+ // An empty text node is refused by ProseMirror, so an empty fence has no
292
+ // content. The info word goes through the schema's own rule, as it does on
293
+ // the HTML path: the parser keeps a word of any length, the schema refuses
294
+ // one past 64 characters, and passed on unchanged it refused the whole
295
+ // document over one fence tagged with a URL.
296
+ return {
297
+ type: 'code_block',
298
+ attrs: { language: languageValue(block.language) },
299
+ ...(block.code ? { content: [{ type: 'text', text: block.code }] } : {}),
300
+ };
301
+ case 'quote': {
302
+ const children = blocksToJson(block.children);
303
+ return { type: 'blockquote', content: children.length ? children : [{ type: 'paragraph' }] };
304
+ }
305
+ case 'list':
306
+ return {
307
+ type: block.ordered ? 'ordered_list' : 'bullet_list',
308
+ attrs: block.ordered ? { order: block.start, tight: block.tight } : { tight: block.tight },
309
+ content: block.items.map(itemToJson),
310
+ };
311
+ case 'table':
312
+ return tableToJson(block.head, block.rows, block.align);
313
+ case 'rule':
314
+ return { type: 'horizontal_rule' };
315
+ }
316
+ }
317
+ /**
318
+ * A list item. Its content has to open with a paragraph — the shape
319
+ * `prosemirror-schema-list`'s commands are written for — so an item that starts
320
+ * with a code block, a quote or a nested list gets an empty paragraph in front,
321
+ * which {@link jsonToMarkdown} drops again on the way out.
322
+ */
323
+ function itemToJson(item) {
324
+ const children = blocksToJson(item.children);
325
+ if (children[0]?.type !== 'paragraph')
326
+ children.unshift({ type: 'paragraph' });
327
+ return { type: 'list_item', attrs: { checked: item.checked }, content: children };
328
+ }
329
+ function tableToJson(head, rows, align) {
330
+ const row = (cells, type) => ({
331
+ type: 'table_row',
332
+ content: cells.map((cell, index) => ({
333
+ type,
334
+ attrs: { align: align[index] ?? null },
335
+ content: [textblock('paragraph', undefined, cell.inlines)],
336
+ })),
337
+ });
338
+ return { type: 'table', content: [row(head, 'table_header'), ...rows.map(cells => row(cells, 'table_cell'))] };
339
+ }
340
+ function textblock(type, attrs, inlines) {
341
+ const content = inlinesToJson(inlines);
342
+ return { type, ...(attrs ? { attrs } : {}), ...(content.length ? { content } : {}) };
343
+ }
344
+ /**
345
+ * Nested inline nodes flattened into leaves that each carry the marks collected
346
+ * on the way down — the shape ProseMirror holds.
347
+ *
348
+ * A mark is added once however often it nests (`**a **b** c**`): a mark set with
349
+ * the same mark twice is invalid to ProseMirror, and the whole value would be
350
+ * refused over it. Adjacent leaves with the same marks are joined, because
351
+ * `Node.fromJSON` does not join them and ProseMirror assumes it never sees two.
352
+ */
353
+ function inlinesToJson(inlines) {
354
+ const out = [];
355
+ collect(inlines, [], out);
356
+ return out;
357
+ }
358
+ function collect(nodes, marks, out) {
359
+ for (const node of nodes) {
360
+ switch (node.kind) {
361
+ case 'text':
362
+ // A soft break and the indentation around it render as one space; kept
363
+ // as `\n`, the editor's `pre-wrap` would draw a line break the markdown
364
+ // never had.
365
+ pushText(out, node.value.replace(/\s*\n\s*/g, ' '), marks);
366
+ break;
367
+ case 'code':
368
+ pushText(out, node.value, withMark(marks, { type: 'code' }));
369
+ break;
370
+ case 'break':
371
+ out.push(withMarks({ type: 'hard_break' }, marks));
372
+ break;
373
+ case 'image':
374
+ out.push(withMarks({ type: 'image', attrs: { src: node.src, alt: node.alt, title: node.title } }, marks.filter(mark => mark.type === 'link')));
375
+ break;
376
+ case 'link':
377
+ collect(node.children, withMark(marks, { type: 'link', attrs: { href: node.href, title: node.title } }), out);
378
+ break;
379
+ case 'strong':
380
+ collect(node.children, withMark(marks, { type: 'strong' }), out);
381
+ break;
382
+ case 'em':
383
+ collect(node.children, withMark(marks, { type: 'em' }), out);
384
+ break;
385
+ case 'del':
386
+ collect(node.children, withMark(marks, { type: 'strike' }), out);
387
+ break;
388
+ }
389
+ }
390
+ }
391
+ function withMark(marks, mark) {
392
+ // The outer one wins — for a link that is the one the reader would follow.
393
+ return marks.some(existing => existing.type === mark.type) ? marks : [...marks, mark];
394
+ }
395
+ function withMarks(node, marks) {
396
+ return marks.length ? { ...node, marks } : node;
397
+ }
398
+ function pushText(out, text, marks) {
399
+ if (!text)
400
+ return;
401
+ const last = out[out.length - 1];
402
+ if (last?.type === 'text' && sameMarks(last.marks ?? [], marks)) {
403
+ out[out.length - 1] = { ...last, text: last.text + text };
404
+ return;
405
+ }
406
+ out.push(withMarks({ type: 'text', text }, marks));
407
+ }
408
+ function sameMarks(a, b) {
409
+ if (a.length !== b.length)
410
+ return false;
411
+ const key = (mark) => `${mark.type}:${JSON.stringify(mark.attrs ?? null)}`;
412
+ const left = a.map(key).sort();
413
+ const right = b.map(key).sort();
414
+ return left.every((value, index) => value === right[index]);
415
+ }
416
+ // ---------------------------------------------------------------------------
417
+ // Document → markdown
418
+ // ---------------------------------------------------------------------------
419
+ /**
420
+ * The editor's document JSON as markdown, through `serializeMarkdown` — so the
421
+ * editor writes exactly the dialect `<wr-markdown>` renders.
422
+ *
423
+ * Underline has no markdown spelling and is dropped (in `markdown` format the
424
+ * schema does not carry it in the first place). Everything else the schema holds
425
+ * has a markdown form; what the serializer normalises is listed on
426
+ * `serializeMarkdown` itself.
427
+ */
428
+ function jsonToMarkdown(doc) {
429
+ return serializeMarkdown(childrenToBlocks(doc));
430
+ }
431
+ function childrenToBlocks(node) {
432
+ return (node.content ?? []).flatMap(jsonToBlock);
433
+ }
434
+ function jsonToBlock(node) {
435
+ const attrs = node.attrs ?? {};
436
+ switch (node.type) {
437
+ case 'paragraph':
438
+ return [{ kind: 'paragraph', inlines: jsonToInlines(node.content ?? []) }];
439
+ case 'heading':
440
+ return [
441
+ {
442
+ kind: 'heading',
443
+ level: headingLevel(attrs['level']),
444
+ inlines: jsonToInlines(node.content ?? []),
445
+ id: '',
446
+ },
447
+ ];
448
+ case 'code_block':
449
+ return [
450
+ {
451
+ kind: 'code',
452
+ language: typeof attrs['language'] === 'string' ? attrs['language'] : null,
453
+ code: (node.content ?? []).map(child => child.text ?? '').join(''),
454
+ closed: true,
455
+ },
456
+ ];
457
+ case 'blockquote':
458
+ return [{ kind: 'quote', children: childrenToBlocks(node) }];
459
+ case 'bullet_list':
460
+ case 'ordered_list':
461
+ return [
462
+ {
463
+ kind: 'list',
464
+ ordered: node.type === 'ordered_list',
465
+ start: node.type === 'ordered_list' && typeof attrs['order'] === 'number' ? attrs['order'] : 1,
466
+ tight: attrs['tight'] !== false,
467
+ items: (node.content ?? []).map(jsonToItem),
468
+ },
469
+ ];
470
+ case 'table':
471
+ return tableBlock(node);
472
+ case 'horizontal_rule':
473
+ return [{ kind: 'rule' }];
474
+ default:
475
+ return [];
476
+ }
477
+ }
478
+ function headingLevel(value) {
479
+ return value === 2 || value === 3 ? value : 1;
480
+ }
481
+ function jsonToItem(item) {
482
+ const content = item.content ?? [];
483
+ // The filler paragraph `itemToJson` put in front of a non-paragraph block.
484
+ const filler = content.length > 1 && content[0].type === 'paragraph' && !(content[0].content ?? []).length;
485
+ const checked = item.attrs?.['checked'];
486
+ return {
487
+ children: (filler ? content.slice(1) : content).flatMap(jsonToBlock),
488
+ checked: typeof checked === 'boolean' ? checked : null,
489
+ };
490
+ }
491
+ /**
492
+ * A table as markdown, every row as wide as the widest one.
493
+ *
494
+ * A markdown table is as wide as its header, and `serializeMarkdown` fits every
495
+ * row to it — but a document can hold a header narrower than the rows below it:
496
+ * a paste whose merged header came across one cell short, since `colspan` is not
497
+ * read. Fitted to that header, the cells past it were on screen and never saved.
498
+ * So the header grows empty cells instead, which markdown can carry.
499
+ */
500
+ function tableBlock(table) {
501
+ const rows = (table.content ?? []).map(row => row.content ?? []);
502
+ const width = rows.reduce((widest, cells) => Math.max(widest, cells.length), 0);
503
+ const [first, ...body] = rows;
504
+ if (!first?.length)
505
+ return [];
506
+ const head = Array.from({ length: width }, (_, index) => first[index]);
507
+ const cellOf = (cell) => ({
508
+ // Markdown has one line per cell: a cell's paragraphs join with a space.
509
+ inlines: (cell?.content ?? []).flatMap((paragraph, index) => [
510
+ ...(index ? [{ kind: 'text', value: ' ' }] : []),
511
+ ...jsonToInlines(paragraph.content ?? []),
512
+ ]),
513
+ });
514
+ const align = head.map(cell => {
515
+ const value = cell?.attrs?.['align'];
516
+ return value === 'start' || value === 'center' || value === 'end' ? value : null;
517
+ });
518
+ return [
519
+ {
520
+ kind: 'table',
521
+ head: head.map(cellOf),
522
+ rows: body.map(cells => head.map((_, index) => cellOf(cells[index]))),
523
+ align,
524
+ },
525
+ ];
526
+ }
527
+ /**
528
+ * Leaves back into an inline tree, each wrapped in its own marks.
529
+ *
530
+ * Deliberately naive: `serializeMarkdown` flattens whatever tree it is given to
531
+ * the same leaves-with-mark-sets and regroups them in its one canonical nesting,
532
+ * so wrapping every leaf separately writes exactly what a hand-merged tree would.
533
+ */
534
+ function jsonToInlines(nodes) {
535
+ return nodes.flatMap((node) => {
536
+ const marks = node.marks ?? [];
537
+ switch (node.type) {
538
+ case 'text': {
539
+ const code = marks.some(mark => mark.type === 'code');
540
+ const text = node.text ?? '';
541
+ return text ? [wrap(code ? { kind: 'code', value: text } : { kind: 'text', value: text }, marks)] : [];
542
+ }
543
+ case 'hard_break':
544
+ return [wrap({ kind: 'break' }, marks)];
545
+ case 'image': {
546
+ const attrs = node.attrs ?? {};
547
+ return [
548
+ wrap({
549
+ kind: 'image',
550
+ src: stringOf(attrs['src']),
551
+ alt: stringOf(attrs['alt']),
552
+ title: typeof attrs['title'] === 'string' ? attrs['title'] : null,
553
+ }, marks),
554
+ ];
555
+ }
556
+ default:
557
+ return [];
558
+ }
559
+ });
560
+ }
561
+ function stringOf(value) {
562
+ return typeof value === 'string' ? value : '';
563
+ }
564
+ // ---------------------------------------------------------------------------
565
+ // Shared with the schema
566
+ // ---------------------------------------------------------------------------
567
+ /**
568
+ * A fence's info word: what markdown keeps, and nothing that could break out of
569
+ * it — or `null`. The one rule for a code block's language on every path in,
570
+ * which is why it lives here, where the markdown walker needs it without
571
+ * importing ProseMirror.
572
+ */
573
+ function languageValue(raw) {
574
+ const word = (raw ?? '').trim().split(/\s+/)[0].toLowerCase();
575
+ return word && word.length <= 64 ? word : null;
576
+ }
577
+ function wrap(leaf, marks) {
578
+ let node = leaf;
579
+ for (const mark of marks) {
580
+ switch (mark.type) {
581
+ case 'strong':
582
+ node = { kind: 'strong', children: [node] };
583
+ break;
584
+ case 'em':
585
+ node = { kind: 'em', children: [node] };
586
+ break;
587
+ case 'strike':
588
+ node = { kind: 'del', children: [node] };
589
+ break;
590
+ case 'link': {
591
+ const attrs = mark.attrs ?? {};
592
+ node = {
593
+ kind: 'link',
594
+ href: stringOf(attrs['href']),
595
+ title: typeof attrs['title'] === 'string' ? attrs['title'] : null,
596
+ children: [node],
597
+ };
598
+ break;
599
+ }
600
+ // `code` is the leaf itself, and `underline` has no markdown form.
601
+ }
602
+ }
603
+ return node;
604
+ }
605
+
606
+ /**
607
+ * @license
608
+ *
609
+ * Use of this source code is governed by an MIT-style license that can be
610
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
611
+ */
612
+ /**
613
+ * How deep blockquotes and list items may nest: the deepest document markdown
614
+ * reads back with its structure. `parseMarkdown` parses twelve block levels and
615
+ * keeps whatever sits at the twelfth as literal text, so the CONTENT of a twelfth
616
+ * quote or item would come back flattened — a heading in there reads back as a
617
+ * paragraph saying `## …`. Each quote and each list item is one level, as it is
618
+ * there.
619
+ */
620
+ const MAX_NESTING = 11;
621
+ /** The `align` values a table cell may carry — `ngwr/markdown`'s own vocabulary. */
622
+ const ALIGNS = new Set([null, 'start', 'center', 'end']);
623
+ /** The largest ordinal the markdown parser reads back (`\d{1,9}`). */
624
+ const MAX_ORDER = 999999999;
625
+ /**
626
+ * The one URL policy of all three formats, as a schema rule.
627
+ *
628
+ * `safeMarkdownUrl` is what `parseMarkdown` already applies, so the markdown path
629
+ * never produces a refused URL. The HTML path asks it in `getAttrs`, where a
630
+ * refusal drops the link and keeps its text. And the JSON path — plus every
631
+ * attribute ProseMirror validates since view 1.42.3, clipboard slice context
632
+ * included — hits these validators, which THROW: a JSON value holding a refused
633
+ * URL is refused as a whole, the same as one holding an unknown node.
634
+ */
635
+ function requireUrl(kind) {
636
+ return (value) => {
637
+ if (typeof value !== 'string' || safeMarkdownUrl(value, kind) !== value) {
638
+ throw new RangeError(`<wr-editor>: refused ${kind} URL`);
639
+ }
640
+ };
641
+ }
642
+ function nullableString(value) {
643
+ if (value !== null && typeof value !== 'string')
644
+ throw new RangeError('<wr-editor>: expected a string or null');
645
+ }
646
+ /**
647
+ * The attributes of an `<a>` a pasted or bound document may keep: the href, and
648
+ * only when the policy allows it. `false` drops the mark and keeps the text —
649
+ * which is why this refuses rather than rewrites.
650
+ */
651
+ function linkAttrs(dom) {
652
+ const href = safeMarkdownUrl(dom.getAttribute('href') ?? '', 'link');
653
+ return href ? { href, title: dom.getAttribute('title') } : false;
654
+ }
655
+ function imageAttrs(dom) {
656
+ const src = safeMarkdownUrl(dom.getAttribute('src') ?? '', 'image');
657
+ return src ? { src, alt: dom.getAttribute('alt') ?? '', title: dom.getAttribute('title') } : false;
658
+ }
659
+ /**
660
+ * A list item's task state, from this schema's own `data-checked` or from the
661
+ * `<input type="checkbox">` GitHub-style renderers put first in the item.
662
+ */
663
+ function checkedOf(dom) {
664
+ const data = dom.getAttribute('data-checked');
665
+ if (data === 'true' || data === 'false')
666
+ return data === 'true';
667
+ for (let child = dom.firstChild; child; child = child.nextSibling) {
668
+ if (child.nodeType === 3 && !(child.textContent ?? '').trim())
669
+ continue;
670
+ if (child.nodeName !== 'INPUT')
671
+ return null;
672
+ const input = child;
673
+ return (input.getAttribute('type') ?? '').toLowerCase() === 'checkbox' ? input.hasAttribute('checked') : null;
674
+ }
675
+ return null;
676
+ }
677
+ function alignOf(dom) {
678
+ // The first one that says anything: an element with no inline style reports
679
+ // `''` for it, not null, so a nullish fallback would stop there.
680
+ const raw = [dom.getAttribute('data-align'), dom.style?.textAlign, dom.getAttribute('align')].find(Boolean) ?? '';
681
+ const word = raw.toLowerCase();
682
+ if (word === 'center')
683
+ return 'center';
684
+ if (word === 'start' || word === 'left')
685
+ return 'start';
686
+ if (word === 'end' || word === 'right')
687
+ return 'end';
688
+ return null;
689
+ }
690
+ function languageOf(dom) {
691
+ const code = dom.querySelector('code');
692
+ const named = dom.getAttribute('data-language') ?? code?.getAttribute('data-language');
693
+ const fromClass = /(?:^|\s)language-(\S+)/.exec(code?.getAttribute('class') ?? '')?.[1];
694
+ return languageValue(named ?? fromClass ?? null);
695
+ }
696
+ function orderOf(dom) {
697
+ const start = Number.parseInt(dom.getAttribute('start') ?? '', 10);
698
+ return Number.isFinite(start) ? Math.min(Math.max(start, 0), MAX_ORDER) : 1;
699
+ }
700
+ const nodes = {
701
+ doc: { content: 'block+' },
702
+ paragraph: {
703
+ group: 'block',
704
+ content: 'inline*',
705
+ parseDOM: [{ tag: 'p' }],
706
+ toDOM: () => ['p', 0],
707
+ },
708
+ heading: {
709
+ group: 'block',
710
+ content: 'inline*',
711
+ defining: true,
712
+ attrs: {
713
+ level: {
714
+ default: 1,
715
+ // A FUNCTION, not `'number'`: the level becomes a tag name in `toDOM`,
716
+ // and a type check alone accepts `{ level: 9 }`.
717
+ validate: (value) => {
718
+ if (value !== 1 && value !== 2 && value !== 3)
719
+ throw new RangeError('<wr-editor>: heading level is 1–3');
720
+ },
721
+ },
722
+ },
723
+ // h4–h6 clamp to h3 rather than falling through to a paragraph, which would
724
+ // lose the fact that the line was a heading at all.
725
+ parseDOM: [
726
+ { tag: 'h1', attrs: { level: 1 } },
727
+ { tag: 'h2', attrs: { level: 2 } },
728
+ { tag: 'h3', attrs: { level: 3 } },
729
+ { tag: 'h4', attrs: { level: 3 } },
730
+ { tag: 'h5', attrs: { level: 3 } },
731
+ { tag: 'h6', attrs: { level: 3 } },
732
+ ],
733
+ toDOM: node => [`h${Math.min(Math.max(Number(node.attrs['level']) || 1, 1), 3)}`, 0],
734
+ },
735
+ blockquote: {
736
+ group: 'block',
737
+ content: 'block+',
738
+ defining: true,
739
+ parseDOM: [{ tag: 'blockquote' }],
740
+ toDOM: () => ['blockquote', 0],
741
+ },
742
+ code_block: {
743
+ group: 'block',
744
+ content: 'text*',
745
+ marks: '',
746
+ code: true,
747
+ defining: true,
748
+ attrs: {
749
+ language: {
750
+ default: null,
751
+ validate: (value) => {
752
+ if (value !== null && (typeof value !== 'string' || languageValue(value) !== value)) {
753
+ throw new RangeError('<wr-editor>: a code language is one lowercase word');
754
+ }
755
+ },
756
+ },
757
+ },
758
+ parseDOM: [{ tag: 'pre', preserveWhitespace: 'full', getAttrs: dom => ({ language: languageOf(dom) }) }],
759
+ toDOM: node => {
760
+ const language = node.attrs['language'];
761
+ return ['pre', ['code', language ? { class: `language-${language}`, 'data-language': language } : {}, 0]];
762
+ },
763
+ },
764
+ horizontal_rule: {
765
+ group: 'block',
766
+ parseDOM: [{ tag: 'hr' }],
767
+ toDOM: () => ['hr'],
768
+ },
769
+ bullet_list: {
770
+ group: 'block',
771
+ content: 'list_item+',
772
+ attrs: { tight: { default: true, validate: 'boolean' } },
773
+ parseDOM: [{ tag: 'ul' }],
774
+ toDOM: () => ['ul', 0],
775
+ },
776
+ ordered_list: {
777
+ group: 'block',
778
+ content: 'list_item+',
779
+ attrs: {
780
+ order: {
781
+ default: 1,
782
+ validate: (value) => {
783
+ if (!Number.isInteger(value) || value < 0 || value > MAX_ORDER) {
784
+ throw new RangeError('<wr-editor>: a list starts at a whole number');
785
+ }
786
+ },
787
+ },
788
+ tight: { default: true, validate: 'boolean' },
789
+ },
790
+ parseDOM: [{ tag: 'ol', getAttrs: dom => ({ order: orderOf(dom) }) }],
791
+ toDOM: node => (node.attrs['order'] === 1 ? ['ol', 0] : ['ol', { start: String(node.attrs['order']) }, 0]),
792
+ },
793
+ list_item: {
794
+ content: 'paragraph block*',
795
+ defining: true,
796
+ attrs: {
797
+ checked: {
798
+ default: null,
799
+ validate: (value) => {
800
+ if (value !== null && typeof value !== 'boolean')
801
+ throw new RangeError('<wr-editor>: checked is a boolean');
802
+ },
803
+ },
804
+ },
805
+ parseDOM: [{ tag: 'li', getAttrs: dom => ({ checked: checkedOf(dom) }) }],
806
+ toDOM: node => {
807
+ const checked = node.attrs['checked'];
808
+ return checked === null ? ['li', 0] : ['li', { 'data-checked': String(checked) }, 0];
809
+ },
810
+ },
811
+ table: {
812
+ group: 'block',
813
+ content: 'table_row+',
814
+ isolating: true,
815
+ parseDOM: [{ tag: 'table' }],
816
+ toDOM: () => ['table', ['tbody', 0]],
817
+ },
818
+ table_row: {
819
+ content: '(table_header | table_cell)+',
820
+ parseDOM: [{ tag: 'tr' }],
821
+ toDOM: () => ['tr', 0],
822
+ },
823
+ table_header: cell('th'),
824
+ table_cell: cell('td'),
825
+ text: { group: 'inline' },
826
+ image: {
827
+ group: 'inline',
828
+ inline: true,
829
+ draggable: true,
830
+ attrs: {
831
+ src: { validate: requireUrl('image') },
832
+ alt: { default: '', validate: 'string' },
833
+ title: { default: null, validate: nullableString },
834
+ },
835
+ parseDOM: [{ tag: 'img[src]', getAttrs: imageAttrs }],
836
+ toDOM: node => {
837
+ // Re-asked here: validators do not run on `type.create()`, so a node built
838
+ // in code never passed through them.
839
+ const src = safeMarkdownUrl(String(node.attrs['src'] ?? ''), 'image');
840
+ const title = node.attrs['title'];
841
+ return ['img', { src: src ?? '', alt: String(node.attrs['alt'] ?? ''), ...(title ? { title } : {}) }];
842
+ },
843
+ },
844
+ hard_break: {
845
+ group: 'inline',
846
+ inline: true,
847
+ selectable: false,
848
+ linebreakReplacement: true,
849
+ parseDOM: [{ tag: 'br' }],
850
+ toDOM: () => ['br'],
851
+ },
852
+ };
853
+ function cell(tag) {
854
+ return {
855
+ content: 'paragraph+',
856
+ isolating: true,
857
+ attrs: {
858
+ align: {
859
+ default: null,
860
+ validate: (value) => {
861
+ if (!ALIGNS.has(value))
862
+ throw new RangeError('<wr-editor>: align is start, center or end');
863
+ },
864
+ },
865
+ },
866
+ parseDOM: [{ tag, getAttrs: dom => ({ align: alignOf(dom) }) }],
867
+ // The value is one of three words — checked by the validator, and again
868
+ // here — so writing it into `style` cannot carry anything the input chose.
869
+ toDOM: node => {
870
+ const align = node.attrs['align'];
871
+ return ALIGNS.has(align) && align ? [tag, { style: `text-align: ${align}` }, 0] : [tag, 0];
872
+ },
873
+ };
874
+ }
875
+ /**
876
+ * Marks in nesting order, outermost first — which is also how `toDOM` nests
877
+ * them in the HTML this writes.
878
+ */
879
+ function marks(underline) {
880
+ return {
881
+ link: {
882
+ inclusive: false,
883
+ attrs: {
884
+ href: { validate: requireUrl('link') },
885
+ title: { default: null, validate: nullableString },
886
+ },
887
+ parseDOM: [{ tag: 'a[href]', getAttrs: linkAttrs }],
888
+ toDOM: (mark) => {
889
+ const href = safeMarkdownUrl(String(mark.attrs['href'] ?? ''), 'link');
890
+ if (!href)
891
+ return ['span', 0];
892
+ const title = mark.attrs['title'];
893
+ return ['a', { href, ...(title ? { title } : {}), rel: 'noopener noreferrer nofollow' }, 0];
894
+ },
895
+ },
896
+ strong: {
897
+ parseDOM: [
898
+ { tag: 'strong' },
899
+ // Google Docs wraps a whole paste in `<b style="font-weight: normal">`.
900
+ { tag: 'b', getAttrs: dom => dom.style.fontWeight !== 'normal' && null },
901
+ { style: 'font-weight=400', clearMark: mark => mark.type.name === 'strong' },
902
+ { style: 'font-weight', getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null },
903
+ ],
904
+ toDOM: () => ['strong', 0],
905
+ },
906
+ em: {
907
+ parseDOM: [
908
+ { tag: 'i' },
909
+ { tag: 'em' },
910
+ { style: 'font-style=italic' },
911
+ { style: 'font-style=normal', clearMark: mark => mark.type.name === 'em' },
912
+ ],
913
+ toDOM: () => ['em', 0],
914
+ },
915
+ ...(underline
916
+ ? {
917
+ underline: {
918
+ parseDOM: [
919
+ { tag: 'u' },
920
+ { style: 'text-decoration=underline' },
921
+ { style: 'text-decoration-line=underline' },
922
+ ],
923
+ toDOM: () => ['u', 0],
924
+ },
925
+ }
926
+ : {}),
927
+ strike: {
928
+ parseDOM: [
929
+ { tag: 's' },
930
+ { tag: 'del' },
931
+ { tag: 'strike' },
932
+ { style: 'text-decoration=line-through' },
933
+ { style: 'text-decoration-line=line-through' },
934
+ ],
935
+ toDOM: () => ['s', 0],
936
+ },
937
+ code: {
938
+ code: true,
939
+ parseDOM: [{ tag: 'code' }],
940
+ toDOM: () => ['code', 0],
941
+ },
942
+ };
943
+ }
944
+ let full = null;
945
+ let markdown = null;
946
+ /**
947
+ * The schema for a format, built on first use and shared after that.
948
+ *
949
+ * Markdown gets its own, one mark short: it has no underline, and a mark the
950
+ * value cannot carry would paint on screen and vanish on save — from a paste,
951
+ * where no toolbar button could have kept it out.
952
+ */
953
+ function editorSchema(format) {
954
+ if (format === 'markdown')
955
+ return (markdown ??= new Schema({ nodes, marks: marks(false) }));
956
+ return (full ??= new Schema({ nodes, marks: marks(true) }));
957
+ }
958
+ /** How many blockquotes and list items deep the document's deepest block sits. */
959
+ function nestingOf(doc) {
960
+ let deepest = 0;
961
+ const walk = (node, depth) => {
962
+ node.forEach(child => {
963
+ if (child.isTextblock || child.isLeaf)
964
+ return;
965
+ const next = child.type.name === 'blockquote' || child.type.name === 'list_item' ? depth + 1 : depth;
966
+ if (next > deepest)
967
+ deepest = next;
968
+ walk(child, next);
969
+ });
970
+ };
971
+ walk(doc, 0);
972
+ return deepest;
973
+ }
974
+ /**
975
+ * A document that is one empty textblock — the shape the placeholder is drawn
976
+ * on. Which textblock is open: an empty heading shows the hint as well.
977
+ *
978
+ * Not the test for "nothing written"; that is {@link isBlankDoc}.
979
+ */
980
+ function isEmptyDoc(doc) {
981
+ if (doc.childCount !== 1)
982
+ return false;
983
+ const only = doc.firstChild;
984
+ return only.isTextblock && only.childCount === 0;
985
+ }
986
+ /**
987
+ * A document with nothing written in it: no text but whitespace, and no leaf
988
+ * but line breaks. An image or a rule is content in its own right; structure
989
+ * alone is not.
990
+ *
991
+ * Decided by content rather than by shape, because every one of these looks
992
+ * empty and markdown already wrote them as `''`: Enter on an empty editor (two
993
+ * empty paragraphs), a lone Shift-Enter, a few spaces, and a list, a quote or a
994
+ * heading chosen on an empty editor. Written as anything else, `required()`
995
+ * passed in `html` and `json` over an editor with nothing in it.
996
+ */
997
+ function isBlankDoc(doc) {
998
+ let written = false;
999
+ doc.descendants(node => {
1000
+ if (node.isText ? /\S/.test(node.text) : node.isLeaf && node.type.name !== 'hard_break')
1001
+ written = true;
1002
+ return !written;
1003
+ });
1004
+ return !written;
1005
+ }
1006
+
1007
+ /**
1008
+ * @license
1009
+ *
1010
+ * Use of this source code is governed by an MIT-style license that can be
1011
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
1012
+ */
1013
+ /**
1014
+ * The three formats meeting in one place. JSON is the pivot: markdown is walked
1015
+ * into it, HTML is parsed by the schema's own rules, and a bound JSON value is
1016
+ * validated like any other untrusted input.
1017
+ */
1018
+ /**
1019
+ * A bound value as a document in `schema`, or a thrown error naming why it was
1020
+ * refused. `null`, `undefined` and `''` are the empty document in every format.
1021
+ *
1022
+ * Throwing is the whole contract: the caller keeps the document it already has,
1023
+ * which is the rule `wr-input-number` and `wr-date-picker` follow for input they
1024
+ * cannot read. Nothing here ever returns a "repaired" document to write back.
1025
+ */
1026
+ function readValue(value, format, schema, document) {
1027
+ if (value === null || value === undefined || value === '')
1028
+ return schema.topNodeType.createAndFill();
1029
+ switch (format) {
1030
+ case 'json':
1031
+ if (typeof value === 'string')
1032
+ throw new RangeError('<wr-editor>: `format="json"` takes an object, not a string');
1033
+ return readJson(value, schema);
1034
+ case 'markdown':
1035
+ if (typeof value !== 'string')
1036
+ throw new RangeError('<wr-editor>: `format="markdown"` takes a string');
1037
+ return readJson(markdownToJson(value), schema);
1038
+ case 'html':
1039
+ if (typeof value !== 'string')
1040
+ throw new RangeError('<wr-editor>: `format="html"` takes a string');
1041
+ return readHtml(value, document, schema);
1042
+ }
1043
+ }
1044
+ /**
1045
+ * A document in the bound format — `''` or `null` when nothing is written in it,
1046
+ * the same in all three: see {@link isBlankDoc}.
1047
+ */
1048
+ function writeValue(doc, format, document) {
1049
+ if (isBlankDoc(doc))
1050
+ return format === 'json' ? null : '';
1051
+ switch (format) {
1052
+ case 'json':
1053
+ return doc.toJSON();
1054
+ case 'markdown':
1055
+ return jsonToMarkdown(doc.toJSON());
1056
+ case 'html':
1057
+ return writeHtml(doc, document);
1058
+ }
1059
+ }
1060
+
1061
+ /**
1062
+ * @license
1063
+ *
1064
+ * Use of this source code is governed by an MIT-style license that can be
1065
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
1066
+ */
1067
+ /**
1068
+ * The keyboard half of each tool — the keymap below binds exactly these, so a
1069
+ * hint can never name a shortcut that does nothing.
1070
+ *
1071
+ * Where a convention exists it is followed: Google Docs for the block types
1072
+ * (Mod-Alt-0…3, Mod-Shift-7/8), and Tiptap / Notion for the three it does not
1073
+ * cover (Mod-Shift-S strikethrough, Mod-E inline code, Mod-Shift-B quote,
1074
+ * Mod-Alt-C code block). The horizontal rule has none. Tab is deliberately not
1075
+ * bound — indenting with it would be a keyboard trap (WCAG 2.1.2), so list items
1076
+ * move with Mod-] and Mod-[ — and neither is Escape, which `<wr-dialog>` needs.
1077
+ *
1078
+ * Where a layout types `[` and `]` with AltGr or Option (German, French, the
1079
+ * Nordic and Polish layouts), the bracket chords are also read with that Alt
1080
+ * held — see {@link bracketKeys} — since nothing else indents a list item. The
1081
+ * Mod-Alt chords are NOT widened the same way: on Windows, Ctrl+Alt is AltGr, and
1082
+ * on those layouts AltGr+0 types `}` and AltGr+C types `ć`. Taking the chord by
1083
+ * physical key would make those characters impossible to type in the editor, so
1084
+ * there the key types its character and the toolbar sets the block type.
1085
+ */
1086
+ const SHORTCUTS = {
1087
+ bold: { key: 'B' },
1088
+ italic: { key: 'I' },
1089
+ underline: { key: 'U' },
1090
+ strike: { key: 'S', shift: true },
1091
+ code: { key: 'E' },
1092
+ paragraph: { key: '0', alt: true },
1093
+ heading1: { key: '1', alt: true },
1094
+ heading2: { key: '2', alt: true },
1095
+ heading3: { key: '3', alt: true },
1096
+ bulletList: { key: '8', shift: true },
1097
+ orderedList: { key: '7', shift: true },
1098
+ blockquote: { key: 'B', shift: true },
1099
+ codeBlock: { key: 'C', alt: true },
1100
+ link: { key: 'K' },
1101
+ undo: { key: 'Z' },
1102
+ redo: { key: 'Z', shift: true },
1103
+ };
1104
+ /**
1105
+ * The tools a click toggles — the ones that carry `aria-pressed`. Not `link`,
1106
+ * which opens a panel: a popup button that also claimed to be pressed would say
1107
+ * two contradictory things.
1108
+ */
1109
+ const TOGGLES = new Set([
1110
+ 'bold',
1111
+ 'italic',
1112
+ 'underline',
1113
+ 'strike',
1114
+ 'code',
1115
+ 'paragraph',
1116
+ 'heading1',
1117
+ 'heading2',
1118
+ 'heading3',
1119
+ 'bulletList',
1120
+ 'orderedList',
1121
+ 'blockquote',
1122
+ 'codeBlock',
1123
+ ]);
1124
+ // ---------------------------------------------------------------------------
1125
+ // State
1126
+ // ---------------------------------------------------------------------------
1127
+ function markActive(state, type) {
1128
+ if (!type)
1129
+ return false;
1130
+ const { empty, from, to, $from } = state.selection;
1131
+ if (empty)
1132
+ return !!type.isInSet(state.storedMarks ?? $from.marks());
1133
+ return state.doc.rangeHasMark(from, to, type);
1134
+ }
1135
+ /**
1136
+ * Whether every textblock the selection touches is `type` — asked of the blocks
1137
+ * themselves rather than of `$from.parent`, which is the DOCUMENT under a
1138
+ * select-all.
1139
+ */
1140
+ function parentIs(state, type, attrs) {
1141
+ const { empty, from, to, $from } = state.selection;
1142
+ if (empty)
1143
+ return $from.parent.hasMarkup(type, attrs);
1144
+ let found = false;
1145
+ let all = true;
1146
+ state.doc.nodesBetween(from, to, node => {
1147
+ if (!node.isTextblock)
1148
+ return true;
1149
+ found = true;
1150
+ if (!node.hasMarkup(type, attrs))
1151
+ all = false;
1152
+ return false;
1153
+ });
1154
+ return found && all;
1155
+ }
1156
+ /** The nearest list around the selection, with the position it starts at. */
1157
+ function nearestList(state) {
1158
+ const { $from } = state.selection;
1159
+ for (let depth = $from.depth; depth > 0; depth--) {
1160
+ const node = $from.node(depth);
1161
+ if (node.type.name === 'bullet_list' || node.type.name === 'ordered_list') {
1162
+ return { type: node.type, pos: $from.before(depth) };
1163
+ }
1164
+ }
1165
+ return null;
1166
+ }
1167
+ function insideNode(state, type) {
1168
+ const { $from } = state.selection;
1169
+ for (let depth = $from.depth; depth > 0; depth--)
1170
+ if ($from.node(depth).type === type)
1171
+ return true;
1172
+ return false;
1173
+ }
1174
+ /** Whether a tool's state is "on" at the selection — what `aria-pressed` reports. */
1175
+ function toolActive(tool, state) {
1176
+ const { marks, nodes } = state.schema;
1177
+ switch (tool) {
1178
+ case 'bold':
1179
+ return markActive(state, marks['strong']);
1180
+ case 'italic':
1181
+ return markActive(state, marks['em']);
1182
+ case 'underline':
1183
+ return markActive(state, marks['underline']);
1184
+ case 'strike':
1185
+ return markActive(state, marks['strike']);
1186
+ case 'code':
1187
+ return markActive(state, marks['code']);
1188
+ case 'link':
1189
+ return linkRange(state) !== null;
1190
+ case 'paragraph':
1191
+ return parentIs(state, nodes['paragraph']);
1192
+ case 'heading1':
1193
+ return parentIs(state, nodes['heading'], { level: 1 });
1194
+ case 'heading2':
1195
+ return parentIs(state, nodes['heading'], { level: 2 });
1196
+ case 'heading3':
1197
+ return parentIs(state, nodes['heading'], { level: 3 });
1198
+ case 'codeBlock':
1199
+ return parentIs(state, nodes['code_block']);
1200
+ case 'bulletList':
1201
+ return nearestList(state)?.type === nodes['bullet_list'];
1202
+ case 'orderedList':
1203
+ return nearestList(state)?.type === nodes['ordered_list'];
1204
+ case 'blockquote':
1205
+ return insideNode(state, nodes['blockquote']);
1206
+ default:
1207
+ return false;
1208
+ }
1209
+ }
1210
+ // ---------------------------------------------------------------------------
1211
+ // Commands
1212
+ // ---------------------------------------------------------------------------
1213
+ /** What a tool does, or `null` for one the schema cannot carry (underline in markdown). */
1214
+ function toolCommand(tool, schema) {
1215
+ const { marks, nodes } = schema;
1216
+ const mark = (type) => (type ? toggleMark(type) : null);
1217
+ const heading = (level) => toggleBlock(nodes['heading'], nodes['paragraph'], { level });
1218
+ switch (tool) {
1219
+ case 'bold':
1220
+ return mark(marks['strong']);
1221
+ case 'italic':
1222
+ return mark(marks['em']);
1223
+ case 'underline':
1224
+ return mark(marks['underline']);
1225
+ case 'strike':
1226
+ return mark(marks['strike']);
1227
+ case 'code':
1228
+ return mark(marks['code']);
1229
+ // The link tool opens a panel; `setLink` is its command.
1230
+ case 'link':
1231
+ return toggleMark(marks['link'], { href: '#' });
1232
+ case 'paragraph':
1233
+ return setBlockType(nodes['paragraph']);
1234
+ case 'heading1':
1235
+ return heading(1);
1236
+ case 'heading2':
1237
+ return heading(2);
1238
+ case 'heading3':
1239
+ return heading(3);
1240
+ case 'codeBlock':
1241
+ return toggleBlock(nodes['code_block'], nodes['paragraph']);
1242
+ case 'bulletList':
1243
+ return toggleList(nodes['bullet_list'], nodes['list_item']);
1244
+ case 'orderedList':
1245
+ return toggleList(nodes['ordered_list'], nodes['list_item']);
1246
+ case 'blockquote':
1247
+ return toggleQuote(nodes['blockquote']);
1248
+ case 'horizontalRule':
1249
+ return insertRule(nodes['horizontal_rule']);
1250
+ case 'undo':
1251
+ return undo;
1252
+ case 'redo':
1253
+ return redo;
1254
+ }
1255
+ }
1256
+ /** A block type that switches back to a paragraph when it is already on. */
1257
+ function toggleBlock(type, paragraph, attrs) {
1258
+ return (state, dispatch) => parentIs(state, type, attrs)
1259
+ ? setBlockType(paragraph)(state, dispatch)
1260
+ : setBlockType(type, attrs)(state, dispatch);
1261
+ }
1262
+ /**
1263
+ * A list toggle: out of this kind of list, over to it from the other kind, or
1264
+ * into a new one.
1265
+ */
1266
+ function toggleList(type, item) {
1267
+ return (state, dispatch) => {
1268
+ const list = nearestList(state);
1269
+ if (!list)
1270
+ return wrapInList(type)(state, dispatch);
1271
+ if (list.type === type)
1272
+ return liftListItem(item)(state, dispatch);
1273
+ if (dispatch) {
1274
+ const tight = state.doc.nodeAt(list.pos)?.attrs['tight'] !== false;
1275
+ const attrs = type.name === 'ordered_list' ? { order: 1, tight } : { tight };
1276
+ dispatch(state.tr.setNodeMarkup(list.pos, type, attrs).scrollIntoView());
1277
+ }
1278
+ return true;
1279
+ };
1280
+ }
1281
+ /**
1282
+ * Into a quote, or out of the nearest one.
1283
+ *
1284
+ * Out is a hand-rolled lift rather than `prosemirror-commands`' `lift`, which
1285
+ * lifts out of whatever the selection's parent is — inside a quoted list that is
1286
+ * the list item, not the quote. The target depth is the quote's own parent; a
1287
+ * lift that does not fit there throws, and the throw is the "cannot".
1288
+ */
1289
+ function toggleQuote(quote) {
1290
+ return (state, dispatch) => {
1291
+ const { $from, $to } = state.selection;
1292
+ const range = insideNode(state, quote) ? $from.blockRange($to, node => node.type === quote) : null;
1293
+ if (!range)
1294
+ return wrapIn(quote)(state, dispatch);
1295
+ try {
1296
+ const tr = state.tr.lift(range, range.depth - 1);
1297
+ if (dispatch)
1298
+ dispatch(tr.scrollIntoView());
1299
+ return true;
1300
+ }
1301
+ catch {
1302
+ return false;
1303
+ }
1304
+ };
1305
+ }
1306
+ /**
1307
+ * A horizontal rule at the selection, where one fits without leaving an
1308
+ * isolating node — a table cell holds paragraphs only, and splitting the table
1309
+ * around a rule is not what anyone clicking the button meant.
1310
+ */
1311
+ function insertRule(rule) {
1312
+ return (state, dispatch) => {
1313
+ const { $from } = state.selection;
1314
+ let fits = false;
1315
+ for (let depth = $from.depth; depth >= 0; depth--) {
1316
+ const node = $from.node(depth);
1317
+ const index = $from.index(depth);
1318
+ if (node.canReplaceWith(index, index, rule)) {
1319
+ fits = true;
1320
+ break;
1321
+ }
1322
+ if (node.type.spec.isolating)
1323
+ break;
1324
+ }
1325
+ if (!fits)
1326
+ return false;
1327
+ if (dispatch)
1328
+ dispatch(state.tr.replaceSelectionWith(rule.create()).scrollIntoView());
1329
+ return true;
1330
+ };
1331
+ }
1332
+ // ---------------------------------------------------------------------------
1333
+ // Tables
1334
+ // ---------------------------------------------------------------------------
1335
+ /** The table around the selection, as a depth of `$from`, or `-1`. */
1336
+ function tableDepth(state) {
1337
+ const { $from } = state.selection;
1338
+ for (let depth = $from.depth; depth > 0; depth--)
1339
+ if ($from.node(depth).type.name === 'table')
1340
+ return depth;
1341
+ return -1;
1342
+ }
1343
+ /**
1344
+ * An empty paragraph beside the table at `depth`, with the caret in it — the one
1345
+ * way to a line the keyboard cannot otherwise reach. There is no gap cursor here,
1346
+ * and a cell holds paragraphs, so Enter only ever splits inside it.
1347
+ */
1348
+ function paragraphBeside(state, depth, side, dispatch) {
1349
+ const { $from } = state.selection;
1350
+ const paragraph = state.schema.nodes['paragraph'];
1351
+ const parent = $from.node(depth - 1);
1352
+ const index = $from.index(depth - 1) + (side === 'after' ? 1 : 0);
1353
+ if (!parent.canReplaceWith(index, index, paragraph))
1354
+ return false;
1355
+ if (dispatch) {
1356
+ const pos = side === 'after' ? $from.after(depth) : $from.before(depth);
1357
+ const tr = state.tr.insert(pos, paragraph.create());
1358
+ dispatch(tr.setSelection(TextSelection.create(tr.doc, pos + 1)).scrollIntoView());
1359
+ }
1360
+ return true;
1361
+ }
1362
+ /** Mod-Enter in a table: out of it, onto a new line below — what `exitCode` does for a code block. */
1363
+ const exitTable = (state, dispatch) => {
1364
+ const depth = tableDepth(state);
1365
+ return depth > 0 && paragraphBeside(state, depth, 'after', dispatch);
1366
+ };
1367
+ /**
1368
+ * The arrow key off the edge of a table nothing can be reached past: ArrowDown on
1369
+ * the last line of its last row with no text anywhere after it, ArrowUp on the
1370
+ * first line of its first row with none before. Anywhere else the key moves the
1371
+ * caret as it always does. Up and down only — left and right swap ends under
1372
+ * `dir="rtl"`, and the vertical pair means the same thing in both directions.
1373
+ */
1374
+ function leaveTable(side) {
1375
+ return (state, dispatch, view) => {
1376
+ if (view && !view.editable)
1377
+ return false;
1378
+ const depth = tableDepth(state);
1379
+ const { $from, empty } = state.selection;
1380
+ if (depth < 0 || !empty)
1381
+ return false;
1382
+ // table > row > cell > paragraph: the row's place in the table, and the
1383
+ // paragraph's in its cell.
1384
+ const table = $from.node(depth);
1385
+ const cell = $from.node(depth + 2);
1386
+ const row = $from.index(depth);
1387
+ const block = $from.index(depth + 2);
1388
+ const edge = side === 'after' ? row === table.childCount - 1 && block === cell.childCount - 1 : row === 0 && block === 0;
1389
+ if (!edge)
1390
+ return false;
1391
+ const onEdgeLine = view
1392
+ ? view.endOfTextblock(side === 'after' ? 'down' : 'up')
1393
+ : $from.parentOffset === (side === 'after' ? $from.parent.content.size : 0);
1394
+ if (!onEdgeLine)
1395
+ return false;
1396
+ const outside = state.doc.resolve(side === 'after' ? $from.after(depth) : $from.before(depth));
1397
+ if (TextSelection.findFrom(outside, side === 'after' ? 1 : -1, true))
1398
+ return false;
1399
+ return paragraphBeside(state, depth, side, dispatch);
1400
+ };
1401
+ }
1402
+ function insertHardBreak(state, dispatch) {
1403
+ const type = state.schema.nodes['hard_break'];
1404
+ const { $from } = state.selection;
1405
+ if (!$from.parent.canReplaceWith($from.index(), $from.index(), type))
1406
+ return false;
1407
+ if (dispatch)
1408
+ dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView());
1409
+ return true;
1410
+ }
1411
+ // ---------------------------------------------------------------------------
1412
+ // Links
1413
+ // ---------------------------------------------------------------------------
1414
+ /**
1415
+ * The run of text carrying one link around the selection — the whole link, when
1416
+ * the caret merely sits in it — or `null` when there is none.
1417
+ */
1418
+ function linkRange(state) {
1419
+ const type = state.schema.marks['link'];
1420
+ const { empty, from, to, $from } = state.selection;
1421
+ if (!empty) {
1422
+ let found = null;
1423
+ state.doc.nodesBetween(from, to, node => {
1424
+ found ??= type.isInSet(node.marks) ?? null;
1425
+ });
1426
+ return found ? { from, to, mark: found } : null;
1427
+ }
1428
+ return markAround($from, type);
1429
+ }
1430
+ function markAround($pos, type) {
1431
+ const parent = $pos.parent;
1432
+ const start = $pos.start();
1433
+ const offsets = [];
1434
+ parent.forEach((_child, offset) => offsets.push(offset));
1435
+ // The child the caret is in — or, at a boundary, the one before it and then
1436
+ // the one after, since a non-inclusive mark does not report itself at its end.
1437
+ const index = $pos.index();
1438
+ const candidates = ($pos.textOffset > 0 ? [index] : [index - 1, index]).filter(i => i >= 0 && i < parent.childCount);
1439
+ for (const at of candidates) {
1440
+ const mark = type.isInSet(parent.child(at).marks);
1441
+ if (!mark)
1442
+ continue;
1443
+ let first = at;
1444
+ let last = at;
1445
+ while (first > 0 && mark.isInSet(parent.child(first - 1).marks))
1446
+ first--;
1447
+ while (last < parent.childCount - 1 && mark.isInSet(parent.child(last + 1).marks))
1448
+ last++;
1449
+ return { from: start + offsets[first], to: start + offsets[last] + parent.child(last).nodeSize, mark };
1450
+ }
1451
+ return null;
1452
+ }
1453
+ /**
1454
+ * Link the selection to `href` — replacing the link it sits in when the caret
1455
+ * is inside one, and inserting the address as its own text when nothing is
1456
+ * selected at all. `href` has already passed the URL policy.
1457
+ */
1458
+ function setLink(href) {
1459
+ return (state, dispatch) => {
1460
+ const type = state.schema.marks['link'];
1461
+ const { empty, from, to } = state.selection;
1462
+ const existing = linkRange(state);
1463
+ const title = existing?.mark.attrs['title'] ?? null;
1464
+ const mark = type.create({ href, title });
1465
+ if (!toggleMark(type, { href })(state))
1466
+ return false;
1467
+ if (!dispatch)
1468
+ return true;
1469
+ const tr = state.tr;
1470
+ if (!empty)
1471
+ tr.removeMark(from, to, type).addMark(from, to, mark);
1472
+ else if (existing)
1473
+ tr.removeMark(existing.from, existing.to, type).addMark(existing.from, existing.to, mark);
1474
+ else
1475
+ tr.insertText(href, from).addMark(from, from + href.length, mark);
1476
+ dispatch(tr.scrollIntoView());
1477
+ return true;
1478
+ };
1479
+ }
1480
+ /** Remove the link the selection is in or covers. */
1481
+ const removeLink = (state, dispatch) => {
1482
+ const range = linkRange(state);
1483
+ if (!range)
1484
+ return false;
1485
+ if (dispatch)
1486
+ dispatch(state.tr.removeMark(range.from, range.to, state.schema.marks['link']).scrollIntoView());
1487
+ return true;
1488
+ };
1489
+ // ---------------------------------------------------------------------------
1490
+ // Plugins
1491
+ // ---------------------------------------------------------------------------
1492
+ /**
1493
+ * The editor's plugin stack, in precedence order: its own keys, history keys,
1494
+ * ProseMirror's base keymap, then history and the markdown-style input rules.
1495
+ *
1496
+ * `canEdit` is asked for every document-changing transaction, because
1497
+ * `view.dispatch()` does not consult `editable` — a toolbar click or a plugin
1498
+ * would otherwise change a read-only document. Outside writes never pass here:
1499
+ * they replace the state wholesale.
1500
+ *
1501
+ * The input rules are the block shortcuts only. The typographic ones (smart
1502
+ * quotes, the em dash, the ellipsis) are left out on purpose: in a form control
1503
+ * they silently rewrite what someone typed into a name, a code or a reference.
1504
+ */
1505
+ function editorPlugins(schema, options) {
1506
+ const { nodes } = schema;
1507
+ const keys = {};
1508
+ const bind = (combo, command) => {
1509
+ if (!command)
1510
+ return;
1511
+ keys[combo] = command;
1512
+ // `event.key` is upper case under Shift or Caps Lock, and ProseMirror
1513
+ // matches on it; bound twice, the way `prosemirror-example-setup` does.
1514
+ const last = combo.slice(combo.lastIndexOf('-') + 1);
1515
+ if (/^[a-z]$/.test(last))
1516
+ keys[combo.slice(0, -1) + last.toUpperCase()] = command;
1517
+ };
1518
+ for (const [tool, shortcut] of Object.entries(SHORTCUTS)) {
1519
+ if (tool === 'link' || tool === 'undo' || tool === 'redo')
1520
+ continue;
1521
+ bind(comboOf(shortcut), toolCommand(tool, schema));
1522
+ }
1523
+ bind('Mod-z', undo);
1524
+ bind('Mod-Shift-z', redo);
1525
+ if (!options.apple)
1526
+ bind('Mod-y', redo);
1527
+ keys['Shift-Enter'] = chainCommands(exitCode, insertHardBreak);
1528
+ keys['Mod-Enter'] = chainCommands(exitCode, exitTable);
1529
+ keys['Enter'] = splitListItem(nodes['list_item']);
1530
+ keys['Mod-['] = liftListItem(nodes['list_item']);
1531
+ keys['Mod-]'] = sinkListItem(nodes['list_item']);
1532
+ keys['ArrowDown'] = leaveTable('after');
1533
+ keys['ArrowUp'] = leaveTable('before');
1534
+ keys['Backspace'] = undoInputRule;
1535
+ return [
1536
+ new Plugin({
1537
+ filterTransaction: (tr, state) => {
1538
+ if (!tr.docChanged)
1539
+ return true;
1540
+ if (!options.canEdit())
1541
+ return false;
1542
+ // `MAX_NESTING` is the deepest markdown reads back with its structure. A
1543
+ // document that already nests deeper (a bound HTML or JSON value) is kept
1544
+ // as it is; what is refused is an edit that makes it deeper still.
1545
+ const depth = nestingOf(tr.doc);
1546
+ return depth <= MAX_NESTING || depth <= nestingOf(state.doc);
1547
+ },
1548
+ }),
1549
+ bracketKeys(nodes['list_item'], options.apple),
1550
+ keymap(keys),
1551
+ keymap(baseKeymap),
1552
+ history(),
1553
+ inputRules({
1554
+ rules: [
1555
+ textblockTypeInputRule(/^(#{1,3})\s$/, nodes['heading'], match => ({ level: match[1].length })),
1556
+ wrappingInputRule(/^\s*>\s$/, nodes['blockquote']),
1557
+ wrappingInputRule(/^\s*([-+*])\s$/, nodes['bullet_list']),
1558
+ wrappingInputRule(/^(\d{1,9})\.\s$/, nodes['ordered_list'], match => ({ order: Number(match[1]) }), (match, node) => node.childCount + node.attrs['order'] === Number(match[1])),
1559
+ textblockTypeInputRule(/^```$/, nodes['code_block']),
1560
+ ],
1561
+ }),
1562
+ ];
1563
+ }
1564
+ /**
1565
+ * Mod-] and Mod-[ as they are TYPED on a layout that needs AltGr or Option for a
1566
+ * bracket: Windows German reports AltGr+8 as Ctrl+Alt with `key` `[`, macOS
1567
+ * German reports Option+5 under ⌘ as ⌘+Alt with `key` `[`. The keymap matches on
1568
+ * the full modifier set, so neither reaches `Mod-[`, and indenting a list item has
1569
+ * no other path — Tab is not bound and the toolbar has no indent tool.
1570
+ *
1571
+ * Matched on the character, never on `event.code`: the physical key at `]` is `+`
1572
+ * on a German keyboard, and Ctrl and `+` is the browser's zoom.
1573
+ */
1574
+ function bracketKeys(item, apple) {
1575
+ const lift = liftListItem(item);
1576
+ const sink = sinkListItem(item);
1577
+ return new Plugin({
1578
+ props: {
1579
+ handleKeyDown: (view, event) => {
1580
+ if (!event.altKey || event.shiftKey || (event.key !== '[' && event.key !== ']'))
1581
+ return false;
1582
+ const mod = apple ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey;
1583
+ return mod && (event.key === ']' ? sink : lift)(view.state, view.dispatch);
1584
+ },
1585
+ },
1586
+ });
1587
+ }
1588
+ /** A shortcut in ProseMirror's key-name syntax. */
1589
+ function comboOf(shortcut) {
1590
+ return `Mod-${shortcut.alt ? 'Alt-' : ''}${shortcut.shift ? 'Shift-' : ''}${shortcut.key.toLowerCase()}`;
1591
+ }
1592
+
1593
+ /**
1594
+ * @license
1595
+ *
1596
+ * Use of this source code is governed by an MIT-style license that can be
1597
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
1598
+ */
1599
+ /**
1600
+ * Toolbar glyphs as path data, drawn inline so the editor needs no
1601
+ * `provideWrIcons()` — the same reason `<wr-textarea>` inlines its grip.
1602
+ *
1603
+ * Lucide's (ISC), on its 24-unit grid with a 2-unit stroke; `line` elements are
1604
+ * rewritten as the equivalent paths so one `<path>` loop draws every icon.
1605
+ */
1606
+ const TOOL_ICONS = {
1607
+ bold: ['M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8'],
1608
+ italic: ['M19 4h-9', 'M14 20H5', 'M15 4 9 20'],
1609
+ underline: ['M6 4v6a6 6 0 0 0 12 0V4', 'M4 20h16'],
1610
+ strike: ['M16 4H9a3 3 0 0 0-2.83 4', 'M14 12a4 4 0 0 1 0 8H6', 'M4 12h16'],
1611
+ code: ['m16 18 6-6-6-6', 'm8 6-6 6 6 6'],
1612
+ paragraph: ['M13 4v16', 'M17 4v16', 'M19 4H9.5a4.5 4.5 0 0 0 0 9H13'],
1613
+ heading1: ['M4 12h8', 'M4 18V6', 'M12 18V6', 'm17 12 3-2v8'],
1614
+ heading2: ['M4 12h8', 'M4 18V6', 'M12 18V6', 'M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1'],
1615
+ heading3: [
1616
+ 'M4 12h8',
1617
+ 'M4 18V6',
1618
+ 'M12 18V6',
1619
+ 'M17.5 10.5c1.7-1 3.5 0 3.5 1.5a2 2 0 0 1-2 2',
1620
+ 'M17 17.5c2 1.5 4 .3 4-1.5a2 2 0 0 0-2-2',
1621
+ ],
1622
+ bulletList: ['M3 5h.01', 'M3 12h.01', 'M3 19h.01', 'M8 5h13', 'M8 12h13', 'M8 19h13'],
1623
+ orderedList: [
1624
+ 'M11 5h10',
1625
+ 'M11 12h10',
1626
+ 'M11 19h10',
1627
+ 'M4 4h1v5',
1628
+ 'M4 9h2',
1629
+ 'M6.5 20H3.4c0-1 2.6-1.925 2.6-3.5a1.5 1.5 0 0 0-2.6-1.02',
1630
+ ],
1631
+ blockquote: ['M17 5H3', 'M21 12H8', 'M21 19H8', 'M3 12v7'],
1632
+ codeBlock: [
1633
+ 'm10 9-3 3 3 3',
1634
+ 'm14 15 3-3-3-3',
1635
+ 'M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z',
1636
+ ],
1637
+ link: [
1638
+ 'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71',
1639
+ 'M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71',
1640
+ ],
1641
+ horizontalRule: ['M5 12h14'],
1642
+ undo: ['M9 14 4 9l5-5', 'M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11'],
1643
+ redo: ['m15 14 5-5-5-5', 'M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13'],
1644
+ };
1645
+
1646
+ /**
1647
+ * @license
1648
+ *
1649
+ * Use of this source code is governed by an MIT-style license that can be
1650
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
1651
+ */
1652
+ let nextId$1 = 0;
1653
+ /**
1654
+ * The field the link panel's URL input belongs to — provided as `WR_FORM_FIELD`
1655
+ * for the editor's whole subtree.
1656
+ *
1657
+ * It does two jobs at once. It is the SHIELD every composite control needs: the
1658
+ * editor is the control a `<wr-form-field>` wraps, and without a nearer provider
1659
+ * the `[wrInput]` in its link panel would adopt the outer field's id, take over
1660
+ * its label and announce its error as its own. And it is the input's own field:
1661
+ * `[wrInput]` binds `id`, `aria-invalid` and `aria-describedby` from whatever
1662
+ * field it finds, so answering here is how a refused URL reaches a screen reader
1663
+ * without binding attributes the directive already owns.
1664
+ *
1665
+ * @internal
1666
+ */
1667
+ class WrEditorLinkField {
1668
+ id = `wr-editor-link-${nextId$1++}`;
1669
+ /** The URL just submitted was refused by the link policy. */
1670
+ invalid = signal(false, /* @ts-ignore */
1671
+ ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
1672
+ controlId = signal(`${this.id}-url`).asReadonly();
1673
+ errorId = `${this.id}-error`;
1674
+ panelId = `${this.id}-panel`;
1675
+ errorKeys = computed(() => (this.invalid() ? ['url'] : []), /* @ts-ignore */
1676
+ ...(ngDevMode ? [{ debugName: "errorKeys" }] : /* istanbul ignore next */ []));
1677
+ describedBy = computed(() => (this.invalid() ? this.errorId : null), /* @ts-ignore */
1678
+ ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
1679
+ labelId = signal(null).asReadonly();
1680
+ adoptControlId() {
1681
+ // The panel's input never carries an id of its own.
1682
+ }
1683
+ }
1684
+
1685
+ /**
1686
+ * @license
1687
+ *
1688
+ * Use of this source code is governed by an MIT-style license that can be
1689
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
1690
+ */
1691
+ /**
1692
+ * List items as the editor draws them, which differs from the HTML it writes
1693
+ * only for a TASK item.
1694
+ *
1695
+ * A task item's box is presentational, with its state as text beside it —
1696
+ * exactly what `<wr-markdown>` renders. A real `<input type="checkbox">` would be
1697
+ * an unlabelled form control inside the text, and making it operable would
1698
+ * promise an interaction v1 does not have. The box and the words sit outside the
1699
+ * editable content, so ProseMirror neither edits nor serializes them; the words
1700
+ * come from the catalog, which is why this is a node view the component builds
1701
+ * rather than a schema `toDOM` (the schema is shared and has no catalog).
1702
+ */
1703
+ function listItemView(labels) {
1704
+ return (node, view) => new WrListItemView(node, view.dom.ownerDocument, labels);
1705
+ }
1706
+ class WrListItemView {
1707
+ dom;
1708
+ contentDOM;
1709
+ node;
1710
+ constructor(node, document, labels) {
1711
+ this.node = node;
1712
+ this.dom = document.createElement('li');
1713
+ const checked = node.attrs['checked'];
1714
+ if (checked === null) {
1715
+ this.contentDOM = this.dom;
1716
+ return;
1717
+ }
1718
+ this.dom.className = 'wr-editor__item wr-editor__item--task';
1719
+ this.dom.setAttribute('data-checked', String(checked));
1720
+ const box = document.createElement('span');
1721
+ box.className = checked ? 'wr-editor__task wr-editor__task--checked' : 'wr-editor__task';
1722
+ box.contentEditable = 'false';
1723
+ box.setAttribute('aria-hidden', 'true');
1724
+ const state = document.createElement('span');
1725
+ state.className = 'wr-editor__sr-only';
1726
+ state.contentEditable = 'false';
1727
+ state.textContent = checked ? labels.done : labels.todo;
1728
+ this.contentDOM = document.createElement('div');
1729
+ this.contentDOM.className = 'wr-editor__task-body';
1730
+ this.dom.append(box, state, this.contentDOM);
1731
+ }
1732
+ update(node) {
1733
+ // A task item becoming a plain one (or the reverse, or toggled) is drawn
1734
+ // again from scratch; there is no in-place way to add or drop the box.
1735
+ if (node.type !== this.node.type || node.attrs['checked'] !== this.node.attrs['checked'])
1736
+ return false;
1737
+ this.node = node;
1738
+ return true;
1739
+ }
1740
+ ignoreMutation(mutation) {
1741
+ if (mutation.type === 'selection' || this.contentDOM === this.dom)
1742
+ return false;
1743
+ return !this.contentDOM.contains(mutation.target);
1744
+ }
1745
+ }
1746
+
1747
+ /**
1748
+ * @license
1749
+ *
1750
+ * Use of this source code is governed by an MIT-style license that can be
1751
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
1752
+ */
1753
+ /**
1754
+ * The toolbar `<wr-editor>` draws when `toolbar` is not bound — every tool, in
1755
+ * five groups. Exported so a toolbar can be built from it rather than retyped:
1756
+ *
1757
+ * ```ts
1758
+ * readonly tools = WR_EDITOR_TOOLBAR.filter(tool => tool !== 'code');
1759
+ * ```
1760
+ *
1761
+ * `underline` is dropped in `markdown` format whether it is listed or not.
1762
+ */
1763
+ const WR_EDITOR_TOOLBAR = [
1764
+ 'bold',
1765
+ 'italic',
1766
+ 'underline',
1767
+ 'strike',
1768
+ 'code',
1769
+ '|',
1770
+ 'paragraph',
1771
+ 'heading1',
1772
+ 'heading2',
1773
+ 'heading3',
1774
+ '|',
1775
+ 'bulletList',
1776
+ 'orderedList',
1777
+ 'blockquote',
1778
+ 'codeBlock',
1779
+ '|',
1780
+ 'link',
1781
+ 'horizontalRule',
1782
+ '|',
1783
+ 'undo',
1784
+ 'redo',
1785
+ ];
1786
+
1787
+ /**
1788
+ * @license
1789
+ *
1790
+ * Use of this source code is governed by an MIT-style license that can be
1791
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
1792
+ */
1793
+ let nextId = 0;
1794
+ function warn(message) {
1795
+ // eslint-disable-next-line no-console -- dev-mode validation
1796
+ console.warn(`[NGWR] <wr-editor>: ${message}`);
1797
+ }
1798
+ /** A document's JSON with every mark the target schema does not know dropped. */
1799
+ function withoutMarks(node, known) {
1800
+ return {
1801
+ ...node,
1802
+ ...(node.marks ? { marks: node.marks.filter(mark => mark.type in known) } : {}),
1803
+ ...(node.content ? { content: node.content.map(child => withoutMarks(child, known)) } : {}),
1804
+ };
1805
+ }
1806
+ /**
1807
+ * Rich-text editor on ProseMirror, with a toolbar, a link panel, keyboard
1808
+ * shortcuts and markdown-style block shortcuts (`# `, `> `, `- `, `1. `,
1809
+ * ` ``` `).
1810
+ *
1811
+ * A signal-forms native control: it implements `FormValueControl<WrEditorValue>`,
1812
+ * so `[formField]` binds straight to its `value` model — no
1813
+ * `ControlValueAccessor` in between. `[(value)]` works standalone.
1814
+ *
1815
+ * The value is an HTML string, a markdown string or the document as JSON, chosen
1816
+ * by `format`. Whatever arrives is treated as untrusted and rebuilt from the
1817
+ * editor's schema, so only what the schema allows survives, and links and images
1818
+ * go through `safeMarkdownUrl` in all three. A value the editor cannot read is
1819
+ * refused — the document it already shows is kept, a warning names the reason in
1820
+ * dev mode, and nothing is written back. Until a value it CAN read arrives the
1821
+ * editor is read-only and carries `wr-editor--refused`: the model still holds the
1822
+ * refused value, and an edit would write the document on screen over it.
1823
+ *
1824
+ * Headings stop at three levels: an h4–h6 read from HTML or markdown becomes an
1825
+ * h3 rather than a paragraph. Underline exists in `html` and `json` only —
1826
+ * markdown cannot spell it, so in that format the tool, the shortcut and the mark
1827
+ * itself are absent.
1828
+ *
1829
+ * ProseMirror is an OPTIONAL peer dependency: only an app that imports
1830
+ * `ngwr/editor` installs `prosemirror-model`, `-state`, `-view`, `-commands`,
1831
+ * `-keymap`, `-history`, `-schema-list` and `-inputrules`.
1832
+ *
1833
+ * @example
1834
+ * ```html
1835
+ * <!-- signal forms -->
1836
+ * <wr-editor format="markdown" placeholder="Describe the change" [formField]="form.body" />
1837
+ *
1838
+ * <!-- standalone two-way binding -->
1839
+ * <wr-editor [(value)]="html" />
1840
+ * ```
1841
+ *
1842
+ * @see https://ngwr.dev/reference/components/editor
1843
+ */
1844
+ class WrEditor {
1845
+ /**
1846
+ * The document. A string in `html` and `markdown` format, a
1847
+ * {@link WrEditorJson} tree in `json`; `''` / `null` when nothing is written.
1848
+ * Bound by `[formField]`, or two-way via `[(value)]`.
1849
+ */
1850
+ value = model(null, /* @ts-ignore */
1851
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
1852
+ /** Emitted on blur so a bound field can mark itself touched. */
1853
+ touch = output();
1854
+ /**
1855
+ * Disable the editor and its toolbar. Bound automatically from the field's
1856
+ * disabled state when used with `[formField]`.
1857
+ *
1858
+ * @default false
1859
+ */
1860
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: coerceBooleanProperty });
1861
+ /**
1862
+ * Refuse edits while the text stays focusable, selectable and announced.
1863
+ * Bound automatically from the field's readonly state when used with
1864
+ * `[formField]`.
1865
+ *
1866
+ * The surface keeps its tab stop and reports `aria-readonly`; typing, paste,
1867
+ * drop, the shortcuts and every toolbar command are refused. The toolbar goes
1868
+ * inert, since none of its tools can apply.
1869
+ *
1870
+ * @default false
1871
+ */
1872
+ readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: coerceBooleanProperty });
1873
+ /**
1874
+ * Hint shown while the document is empty, and exposed as `aria-placeholder`.
1875
+ *
1876
+ * @default ''
1877
+ */
1878
+ placeholder = input('', /* @ts-ignore */
1879
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
1880
+ /**
1881
+ * Accessible name of the text surface. Falls back to the surrounding
1882
+ * `<wr-form-field>`'s label, then to `editor.label`.
1883
+ */
1884
+ ariaLabel = input(null, /* @ts-ignore */
1885
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
1886
+ /**
1887
+ * What `value` holds — see {@link WrEditorFormat}. Unset, it falls back to
1888
+ * `provideWrConfig({ editor: { format } })`. Changing it re-reads `value` in
1889
+ * the new format.
1890
+ *
1891
+ * @default 'html'
1892
+ */
1893
+ format = input(null, /* @ts-ignore */
1894
+ ...(ngDevMode ? [{ debugName: "format" }] : /* istanbul ignore next */ []));
1895
+ // `null` as the input's own default, not `'html'`, so "the template said
1896
+ // nothing" stays distinguishable from "the template said html" and an app-wide
1897
+ // default can apply.
1898
+ resolvedFormat = useConfigValue(this.format, c => c.editor?.format, 'html');
1899
+ /**
1900
+ * The toolbar's tools, in order, with `'|'` between groups; `false` hides it.
1901
+ * `underline` is left out in `markdown` format, which cannot store it.
1902
+ *
1903
+ * @default WR_EDITOR_TOOLBAR
1904
+ */
1905
+ toolbar = input(WR_EDITOR_TOOLBAR, /* @ts-ignore */
1906
+ ...(ngDevMode ? [{ debugName: "toolbar" }] : /* istanbul ignore next */ []));
1907
+ document = inject(DOCUMENT);
1908
+ injector = inject(Injector);
1909
+ destroyRef = inject(DestroyRef);
1910
+ host = inject(ElementRef);
1911
+ platform = inject(WrPlatform);
1912
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
1913
+ /**
1914
+ * Ambient reading direction, read by the toolbar's arrow keys. Optional so a
1915
+ * bare `TestBed` needs no provider; `null` means LTR.
1916
+ */
1917
+ dir = inject(Directionality, { optional: true });
1918
+ /** @internal */
1919
+ linkField = inject(WrEditorLinkField, { self: true });
1920
+ /** The surrounding `<wr-form-field>`'s error state, read past the editor's own shield. @internal */
1921
+ fieldAria = useFormFieldAria({ skipSelf: true });
1922
+ resolvedAriaLabel = useI18nText(this.ariaLabel, 'editor.label', 'Rich text');
1923
+ /**
1924
+ * The field's label names the surface, when there is one — a
1925
+ * `contenteditable` is not labelable, so the field's `<label for>` cannot
1926
+ * reach it. An explicit `ariaLabel` still wins over both.
1927
+ */
1928
+ labelledBy = computed(() => (this.ariaLabel() ? null : this.fieldAria.labelledBy()), /* @ts-ignore */
1929
+ ...(ngDevMode ? [{ debugName: "labelledBy" }] : /* istanbul ignore next */ []));
1930
+ /** `null` while the field's label carries the name. */
1931
+ accessibleName = computed(() => (this.labelledBy() ? null : this.resolvedAriaLabel()), /* @ts-ignore */
1932
+ ...(ngDevMode ? [{ debugName: "accessibleName" }] : /* istanbul ignore next */ []));
1933
+ surfaceId = `wr-editor-${nextId++}`;
1934
+ /** ProseMirror owns the surface from here on; until then the preview stands in. */
1935
+ mounted = signal(false, /* @ts-ignore */
1936
+ ...(ngDevMode ? [{ debugName: "mounted" }] : /* istanbul ignore next */ []));
1937
+ focused = signal(false, /* @ts-ignore */
1938
+ ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
1939
+ editorState = signal(null, /* @ts-ignore */
1940
+ ...(ngDevMode ? [{ debugName: "editorState" }] : /* istanbul ignore next */ []));
1941
+ /**
1942
+ * Whether `value` holds something the editor could not read, so the document
1943
+ * on screen is not the one the model holds — empty at mount, or the previous
1944
+ * record after a write. Edits are refused until a readable value arrives:
1945
+ * the first keystroke would otherwise replace the stored value with whatever
1946
+ * is on screen, and the only sign of it was a dev-mode warning.
1947
+ */
1948
+ refused = signal(false, /* @ts-ignore */
1949
+ ...(ngDevMode ? [{ debugName: "refused" }] : /* istanbul ignore next */ []));
1950
+ /** Read-only by the input, or because the value was refused. @internal */
1951
+ locked = computed(() => this.readonly() || this.refused(), /* @ts-ignore */
1952
+ ...(ngDevMode ? [{ debugName: "locked" }] : /* istanbul ignore next */ []));
1953
+ /** Apple keyboards name the modifier ⌘. Read in the browser only — see `mount()`. */
1954
+ apple = signal(false, /* @ts-ignore */
1955
+ ...(ngDevMode ? [{ debugName: "apple" }] : /* istanbul ignore next */ []));
1956
+ surface = viewChild.required('surface', /* @ts-ignore */
1957
+ ...(ngDevMode ? [{ debugName: "surface" }] : /* istanbul ignore next */ []));
1958
+ // `read`, because `#tool` sits on a `wr-btn` and would otherwise resolve to the component.
1959
+ toolButtons = viewChildren('tool', { ...(ngDevMode ? { debugName: "toolButtons" } : /* istanbul ignore next */ {}), read: ElementRef });
1960
+ linkPopover = viewChild('linkPopover', /* @ts-ignore */
1961
+ ...(ngDevMode ? [{ debugName: "linkPopover" }] : /* istanbul ignore next */ []));
1962
+ view = null;
1963
+ /** The format the live document was read in, which is the format it is written in. */
1964
+ viewFormat = 'html';
1965
+ /**
1966
+ * The value the live document was last read from or written as. A write from
1967
+ * outside that equals it is the document already on screen — an echo of the
1968
+ * editor's own emit, or a consumer writing back what it was handed — and must
1969
+ * neither re-parse nor reset the selection.
1970
+ */
1971
+ lastSerialized = undefined;
1972
+ linkToSurface = false;
1973
+ // Labels
1974
+ toolbarLabel = readI18nText('editor.toolbar', 'Formatting');
1975
+ linkUrlLabel = readI18nText('editor.linkUrl', 'Link address');
1976
+ linkApplyLabel = readI18nText('editor.linkApply', 'Apply');
1977
+ linkRemoveLabel = readI18nText('editor.linkRemove', 'Remove link');
1978
+ linkInvalidLabel = readI18nText('editor.linkInvalid', 'This address cannot be used as a link.');
1979
+ taskDoneLabel = readI18nText('editor.taskDone', 'Done:');
1980
+ taskTodoLabel = readI18nText('editor.taskTodo', 'To do:');
1981
+ keyCtrl = readI18nText('editor.keyCtrl', 'Ctrl');
1982
+ keyShift = readI18nText('editor.keyShift', 'Shift');
1983
+ keyAlt = readI18nText('editor.keyAlt', 'Alt');
1984
+ headingLabel = useI18nFormatter('editor.heading', 'Heading {{level}}');
1985
+ shortcutLabel = useI18nFormatter('editor.shortcut', '{{label}} ({{keys}})');
1986
+ toolLabels = {
1987
+ bold: readI18nText('editor.bold', 'Bold'),
1988
+ italic: readI18nText('editor.italic', 'Italic'),
1989
+ underline: readI18nText('editor.underline', 'Underline'),
1990
+ strike: readI18nText('editor.strike', 'Strikethrough'),
1991
+ code: readI18nText('editor.code', 'Inline code'),
1992
+ paragraph: readI18nText('editor.paragraph', 'Paragraph'),
1993
+ heading1: computed(() => this.headingLabel({ level: 1 })),
1994
+ heading2: computed(() => this.headingLabel({ level: 2 })),
1995
+ heading3: computed(() => this.headingLabel({ level: 3 })),
1996
+ bulletList: readI18nText('editor.bulletList', 'Bulleted list'),
1997
+ orderedList: readI18nText('editor.orderedList', 'Numbered list'),
1998
+ blockquote: readI18nText('editor.blockquote', 'Quote'),
1999
+ codeBlock: readI18nText('editor.codeBlock', 'Code block'),
2000
+ link: readI18nText('editor.link', 'Insert link'),
2001
+ horizontalRule: readI18nText('editor.horizontalRule', 'Horizontal line'),
2002
+ undo: readI18nText('editor.undo', 'Undo'),
2003
+ redo: readI18nText('editor.redo', 'Redo'),
2004
+ };
2005
+ // Toolbar
2006
+ /**
2007
+ * The bound tools as drawn: underline dropped where the format cannot store
2008
+ * it, and separators only BETWEEN two groups — a leading, trailing or doubled
2009
+ * one would be left behind by that filter or by a hand-built list.
2010
+ */
2011
+ tools = computed(() => {
2012
+ const bound = this.toolbar();
2013
+ if (!bound)
2014
+ return [];
2015
+ const markdown = this.resolvedFormat() === 'markdown';
2016
+ const out = [];
2017
+ for (const tool of bound) {
2018
+ if (markdown && tool === 'underline')
2019
+ continue;
2020
+ if (tool === '|' && (out.length === 0 || out[out.length - 1] === '|'))
2021
+ continue;
2022
+ if (tool !== '|' && out.includes(tool))
2023
+ continue;
2024
+ out.push(tool);
2025
+ }
2026
+ if (out[out.length - 1] === '|')
2027
+ out.pop();
2028
+ return out;
2029
+ }, /* @ts-ignore */
2030
+ ...(ngDevMode ? [{ debugName: "tools" }] : /* istanbul ignore next */ []));
2031
+ /** The tools that are buttons, in order — what the roving tab stop moves over. */
2032
+ buttons = computed(() => this.tools().filter((tool) => tool !== '|'), /* @ts-ignore */
2033
+ ...(ngDevMode ? [{ debugName: "buttons" }] : /* istanbul ignore next */ []));
2034
+ /** Each drawn entry's index among the buttons, `-1` for a separator. @internal */
2035
+ buttonIndex = computed(() => {
2036
+ let index = 0;
2037
+ return this.tools().map(tool => (tool === '|' ? -1 : index++));
2038
+ }, /* @ts-ignore */
2039
+ ...(ngDevMode ? [{ debugName: "buttonIndex" }] : /* istanbul ignore next */ []));
2040
+ commands = computed(() => {
2041
+ const schema = editorSchema(this.resolvedFormat());
2042
+ return new Map(this.buttons().map(tool => [tool, toolCommand(tool, schema)]));
2043
+ }, /* @ts-ignore */
2044
+ ...(ngDevMode ? [{ debugName: "commands" }] : /* istanbul ignore next */ []));
2045
+ /** Every button's pressed and available state at the current selection. @internal */
2046
+ toolStates = computed(() => {
2047
+ const state = this.editorState();
2048
+ const editable = this.canEdit();
2049
+ const commands = this.commands();
2050
+ return new Map(this.buttons().map(tool => {
2051
+ const command = commands.get(tool);
2052
+ return [
2053
+ tool,
2054
+ {
2055
+ tool,
2056
+ active: !!state && toolActive(tool, state),
2057
+ available: !!state && editable && !!command && command(state),
2058
+ },
2059
+ ];
2060
+ }));
2061
+ }, /* @ts-ignore */
2062
+ ...(ngDevMode ? [{ debugName: "toolStates" }] : /* istanbul ignore next */ []));
2063
+ rovingIndex = signal(0, /* @ts-ignore */
2064
+ ...(ngDevMode ? [{ debugName: "rovingIndex" }] : /* istanbul ignore next */ []));
2065
+ /**
2066
+ * The one button in the tab order. It follows the user, and moves off a tool
2067
+ * that stopped applying so the toolbar never loses its tab stop — unless NO
2068
+ * tool applies (read-only, disabled), where it stays put, because there is
2069
+ * nothing to reach.
2070
+ */
2071
+ tabStop = computed(() => {
2072
+ const buttons = this.buttons();
2073
+ const states = this.toolStates();
2074
+ const current = Math.min(this.rovingIndex(), Math.max(buttons.length - 1, 0));
2075
+ if (states.get(buttons[current])?.available)
2076
+ return current;
2077
+ const first = buttons.findIndex(tool => states.get(tool)?.available);
2078
+ return first >= 0 ? first : current;
2079
+ }, /* @ts-ignore */
2080
+ ...(ngDevMode ? [{ debugName: "tabStop" }] : /* istanbul ignore next */ []));
2081
+ /** Whether the live document is empty — pre-mount, whether the value is. */
2082
+ empty = computed(() => {
2083
+ const state = this.editorState();
2084
+ if (state)
2085
+ return isEmptyDoc(state.doc);
2086
+ const value = this.value();
2087
+ return value === null || value === undefined || value === '';
2088
+ }, /* @ts-ignore */
2089
+ ...(ngDevMode ? [{ debugName: "empty" }] : /* istanbul ignore next */ []));
2090
+ /** The task-item words, handed to ProseMirror through its node views. */
2091
+ nodeViews = computed(() => ({
2092
+ list_item: listItemView({ done: this.taskDoneLabel(), todo: this.taskTodoLabel() }),
2093
+ }), /* @ts-ignore */
2094
+ ...(ngDevMode ? [{ debugName: "nodeViews" }] : /* istanbul ignore next */ []));
2095
+ ariaPlaceholder = computed(() => (this.placeholder() && this.empty() ? this.placeholder() : null), /* @ts-ignore */
2096
+ ...(ngDevMode ? [{ debugName: "ariaPlaceholder" }] : /* istanbul ignore next */ []));
2097
+ // Link panel
2098
+ linkDraft = signal('', /* @ts-ignore */
2099
+ ...(ngDevMode ? [{ debugName: "linkDraft" }] : /* istanbul ignore next */ []));
2100
+ /** Whether the selection is in a link — what the panel's remove button needs. */
2101
+ linkActive = computed(() => this.toolStates().get('link')?.active ?? false, /* @ts-ignore */
2102
+ ...(ngDevMode ? [{ debugName: "linkActive" }] : /* istanbul ignore next */ []));
2103
+ /**
2104
+ * The value rendered by Angular until ProseMirror takes over: on the server,
2105
+ * and for the first browser frame. Read from the doc JSON, so it shows exactly
2106
+ * what the schema kept — the same sanitised document the editor will mount.
2107
+ */
2108
+ preview = computed(() => {
2109
+ if (this.mounted())
2110
+ return null;
2111
+ const format = this.resolvedFormat();
2112
+ try {
2113
+ return readValue(this.value(), format, editorSchema(format), this.document).toJSON();
2114
+ }
2115
+ catch (error) {
2116
+ // The browser warns once, when it mounts; the server has no mount to wait for.
2117
+ if (!this.isBrowser && isDevMode())
2118
+ this.warnRefused(format, error);
2119
+ return null;
2120
+ }
2121
+ }, /* @ts-ignore */
2122
+ ...(ngDevMode ? [{ debugName: "preview" }] : /* istanbul ignore next */ []));
2123
+ classes = computed(() => {
2124
+ const parts = ['wr-editor'];
2125
+ if (this.focused())
2126
+ parts.push('wr-editor--focused');
2127
+ if (this.disabled())
2128
+ parts.push('wr-editor--disabled');
2129
+ else if (this.locked())
2130
+ parts.push('wr-editor--readonly');
2131
+ if (this.refused())
2132
+ parts.push('wr-editor--refused');
2133
+ return parts.join(' ');
2134
+ }, /* @ts-ignore */
2135
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
2136
+ constructor() {
2137
+ // Outside writes: a form reset, a server load, a patch. Skipped when the
2138
+ // value is the document already on screen — see `lastSerialized`.
2139
+ effect(() => {
2140
+ const value = this.value();
2141
+ const format = this.resolvedFormat();
2142
+ untracked(() => this.sync(value, format));
2143
+ });
2144
+ // `editable` and the placeholder are functions ProseMirror asks on every
2145
+ // update, so poking it is enough to make it ask again.
2146
+ effect(() => {
2147
+ this.readonly();
2148
+ this.disabled();
2149
+ this.refused();
2150
+ this.placeholder();
2151
+ untracked(() => this.view?.setProps({}));
2152
+ });
2153
+ // The task-item words live in node views, which ProseMirror only redraws
2154
+ // when the `nodeViews` prop itself changes.
2155
+ effect(() => {
2156
+ const nodeViews = this.nodeViews();
2157
+ untracked(() => this.view?.setProps({ nodeViews }));
2158
+ });
2159
+ // The roving tab stop is written to the DOM rather than bound: `wr-btn`
2160
+ // binds `[attr.tabindex]` on its host (`null` for a native button), and a
2161
+ // host binding runs after the template's and removes the attribute on the
2162
+ // first pass. Nothing rewrites it after that, so a write after render sticks.
2163
+ afterRenderEffect({
2164
+ write: () => {
2165
+ const stop = this.tabStop();
2166
+ this.toolButtons().forEach((ref, index) => (ref.nativeElement.tabIndex = index === stop ? 0 : -1));
2167
+ },
2168
+ });
2169
+ // `afterNextRender` alone is not the guard: it keys on the global server
2170
+ // flag, not on `PLATFORM_ID`, so a platform test would still run it.
2171
+ if (!this.isBrowser)
2172
+ return;
2173
+ afterNextRender(() => this.mount());
2174
+ }
2175
+ /** Focus the text surface — what Signal Forms calls to focus this control. */
2176
+ focus(options) {
2177
+ const view = this.view;
2178
+ // ProseMirror's own `focus()` is a no-op while the view is not editable,
2179
+ // and read-only has to stay focusable.
2180
+ if (view?.editable)
2181
+ view.focus();
2182
+ else
2183
+ this.surface().nativeElement.focus(options);
2184
+ }
2185
+ // Template handlers
2186
+ label(tool) {
2187
+ return this.toolLabels[tool]();
2188
+ }
2189
+ icon(tool) {
2190
+ return TOOL_ICONS[tool];
2191
+ }
2192
+ isToggle(tool) {
2193
+ return TOGGLES.has(tool);
2194
+ }
2195
+ /** The tooltip: the label, with the shortcut when the tool has one. */
2196
+ tooltip(tool) {
2197
+ const shortcut = SHORTCUTS[tool];
2198
+ if (!shortcut)
2199
+ return this.label(tool);
2200
+ const keys = this.apple()
2201
+ ? `${shortcut.alt ? '⌥' : ''}${shortcut.shift ? '⇧' : ''}⌘${shortcut.key}`
2202
+ : [
2203
+ this.keyCtrl(),
2204
+ ...(shortcut.alt ? [this.keyAlt()] : []),
2205
+ ...(shortcut.shift ? [this.keyShift()] : []),
2206
+ shortcut.key,
2207
+ ].join('+');
2208
+ return this.shortcutLabel({ label: this.label(tool), keys });
2209
+ }
2210
+ /** `aria-keyshortcuts` — key names from the ARIA spec, never localised. */
2211
+ keyShortcuts(tool) {
2212
+ const shortcut = SHORTCUTS[tool];
2213
+ if (!shortcut)
2214
+ return null;
2215
+ const mod = this.apple() ? 'Meta' : 'Control';
2216
+ const combo = `${mod}+${shortcut.alt ? 'Alt+' : ''}${shortcut.shift ? 'Shift+' : ''}${shortcut.key}`;
2217
+ return tool === 'redo' && !this.apple() ? `${combo} Control+Y` : combo;
2218
+ }
2219
+ state(tool) {
2220
+ return this.toolStates().get(tool);
2221
+ }
2222
+ /**
2223
+ * A toolbar button was activated.
2224
+ *
2225
+ * By pointer, focus never left the text (`mousedown` is cancelled), so the
2226
+ * caret is put back where the command left it. By keyboard, focus stays on the
2227
+ * button, as the toolbar pattern asks — unless the tool no longer applies,
2228
+ * which disables it: a disabled button drops its focus on `<body>`, so the text
2229
+ * takes it first.
2230
+ */
2231
+ onToolClick(tool, event) {
2232
+ const view = this.view;
2233
+ const command = this.commands().get(tool);
2234
+ if (!view || !command || !this.canEdit())
2235
+ return;
2236
+ command(view.state, view.dispatch, view);
2237
+ if (event.detail !== 0 || !this.state(tool)?.available)
2238
+ view.focus();
2239
+ }
2240
+ onToolFocus(index) {
2241
+ this.rovingIndex.set(index);
2242
+ }
2243
+ /** The APG toolbar keys: the arrows rove and wrap, Home and End jump. */
2244
+ onToolbarKeydown(event) {
2245
+ if (isComposing(event) || event.altKey || event.ctrlKey || event.metaKey)
2246
+ return;
2247
+ const buttons = this.toolButtons().map(ref => ref.nativeElement);
2248
+ const current = buttons.indexOf(event.target);
2249
+ if (current < 0)
2250
+ return;
2251
+ const reachable = buttons.map((button, index) => (button.disabled ? -1 : index)).filter(index => index >= 0);
2252
+ if (!reachable.length)
2253
+ return;
2254
+ const rtl = this.dir?.value === 'rtl';
2255
+ const at = reachable.indexOf(current);
2256
+ let next;
2257
+ switch (event.key) {
2258
+ case 'ArrowRight':
2259
+ next = reachable[(at + (rtl ? -1 : 1) + reachable.length) % reachable.length];
2260
+ break;
2261
+ case 'ArrowLeft':
2262
+ next = reachable[(at + (rtl ? 1 : -1) + reachable.length) % reachable.length];
2263
+ break;
2264
+ case 'Home':
2265
+ next = reachable[0];
2266
+ break;
2267
+ case 'End':
2268
+ next = reachable[reachable.length - 1];
2269
+ break;
2270
+ default:
2271
+ return;
2272
+ }
2273
+ event.preventDefault();
2274
+ this.rovingIndex.set(next);
2275
+ buttons[next].focus();
2276
+ }
2277
+ /** `touch` once focus leaves the whole control — not when it moves between its own parts. */
2278
+ onFocusOut(event) {
2279
+ const next = event.relatedTarget;
2280
+ if (next && this.withinControl(next))
2281
+ return;
2282
+ const fromPanel = this.inLinkPanel(event.target);
2283
+ // Chromium fires a `focusout` with no `relatedTarget` when the focused
2284
+ // element is REMOVED — the link panel closing with focus inside it, or its
2285
+ // remove button leaving — and the popover hands focus back only after that.
2286
+ // It does the same when Tab runs off the end of the document, which is where
2287
+ // the overlay container sits. So where focus went is decided once it has
2288
+ // settled, or Escape on the panel marked the field touched with focus never
2289
+ // leaving the control.
2290
+ if (!next && fromPanel) {
2291
+ afterNextRender(() => {
2292
+ const active = this.document.activeElement;
2293
+ if (active && this.withinControl(active))
2294
+ return;
2295
+ this.leaveLinkPanel();
2296
+ this.touch.emit();
2297
+ }, { injector: this.injector });
2298
+ return;
2299
+ }
2300
+ if (next && fromPanel)
2301
+ this.leaveLinkPanel();
2302
+ this.touch.emit();
2303
+ }
2304
+ onLinkOpened() {
2305
+ const state = this.view?.state;
2306
+ const range = state ? linkRange(state) : null;
2307
+ this.linkDraft.set(range ? String(range.mark.attrs['href'] ?? '') : '');
2308
+ this.linkField.invalid.set(false);
2309
+ afterNextRender(() => this.focusLinkInput(), { injector: this.injector });
2310
+ }
2311
+ onLinkClosed() {
2312
+ // Back to the text when the panel was opened from it (Mod-K) or when a link
2313
+ // was just applied; otherwise the popover has already handed focus back to
2314
+ // the toolbar button, which is where Escape should leave it.
2315
+ //
2316
+ // Only while focus is where the popover left it — on the link button — or
2317
+ // nowhere. A click elsewhere on the page closes the panel too, and by then
2318
+ // it has put focus where the user wanted it; taking it back sent their next
2319
+ // keystrokes into this editor.
2320
+ const toSurface = this.linkToSurface;
2321
+ this.linkToSurface = false;
2322
+ this.linkField.invalid.set(false);
2323
+ if (!toSurface)
2324
+ return;
2325
+ const active = this.document.activeElement;
2326
+ const linkButton = this.toolButtons()[this.buttons().indexOf('link')]?.nativeElement;
2327
+ if (!active || active === this.document.body || active === linkButton)
2328
+ this.view?.focus();
2329
+ }
2330
+ onLinkInput(event) {
2331
+ this.linkDraft.set(event.target.value);
2332
+ this.linkField.invalid.set(false);
2333
+ }
2334
+ applyLink(event) {
2335
+ event.preventDefault();
2336
+ const view = this.view;
2337
+ if (!view || !this.canEdit())
2338
+ return;
2339
+ const draft = this.linkDraft().trim();
2340
+ // An emptied field on an existing link means "no link".
2341
+ if (!draft && this.linkActive()) {
2342
+ this.removeLink();
2343
+ return;
2344
+ }
2345
+ const href = draft ? safeMarkdownUrl(draft, 'link') : null;
2346
+ if (!href) {
2347
+ this.linkField.invalid.set(true);
2348
+ this.focusLinkInput();
2349
+ return;
2350
+ }
2351
+ if (!setLink(href)(view.state, view.dispatch))
2352
+ return;
2353
+ this.linkToSurface = true;
2354
+ this.linkPopover()?.close();
2355
+ }
2356
+ removeLink() {
2357
+ const view = this.view;
2358
+ if (!view || !this.canEdit())
2359
+ return;
2360
+ removeLink(view.state, view.dispatch);
2361
+ this.linkToSurface = true;
2362
+ this.linkPopover()?.close();
2363
+ }
2364
+ // Preview helpers — the template's typed view of untyped JSON.
2365
+ nodesOf(raw) {
2366
+ return Array.isArray(raw) ? raw : [];
2367
+ }
2368
+ nodeOf(raw) {
2369
+ return raw;
2370
+ }
2371
+ markAt(node, depth) {
2372
+ return node.marks?.[depth] ?? null;
2373
+ }
2374
+ attr(node, name) {
2375
+ return node.attrs?.[name] ?? null;
2376
+ }
2377
+ text(node, name) {
2378
+ const value = node.attrs?.[name];
2379
+ return typeof value === 'string' ? value : null;
2380
+ }
2381
+ textOf(node) {
2382
+ return (node.content ?? []).map(child => child.text ?? '').join('');
2383
+ }
2384
+ // Internals
2385
+ /** Whether an edit may happen at all — not while read-only, disabled or refused. */
2386
+ canEdit() {
2387
+ return !this.readonly() && !this.disabled() && !this.refused();
2388
+ }
2389
+ mount() {
2390
+ this.apple.set((this.platform.userAgent ?? '').toLowerCase().includes('mac'));
2391
+ const format = untracked(this.resolvedFormat);
2392
+ const value = untracked(this.value);
2393
+ const schema = editorSchema(format);
2394
+ let doc;
2395
+ try {
2396
+ doc = readValue(value, format, schema, this.document);
2397
+ }
2398
+ catch (error) {
2399
+ if (isDevMode())
2400
+ this.warnRefused(format, error);
2401
+ doc = schema.topNodeType.createAndFill();
2402
+ // An empty document over a stored value: see `refused`.
2403
+ this.refused.set(true);
2404
+ }
2405
+ const state = this.createState(doc);
2406
+ this.view = new EditorView({ mount: this.surface().nativeElement }, {
2407
+ state,
2408
+ editable: () => untracked(() => this.canEdit()),
2409
+ dispatchTransaction: tr => this.dispatch(tr),
2410
+ decorations: current => this.placeholderDecorations(current),
2411
+ nodeViews: untracked(this.nodeViews),
2412
+ handleKeyDown: (_view, event) => this.onSurfaceKeydown(event),
2413
+ });
2414
+ this.viewFormat = format;
2415
+ this.lastSerialized = value;
2416
+ this.editorState.set(state);
2417
+ this.mounted.set(true);
2418
+ this.destroyRef.onDestroy(() => {
2419
+ this.view?.destroy();
2420
+ this.view = null;
2421
+ });
2422
+ }
2423
+ createState(doc) {
2424
+ const current = this.view?.state;
2425
+ const plugins = current?.schema === doc.type.schema
2426
+ ? current.plugins
2427
+ : editorPlugins(doc.type.schema, {
2428
+ apple: untracked(this.apple),
2429
+ canEdit: () => untracked(() => this.canEdit()),
2430
+ });
2431
+ return EditorState.create({ doc, plugins });
2432
+ }
2433
+ dispatch(tr) {
2434
+ const view = this.view;
2435
+ if (!view)
2436
+ return;
2437
+ const previous = view.state;
2438
+ const next = previous.apply(tr);
2439
+ view.updateState(next);
2440
+ this.editorState.set(next);
2441
+ // A refused transaction leaves the document as it was even though it asked
2442
+ // to change it; that is not an edit, and must not reach the model.
2443
+ if (next.doc !== previous.doc)
2444
+ this.emit(next.doc);
2445
+ }
2446
+ emit(doc) {
2447
+ const out = writeValue(doc, this.viewFormat, this.document);
2448
+ if (out === this.lastSerialized)
2449
+ return;
2450
+ this.lastSerialized = out;
2451
+ this.value.set(out);
2452
+ }
2453
+ /**
2454
+ * Bring an outside write into the view.
2455
+ *
2456
+ * Compared as DOCUMENTS, never as strings: `<b>` comes back as `<strong>`, so a
2457
+ * normalised string differs from what the consumer wrote while the documents
2458
+ * are equal — and an equal document is left alone, selection and history
2459
+ * included. A different one replaces the state wholesale, history with it,
2460
+ * which is right for a form reset or a server load: undo back into content the
2461
+ * app replaced would be wrong. The normalised form is never written back —
2462
+ * that would mark the field dirty on load. A write that arrives mid-composition
2463
+ * (IME) replaces the composition too: the model is the source of truth.
2464
+ */
2465
+ sync(value, format) {
2466
+ const view = this.view;
2467
+ if (!view)
2468
+ return;
2469
+ if (format === this.viewFormat && value === this.lastSerialized)
2470
+ return;
2471
+ const schema = editorSchema(format);
2472
+ let doc;
2473
+ try {
2474
+ doc = readValue(value, format, schema, this.document);
2475
+ }
2476
+ catch (error) {
2477
+ if (isDevMode())
2478
+ this.warnRefused(format, error);
2479
+ // Only the format moved, and the screen still shows this very value read
2480
+ // in the old one: carried across, an edit converts it and loses nothing.
2481
+ // Any other refusal leaves the model holding what the screen does not show.
2482
+ const carried = format !== this.viewFormat && value === this.lastSerialized && !this.refused();
2483
+ if (format !== this.viewFormat)
2484
+ this.switchFormat(format, schema);
2485
+ this.refused.set(!carried);
2486
+ return;
2487
+ }
2488
+ this.refused.set(false);
2489
+ this.lastSerialized = value;
2490
+ if (format === this.viewFormat && doc.eq(view.state.doc))
2491
+ return;
2492
+ this.viewFormat = format;
2493
+ this.replaceState(doc);
2494
+ }
2495
+ /**
2496
+ * A new format whose value could not be read: keep the document, moved into
2497
+ * the new format's schema — markdown's lacks underline, so that mark is
2498
+ * dropped on the way. What is written from here on is in the new format.
2499
+ */
2500
+ switchFormat(format, schema) {
2501
+ const view = this.view;
2502
+ this.viewFormat = format;
2503
+ if (view.state.schema === schema)
2504
+ return;
2505
+ let doc;
2506
+ try {
2507
+ doc = schema.nodeFromJSON(withoutMarks(view.state.doc.toJSON(), schema.marks));
2508
+ doc.check();
2509
+ }
2510
+ catch {
2511
+ doc = schema.topNodeType.createAndFill();
2512
+ }
2513
+ this.replaceState(doc);
2514
+ }
2515
+ replaceState(doc) {
2516
+ const state = this.createState(doc);
2517
+ this.view.updateState(state);
2518
+ this.editorState.set(state);
2519
+ }
2520
+ placeholderDecorations(state) {
2521
+ const text = untracked(this.placeholder);
2522
+ if (!text || !isEmptyDoc(state.doc))
2523
+ return null;
2524
+ const first = state.doc.firstChild;
2525
+ return DecorationSet.create(state.doc, [
2526
+ Decoration.node(0, first.nodeSize, { class: 'wr-editor__placeholder', 'data-placeholder': text }),
2527
+ ]);
2528
+ }
2529
+ /**
2530
+ * Mod-K opens the link panel. Its propagation stops here: `<wr-command-palette>`
2531
+ * binds the same chord on the document, and `WrHotkey` deliberately does not
2532
+ * skip an event another listener already handled.
2533
+ */
2534
+ onSurfaceKeydown(event) {
2535
+ if (isComposing(event) || event.altKey || event.shiftKey)
2536
+ return false;
2537
+ const mod = untracked(this.apple) ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey;
2538
+ if (!mod || (event.key.toLowerCase() !== 'k' && event.code !== 'KeyK'))
2539
+ return false;
2540
+ const popover = this.linkPopover();
2541
+ if (!popover || !untracked(this.toolStates).get('link')?.available)
2542
+ return false;
2543
+ event.stopPropagation();
2544
+ this.linkToSurface = true;
2545
+ popover.open();
2546
+ return true;
2547
+ }
2548
+ focusLinkInput() {
2549
+ const input = this.document.getElementById(this.linkField.controlId());
2550
+ input?.focus();
2551
+ input?.select();
2552
+ }
2553
+ /**
2554
+ * Focus left the link panel for somewhere outside the control — Tab, Shift+Tab,
2555
+ * a click elsewhere. The panel is not a focus trap, and left open it stays
2556
+ * anchored to a control the user has left, whose next close would then pull
2557
+ * focus back into the text. Closed with focus left where it went.
2558
+ */
2559
+ leaveLinkPanel() {
2560
+ this.linkToSurface = false;
2561
+ this.linkPopover()?.close();
2562
+ }
2563
+ /** Whether a node is part of this control: the host, or its link panel. */
2564
+ withinControl(node) {
2565
+ return this.host.nativeElement.contains(node) || this.inLinkPanel(node);
2566
+ }
2567
+ inLinkPanel(node) {
2568
+ const panel = this.document.getElementById(this.linkField.panelId);
2569
+ if (!panel)
2570
+ return false;
2571
+ // The popover focuses its own pane before the input takes over.
2572
+ return panel.contains(node) || node === panel.closest('.cdk-overlay-pane');
2573
+ }
2574
+ warnRefused(format, error) {
2575
+ const reason = error instanceof Error ? error.message : String(error);
2576
+ warn(`the bound value could not be read as ${format} and was ignored — the editor keeps the document it had and ` +
2577
+ `refuses edits until it is given a value it can read, so the stored one is not overwritten. ${reason}`);
2578
+ }
2579
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: WrEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
2580
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: WrEditor, isStandalone: true, selector: "wr-editor", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, toolbar: { classPropertyName: "toolbar", publicName: "toolbar", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touch: "touch" }, host: { listeners: { "focusout": "onFocusOut($event)" }, properties: { "class": "classes()" } }, providers: [
2581
+ { provide: WrEditorLinkField, useFactory: () => new WrEditorLinkField() },
2582
+ { provide: WR_FORM_FIELD, useExisting: WrEditorLinkField },
2583
+ ], viewQueries: [{ propertyName: "surface", first: true, predicate: ["surface"], descendants: true, isSignal: true }, { propertyName: "toolButtons", predicate: ["tool"], descendants: true, read: ElementRef, isSignal: true }, { propertyName: "linkPopover", first: true, predicate: ["linkPopover"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (tools().length) {\n <div class=\"wr-editor__toolbar\" role=\"toolbar\" [attr.aria-label]=\"toolbarLabel()\" [attr.aria-controls]=\"surfaceId\">\n @for (item of tools(); track $index; let n = $index) {\n @if (item === '|') {\n <span class=\"wr-editor__separator\" aria-hidden=\"true\"></span>\n } @else if (item === 'link') {\n <!-- The link button hosts the link panel, so it carries the popover's own\n `aria-haspopup` / `aria-expanded` / `aria-controls` \u2014 and no tooltip,\n since one element cannot host `[wrPopover]` twice; its name and\n `aria-keyshortcuts` still say what the others' tooltips say. No\n `aria-pressed` either: a button that opens a popup is not a toggle,\n and the panel's remove button is where \"in a link\" is announced. -->\n <button\n #tool\n #linkPopover=\"wrPopover\"\n wr-btn\n type=\"button\"\n class=\"wr-editor__tool\"\n [class.wr-editor__tool--active]=\"state(item)?.active\"\n [disabled]=\"!state(item)?.available\"\n [attr.aria-label]=\"label(item)\"\n [attr.aria-keyshortcuts]=\"keyShortcuts(item)\"\n [wrPopover]=\"linkPanel\"\n [ariaLabel]=\"label(item)\"\n (opened)=\"onLinkOpened()\"\n (closed)=\"onLinkClosed()\"\n (focus)=\"onToolFocus(buttonIndex()[n])\"\n (keydown)=\"onToolbarKeydown($event)\"\n (mousedown)=\"$event.preventDefault()\"\n >\n <svg\n class=\"wr-editor__icon\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n @for (d of icon(item); track $index) {\n <path [attr.d]=\"d\" />\n }\n </svg>\n </button>\n } @else {\n <button\n #tool\n wr-btn\n type=\"button\"\n class=\"wr-editor__tool\"\n mode=\"tooltip\"\n [class.wr-editor__tool--active]=\"state(item)?.active\"\n [disabled]=\"!state(item)?.available\"\n [attr.aria-pressed]=\"isToggle(item) ? (state(item)?.active ? 'true' : 'false') : null\"\n [attr.aria-label]=\"label(item)\"\n [attr.aria-keyshortcuts]=\"keyShortcuts(item)\"\n [wrPopover]=\"tooltip(item)\"\n (click)=\"onToolClick(item, $event)\"\n (focus)=\"onToolFocus(buttonIndex()[n])\"\n (keydown)=\"onToolbarKeydown($event)\"\n (mousedown)=\"$event.preventDefault()\"\n >\n <svg\n class=\"wr-editor__icon\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n @for (d of icon(item); track $index) {\n <path [attr.d]=\"d\" />\n }\n </svg>\n </button>\n }\n }\n </div>\n}\n\n<!--\n The static preview: what Angular renders until ProseMirror mounts \u2014 on the\n server, and for the first browser frame. It is a SIBLING of the mount surface,\n never inside it: `{ mount }` replaces the element's children, and nodes Angular\n owns would be deleted out from under the next change detection.\n-->\n@if (preview(); as doc) {\n <div class=\"wr-editor__preview wr-editor__content\">\n <ng-container [ngTemplateOutlet]=\"previewBlocks\" [ngTemplateOutletContext]=\"{ $implicit: doc.content }\" />\n </div>\n}\n\n<!--\n The mount surface. Angular binds every attribute it needs, so the prerendered\n page and the synchronous first render already carry the role, the name and the\n field's error state; ProseMirror adds only its own class, `contenteditable` and\n `translate`, and never touches an attribute it did not set. No Angular child\n ever lives in here.\n-->\n<div\n #surface\n class=\"wr-editor__surface wr-editor__content\"\n role=\"textbox\"\n aria-multiline=\"true\"\n [id]=\"surfaceId\"\n [hidden]=\"!mounted()\"\n [attr.aria-label]=\"accessibleName()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"fieldAria.describedBy()\"\n [attr.aria-invalid]=\"fieldAria.ariaInvalid()\"\n [attr.aria-readonly]=\"locked() && !disabled() ? 'true' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n [attr.aria-placeholder]=\"ariaPlaceholder()\"\n [attr.tabindex]=\"locked() && !disabled() ? 0 : null\"\n (focus)=\"focused.set(true)\"\n (blur)=\"focused.set(false)\"\n></div>\n\n<!--\n The popover styles its panel as `.wr-popover-overlay > *`, which is this form,\n so the form stays a plain block and the layout lives one level in.\n-->\n<ng-template #linkPanel>\n <form\n class=\"wr-editor-link\"\n novalidate\n [id]=\"linkField.panelId\"\n (submit)=\"applyLink($event)\"\n (focusout)=\"onFocusOut($event)\"\n >\n <div class=\"wr-editor-link__body\">\n <label class=\"wr-editor-link__label\" [attr.for]=\"linkField.controlId()\">{{ linkUrlLabel() }}</label>\n <input\n wrInput\n class=\"wr-editor-link__input\"\n type=\"text\"\n inputmode=\"url\"\n autocomplete=\"url\"\n spellcheck=\"false\"\n [value]=\"linkDraft()\"\n (input)=\"onLinkInput($event)\"\n />\n @if (linkField.invalid()) {\n <p class=\"wr-editor-link__error\" role=\"alert\" [id]=\"linkField.errorId\">{{ linkInvalidLabel() }}</p>\n }\n <div class=\"wr-editor-link__actions\">\n @if (linkActive()) {\n <button wr-btn type=\"button\" class=\"wr-editor-link__remove\" (click)=\"removeLink()\">\n {{ linkRemoveLabel() }}\n </button>\n }\n <button wr-btn type=\"submit\" color=\"primary\" class=\"wr-editor-link__apply\">{{ linkApplyLabel() }}</button>\n </div>\n </div>\n </form>\n</ng-template>\n\n<!--\n Recursion runs through `ngTemplateOutlet`, as in `<wr-markdown>`, so the preview\n is exactly the document with no wrapper between a `<ul>` and its `<li>`.\n-->\n<ng-template #previewBlocks let-raw>\n @for (node of nodesOf(raw); track $index) {\n @switch (node.type) {\n @case ('paragraph') {\n <p [class.wr-editor__placeholder]=\"ariaPlaceholder() !== null\" [attr.data-placeholder]=\"ariaPlaceholder()\">\n <ng-container [ngTemplateOutlet]=\"previewInline\" [ngTemplateOutletContext]=\"{ $implicit: node.content }\" />\n </p>\n }\n @case ('heading') {\n @switch (attr(node, 'level')) {\n @case (2) {\n <h2>\n <ng-container\n [ngTemplateOutlet]=\"previewInline\"\n [ngTemplateOutletContext]=\"{ $implicit: node.content }\"\n />\n </h2>\n }\n @case (3) {\n <h3>\n <ng-container\n [ngTemplateOutlet]=\"previewInline\"\n [ngTemplateOutletContext]=\"{ $implicit: node.content }\"\n />\n </h3>\n }\n @default {\n <h1>\n <ng-container\n [ngTemplateOutlet]=\"previewInline\"\n [ngTemplateOutletContext]=\"{ $implicit: node.content }\"\n />\n </h1>\n }\n }\n }\n @case ('blockquote') {\n <blockquote>\n <ng-container [ngTemplateOutlet]=\"previewBlocks\" [ngTemplateOutletContext]=\"{ $implicit: node.content }\" />\n </blockquote>\n }\n @case ('code_block') {\n <pre><code>{{ textOf(node) }}</code></pre>\n }\n @case ('horizontal_rule') {\n <hr />\n }\n @case ('bullet_list') {\n <ul>\n <ng-container [ngTemplateOutlet]=\"previewBlocks\" [ngTemplateOutletContext]=\"{ $implicit: node.content }\" />\n </ul>\n }\n @case ('ordered_list') {\n <ol [attr.start]=\"attr(node, 'order') === 1 ? null : attr(node, 'order')\">\n <ng-container [ngTemplateOutlet]=\"previewBlocks\" [ngTemplateOutletContext]=\"{ $implicit: node.content }\" />\n </ol>\n }\n @case ('list_item') {\n @let checked = attr(node, 'checked');\n @if (checked === null) {\n <li>\n <ng-container [ngTemplateOutlet]=\"previewBlocks\" [ngTemplateOutletContext]=\"{ $implicit: node.content }\" />\n </li>\n } @else {\n <li class=\"wr-editor__item wr-editor__item--task\" [attr.data-checked]=\"checked\">\n <span class=\"wr-editor__task\" [class.wr-editor__task--checked]=\"checked === true\" aria-hidden=\"true\"></span>\n <span class=\"wr-editor__sr-only\">{{ checked === true ? taskDoneLabel() : taskTodoLabel() }}</span>\n <div class=\"wr-editor__task-body\">\n <ng-container\n [ngTemplateOutlet]=\"previewBlocks\"\n [ngTemplateOutletContext]=\"{ $implicit: node.content }\"\n />\n </div>\n </li>\n }\n }\n @case ('table') {\n <table>\n <tbody>\n <ng-container [ngTemplateOutlet]=\"previewBlocks\" [ngTemplateOutletContext]=\"{ $implicit: node.content }\" />\n </tbody>\n </table>\n }\n @case ('table_row') {\n <tr>\n <ng-container [ngTemplateOutlet]=\"previewBlocks\" [ngTemplateOutletContext]=\"{ $implicit: node.content }\" />\n </tr>\n }\n @case ('table_header') {\n <th [style.text-align]=\"text(node, 'align')\">\n <ng-container [ngTemplateOutlet]=\"previewBlocks\" [ngTemplateOutletContext]=\"{ $implicit: node.content }\" />\n </th>\n }\n @case ('table_cell') {\n <td [style.text-align]=\"text(node, 'align')\">\n <ng-container [ngTemplateOutlet]=\"previewBlocks\" [ngTemplateOutletContext]=\"{ $implicit: node.content }\" />\n </td>\n }\n }\n }\n</ng-template>\n\n<ng-template #previewInline let-raw>\n @for (node of nodesOf(raw); track $index) {\n <ng-container [ngTemplateOutlet]=\"previewMarked\" [ngTemplateOutletContext]=\"{ $implicit: node, depth: 0 }\" />\n }\n</ng-template>\n\n<!-- One mark per level, outermost first \u2014 the order the schema ranks them in. -->\n<ng-template #previewMarked let-raw let-depth=\"depth\">\n @let node = nodeOf(raw);\n @let mark = markAt(node, depth);\n @if (mark) {\n @switch (mark.type) {\n @case ('link') {\n <a [href]=\"text(mark, 'href')\" [attr.title]=\"text(mark, 'title')\" rel=\"noopener noreferrer nofollow\"\n ><ng-container\n [ngTemplateOutlet]=\"previewMarked\"\n [ngTemplateOutletContext]=\"{ $implicit: node, depth: depth + 1 }\"\n /></a>\n }\n @case ('strong') {\n <strong\n ><ng-container\n [ngTemplateOutlet]=\"previewMarked\"\n [ngTemplateOutletContext]=\"{ $implicit: node, depth: depth + 1 }\"\n /></strong>\n }\n @case ('em') {\n <em\n ><ng-container\n [ngTemplateOutlet]=\"previewMarked\"\n [ngTemplateOutletContext]=\"{ $implicit: node, depth: depth + 1 }\"\n /></em>\n }\n @case ('underline') {\n <u\n ><ng-container\n [ngTemplateOutlet]=\"previewMarked\"\n [ngTemplateOutletContext]=\"{ $implicit: node, depth: depth + 1 }\"\n /></u>\n }\n @case ('strike') {\n <s\n ><ng-container\n [ngTemplateOutlet]=\"previewMarked\"\n [ngTemplateOutletContext]=\"{ $implicit: node, depth: depth + 1 }\"\n /></s>\n }\n @case ('code') {\n <code\n ><ng-container\n [ngTemplateOutlet]=\"previewMarked\"\n [ngTemplateOutletContext]=\"{ $implicit: node, depth: depth + 1 }\"\n /></code>\n }\n @default {\n <ng-container\n [ngTemplateOutlet]=\"previewMarked\"\n [ngTemplateOutletContext]=\"{ $implicit: node, depth: depth + 1 }\"\n />\n }\n }\n } @else {\n @switch (node.type) {\n @case ('text') {\n <ng-container>{{ node.text }}</ng-container>\n }\n @case ('hard_break') {\n <br />\n }\n @case ('image') {\n <img [src]=\"text(node, 'src')\" [alt]=\"text(node, 'alt') ?? ''\" [attr.title]=\"text(node, 'title')\" />\n }\n }\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: WrButton, selector: "wr-btn, button[wr-btn], a[wr-btn]", inputs: ["color", "size", "shape", "icon", "iconPosition", "disabled", "outlined", "block", "loading", "disabledWhenLoading"] }, { kind: "directive", type: WrInput, selector: "input[wrInput], textarea[wrInput]", inputs: ["size", "rounded"] }, { kind: "directive", type: WrPopover, selector: "[wrPopover]", inputs: ["wrPopover", "mode", "trigger", "position", "showDelay", "hideDelay", "responsive", "ariaLabel"], outputs: ["opened", "closed"], exportAs: ["wrPopover"] }], encapsulation: i0.ViewEncapsulation.None });
2584
+ }
2585
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: WrEditor, decorators: [{
2586
+ type: Component,
2587
+ args: [{ selector: 'wr-editor', encapsulation: ViewEncapsulation.None, host: {
2588
+ '[class]': 'classes()',
2589
+ '(focusout)': 'onFocusOut($event)',
2590
+ }, providers: [
2591
+ { provide: WrEditorLinkField, useFactory: () => new WrEditorLinkField() },
2592
+ { provide: WR_FORM_FIELD, useExisting: WrEditorLinkField },
2593
+ ], imports: [NgTemplateOutlet, WrButton, WrInput, WrPopover], template: "@if (tools().length) {\n <div class=\"wr-editor__toolbar\" role=\"toolbar\" [attr.aria-label]=\"toolbarLabel()\" [attr.aria-controls]=\"surfaceId\">\n @for (item of tools(); track $index; let n = $index) {\n @if (item === '|') {\n <span class=\"wr-editor__separator\" aria-hidden=\"true\"></span>\n } @else if (item === 'link') {\n <!-- The link button hosts the link panel, so it carries the popover's own\n `aria-haspopup` / `aria-expanded` / `aria-controls` \u2014 and no tooltip,\n since one element cannot host `[wrPopover]` twice; its name and\n `aria-keyshortcuts` still say what the others' tooltips say. No\n `aria-pressed` either: a button that opens a popup is not a toggle,\n and the panel's remove button is where \"in a link\" is announced. -->\n <button\n #tool\n #linkPopover=\"wrPopover\"\n wr-btn\n type=\"button\"\n class=\"wr-editor__tool\"\n [class.wr-editor__tool--active]=\"state(item)?.active\"\n [disabled]=\"!state(item)?.available\"\n [attr.aria-label]=\"label(item)\"\n [attr.aria-keyshortcuts]=\"keyShortcuts(item)\"\n [wrPopover]=\"linkPanel\"\n [ariaLabel]=\"label(item)\"\n (opened)=\"onLinkOpened()\"\n (closed)=\"onLinkClosed()\"\n (focus)=\"onToolFocus(buttonIndex()[n])\"\n (keydown)=\"onToolbarKeydown($event)\"\n (mousedown)=\"$event.preventDefault()\"\n >\n <svg\n class=\"wr-editor__icon\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n @for (d of icon(item); track $index) {\n <path [attr.d]=\"d\" />\n }\n </svg>\n </button>\n } @else {\n <button\n #tool\n wr-btn\n type=\"button\"\n class=\"wr-editor__tool\"\n mode=\"tooltip\"\n [class.wr-editor__tool--active]=\"state(item)?.active\"\n [disabled]=\"!state(item)?.available\"\n [attr.aria-pressed]=\"isToggle(item) ? (state(item)?.active ? 'true' : 'false') : null\"\n [attr.aria-label]=\"label(item)\"\n [attr.aria-keyshortcuts]=\"keyShortcuts(item)\"\n [wrPopover]=\"tooltip(item)\"\n (click)=\"onToolClick(item, $event)\"\n (focus)=\"onToolFocus(buttonIndex()[n])\"\n (keydown)=\"onToolbarKeydown($event)\"\n (mousedown)=\"$event.preventDefault()\"\n >\n <svg\n class=\"wr-editor__icon\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n @for (d of icon(item); track $index) {\n <path [attr.d]=\"d\" />\n }\n </svg>\n </button>\n }\n }\n </div>\n}\n\n<!--\n The static preview: what Angular renders until ProseMirror mounts \u2014 on the\n server, and for the first browser frame. It is a SIBLING of the mount surface,\n never inside it: `{ mount }` replaces the element's children, and nodes Angular\n owns would be deleted out from under the next change detection.\n-->\n@if (preview(); as doc) {\n <div class=\"wr-editor__preview wr-editor__content\">\n <ng-container [ngTemplateOutlet]=\"previewBlocks\" [ngTemplateOutletContext]=\"{ $implicit: doc.content }\" />\n </div>\n}\n\n<!--\n The mount surface. Angular binds every attribute it needs, so the prerendered\n page and the synchronous first render already carry the role, the name and the\n field's error state; ProseMirror adds only its own class, `contenteditable` and\n `translate`, and never touches an attribute it did not set. No Angular child\n ever lives in here.\n-->\n<div\n #surface\n class=\"wr-editor__surface wr-editor__content\"\n role=\"textbox\"\n aria-multiline=\"true\"\n [id]=\"surfaceId\"\n [hidden]=\"!mounted()\"\n [attr.aria-label]=\"accessibleName()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"fieldAria.describedBy()\"\n [attr.aria-invalid]=\"fieldAria.ariaInvalid()\"\n [attr.aria-readonly]=\"locked() && !disabled() ? 'true' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n [attr.aria-placeholder]=\"ariaPlaceholder()\"\n [attr.tabindex]=\"locked() && !disabled() ? 0 : null\"\n (focus)=\"focused.set(true)\"\n (blur)=\"focused.set(false)\"\n></div>\n\n<!--\n The popover styles its panel as `.wr-popover-overlay > *`, which is this form,\n so the form stays a plain block and the layout lives one level in.\n-->\n<ng-template #linkPanel>\n <form\n class=\"wr-editor-link\"\n novalidate\n [id]=\"linkField.panelId\"\n (submit)=\"applyLink($event)\"\n (focusout)=\"onFocusOut($event)\"\n >\n <div class=\"wr-editor-link__body\">\n <label class=\"wr-editor-link__label\" [attr.for]=\"linkField.controlId()\">{{ linkUrlLabel() }}</label>\n <input\n wrInput\n class=\"wr-editor-link__input\"\n type=\"text\"\n inputmode=\"url\"\n autocomplete=\"url\"\n spellcheck=\"false\"\n [value]=\"linkDraft()\"\n (input)=\"onLinkInput($event)\"\n />\n @if (linkField.invalid()) {\n <p class=\"wr-editor-link__error\" role=\"alert\" [id]=\"linkField.errorId\">{{ linkInvalidLabel() }}</p>\n }\n <div class=\"wr-editor-link__actions\">\n @if (linkActive()) {\n <button wr-btn type=\"button\" class=\"wr-editor-link__remove\" (click)=\"removeLink()\">\n {{ linkRemoveLabel() }}\n </button>\n }\n <button wr-btn type=\"submit\" color=\"primary\" class=\"wr-editor-link__apply\">{{ linkApplyLabel() }}</button>\n </div>\n </div>\n </form>\n</ng-template>\n\n<!--\n Recursion runs through `ngTemplateOutlet`, as in `<wr-markdown>`, so the preview\n is exactly the document with no wrapper between a `<ul>` and its `<li>`.\n-->\n<ng-template #previewBlocks let-raw>\n @for (node of nodesOf(raw); track $index) {\n @switch (node.type) {\n @case ('paragraph') {\n <p [class.wr-editor__placeholder]=\"ariaPlaceholder() !== null\" [attr.data-placeholder]=\"ariaPlaceholder()\">\n <ng-container [ngTemplateOutlet]=\"previewInline\" [ngTemplateOutletContext]=\"{ $implicit: node.content }\" />\n </p>\n }\n @case ('heading') {\n @switch (attr(node, 'level')) {\n @case (2) {\n <h2>\n <ng-container\n [ngTemplateOutlet]=\"previewInline\"\n [ngTemplateOutletContext]=\"{ $implicit: node.content }\"\n />\n </h2>\n }\n @case (3) {\n <h3>\n <ng-container\n [ngTemplateOutlet]=\"previewInline\"\n [ngTemplateOutletContext]=\"{ $implicit: node.content }\"\n />\n </h3>\n }\n @default {\n <h1>\n <ng-container\n [ngTemplateOutlet]=\"previewInline\"\n [ngTemplateOutletContext]=\"{ $implicit: node.content }\"\n />\n </h1>\n }\n }\n }\n @case ('blockquote') {\n <blockquote>\n <ng-container [ngTemplateOutlet]=\"previewBlocks\" [ngTemplateOutletContext]=\"{ $implicit: node.content }\" />\n </blockquote>\n }\n @case ('code_block') {\n <pre><code>{{ textOf(node) }}</code></pre>\n }\n @case ('horizontal_rule') {\n <hr />\n }\n @case ('bullet_list') {\n <ul>\n <ng-container [ngTemplateOutlet]=\"previewBlocks\" [ngTemplateOutletContext]=\"{ $implicit: node.content }\" />\n </ul>\n }\n @case ('ordered_list') {\n <ol [attr.start]=\"attr(node, 'order') === 1 ? null : attr(node, 'order')\">\n <ng-container [ngTemplateOutlet]=\"previewBlocks\" [ngTemplateOutletContext]=\"{ $implicit: node.content }\" />\n </ol>\n }\n @case ('list_item') {\n @let checked = attr(node, 'checked');\n @if (checked === null) {\n <li>\n <ng-container [ngTemplateOutlet]=\"previewBlocks\" [ngTemplateOutletContext]=\"{ $implicit: node.content }\" />\n </li>\n } @else {\n <li class=\"wr-editor__item wr-editor__item--task\" [attr.data-checked]=\"checked\">\n <span class=\"wr-editor__task\" [class.wr-editor__task--checked]=\"checked === true\" aria-hidden=\"true\"></span>\n <span class=\"wr-editor__sr-only\">{{ checked === true ? taskDoneLabel() : taskTodoLabel() }}</span>\n <div class=\"wr-editor__task-body\">\n <ng-container\n [ngTemplateOutlet]=\"previewBlocks\"\n [ngTemplateOutletContext]=\"{ $implicit: node.content }\"\n />\n </div>\n </li>\n }\n }\n @case ('table') {\n <table>\n <tbody>\n <ng-container [ngTemplateOutlet]=\"previewBlocks\" [ngTemplateOutletContext]=\"{ $implicit: node.content }\" />\n </tbody>\n </table>\n }\n @case ('table_row') {\n <tr>\n <ng-container [ngTemplateOutlet]=\"previewBlocks\" [ngTemplateOutletContext]=\"{ $implicit: node.content }\" />\n </tr>\n }\n @case ('table_header') {\n <th [style.text-align]=\"text(node, 'align')\">\n <ng-container [ngTemplateOutlet]=\"previewBlocks\" [ngTemplateOutletContext]=\"{ $implicit: node.content }\" />\n </th>\n }\n @case ('table_cell') {\n <td [style.text-align]=\"text(node, 'align')\">\n <ng-container [ngTemplateOutlet]=\"previewBlocks\" [ngTemplateOutletContext]=\"{ $implicit: node.content }\" />\n </td>\n }\n }\n }\n</ng-template>\n\n<ng-template #previewInline let-raw>\n @for (node of nodesOf(raw); track $index) {\n <ng-container [ngTemplateOutlet]=\"previewMarked\" [ngTemplateOutletContext]=\"{ $implicit: node, depth: 0 }\" />\n }\n</ng-template>\n\n<!-- One mark per level, outermost first \u2014 the order the schema ranks them in. -->\n<ng-template #previewMarked let-raw let-depth=\"depth\">\n @let node = nodeOf(raw);\n @let mark = markAt(node, depth);\n @if (mark) {\n @switch (mark.type) {\n @case ('link') {\n <a [href]=\"text(mark, 'href')\" [attr.title]=\"text(mark, 'title')\" rel=\"noopener noreferrer nofollow\"\n ><ng-container\n [ngTemplateOutlet]=\"previewMarked\"\n [ngTemplateOutletContext]=\"{ $implicit: node, depth: depth + 1 }\"\n /></a>\n }\n @case ('strong') {\n <strong\n ><ng-container\n [ngTemplateOutlet]=\"previewMarked\"\n [ngTemplateOutletContext]=\"{ $implicit: node, depth: depth + 1 }\"\n /></strong>\n }\n @case ('em') {\n <em\n ><ng-container\n [ngTemplateOutlet]=\"previewMarked\"\n [ngTemplateOutletContext]=\"{ $implicit: node, depth: depth + 1 }\"\n /></em>\n }\n @case ('underline') {\n <u\n ><ng-container\n [ngTemplateOutlet]=\"previewMarked\"\n [ngTemplateOutletContext]=\"{ $implicit: node, depth: depth + 1 }\"\n /></u>\n }\n @case ('strike') {\n <s\n ><ng-container\n [ngTemplateOutlet]=\"previewMarked\"\n [ngTemplateOutletContext]=\"{ $implicit: node, depth: depth + 1 }\"\n /></s>\n }\n @case ('code') {\n <code\n ><ng-container\n [ngTemplateOutlet]=\"previewMarked\"\n [ngTemplateOutletContext]=\"{ $implicit: node, depth: depth + 1 }\"\n /></code>\n }\n @default {\n <ng-container\n [ngTemplateOutlet]=\"previewMarked\"\n [ngTemplateOutletContext]=\"{ $implicit: node, depth: depth + 1 }\"\n />\n }\n }\n } @else {\n @switch (node.type) {\n @case ('text') {\n <ng-container>{{ node.text }}</ng-container>\n }\n @case ('hard_break') {\n <br />\n }\n @case ('image') {\n <img [src]=\"text(node, 'src')\" [alt]=\"text(node, 'alt') ?? ''\" [attr.title]=\"text(node, 'title')\" />\n }\n }\n }\n</ng-template>\n" }]
2594
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], touch: [{ type: i0.Output, args: ["touch"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], toolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolbar", required: false }] }], surface: [{ type: i0.ViewChild, args: ['surface', { isSignal: true }] }], toolButtons: [{ type: i0.ViewChildren, args: ['tool', { ...{ read: ElementRef }, isSignal: true }] }], linkPopover: [{ type: i0.ViewChild, args: ['linkPopover', { isSignal: true }] }] } });
2595
+
2596
+ /**
2597
+ * Generated bundle index. Do not edit.
2598
+ */
2599
+
2600
+ export { WR_EDITOR_TOOLBAR, WrEditor };
2601
+ //# sourceMappingURL=ngwr-editor.mjs.map