@aquera/nile-elements 1.9.5 → 1.9.7

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 (565) hide show
  1. package/README.md +9 -0
  2. package/dist/index-178e1e57.cjs.js +2 -0
  3. package/dist/index-178e1e57.cjs.js.map +1 -0
  4. package/dist/index-1faa963c.esm.js +1 -0
  5. package/dist/index-249de90a.esm.js +1 -0
  6. package/dist/index-353172ed.esm.js +1 -0
  7. package/dist/index-6f3fa4ab.esm.js +1 -0
  8. package/dist/index-89c520e9.cjs.js +5 -0
  9. package/dist/index-89c520e9.cjs.js.map +1 -0
  10. package/dist/index-920675c4.esm.js +1 -0
  11. package/dist/index-b387a9f4.cjs.js +2 -0
  12. package/dist/index-b387a9f4.cjs.js.map +1 -0
  13. package/dist/index-cc1385e0.cjs.js +2 -0
  14. package/dist/index-cc1385e0.cjs.js.map +1 -0
  15. package/dist/index-dedc38a6.esm.js +1 -0
  16. package/dist/index-df196058.cjs.js +2 -0
  17. package/dist/index-df196058.cjs.js.map +1 -0
  18. package/dist/index-f956f56d.cjs.js +7 -0
  19. package/dist/index-f956f56d.cjs.js.map +1 -0
  20. package/dist/index.cjs.js +1 -1
  21. package/dist/index.esm.js +1 -1
  22. package/dist/index.js +3802 -664
  23. package/dist/internal/glyph.cjs.js +2 -0
  24. package/dist/internal/glyph.cjs.js.map +1 -0
  25. package/dist/internal/glyph.esm.js +5 -0
  26. package/dist/nile-auto-complete/index.cjs.js +1 -1
  27. package/dist/nile-auto-complete/nile-auto-complete.cjs.js +1 -1
  28. package/dist/nile-auto-complete/portal-manager.cjs.js +1 -1
  29. package/dist/nile-auto-complete/portal-utils.cjs.js +1 -1
  30. package/dist/nile-chip/nile-chip.css.cjs.js +1 -1
  31. package/dist/nile-chip/nile-chip.css.cjs.js.map +1 -1
  32. package/dist/nile-chip/nile-chip.css.esm.js +8 -1
  33. package/dist/nile-code-editor/extensionSetup.cjs.js +7 -7
  34. package/dist/nile-code-editor/extensionSetup.cjs.js.map +1 -1
  35. package/dist/nile-code-editor/extensionSetup.esm.js +1 -1
  36. package/dist/nile-code-editor/index.cjs.js +1 -1
  37. package/dist/nile-code-editor/index.esm.js +1 -1
  38. package/dist/nile-code-editor/nile-code-editor.cjs.js +2 -2
  39. package/dist/nile-code-editor/nile-code-editor.cjs.js.map +1 -1
  40. package/dist/nile-code-editor/nile-code-editor.esm.js +4 -4
  41. package/dist/nile-color-picker/index.cjs.js +1 -1
  42. package/dist/nile-color-picker/nile-color-picker.cjs.js +1 -1
  43. package/dist/nile-combobox/index.cjs.js +1 -1
  44. package/dist/nile-combobox/index.esm.js +1 -1
  45. package/dist/nile-combobox/nile-combobox.cjs.js +1 -1
  46. package/dist/nile-combobox/nile-combobox.esm.js +1 -1
  47. package/dist/nile-combobox/portal-manager.cjs.js +1 -1
  48. package/dist/nile-context-menu/index.cjs.js +1 -1
  49. package/dist/nile-context-menu/nile-context-menu.cjs.js +1 -1
  50. package/dist/nile-context-submenu/index.cjs.js +1 -1
  51. package/dist/nile-context-submenu/nile-context-submenu.cjs.js +1 -1
  52. package/dist/nile-detail/index.cjs.js +1 -1
  53. package/dist/nile-detail/index.esm.js +1 -1
  54. package/dist/nile-detail/nile-detail.cjs.js +1 -1
  55. package/dist/nile-detail/nile-detail.esm.js +1 -1
  56. package/dist/nile-dropdown/index.cjs.js +1 -1
  57. package/dist/nile-dropdown/nile-dropdown.cjs.js +1 -1
  58. package/dist/nile-dropdown/portal-manager.cjs.js +1 -1
  59. package/dist/nile-file-upload/index.cjs.js +1 -1
  60. package/dist/nile-file-upload/nile-file-upload.cjs.js +1 -1
  61. package/dist/nile-file-upload/nile-file-upload.template.cjs.js +1 -1
  62. package/dist/nile-floating-panel/index.cjs.js +1 -1
  63. package/dist/nile-floating-panel/nile-floating-panel.cjs.js +1 -1
  64. package/dist/nile-hero/nile-hero.cjs.js +1 -1
  65. package/dist/nile-hero/nile-hero.cjs.js.map +1 -1
  66. package/dist/nile-hero/nile-hero.css.cjs.js +1 -1
  67. package/dist/nile-hero/nile-hero.css.cjs.js.map +1 -1
  68. package/dist/nile-hero/nile-hero.css.esm.js +14 -14
  69. package/dist/nile-hero/nile-hero.esm.js +1 -1
  70. package/dist/nile-hero-header/nile-hero-header.cjs.js +1 -1
  71. package/dist/nile-hero-header/nile-hero-header.cjs.js.map +1 -1
  72. package/dist/nile-hero-header/nile-hero-header.css.cjs.js +1 -1
  73. package/dist/nile-hero-header/nile-hero-header.css.cjs.js.map +1 -1
  74. package/dist/nile-hero-header/nile-hero-header.css.esm.js +11 -11
  75. package/dist/nile-hero-header/nile-hero-header.esm.js +3 -3
  76. package/dist/nile-lite-tooltip/index.cjs.js +1 -1
  77. package/dist/nile-lite-tooltip/nile-lite-tooltip.cjs.js +1 -1
  78. package/dist/nile-markdown-editor/index.cjs.js +1 -1
  79. package/dist/nile-markdown-editor/nile-markdown-editor.cjs.js +1 -1
  80. package/dist/nile-popover/index.cjs.js +1 -1
  81. package/dist/nile-popover/nile-popover.cjs.js +1 -1
  82. package/dist/nile-popover/portal-manager.cjs.js +1 -1
  83. package/dist/nile-popup/index.cjs.js +1 -1
  84. package/dist/nile-popup/nile-popup.cjs.js +1 -1
  85. package/dist/nile-rich-text-editor/index.cjs.js +1 -1
  86. package/dist/nile-rich-text-editor/index.esm.js +1 -1
  87. package/dist/nile-rich-text-editor/nile-rich-text-editor.cjs.js +1 -1
  88. package/dist/nile-rich-text-editor/nile-rich-text-editor.cjs.js.map +1 -1
  89. package/dist/nile-rich-text-editor/nile-rich-text-editor.css.cjs.js +1 -1
  90. package/dist/nile-rich-text-editor/nile-rich-text-editor.css.cjs.js.map +1 -1
  91. package/dist/nile-rich-text-editor/nile-rich-text-editor.css.esm.js +70 -0
  92. package/dist/nile-rich-text-editor/nile-rich-text-editor.esm.js +1 -1
  93. package/dist/nile-rich-text-editor/nile-rte-image.cjs.js +3 -0
  94. package/dist/nile-rich-text-editor/nile-rte-image.cjs.js.map +1 -0
  95. package/dist/nile-rich-text-editor/nile-rte-image.esm.js +96 -0
  96. package/dist/nile-select/index.cjs.js +1 -1
  97. package/dist/nile-select/nile-select.cjs.js +1 -1
  98. package/dist/nile-select/portal-manager.cjs.js +1 -1
  99. package/dist/nile-side-bar-action/index.cjs.js +1 -1
  100. package/dist/nile-side-bar-action/nile-side-bar-action.cjs.js +1 -1
  101. package/dist/nile-side-bar-action/portal-manager.cjs.js +1 -1
  102. package/dist/nile-tooltip/index.cjs.js +1 -1
  103. package/dist/nile-tooltip/nile-tooltip.cjs.js +1 -1
  104. package/dist/nile-virtual-select/index.cjs.js +1 -1
  105. package/dist/nile-virtual-select/nile-virtual-select.cjs.js +1 -1
  106. package/dist/nile-virtual-select/portal-manager.cjs.js +1 -1
  107. package/dist/nile-virtual-select/renderer.cjs.js +1 -1
  108. package/dist/nile-wysiwyg-editor/emoji.cjs.js +2 -0
  109. package/dist/nile-wysiwyg-editor/emoji.cjs.js.map +1 -0
  110. package/dist/nile-wysiwyg-editor/emoji.esm.js +1 -0
  111. package/dist/nile-wysiwyg-editor/engine/commands.cjs.js +2 -0
  112. package/dist/nile-wysiwyg-editor/engine/commands.cjs.js.map +1 -0
  113. package/dist/nile-wysiwyg-editor/engine/commands.esm.js +1 -0
  114. package/dist/nile-wysiwyg-editor/engine/create-state.cjs.js +2 -0
  115. package/dist/nile-wysiwyg-editor/engine/create-state.cjs.js.map +1 -0
  116. package/dist/nile-wysiwyg-editor/engine/create-state.esm.js +1 -0
  117. package/dist/nile-wysiwyg-editor/engine/export.cjs.js +2 -0
  118. package/dist/nile-wysiwyg-editor/engine/export.cjs.js.map +1 -0
  119. package/dist/nile-wysiwyg-editor/engine/export.esm.js +1 -0
  120. package/dist/nile-wysiwyg-editor/engine/index.cjs.js +2 -0
  121. package/dist/nile-wysiwyg-editor/engine/index.cjs.js.map +1 -0
  122. package/dist/nile-wysiwyg-editor/engine/index.esm.js +1 -0
  123. package/dist/nile-wysiwyg-editor/engine/inputrules.cjs.js +2 -0
  124. package/dist/nile-wysiwyg-editor/engine/inputrules.cjs.js.map +1 -0
  125. package/dist/nile-wysiwyg-editor/engine/inputrules.esm.js +1 -0
  126. package/dist/nile-wysiwyg-editor/engine/keymap.cjs.js +2 -0
  127. package/dist/nile-wysiwyg-editor/engine/keymap.cjs.js.map +1 -0
  128. package/dist/nile-wysiwyg-editor/engine/keymap.esm.js +1 -0
  129. package/dist/nile-wysiwyg-editor/engine/markdown-parse.cjs.js +2 -0
  130. package/dist/nile-wysiwyg-editor/engine/markdown-parse.cjs.js.map +1 -0
  131. package/dist/nile-wysiwyg-editor/engine/markdown-parse.esm.js +1 -0
  132. package/dist/nile-wysiwyg-editor/engine/markdown.cjs.js +2 -0
  133. package/dist/nile-wysiwyg-editor/engine/markdown.cjs.js.map +1 -0
  134. package/dist/nile-wysiwyg-editor/engine/markdown.esm.js +1 -0
  135. package/dist/nile-wysiwyg-editor/engine/office-paste.cjs.js +2 -0
  136. package/dist/nile-wysiwyg-editor/engine/office-paste.cjs.js.map +1 -0
  137. package/dist/nile-wysiwyg-editor/engine/office-paste.esm.js +1 -0
  138. package/dist/nile-wysiwyg-editor/engine/plugins/code-highlight.cjs.js +2 -0
  139. package/dist/nile-wysiwyg-editor/engine/plugins/code-highlight.cjs.js.map +1 -0
  140. package/dist/nile-wysiwyg-editor/engine/plugins/code-highlight.esm.js +1 -0
  141. package/dist/nile-wysiwyg-editor/engine/plugins/embed-view.cjs.js +2 -0
  142. package/dist/nile-wysiwyg-editor/engine/plugins/embed-view.cjs.js.map +1 -0
  143. package/dist/nile-wysiwyg-editor/engine/plugins/embed-view.esm.js +1 -0
  144. package/dist/nile-wysiwyg-editor/engine/plugins/embeds.cjs.js +2 -0
  145. package/dist/nile-wysiwyg-editor/engine/plugins/embeds.cjs.js.map +1 -0
  146. package/dist/nile-wysiwyg-editor/engine/plugins/embeds.esm.js +1 -0
  147. package/dist/nile-wysiwyg-editor/engine/plugins/find-replace.cjs.js +2 -0
  148. package/dist/nile-wysiwyg-editor/engine/plugins/find-replace.cjs.js.map +1 -0
  149. package/dist/nile-wysiwyg-editor/engine/plugins/find-replace.esm.js +1 -0
  150. package/dist/nile-wysiwyg-editor/engine/plugins/format-painter.cjs.js +2 -0
  151. package/dist/nile-wysiwyg-editor/engine/plugins/format-painter.cjs.js.map +1 -0
  152. package/dist/nile-wysiwyg-editor/engine/plugins/format-painter.esm.js +1 -0
  153. package/dist/nile-wysiwyg-editor/engine/plugins/image-view.cjs.js +2 -0
  154. package/dist/nile-wysiwyg-editor/engine/plugins/image-view.cjs.js.map +1 -0
  155. package/dist/nile-wysiwyg-editor/engine/plugins/image-view.esm.js +1 -0
  156. package/dist/nile-wysiwyg-editor/engine/plugins/image.cjs.js +2 -0
  157. package/dist/nile-wysiwyg-editor/engine/plugins/image.cjs.js.map +1 -0
  158. package/dist/nile-wysiwyg-editor/engine/plugins/image.esm.js +1 -0
  159. package/dist/nile-wysiwyg-editor/engine/plugins/link.cjs.js +2 -0
  160. package/dist/nile-wysiwyg-editor/engine/plugins/link.cjs.js.map +1 -0
  161. package/dist/nile-wysiwyg-editor/engine/plugins/link.esm.js +1 -0
  162. package/dist/nile-wysiwyg-editor/engine/plugins/math-view.cjs.js +2 -0
  163. package/dist/nile-wysiwyg-editor/engine/plugins/math-view.cjs.js.map +1 -0
  164. package/dist/nile-wysiwyg-editor/engine/plugins/math-view.esm.js +1 -0
  165. package/dist/nile-wysiwyg-editor/engine/plugins/max-length.cjs.js +2 -0
  166. package/dist/nile-wysiwyg-editor/engine/plugins/max-length.cjs.js.map +1 -0
  167. package/dist/nile-wysiwyg-editor/engine/plugins/max-length.esm.js +1 -0
  168. package/dist/nile-wysiwyg-editor/engine/plugins/paste.cjs.js +2 -0
  169. package/dist/nile-wysiwyg-editor/engine/plugins/paste.cjs.js.map +1 -0
  170. package/dist/nile-wysiwyg-editor/engine/plugins/paste.esm.js +1 -0
  171. package/dist/nile-wysiwyg-editor/engine/plugins/placeholder.cjs.js +2 -0
  172. package/dist/nile-wysiwyg-editor/engine/plugins/placeholder.cjs.js.map +1 -0
  173. package/dist/nile-wysiwyg-editor/engine/plugins/placeholder.esm.js +1 -0
  174. package/dist/nile-wysiwyg-editor/engine/plugins/single-line.cjs.js +2 -0
  175. package/dist/nile-wysiwyg-editor/engine/plugins/single-line.cjs.js.map +1 -0
  176. package/dist/nile-wysiwyg-editor/engine/plugins/single-line.esm.js +1 -0
  177. package/dist/nile-wysiwyg-editor/engine/plugins/suggestions.cjs.js +2 -0
  178. package/dist/nile-wysiwyg-editor/engine/plugins/suggestions.cjs.js.map +1 -0
  179. package/dist/nile-wysiwyg-editor/engine/plugins/suggestions.esm.js +1 -0
  180. package/dist/nile-wysiwyg-editor/engine/plugins/tables.cjs.js +2 -0
  181. package/dist/nile-wysiwyg-editor/engine/plugins/tables.cjs.js.map +1 -0
  182. package/dist/nile-wysiwyg-editor/engine/plugins/tables.esm.js +1 -0
  183. package/dist/nile-wysiwyg-editor/engine/plugins/task-list.cjs.js +2 -0
  184. package/dist/nile-wysiwyg-editor/engine/plugins/task-list.cjs.js.map +1 -0
  185. package/dist/nile-wysiwyg-editor/engine/plugins/task-list.esm.js +1 -0
  186. package/dist/nile-wysiwyg-editor/engine/sanitize.cjs.js +2 -0
  187. package/dist/nile-wysiwyg-editor/engine/sanitize.cjs.js.map +1 -0
  188. package/dist/nile-wysiwyg-editor/engine/sanitize.esm.js +1 -0
  189. package/dist/nile-wysiwyg-editor/engine/schema.cjs.js +2 -0
  190. package/dist/nile-wysiwyg-editor/engine/schema.cjs.js.map +1 -0
  191. package/dist/nile-wysiwyg-editor/engine/schema.esm.js +1 -0
  192. package/dist/nile-wysiwyg-editor/engine/selection-state.cjs.js +2 -0
  193. package/dist/nile-wysiwyg-editor/engine/selection-state.cjs.js.map +1 -0
  194. package/dist/nile-wysiwyg-editor/engine/selection-state.esm.js +1 -0
  195. package/dist/nile-wysiwyg-editor/engine/serializer.cjs.js +2 -0
  196. package/dist/nile-wysiwyg-editor/engine/serializer.cjs.js.map +1 -0
  197. package/dist/nile-wysiwyg-editor/engine/serializer.esm.js +1 -0
  198. package/dist/nile-wysiwyg-editor/index.cjs.js +2 -0
  199. package/dist/nile-wysiwyg-editor/index.cjs.js.map +1 -0
  200. package/dist/nile-wysiwyg-editor/index.esm.js +1 -0
  201. package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.cjs.js +2 -0
  202. package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.cjs.js.map +1 -0
  203. package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.css.cjs.js +2 -0
  204. package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.css.cjs.js.map +1 -0
  205. package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.css.esm.js +780 -0
  206. package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.esm.js +97 -0
  207. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.cjs.js +2 -0
  208. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.cjs.js.map +1 -0
  209. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.esm.js +1 -0
  210. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.cjs.js +2 -0
  211. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.cjs.js.map +1 -0
  212. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.cjs.js +2 -0
  213. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.cjs.js.map +1 -0
  214. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.esm.js +244 -0
  215. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.esm.js +137 -0
  216. package/dist/nile-wysiwyg-editor/types.cjs.js +2 -0
  217. package/dist/nile-wysiwyg-editor/types.cjs.js.map +1 -0
  218. package/dist/nile-wysiwyg-editor/types.esm.js +1 -0
  219. package/dist/nile-wysiwyg-editor/ui/bookmark-panel.cjs.js +2 -0
  220. package/dist/nile-wysiwyg-editor/ui/bookmark-panel.cjs.js.map +1 -0
  221. package/dist/nile-wysiwyg-editor/ui/bookmark-panel.esm.js +97 -0
  222. package/dist/nile-wysiwyg-editor/ui/bubble-menu.cjs.js +2 -0
  223. package/dist/nile-wysiwyg-editor/ui/bubble-menu.cjs.js.map +1 -0
  224. package/dist/nile-wysiwyg-editor/ui/bubble-menu.esm.js +67 -0
  225. package/dist/nile-wysiwyg-editor/ui/code-block-menu.cjs.js +2 -0
  226. package/dist/nile-wysiwyg-editor/ui/code-block-menu.cjs.js.map +1 -0
  227. package/dist/nile-wysiwyg-editor/ui/code-block-menu.esm.js +65 -0
  228. package/dist/nile-wysiwyg-editor/ui/embed-menu.cjs.js +2 -0
  229. package/dist/nile-wysiwyg-editor/ui/embed-menu.cjs.js.map +1 -0
  230. package/dist/nile-wysiwyg-editor/ui/embed-menu.esm.js +64 -0
  231. package/dist/nile-wysiwyg-editor/ui/embed-panel.cjs.js +2 -0
  232. package/dist/nile-wysiwyg-editor/ui/embed-panel.cjs.js.map +1 -0
  233. package/dist/nile-wysiwyg-editor/ui/embed-panel.esm.js +68 -0
  234. package/dist/nile-wysiwyg-editor/ui/emoji-picker.cjs.js +2 -0
  235. package/dist/nile-wysiwyg-editor/ui/emoji-picker.cjs.js.map +1 -0
  236. package/dist/nile-wysiwyg-editor/ui/emoji-picker.esm.js +96 -0
  237. package/dist/nile-wysiwyg-editor/ui/find-replace.cjs.js +2 -0
  238. package/dist/nile-wysiwyg-editor/ui/find-replace.cjs.js.map +1 -0
  239. package/dist/nile-wysiwyg-editor/ui/find-replace.esm.js +112 -0
  240. package/dist/nile-wysiwyg-editor/ui/image-menu.cjs.js +2 -0
  241. package/dist/nile-wysiwyg-editor/ui/image-menu.cjs.js.map +1 -0
  242. package/dist/nile-wysiwyg-editor/ui/image-menu.esm.js +64 -0
  243. package/dist/nile-wysiwyg-editor/ui/image-panel.cjs.js +2 -0
  244. package/dist/nile-wysiwyg-editor/ui/image-panel.cjs.js.map +1 -0
  245. package/dist/nile-wysiwyg-editor/ui/image-panel.esm.js +171 -0
  246. package/dist/nile-wysiwyg-editor/ui/insert-menu.cjs.js +2 -0
  247. package/dist/nile-wysiwyg-editor/ui/insert-menu.cjs.js.map +1 -0
  248. package/dist/nile-wysiwyg-editor/ui/insert-menu.esm.js +90 -0
  249. package/dist/nile-wysiwyg-editor/ui/link-editor.cjs.js +2 -0
  250. package/dist/nile-wysiwyg-editor/ui/link-editor.cjs.js.map +1 -0
  251. package/dist/nile-wysiwyg-editor/ui/link-editor.esm.js +114 -0
  252. package/dist/nile-wysiwyg-editor/ui/markdown-panel.cjs.js +2 -0
  253. package/dist/nile-wysiwyg-editor/ui/markdown-panel.cjs.js.map +1 -0
  254. package/dist/nile-wysiwyg-editor/ui/markdown-panel.esm.js +80 -0
  255. package/dist/nile-wysiwyg-editor/ui/math-panel.cjs.js +2 -0
  256. package/dist/nile-wysiwyg-editor/ui/math-panel.cjs.js.map +1 -0
  257. package/dist/nile-wysiwyg-editor/ui/math-panel.esm.js +92 -0
  258. package/dist/nile-wysiwyg-editor/ui/shortcut-help.cjs.js +2 -0
  259. package/dist/nile-wysiwyg-editor/ui/shortcut-help.cjs.js.map +1 -0
  260. package/dist/nile-wysiwyg-editor/ui/shortcut-help.esm.js +147 -0
  261. package/dist/nile-wysiwyg-editor/ui/source-view.cjs.js +2 -0
  262. package/dist/nile-wysiwyg-editor/ui/source-view.cjs.js.map +1 -0
  263. package/dist/nile-wysiwyg-editor/ui/source-view.esm.js +79 -0
  264. package/dist/nile-wysiwyg-editor/ui/special-chars.cjs.js +2 -0
  265. package/dist/nile-wysiwyg-editor/ui/special-chars.cjs.js.map +1 -0
  266. package/dist/nile-wysiwyg-editor/ui/special-chars.esm.js +96 -0
  267. package/dist/nile-wysiwyg-editor/ui/suggest-list.cjs.js +2 -0
  268. package/dist/nile-wysiwyg-editor/ui/suggest-list.cjs.js.map +1 -0
  269. package/dist/nile-wysiwyg-editor/ui/suggest-list.esm.js +67 -0
  270. package/dist/nile-wysiwyg-editor/ui/table-grid-picker.cjs.js +2 -0
  271. package/dist/nile-wysiwyg-editor/ui/table-grid-picker.cjs.js.map +1 -0
  272. package/dist/nile-wysiwyg-editor/ui/table-grid-picker.esm.js +100 -0
  273. package/dist/nile-wysiwyg-editor/ui/table-menu.cjs.js +2 -0
  274. package/dist/nile-wysiwyg-editor/ui/table-menu.cjs.js.map +1 -0
  275. package/dist/nile-wysiwyg-editor/ui/table-menu.esm.js +58 -0
  276. package/dist/src/index.d.ts +24 -0
  277. package/dist/src/index.js +19 -0
  278. package/dist/src/index.js.map +1 -1
  279. package/dist/src/internal/glyph.d.ts +6 -0
  280. package/dist/src/internal/glyph.js +13 -0
  281. package/dist/src/internal/glyph.js.map +1 -0
  282. package/dist/src/nile-chip/nile-chip.css.js +8 -1
  283. package/dist/src/nile-chip/nile-chip.css.js.map +1 -1
  284. package/dist/src/nile-code-editor/nile-code-editor.js +3 -3
  285. package/dist/src/nile-code-editor/nile-code-editor.js.map +1 -1
  286. package/dist/src/nile-hero/nile-hero.css.js +12 -12
  287. package/dist/src/nile-hero/nile-hero.css.js.map +1 -1
  288. package/dist/src/nile-hero/nile-hero.js +1 -1
  289. package/dist/src/nile-hero/nile-hero.js.map +1 -1
  290. package/dist/src/nile-hero-header/nile-hero-header.css.js +9 -9
  291. package/dist/src/nile-hero-header/nile-hero-header.css.js.map +1 -1
  292. package/dist/src/nile-hero-header/nile-hero-header.js +1 -1
  293. package/dist/src/nile-hero-header/nile-hero-header.js.map +1 -1
  294. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.css.js +70 -0
  295. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.css.js.map +1 -1
  296. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.d.ts +3 -0
  297. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.js +81 -3
  298. package/dist/src/nile-rich-text-editor/nile-rich-text-editor.js.map +1 -1
  299. package/dist/src/nile-rich-text-editor/nile-rte-image.d.ts +87 -0
  300. package/dist/src/nile-rich-text-editor/nile-rte-image.js +1073 -0
  301. package/dist/src/nile-rich-text-editor/nile-rte-image.js.map +1 -0
  302. package/dist/src/nile-wysiwyg-editor/emoji.d.ts +5 -0
  303. package/dist/src/nile-wysiwyg-editor/emoji.js +138 -0
  304. package/dist/src/nile-wysiwyg-editor/emoji.js.map +1 -0
  305. package/dist/src/nile-wysiwyg-editor/engine/commands.d.ts +69 -0
  306. package/dist/src/nile-wysiwyg-editor/engine/commands.js +863 -0
  307. package/dist/src/nile-wysiwyg-editor/engine/commands.js.map +1 -0
  308. package/dist/src/nile-wysiwyg-editor/engine/create-state.d.ts +35 -0
  309. package/dist/src/nile-wysiwyg-editor/engine/create-state.js +67 -0
  310. package/dist/src/nile-wysiwyg-editor/engine/create-state.js.map +1 -0
  311. package/dist/src/nile-wysiwyg-editor/engine/export.d.ts +38 -0
  312. package/dist/src/nile-wysiwyg-editor/engine/export.js +208 -0
  313. package/dist/src/nile-wysiwyg-editor/engine/export.js.map +1 -0
  314. package/dist/src/nile-wysiwyg-editor/engine/export.test.d.ts +1 -0
  315. package/dist/src/nile-wysiwyg-editor/engine/export.test.js +96 -0
  316. package/dist/src/nile-wysiwyg-editor/engine/export.test.js.map +1 -0
  317. package/dist/src/nile-wysiwyg-editor/engine/index.d.ts +37 -0
  318. package/dist/src/nile-wysiwyg-editor/engine/index.js +29 -0
  319. package/dist/src/nile-wysiwyg-editor/engine/index.js.map +1 -0
  320. package/dist/src/nile-wysiwyg-editor/engine/inputrules.d.ts +12 -0
  321. package/dist/src/nile-wysiwyg-editor/engine/inputrules.js +144 -0
  322. package/dist/src/nile-wysiwyg-editor/engine/inputrules.js.map +1 -0
  323. package/dist/src/nile-wysiwyg-editor/engine/keymap.d.ts +15 -0
  324. package/dist/src/nile-wysiwyg-editor/engine/keymap.js +94 -0
  325. package/dist/src/nile-wysiwyg-editor/engine/keymap.js.map +1 -0
  326. package/dist/src/nile-wysiwyg-editor/engine/markdown-parse.d.ts +22 -0
  327. package/dist/src/nile-wysiwyg-editor/engine/markdown-parse.js +287 -0
  328. package/dist/src/nile-wysiwyg-editor/engine/markdown-parse.js.map +1 -0
  329. package/dist/src/nile-wysiwyg-editor/engine/markdown-parse.test.d.ts +1 -0
  330. package/dist/src/nile-wysiwyg-editor/engine/markdown-parse.test.js +106 -0
  331. package/dist/src/nile-wysiwyg-editor/engine/markdown-parse.test.js.map +1 -0
  332. package/dist/src/nile-wysiwyg-editor/engine/markdown.d.ts +10 -0
  333. package/dist/src/nile-wysiwyg-editor/engine/markdown.js +162 -0
  334. package/dist/src/nile-wysiwyg-editor/engine/markdown.js.map +1 -0
  335. package/dist/src/nile-wysiwyg-editor/engine/office-paste.d.ts +13 -0
  336. package/dist/src/nile-wysiwyg-editor/engine/office-paste.js +182 -0
  337. package/dist/src/nile-wysiwyg-editor/engine/office-paste.js.map +1 -0
  338. package/dist/src/nile-wysiwyg-editor/engine/plugins/code-highlight.d.ts +10 -0
  339. package/dist/src/nile-wysiwyg-editor/engine/plugins/code-highlight.js +165 -0
  340. package/dist/src/nile-wysiwyg-editor/engine/plugins/code-highlight.js.map +1 -0
  341. package/dist/src/nile-wysiwyg-editor/engine/plugins/embed-view.d.ts +17 -0
  342. package/dist/src/nile-wysiwyg-editor/engine/plugins/embed-view.js +69 -0
  343. package/dist/src/nile-wysiwyg-editor/engine/plugins/embed-view.js.map +1 -0
  344. package/dist/src/nile-wysiwyg-editor/engine/plugins/embeds.d.ts +17 -0
  345. package/dist/src/nile-wysiwyg-editor/engine/plugins/embeds.js +96 -0
  346. package/dist/src/nile-wysiwyg-editor/engine/plugins/embeds.js.map +1 -0
  347. package/dist/src/nile-wysiwyg-editor/engine/plugins/find-replace.d.ts +26 -0
  348. package/dist/src/nile-wysiwyg-editor/engine/plugins/find-replace.js +123 -0
  349. package/dist/src/nile-wysiwyg-editor/engine/plugins/find-replace.js.map +1 -0
  350. package/dist/src/nile-wysiwyg-editor/engine/plugins/format-painter.d.ts +28 -0
  351. package/dist/src/nile-wysiwyg-editor/engine/plugins/format-painter.js +72 -0
  352. package/dist/src/nile-wysiwyg-editor/engine/plugins/format-painter.js.map +1 -0
  353. package/dist/src/nile-wysiwyg-editor/engine/plugins/image-view.d.ts +41 -0
  354. package/dist/src/nile-wysiwyg-editor/engine/plugins/image-view.js +176 -0
  355. package/dist/src/nile-wysiwyg-editor/engine/plugins/image-view.js.map +1 -0
  356. package/dist/src/nile-wysiwyg-editor/engine/plugins/image.d.ts +15 -0
  357. package/dist/src/nile-wysiwyg-editor/engine/plugins/image.js +64 -0
  358. package/dist/src/nile-wysiwyg-editor/engine/plugins/image.js.map +1 -0
  359. package/dist/src/nile-wysiwyg-editor/engine/plugins/link.d.ts +11 -0
  360. package/dist/src/nile-wysiwyg-editor/engine/plugins/link.js +42 -0
  361. package/dist/src/nile-wysiwyg-editor/engine/plugins/link.js.map +1 -0
  362. package/dist/src/nile-wysiwyg-editor/engine/plugins/math-view.d.ts +18 -0
  363. package/dist/src/nile-wysiwyg-editor/engine/plugins/math-view.js +46 -0
  364. package/dist/src/nile-wysiwyg-editor/engine/plugins/math-view.js.map +1 -0
  365. package/dist/src/nile-wysiwyg-editor/engine/plugins/max-length.d.ts +7 -0
  366. package/dist/src/nile-wysiwyg-editor/engine/plugins/max-length.js +26 -0
  367. package/dist/src/nile-wysiwyg-editor/engine/plugins/max-length.js.map +1 -0
  368. package/dist/src/nile-wysiwyg-editor/engine/plugins/paste.d.ts +31 -0
  369. package/dist/src/nile-wysiwyg-editor/engine/plugins/paste.js +55 -0
  370. package/dist/src/nile-wysiwyg-editor/engine/plugins/paste.js.map +1 -0
  371. package/dist/src/nile-wysiwyg-editor/engine/plugins/placeholder.d.ts +7 -0
  372. package/dist/src/nile-wysiwyg-editor/engine/plugins/placeholder.js +25 -0
  373. package/dist/src/nile-wysiwyg-editor/engine/plugins/placeholder.js.map +1 -0
  374. package/dist/src/nile-wysiwyg-editor/engine/plugins/single-line.d.ts +10 -0
  375. package/dist/src/nile-wysiwyg-editor/engine/plugins/single-line.js +48 -0
  376. package/dist/src/nile-wysiwyg-editor/engine/plugins/single-line.js.map +1 -0
  377. package/dist/src/nile-wysiwyg-editor/engine/plugins/suggestions.d.ts +29 -0
  378. package/dist/src/nile-wysiwyg-editor/engine/plugins/suggestions.js +87 -0
  379. package/dist/src/nile-wysiwyg-editor/engine/plugins/suggestions.js.map +1 -0
  380. package/dist/src/nile-wysiwyg-editor/engine/plugins/tables.d.ts +8 -0
  381. package/dist/src/nile-wysiwyg-editor/engine/plugins/tables.js +11 -0
  382. package/dist/src/nile-wysiwyg-editor/engine/plugins/tables.js.map +1 -0
  383. package/dist/src/nile-wysiwyg-editor/engine/plugins/task-list.d.ts +16 -0
  384. package/dist/src/nile-wysiwyg-editor/engine/plugins/task-list.js +49 -0
  385. package/dist/src/nile-wysiwyg-editor/engine/plugins/task-list.js.map +1 -0
  386. package/dist/src/nile-wysiwyg-editor/engine/pm.test.d.ts +1 -0
  387. package/dist/src/nile-wysiwyg-editor/engine/pm.test.js +883 -0
  388. package/dist/src/nile-wysiwyg-editor/engine/pm.test.js.map +1 -0
  389. package/dist/src/nile-wysiwyg-editor/engine/productivity.test.d.ts +1 -0
  390. package/dist/src/nile-wysiwyg-editor/engine/productivity.test.js +129 -0
  391. package/dist/src/nile-wysiwyg-editor/engine/productivity.test.js.map +1 -0
  392. package/dist/src/nile-wysiwyg-editor/engine/sanitize.d.ts +13 -0
  393. package/dist/src/nile-wysiwyg-editor/engine/sanitize.js +155 -0
  394. package/dist/src/nile-wysiwyg-editor/engine/sanitize.js.map +1 -0
  395. package/dist/src/nile-wysiwyg-editor/engine/schema.d.ts +20 -0
  396. package/dist/src/nile-wysiwyg-editor/engine/schema.js +823 -0
  397. package/dist/src/nile-wysiwyg-editor/engine/schema.js.map +1 -0
  398. package/dist/src/nile-wysiwyg-editor/engine/selection-state.d.ts +70 -0
  399. package/dist/src/nile-wysiwyg-editor/engine/selection-state.js +138 -0
  400. package/dist/src/nile-wysiwyg-editor/engine/selection-state.js.map +1 -0
  401. package/dist/src/nile-wysiwyg-editor/engine/serializer.d.ts +18 -0
  402. package/dist/src/nile-wysiwyg-editor/engine/serializer.js +63 -0
  403. package/dist/src/nile-wysiwyg-editor/engine/serializer.js.map +1 -0
  404. package/dist/src/nile-wysiwyg-editor/index.d.ts +3 -0
  405. package/dist/src/nile-wysiwyg-editor/index.js +3 -0
  406. package/dist/src/nile-wysiwyg-editor/index.js.map +1 -0
  407. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.css.d.ts +1 -0
  408. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.css.js +782 -0
  409. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.css.js.map +1 -0
  410. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.d.ts +364 -0
  411. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.js +2038 -0
  412. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.js.map +1 -0
  413. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.test.d.ts +1 -0
  414. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.test.js +636 -0
  415. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.test.js.map +1 -0
  416. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.d.ts +1 -0
  417. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.js +2 -0
  418. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.js.map +1 -0
  419. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.d.ts +6 -0
  420. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.js +251 -0
  421. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.js.map +1 -0
  422. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.d.ts +41 -0
  423. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.js +700 -0
  424. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.js.map +1 -0
  425. package/dist/src/nile-wysiwyg-editor/types.d.ts +49 -0
  426. package/dist/src/nile-wysiwyg-editor/types.js +8 -0
  427. package/dist/src/nile-wysiwyg-editor/types.js.map +1 -0
  428. package/dist/src/nile-wysiwyg-editor/ui/bookmark-panel.d.ts +20 -0
  429. package/dist/src/nile-wysiwyg-editor/ui/bookmark-panel.js +159 -0
  430. package/dist/src/nile-wysiwyg-editor/ui/bookmark-panel.js.map +1 -0
  431. package/dist/src/nile-wysiwyg-editor/ui/bubble-menu.d.ts +21 -0
  432. package/dist/src/nile-wysiwyg-editor/ui/bubble-menu.js +125 -0
  433. package/dist/src/nile-wysiwyg-editor/ui/bubble-menu.js.map +1 -0
  434. package/dist/src/nile-wysiwyg-editor/ui/code-block-menu.d.ts +18 -0
  435. package/dist/src/nile-wysiwyg-editor/ui/code-block-menu.js +107 -0
  436. package/dist/src/nile-wysiwyg-editor/ui/code-block-menu.js.map +1 -0
  437. package/dist/src/nile-wysiwyg-editor/ui/embed-menu.d.ts +19 -0
  438. package/dist/src/nile-wysiwyg-editor/ui/embed-menu.js +123 -0
  439. package/dist/src/nile-wysiwyg-editor/ui/embed-menu.js.map +1 -0
  440. package/dist/src/nile-wysiwyg-editor/ui/embed-panel.d.ts +27 -0
  441. package/dist/src/nile-wysiwyg-editor/ui/embed-panel.js +146 -0
  442. package/dist/src/nile-wysiwyg-editor/ui/embed-panel.js.map +1 -0
  443. package/dist/src/nile-wysiwyg-editor/ui/emoji-picker.d.ts +21 -0
  444. package/dist/src/nile-wysiwyg-editor/ui/emoji-picker.js +154 -0
  445. package/dist/src/nile-wysiwyg-editor/ui/emoji-picker.js.map +1 -0
  446. package/dist/src/nile-wysiwyg-editor/ui/find-replace.d.ts +26 -0
  447. package/dist/src/nile-wysiwyg-editor/ui/find-replace.js +178 -0
  448. package/dist/src/nile-wysiwyg-editor/ui/find-replace.js.map +1 -0
  449. package/dist/src/nile-wysiwyg-editor/ui/image-menu.d.ts +18 -0
  450. package/dist/src/nile-wysiwyg-editor/ui/image-menu.js +129 -0
  451. package/dist/src/nile-wysiwyg-editor/ui/image-menu.js.map +1 -0
  452. package/dist/src/nile-wysiwyg-editor/ui/image-panel.d.ts +56 -0
  453. package/dist/src/nile-wysiwyg-editor/ui/image-panel.js +349 -0
  454. package/dist/src/nile-wysiwyg-editor/ui/image-panel.js.map +1 -0
  455. package/dist/src/nile-wysiwyg-editor/ui/insert-menu.d.ts +25 -0
  456. package/dist/src/nile-wysiwyg-editor/ui/insert-menu.js +142 -0
  457. package/dist/src/nile-wysiwyg-editor/ui/insert-menu.js.map +1 -0
  458. package/dist/src/nile-wysiwyg-editor/ui/link-editor.d.ts +30 -0
  459. package/dist/src/nile-wysiwyg-editor/ui/link-editor.js +189 -0
  460. package/dist/src/nile-wysiwyg-editor/ui/link-editor.js.map +1 -0
  461. package/dist/src/nile-wysiwyg-editor/ui/markdown-panel.d.ts +24 -0
  462. package/dist/src/nile-wysiwyg-editor/ui/markdown-panel.js +144 -0
  463. package/dist/src/nile-wysiwyg-editor/ui/markdown-panel.js.map +1 -0
  464. package/dist/src/nile-wysiwyg-editor/ui/math-panel.d.ts +31 -0
  465. package/dist/src/nile-wysiwyg-editor/ui/math-panel.js +172 -0
  466. package/dist/src/nile-wysiwyg-editor/ui/math-panel.js.map +1 -0
  467. package/dist/src/nile-wysiwyg-editor/ui/shortcut-help.d.ts +30 -0
  468. package/dist/src/nile-wysiwyg-editor/ui/shortcut-help.js +296 -0
  469. package/dist/src/nile-wysiwyg-editor/ui/shortcut-help.js.map +1 -0
  470. package/dist/src/nile-wysiwyg-editor/ui/source-view.d.ts +19 -0
  471. package/dist/src/nile-wysiwyg-editor/ui/source-view.js +120 -0
  472. package/dist/src/nile-wysiwyg-editor/ui/source-view.js.map +1 -0
  473. package/dist/src/nile-wysiwyg-editor/ui/special-chars.d.ts +20 -0
  474. package/dist/src/nile-wysiwyg-editor/ui/special-chars.js +199 -0
  475. package/dist/src/nile-wysiwyg-editor/ui/special-chars.js.map +1 -0
  476. package/dist/src/nile-wysiwyg-editor/ui/suggest-list.d.ts +31 -0
  477. package/dist/src/nile-wysiwyg-editor/ui/suggest-list.js +133 -0
  478. package/dist/src/nile-wysiwyg-editor/ui/suggest-list.js.map +1 -0
  479. package/dist/src/nile-wysiwyg-editor/ui/table-grid-picker.d.ts +23 -0
  480. package/dist/src/nile-wysiwyg-editor/ui/table-grid-picker.js +177 -0
  481. package/dist/src/nile-wysiwyg-editor/ui/table-grid-picker.js.map +1 -0
  482. package/dist/src/nile-wysiwyg-editor/ui/table-menu.d.ts +16 -0
  483. package/dist/src/nile-wysiwyg-editor/ui/table-menu.js +101 -0
  484. package/dist/src/nile-wysiwyg-editor/ui/table-menu.js.map +1 -0
  485. package/dist/src/version.js +1 -1
  486. package/dist/src/version.js.map +1 -1
  487. package/dist/tsconfig.tsbuildinfo +1 -1
  488. package/package.json +18 -2
  489. package/src/index.ts +57 -1
  490. package/src/internal/glyph.ts +13 -0
  491. package/src/nile-chip/nile-chip.css.ts +8 -1
  492. package/src/nile-code-editor/nile-code-editor.ts +3 -3
  493. package/src/nile-hero/nile-hero.css.ts +12 -12
  494. package/src/nile-hero/nile-hero.ts +1 -1
  495. package/src/nile-hero-header/nile-hero-header.css.ts +9 -9
  496. package/src/nile-hero-header/nile-hero-header.ts +1 -1
  497. package/src/nile-rich-text-editor/nile-rich-text-editor.css.ts +70 -0
  498. package/src/nile-rich-text-editor/nile-rich-text-editor.ts +89 -3
  499. package/src/nile-rich-text-editor/nile-rte-image.ts +1132 -0
  500. package/src/nile-wysiwyg-editor/emoji.ts +137 -0
  501. package/src/nile-wysiwyg-editor/engine/commands.ts +1035 -0
  502. package/src/nile-wysiwyg-editor/engine/create-state.ts +117 -0
  503. package/src/nile-wysiwyg-editor/engine/export.test.ts +113 -0
  504. package/src/nile-wysiwyg-editor/engine/export.ts +242 -0
  505. package/src/nile-wysiwyg-editor/engine/index.ts +69 -0
  506. package/src/nile-wysiwyg-editor/engine/inputrules.ts +169 -0
  507. package/src/nile-wysiwyg-editor/engine/keymap.ts +146 -0
  508. package/src/nile-wysiwyg-editor/engine/markdown-parse.test.ts +148 -0
  509. package/src/nile-wysiwyg-editor/engine/markdown-parse.ts +337 -0
  510. package/src/nile-wysiwyg-editor/engine/markdown.ts +165 -0
  511. package/src/nile-wysiwyg-editor/engine/office-paste.ts +199 -0
  512. package/src/nile-wysiwyg-editor/engine/plugins/code-highlight.ts +173 -0
  513. package/src/nile-wysiwyg-editor/engine/plugins/embed-view.ts +85 -0
  514. package/src/nile-wysiwyg-editor/engine/plugins/embeds.ts +99 -0
  515. package/src/nile-wysiwyg-editor/engine/plugins/find-replace.ts +155 -0
  516. package/src/nile-wysiwyg-editor/engine/plugins/format-painter.ts +80 -0
  517. package/src/nile-wysiwyg-editor/engine/plugins/image-view.ts +220 -0
  518. package/src/nile-wysiwyg-editor/engine/plugins/image.ts +80 -0
  519. package/src/nile-wysiwyg-editor/engine/plugins/link.ts +42 -0
  520. package/src/nile-wysiwyg-editor/engine/plugins/math-view.ts +56 -0
  521. package/src/nile-wysiwyg-editor/engine/plugins/max-length.ts +24 -0
  522. package/src/nile-wysiwyg-editor/engine/plugins/paste.ts +74 -0
  523. package/src/nile-wysiwyg-editor/engine/plugins/placeholder.ts +24 -0
  524. package/src/nile-wysiwyg-editor/engine/plugins/single-line.ts +47 -0
  525. package/src/nile-wysiwyg-editor/engine/plugins/suggestions.ts +110 -0
  526. package/src/nile-wysiwyg-editor/engine/plugins/tables.ts +12 -0
  527. package/src/nile-wysiwyg-editor/engine/plugins/task-list.ts +65 -0
  528. package/src/nile-wysiwyg-editor/engine/pm.test.ts +1088 -0
  529. package/src/nile-wysiwyg-editor/engine/productivity.test.ts +163 -0
  530. package/src/nile-wysiwyg-editor/engine/sanitize.ts +156 -0
  531. package/src/nile-wysiwyg-editor/engine/schema.ts +859 -0
  532. package/src/nile-wysiwyg-editor/engine/selection-state.ts +210 -0
  533. package/src/nile-wysiwyg-editor/engine/serializer.ts +78 -0
  534. package/src/nile-wysiwyg-editor/index.ts +3 -0
  535. package/src/nile-wysiwyg-editor/nile-wysiwyg-editor.css.ts +782 -0
  536. package/src/nile-wysiwyg-editor/nile-wysiwyg-editor.test.ts +750 -0
  537. package/src/nile-wysiwyg-editor/nile-wysiwyg-editor.ts +2155 -0
  538. package/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.ts +1 -0
  539. package/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.ts +251 -0
  540. package/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.ts +772 -0
  541. package/src/nile-wysiwyg-editor/types.ts +83 -0
  542. package/src/nile-wysiwyg-editor/ui/bookmark-panel.ts +155 -0
  543. package/src/nile-wysiwyg-editor/ui/bubble-menu.ts +135 -0
  544. package/src/nile-wysiwyg-editor/ui/code-block-menu.ts +105 -0
  545. package/src/nile-wysiwyg-editor/ui/embed-menu.ts +133 -0
  546. package/src/nile-wysiwyg-editor/ui/embed-panel.ts +143 -0
  547. package/src/nile-wysiwyg-editor/ui/emoji-picker.ts +152 -0
  548. package/src/nile-wysiwyg-editor/ui/find-replace.ts +174 -0
  549. package/src/nile-wysiwyg-editor/ui/image-menu.ts +140 -0
  550. package/src/nile-wysiwyg-editor/ui/image-panel.ts +337 -0
  551. package/src/nile-wysiwyg-editor/ui/insert-menu.ts +142 -0
  552. package/src/nile-wysiwyg-editor/ui/link-editor.ts +180 -0
  553. package/src/nile-wysiwyg-editor/ui/markdown-panel.ts +143 -0
  554. package/src/nile-wysiwyg-editor/ui/math-panel.ts +169 -0
  555. package/src/nile-wysiwyg-editor/ui/shortcut-help.ts +311 -0
  556. package/src/nile-wysiwyg-editor/ui/source-view.ts +119 -0
  557. package/src/nile-wysiwyg-editor/ui/special-chars.ts +209 -0
  558. package/src/nile-wysiwyg-editor/ui/suggest-list.ts +138 -0
  559. package/src/nile-wysiwyg-editor/ui/table-grid-picker.ts +173 -0
  560. package/src/nile-wysiwyg-editor/ui/table-menu.ts +109 -0
  561. package/vscode-html-custom-data.json +491 -1
  562. package/web-dev-server.config.mjs +6 -2
  563. package/dist/index-5fef1a0d.esm.js +0 -1
  564. package/dist/index-8f38fae3.cjs.js +0 -2
  565. package/dist/index-8f38fae3.cjs.js.map +0 -1
@@ -0,0 +1,2155 @@
1
+ import { html, TemplateResult, CSSResultArray, PropertyValues, nothing } from 'lit';
2
+ import { customElement, property, query, state } from 'lit/decorators.js';
3
+ import { EditorView } from 'prosemirror-view';
4
+ import { EditorState, Transaction, TextSelection } from 'prosemirror-state';
5
+ import { Node as PMNode } from 'prosemirror-model';
6
+
7
+ import NileElement from '../internal/nile-element';
8
+ import { styles } from './nile-wysiwyg-editor.css';
9
+ import {
10
+ defaultSchema,
11
+ createEditorState,
12
+ computeToolbarState,
13
+ htmlToDoc,
14
+ htmlToSlice,
15
+ docToHtml,
16
+ docToText,
17
+ docToMarkdown,
18
+ markdownToHtml,
19
+ docToJson,
20
+ jsonToDoc,
21
+ isDocEmpty,
22
+ sanitizeHtml,
23
+ getCommand,
24
+ getMarkRange,
25
+ flattenToSingleParagraph,
26
+ imageUploadKey,
27
+ findUploadPos,
28
+ countWords,
29
+ TaskItemView,
30
+ ImageView,
31
+ FigureView,
32
+ EmbedView,
33
+ MathView,
34
+ isOfficeHtml,
35
+ cleanOfficeHtml,
36
+ toInlineStyledHtml,
37
+ buildWordDocument,
38
+ buildPrintDocument,
39
+ isFormatPainterArmed,
40
+ armFormatPainter,
41
+ disarmFormatPainter,
42
+ applyFormatPainter,
43
+ getFindState,
44
+ searchTr,
45
+ findStepTr,
46
+ clearFindTr,
47
+ replaceActiveTr,
48
+ replaceAllTr,
49
+ } from './engine';
50
+ import type { ToolbarState } from './engine/selection-state';
51
+ import type { CommandAttrs, CommandName } from './engine/commands';
52
+ import { selectedImageInfo } from './engine/commands';
53
+ import type { MathRenderer } from './engine/plugins/math-view';
54
+ import type { SuggestionMatch } from './engine/plugins/suggestions';
55
+ import type {
56
+ WysiwygChangeDetail,
57
+ WysiwygCommandDetail,
58
+ MentionsConfig,
59
+ SnippetConfig,
60
+ MergeFieldConfig,
61
+ TemplateConfig,
62
+ } from './types';
63
+ import { DEFAULT_TOOLBAR } from './types';
64
+ import { EMOJI } from './emoji';
65
+
66
+ import './nile-wysiwyg-toolbar/nile-wysiwyg-toolbar';
67
+ import './ui/link-editor';
68
+ import './ui/bubble-menu';
69
+ import './ui/image-panel';
70
+ import './ui/table-menu';
71
+ import './ui/suggest-list';
72
+ import './ui/find-replace';
73
+ import './ui/source-view';
74
+ import './ui/embed-panel';
75
+ import './ui/bookmark-panel';
76
+ import './ui/code-block-menu';
77
+ import './ui/shortcut-help';
78
+ import './ui/image-menu';
79
+ import './ui/embed-menu';
80
+ import './ui/special-chars';
81
+ import './ui/emoji-picker';
82
+ import './ui/insert-menu';
83
+ import './ui/math-panel';
84
+ import './ui/markdown-panel';
85
+ import type { NileWysiwygLinkEditor } from './ui/link-editor';
86
+ import type { NileWysiwygBubbleMenu } from './ui/bubble-menu';
87
+ import type { NileWysiwygImagePanel } from './ui/image-panel';
88
+ import type { NileWysiwygTableMenu } from './ui/table-menu';
89
+ import type { NileWysiwygSuggestList, SuggestItem } from './ui/suggest-list';
90
+ import type { NileWysiwygFindBar } from './ui/find-replace';
91
+ import type { NileWysiwygSourceView } from './ui/source-view';
92
+ import type { NileWysiwygEmbedPanel } from './ui/embed-panel';
93
+ import type { NileWysiwygBookmarkPanel } from './ui/bookmark-panel';
94
+ import type { NileWysiwygCodeMenu } from './ui/code-block-menu';
95
+ import type { NileWysiwygShortcutHelp } from './ui/shortcut-help';
96
+ import type { NileWysiwygImageMenu } from './ui/image-menu';
97
+ import type { NileWysiwygEmbedMenu } from './ui/embed-menu';
98
+ import type { NileWysiwygSpecialChars } from './ui/special-chars';
99
+ import type { NileWysiwygEmojiPicker } from './ui/emoji-picker';
100
+ import type { NileWysiwygInsertMenu } from './ui/insert-menu';
101
+ import type { NileWysiwygMathPanel } from './ui/math-panel';
102
+ import type { NileWysiwygMarkdownPanel } from './ui/markdown-panel';
103
+
104
+ interface SlashItem extends SuggestItem {
105
+ command: CommandName | 'openImage' | 'openEmbed' | 'openBookmark';
106
+ attrs?: CommandAttrs;
107
+ }
108
+
109
+ const SLASH_ITEMS: SlashItem[] = [
110
+ { key: 'heading1', label: 'Heading 1', hint: '#', command: 'heading', attrs: { level: 1 } },
111
+ { key: 'heading2', label: 'Heading 2', hint: '##', command: 'heading', attrs: { level: 2 } },
112
+ { key: 'heading3', label: 'Heading 3', hint: '###', command: 'heading', attrs: { level: 3 } },
113
+ { key: 'bulletList', label: 'Bulleted list', hint: '-', command: 'bulletList' },
114
+ { key: 'orderedList', label: 'Numbered list', hint: '1.', command: 'orderedList' },
115
+ { key: 'taskList', label: 'Checklist', hint: '[ ]', command: 'taskList' },
116
+ { key: 'blockquote', label: 'Blockquote', hint: '>', command: 'blockquote' },
117
+ { key: 'codeBlock', label: 'Code block', hint: '```', command: 'codeBlock' },
118
+ { key: 'table', label: 'Table', hint: '3×3', command: 'insertTable', attrs: { rows: 3, cols: 3 } },
119
+ { key: 'image', label: 'Image', command: 'openImage' },
120
+ { key: 'embed', label: 'Video embed', hint: 'YouTube…', command: 'openEmbed' },
121
+ { key: 'bookmark', label: 'Bookmark (anchor)', command: 'openBookmark' },
122
+ { key: 'horizontalRule', label: 'Divider', hint: '---', command: 'horizontalRule' },
123
+ ];
124
+
125
+ let uploadCounter = 0;
126
+
127
+ function readFileAsDataURL(file: File): Promise<string> {
128
+ return new Promise((resolve, reject) => {
129
+ const reader = new FileReader();
130
+ reader.onload = () => resolve(reader.result as string);
131
+ reader.onerror = () => reject(reader.error);
132
+ reader.readAsDataURL(file);
133
+ });
134
+ }
135
+
136
+ const CHANGE_DEBOUNCE_MS = 250;
137
+
138
+ /**
139
+ * WYSIWYG rich text editor powered by ProseMirror.
140
+ *
141
+ * Requires evergreen browsers: selection inside shadow DOM relies on
142
+ * `Selection.getComposedRanges()` (Safari 17+, recent Firefox) or Chrome's
143
+ * `shadowRoot.getSelection()`.
144
+ *
145
+ * @tag nile-wysiwyg-editor
146
+ *
147
+ * @event nile-change - Debounced content change. detail: { value, json, content }
148
+ * @event nile-input - Every document change. detail: { value, json, content }
149
+ * @event nile-focus - Editor gained focus.
150
+ * @event nile-blur - Editor lost focus.
151
+ * @event nile-selection-change - detail: ToolbarState snapshot.
152
+ * @event nile-image-upload-request - Cancelable. detail: { file, insert(src, alt) }.
153
+ * Call preventDefault() and use insert() to fully own the upload.
154
+ * @event nile-image-upload-error - detail: { file, error } when uploadHandler rejects.
155
+ * @event nile-mention-query - detail: { trigger, query, provide(items) } for async sources.
156
+ * @event nile-mention-select - detail: { trigger, key, label } when a mention is inserted.
157
+ * @event nile-word-count - detail: { words, characters } on every document change.
158
+ * @event nile-autosave - detail: { value, json, content } on the autosave interval.
159
+ *
160
+ * @csspart base - Outer container.
161
+ * @csspart toolbar - The toolbar element.
162
+ * @csspart surface - The scrollable editing surface.
163
+ */
164
+ @customElement('nile-wysiwyg-editor')
165
+ export class NileWysiwygEditor extends NileElement {
166
+ static formAssociated = true;
167
+
168
+ /** Document content as HTML. Not reflected to the attribute. */
169
+ @property({ type: String }) value = '';
170
+
171
+ @property({ type: String }) placeholder = '';
172
+
173
+ @property({ type: Boolean, reflect: true }) disabled = false;
174
+
175
+ @property({ type: Boolean, reflect: true }) readonly = false;
176
+
177
+ /** Toolbar config string; `"none"` hides the toolbar. */
178
+ @property({ type: String }) toolbar = DEFAULT_TOOLBAR;
179
+
180
+ /** Shows a balloon toolbar over text selections. */
181
+ @property({ type: Boolean, attribute: 'bubble-menu' }) bubbleMenu = false;
182
+
183
+ /** Sanitizes incoming HTML (value, paste, insertHTML). Leave on. */
184
+ @property({ type: Boolean }) sanitize = true;
185
+
186
+ /** Constrains content to a single paragraph. */
187
+ @property({ type: Boolean, attribute: 'singleline' }) singleLine = false;
188
+
189
+ /** Form field name. */
190
+ @property({ type: String }) name = '';
191
+
192
+ @property({ type: Boolean, reflect: true }) required = false;
193
+
194
+ /**
195
+ * Async image upload handler. When set, pasted/dropped/picked image files
196
+ * are uploaded through it (an "Uploading…" placeholder shows meanwhile)
197
+ * and the returned src is inserted.
198
+ */
199
+ @property({ attribute: false }) uploadHandler?: (
200
+ file: File
201
+ ) => Promise<{ src: string; alt?: string }>;
202
+
203
+ /**
204
+ * Fallback when no uploadHandler is set and no nile-image-upload-request
205
+ * listener takes over: embed image files as base64 data URLs.
206
+ */
207
+ @property({ attribute: false }) allowBase64Images = true;
208
+
209
+ /**
210
+ * Shows an "Upload from computer" button in the Insert Image dialog. Off by
211
+ * default (URL-only, like the RTE's default); files can always be added via
212
+ * drag & drop or paste regardless.
213
+ */
214
+ @property({ type: Boolean, attribute: 'upload-from-computer' })
215
+ uploadFromComputer = false;
216
+
217
+ /** Max upload size (bytes) shown/enforced by the image dialog. Default 2 MB. */
218
+ @property({ type: Number, attribute: 'image-max-file-size' })
219
+ imageMaxFileSize = 2 * 1024 * 1024;
220
+
221
+ /**
222
+ * Mention config: trigger character → items, same shape and JSON attribute
223
+ * format as nile-rich-text-editor. For async sources, configure the
224
+ * trigger with an empty array and listen to nile-mention-query.
225
+ */
226
+ @property({
227
+ attribute: 'mentions',
228
+ converter: {
229
+ fromAttribute: (value: string): MentionsConfig => {
230
+ try {
231
+ const parsed = JSON.parse(value);
232
+ const out: MentionsConfig = {};
233
+ for (const trigger of Object.keys(parsed)) {
234
+ const items = parsed[trigger];
235
+ if (Array.isArray(items)) {
236
+ out[trigger] = items
237
+ .filter(i => i && typeof i.key === 'string' && typeof i.label === 'string')
238
+ .map(i => ({ key: i.key, label: i.label }));
239
+ }
240
+ }
241
+ return out;
242
+ } catch {
243
+ return {};
244
+ }
245
+ },
246
+ toAttribute: (value: MentionsConfig) => JSON.stringify(value),
247
+ },
248
+ })
249
+ mentions: MentionsConfig = {};
250
+
251
+ /** Disables the "/" command menu. */
252
+ @property({ type: Boolean, attribute: 'no-slash-commands' }) noSlashCommands = false;
253
+
254
+ /** Disables the ":" emoji picker. */
255
+ @property({ type: Boolean, attribute: 'no-emoji' }) noEmoji = false;
256
+
257
+ /** Maximum character count; further input is rejected. 0 = unlimited. */
258
+ @property({ type: Number, attribute: 'maxlength' }) maxLength = 0;
259
+
260
+ /** Always show the word/character counter footer (implied by maxlength). */
261
+ @property({ type: Boolean, attribute: 'show-counter' }) showCounter = false;
262
+
263
+ /** Keeps the toolbar pinned to the viewport while scrolling long content. */
264
+ @property({ type: Boolean, attribute: 'sticky-toolbar', reflect: true })
265
+ stickyToolbar = false;
266
+
267
+ /** Reusable snippets surfaced in the "/" menu (JSON attribute). */
268
+ @property({
269
+ attribute: 'snippets',
270
+ converter: {
271
+ fromAttribute: (value: string): SnippetConfig[] => {
272
+ try {
273
+ const parsed = JSON.parse(value);
274
+ return Array.isArray(parsed)
275
+ ? parsed.filter(
276
+ s =>
277
+ s &&
278
+ typeof s.key === 'string' &&
279
+ typeof s.label === 'string' &&
280
+ typeof s.html === 'string'
281
+ )
282
+ : [];
283
+ } catch {
284
+ return [];
285
+ }
286
+ },
287
+ toAttribute: (value: SnippetConfig[]) => JSON.stringify(value),
288
+ },
289
+ })
290
+ snippets: SnippetConfig[] = [];
291
+
292
+ /** Merge fields offered in the merge-field picker (JSON attribute). */
293
+ @property({
294
+ attribute: 'merge-fields',
295
+ converter: {
296
+ fromAttribute: (value: string): MergeFieldConfig[] => {
297
+ try {
298
+ const parsed = JSON.parse(value);
299
+ return Array.isArray(parsed)
300
+ ? parsed.filter(f => f && typeof f.id === 'string' && typeof f.label === 'string')
301
+ : [];
302
+ } catch {
303
+ return [];
304
+ }
305
+ },
306
+ toAttribute: (value: MergeFieldConfig[]) => JSON.stringify(value),
307
+ },
308
+ })
309
+ mergeFields: MergeFieldConfig[] = [];
310
+
311
+ /** Content templates offered in the template picker (JSON attribute). */
312
+ @property({
313
+ attribute: 'templates',
314
+ converter: {
315
+ fromAttribute: (value: string): TemplateConfig[] => {
316
+ try {
317
+ const parsed = JSON.parse(value);
318
+ return Array.isArray(parsed)
319
+ ? parsed.filter(t => t && typeof t.label === 'string' && typeof t.html === 'string')
320
+ : [];
321
+ } catch {
322
+ return [];
323
+ }
324
+ },
325
+ toAttribute: (value: TemplateConfig[]) => JSON.stringify(value),
326
+ },
327
+ })
328
+ templates: TemplateConfig[] = [];
329
+
330
+ /**
331
+ * Renders a math node's LaTeX to HTML (KaTeX/MathJax/MathML). When unset,
332
+ * the raw LaTeX is shown. Same trust level as uploadHandler — output is
333
+ * inserted as HTML.
334
+ */
335
+ @property({ attribute: false }) mathRenderer?: MathRenderer;
336
+
337
+ /** Emit nile-autosave every N ms while the document has changed. 0 = off. */
338
+ @property({ type: Number, attribute: 'autosave-interval' }) autosaveInterval = 0;
339
+
340
+ /** When on, every paste drops formatting and inserts plain text. */
341
+ @property({ type: Boolean, attribute: 'paste-as-plain-text', reflect: true })
342
+ pasteAsPlainText = false;
343
+
344
+ /** Disables Markdown detection on plain-text pastes. */
345
+ @property({ type: Boolean, attribute: 'no-markdown-paste' })
346
+ noMarkdownPaste = false;
347
+
348
+ /** Disables typography autocorrect (smart quotes, dashes, (c) → ©). */
349
+ @property({ type: Boolean, attribute: 'no-text-transform' })
350
+ noTextTransform = false;
351
+
352
+ /** Expands the editor to fill the viewport. Esc exits. */
353
+ @property({ type: Boolean, reflect: true }) fullscreen = false;
354
+
355
+ /** Outlines block boundaries (paragraphs, headings, lists…) while editing. */
356
+ @property({ type: Boolean, attribute: 'show-blocks', reflect: true })
357
+ showBlocks = false;
358
+
359
+ @state() private toolbarState?: ToolbarState;
360
+
361
+ @state() private hasFocus = false;
362
+
363
+ @state() private sourceMode = false;
364
+
365
+ @state() private counter = { words: 0, characters: 0 };
366
+
367
+ /** Which list the shared insert-menu is showing, if open. */
368
+ @state() private insertMenuMode: 'template' | 'merge' | null = null;
369
+
370
+ @query('.nile-wysiwyg') private baseEl!: HTMLElement;
371
+
372
+ @query('nile-wysiwyg-toolbar') private toolbarEl?: HTMLElement;
373
+
374
+ @query('.nile-wysiwyg__surface') private surfaceEl!: HTMLElement;
375
+
376
+ @query('nile-wysiwyg-link-editor') private linkEditorEl!: NileWysiwygLinkEditor;
377
+
378
+ @query('nile-wysiwyg-bubble-menu') private bubbleMenuEl!: NileWysiwygBubbleMenu;
379
+
380
+ @query('nile-wysiwyg-image-panel') private imagePanelEl!: NileWysiwygImagePanel;
381
+
382
+ @query('nile-wysiwyg-table-menu') private tableMenuEl!: NileWysiwygTableMenu;
383
+
384
+ @query('nile-wysiwyg-suggest-list') private suggestListEl!: NileWysiwygSuggestList;
385
+
386
+ @query('nile-wysiwyg-find-bar') private findBarEl!: NileWysiwygFindBar;
387
+
388
+ @query('nile-wysiwyg-source-view') private sourceViewEl?: NileWysiwygSourceView;
389
+
390
+ @query('nile-wysiwyg-embed-panel') private embedPanelEl!: NileWysiwygEmbedPanel;
391
+
392
+ @query('nile-wysiwyg-bookmark-panel') private bookmarkPanelEl!: NileWysiwygBookmarkPanel;
393
+
394
+ @query('nile-wysiwyg-code-menu') private codeMenuEl!: NileWysiwygCodeMenu;
395
+
396
+ @query('nile-wysiwyg-shortcut-help') private shortcutHelpEl!: NileWysiwygShortcutHelp;
397
+
398
+ @query('nile-wysiwyg-image-menu') private imageMenuEl!: NileWysiwygImageMenu;
399
+
400
+ @query('nile-wysiwyg-embed-menu') private embedMenuEl!: NileWysiwygEmbedMenu;
401
+
402
+ @query('nile-wysiwyg-special-chars') private specialCharsEl!: NileWysiwygSpecialChars;
403
+
404
+ @query('nile-wysiwyg-emoji-picker') private emojiPickerEl!: NileWysiwygEmojiPicker;
405
+
406
+ @query('nile-wysiwyg-insert-menu') private insertMenuEl!: NileWysiwygInsertMenu;
407
+
408
+ @query('nile-wysiwyg-math-panel') private mathPanelEl!: NileWysiwygMathPanel;
409
+
410
+ @query('nile-wysiwyg-markdown-panel') private markdownPanelEl!: NileWysiwygMarkdownPanel;
411
+
412
+ private view?: EditorView;
413
+
414
+ private mentionMatch: SuggestionMatch | null = null;
415
+
416
+ private slashMatch: SuggestionMatch | null = null;
417
+
418
+ private emojiMatch: SuggestionMatch | null = null;
419
+
420
+ private activeSuggestSource: 'mention' | 'slash' | 'emoji' | null = null;
421
+
422
+ private suggestDismissed: string | null = null;
423
+
424
+ private mentionQueryToken = '';
425
+
426
+ private autosaveTimer?: ReturnType<typeof setInterval>;
427
+
428
+ private lastAutosaveValue = '';
429
+
430
+ private internals?: ElementInternals;
431
+
432
+ private defaultValue = '';
433
+
434
+ private syncingValue = false;
435
+
436
+ private changeTimer?: ReturnType<typeof setTimeout>;
437
+
438
+ private pendingChange = false;
439
+
440
+ /** Body overflow value before fullscreen locked it; null = not locked. */
441
+ private previousBodyOverflow: string | null = null;
442
+
443
+ public static get styles(): CSSResultArray {
444
+ return [styles];
445
+ }
446
+
447
+ constructor() {
448
+ super();
449
+ if (typeof this.attachInternals === 'function') {
450
+ this.internals = this.attachInternals();
451
+ }
452
+ }
453
+
454
+ connectedCallback(): void {
455
+ super.connectedCallback();
456
+ // Remount after a disconnect/reconnect (e.g. host moved in the DOM).
457
+ if (this.hasUpdated && !this.view) {
458
+ this.updateComplete.then(() => this.mountView());
459
+ }
460
+ // Close floating panels when the user clicks anywhere outside them.
461
+ document.addEventListener('pointerdown', this.onOutsidePointerDown, true);
462
+ }
463
+
464
+ disconnectedCallback(): void {
465
+ super.disconnectedCallback();
466
+ document.removeEventListener('pointerdown', this.onOutsidePointerDown, true);
467
+ if (this.changeTimer) clearTimeout(this.changeTimer);
468
+ if (this.autosaveTimer) clearInterval(this.autosaveTimer);
469
+ if (this.previousBodyOverflow !== null) {
470
+ document.body.style.overflow = this.previousBodyOverflow;
471
+ this.previousBodyOverflow = null;
472
+ }
473
+ this.view?.destroy();
474
+ this.view = undefined;
475
+ }
476
+
477
+ protected firstUpdated(): void {
478
+ this.defaultValue = this.value;
479
+ this.mountView();
480
+ }
481
+
482
+ protected updated(changed: PropertyValues): void {
483
+ if (changed.has('value') && this.view && !this.syncingValue) {
484
+ const current = docToHtml(this.view.state.doc);
485
+ if (current !== this.value) this.setContent(this.value);
486
+ }
487
+ this.syncingValue = false;
488
+
489
+ if ((changed.has('disabled') || changed.has('readonly')) && this.view) {
490
+ this.view.setProps({});
491
+ }
492
+ if (changed.has('placeholder') && this.view) {
493
+ // Decorations are recomputed on any transaction.
494
+ this.view.dispatch(this.view.state.tr);
495
+ }
496
+ if (changed.has('required')) this.updateValidity();
497
+ if (changed.has('autosaveInterval')) this.restartAutosave();
498
+ if (
499
+ (changed.has('pasteAsPlainText') ||
500
+ changed.has('fullscreen') ||
501
+ changed.has('showBlocks')) &&
502
+ this.view
503
+ ) {
504
+ this.toolbarState = this.snapshotToolbar(this.view.state);
505
+ }
506
+ // Input rules are baked into the state at build time; rebuild in place
507
+ // (same doc, fresh plugins) when the autocorrect toggle changes.
508
+ if (changed.has('noTextTransform') && this.view && !changed.has('value')) {
509
+ this.view.updateState(this.buildState(this.view.state.doc));
510
+ }
511
+ // Lock page scrolling while fullscreen covers the viewport.
512
+ if (changed.has('fullscreen')) {
513
+ if (this.fullscreen) {
514
+ if (this.previousBodyOverflow === null) {
515
+ this.previousBodyOverflow = document.body.style.overflow;
516
+ document.body.style.overflow = 'hidden';
517
+ }
518
+ } else if (this.previousBodyOverflow !== null) {
519
+ document.body.style.overflow = this.previousBodyOverflow;
520
+ this.previousBodyOverflow = null;
521
+ }
522
+ }
523
+ }
524
+
525
+ private restartAutosave(): void {
526
+ if (this.autosaveTimer) clearInterval(this.autosaveTimer);
527
+ this.autosaveTimer = undefined;
528
+ if (this.autosaveInterval > 0) {
529
+ this.lastAutosaveValue = this.value;
530
+ this.autosaveTimer = setInterval(() => {
531
+ if (!this.view || this.value === this.lastAutosaveValue) return;
532
+ this.lastAutosaveValue = this.value;
533
+ this.emit('nile-autosave', this.buildChangeDetail(this.view.state.doc));
534
+ }, this.autosaveInterval);
535
+ }
536
+ }
537
+
538
+ // ------------------------------------------------------------------ view
539
+
540
+ private buildState(doc?: PMNode): EditorState {
541
+ return createEditorState({
542
+ schema: defaultSchema,
543
+ doc,
544
+ getPlaceholder: () => this.placeholder,
545
+ singleLine: this.singleLine,
546
+ onLinkShortcut: () => this.openLinkEditor(),
547
+ onFindShortcut: () => this.openFindBar(),
548
+ onShortcutHelp: () => this.openShortcutHelp(),
549
+ onImageFiles: (files, pos) => {
550
+ for (const file of files) this.insertImageFile(file, pos);
551
+ },
552
+ mentions: {
553
+ getTriggers: () => Object.keys(this.mentions),
554
+ onChange: match => {
555
+ this.mentionMatch = match;
556
+ this.updateSuggestList();
557
+ },
558
+ onKeyDown: event => this.handleSuggestKey(event, 'mention'),
559
+ },
560
+ slashCommands: {
561
+ onChange: match => {
562
+ this.slashMatch = match;
563
+ this.updateSuggestList();
564
+ },
565
+ onKeyDown: event => this.handleSuggestKey(event, 'slash'),
566
+ },
567
+ emoji: {
568
+ onChange: match => {
569
+ this.emojiMatch = match;
570
+ this.updateSuggestList();
571
+ },
572
+ onKeyDown: event => this.handleSuggestKey(event, 'emoji'),
573
+ },
574
+ getMaxLength: () => this.maxLength,
575
+ getForcePlainText: () => this.pasteAsPlainText,
576
+ getMarkdownPaste: () => !this.noMarkdownPaste,
577
+ getSanitize: () => this.sanitize,
578
+ textTransform: !this.noTextTransform,
579
+ });
580
+ }
581
+
582
+ /**
583
+ * ToolbarState snapshot augmented with host-only flags the pure
584
+ * computeToolbarState can't know about (e.g. the paste-as-plain-text toggle).
585
+ */
586
+ private snapshotToolbar(state: EditorState): ToolbarState {
587
+ return {
588
+ ...computeToolbarState(state),
589
+ pasteAsPlainText: this.pasteAsPlainText,
590
+ fullscreen: this.fullscreen,
591
+ showBlocks: this.showBlocks,
592
+ formatPainter: isFormatPainterArmed(state),
593
+ };
594
+ }
595
+
596
+ private mountView(): void {
597
+ if (this.view || !this.surfaceEl) return;
598
+ let doc = this.value
599
+ ? htmlToDoc(defaultSchema, this.value, this.sanitize)
600
+ : undefined;
601
+ if (doc && this.singleLine) doc = flattenToSingleParagraph(doc);
602
+
603
+ this.view = new EditorView(this.surfaceEl, {
604
+ state: this.buildState(doc),
605
+ dispatchTransaction: tr => this.handleTransaction(tr),
606
+ editable: () => !this.disabled && !this.readonly,
607
+ transformPastedHTML: pasted => {
608
+ // Word/Google Docs clipboard HTML gets normalized (real lists, no
609
+ // mso noise) before the sanitizer clamps it to the schema.
610
+ const cleaned = isOfficeHtml(pasted) ? cleanOfficeHtml(pasted) : pasted;
611
+ return this.sanitize ? sanitizeHtml(cleaned) : cleaned;
612
+ },
613
+ handleDOMEvents: {
614
+ mousedown: () => {
615
+ // Clicking into the document dismisses the never-focused popovers
616
+ // (they take no focus, so blur can't close them).
617
+ this.closeSpecialChars(false);
618
+ this.closeInsertMenu(false);
619
+ return false;
620
+ },
621
+ focus: () => {
622
+ this.hasFocus = true;
623
+ this.updateOverlays();
624
+ this.emit('nile-focus');
625
+ return false;
626
+ },
627
+ blur: (_view, event) => {
628
+ // Focus moving into the editor's own chrome (language picker,
629
+ // panels) is not a blur — closing overlays here would dismiss
630
+ // the control the user just clicked.
631
+ const related = (event as FocusEvent).relatedTarget as Node | null;
632
+ if (related && (this.shadowRoot?.contains(related) || this.contains(related))) {
633
+ return false;
634
+ }
635
+ this.hasFocus = false;
636
+ this.flushChange();
637
+ this.updateOverlays();
638
+ this.closeSuggestList();
639
+ this.closeSpecialChars(false);
640
+ this.emit('nile-blur');
641
+ return false;
642
+ },
643
+ },
644
+ attributes: {
645
+ role: 'textbox',
646
+ 'aria-multiline': this.singleLine ? 'false' : 'true',
647
+ },
648
+ nodeViews: {
649
+ task_item: (node, view, getPos) =>
650
+ new TaskItemView(node, view, getPos as () => number | undefined),
651
+ image: (node, view, getPos) =>
652
+ new ImageView(node, view, getPos as () => number | undefined),
653
+ figure: (node, view, getPos) =>
654
+ new FigureView(node, view, getPos as () => number | undefined),
655
+ embed: (node, view, getPos) =>
656
+ new EmbedView(node, view, getPos as () => number | undefined),
657
+ math: node => new MathView(node, this.mathRenderer),
658
+ },
659
+ });
660
+
661
+ this.toolbarState = this.snapshotToolbar(this.view.state);
662
+ this.counter = countWords(this.view.state.doc);
663
+ this.syncFormValue(this.view.state.doc);
664
+ }
665
+
666
+ private handleTransaction(tr: Transaction): void {
667
+ if (!this.view) return;
668
+ const newState = this.view.state.apply(tr);
669
+ this.view.updateState(newState);
670
+
671
+ // Format painter: once armed, the next non-empty selection the user makes
672
+ // receives the copied formatting, then the painter disarms.
673
+ if (tr.selectionSet && !newState.selection.empty && isFormatPainterArmed(newState)) {
674
+ const applyTr = applyFormatPainter(newState);
675
+ if (applyTr) {
676
+ this.view.dispatch(applyTr);
677
+ return;
678
+ }
679
+ }
680
+
681
+ this.toolbarState = this.snapshotToolbar(newState);
682
+ this.emit('nile-selection-change', this.toolbarState);
683
+ this.updateOverlays();
684
+
685
+ if (this.findBarEl?.open) {
686
+ const find = getFindState(newState);
687
+ this.findBarEl.matchCount = find.matches.length;
688
+ this.findBarEl.activeIndex = find.activeIndex;
689
+ }
690
+
691
+ if (tr.docChanged) {
692
+ const detail = this.buildChangeDetail(newState.doc);
693
+ this.syncingValue = true;
694
+ this.value = detail.value;
695
+ this.syncFormValue(newState.doc);
696
+ this.emit('nile-input', detail);
697
+ this.counter = countWords(newState.doc);
698
+ this.emit('nile-word-count', this.counter);
699
+ this.scheduleChange();
700
+ }
701
+ }
702
+
703
+ private buildChangeDetail(doc: PMNode): WysiwygChangeDetail {
704
+ const value = isDocEmpty(doc) ? '' : docToHtml(doc);
705
+ return { value, json: docToJson(doc), content: value };
706
+ }
707
+
708
+ private scheduleChange(): void {
709
+ this.pendingChange = true;
710
+ if (this.changeTimer) clearTimeout(this.changeTimer);
711
+ this.changeTimer = setTimeout(() => this.flushChange(), CHANGE_DEBOUNCE_MS);
712
+ }
713
+
714
+ private flushChange(): void {
715
+ if (!this.pendingChange || !this.view) return;
716
+ this.pendingChange = false;
717
+ if (this.changeTimer) clearTimeout(this.changeTimer);
718
+ this.emit('nile-change', this.buildChangeDetail(this.view.state.doc));
719
+ }
720
+
721
+ // ------------------------------------------------------------------ form
722
+
723
+ private syncFormValue(doc: PMNode): void {
724
+ if (!this.internals) return;
725
+ const value = isDocEmpty(doc) ? '' : docToHtml(doc);
726
+ this.internals.setFormValue(value === '' ? null : value);
727
+ this.updateValidity();
728
+ }
729
+
730
+ private updateValidity(): void {
731
+ if (!this.internals) return;
732
+ const empty = this.view ? isDocEmpty(this.view.state.doc) : this.value === '';
733
+ if (this.required && empty) {
734
+ this.internals.setValidity(
735
+ { valueMissing: true },
736
+ 'Please fill out this field.',
737
+ this.surfaceEl ?? undefined
738
+ );
739
+ } else {
740
+ this.internals.setValidity({});
741
+ }
742
+ }
743
+
744
+ formResetCallback(): void {
745
+ this.setContent(this.defaultValue);
746
+ }
747
+
748
+ formDisabledCallback(disabled: boolean): void {
749
+ this.disabled = disabled;
750
+ }
751
+
752
+ public checkValidity(): boolean {
753
+ return this.internals ? this.internals.checkValidity() : true;
754
+ }
755
+
756
+ public reportValidity(): boolean {
757
+ return this.internals ? this.internals.reportValidity() : true;
758
+ }
759
+
760
+ // -------------------------------------------------------------- overlays
761
+
762
+ /** Positions an overlay element near a doc position (default: selection). */
763
+ private positionOverlay(
764
+ el: HTMLElement,
765
+ placement: 'above' | 'below',
766
+ pos?: number
767
+ ): void {
768
+ if (!this.view || !this.baseEl) return;
769
+ const from = pos ?? this.view.state.selection.from;
770
+ const coords = this.view.coordsAtPos(from);
771
+ const baseRect = this.baseEl.getBoundingClientRect();
772
+ const left = Math.max(
773
+ 4,
774
+ Math.min(coords.left - baseRect.left, baseRect.width - el.offsetWidth - 4)
775
+ );
776
+ el.style.left = `${left}px`;
777
+ if (placement === 'above') {
778
+ el.style.top = `${Math.max(4, coords.top - baseRect.top - el.offsetHeight - 8)}px`;
779
+ } else {
780
+ el.style.top = `${coords.bottom - baseRect.top + 6}px`;
781
+ }
782
+ }
783
+
784
+ /** Positions a floating panel directly below a toolbar/anchor element. */
785
+ private positionOverlayBelowAnchor(el: HTMLElement, anchor: HTMLElement): void {
786
+ if (!this.baseEl) return;
787
+ const baseRect = this.baseEl.getBoundingClientRect();
788
+ const anchorRect = anchor.getBoundingClientRect();
789
+ const left = Math.max(
790
+ 4,
791
+ Math.min(anchorRect.left - baseRect.left, baseRect.width - el.offsetWidth - 4)
792
+ );
793
+ // Clear any CSS-pinned `right` (e.g. the find bar) so `left` wins.
794
+ el.style.right = 'auto';
795
+ el.style.left = `${left}px`;
796
+ el.style.top = `${anchorRect.bottom - baseRect.top + 6}px`;
797
+ }
798
+
799
+ /** Finds a toolbar button by its aria-label, to anchor a panel below it. */
800
+ private toolbarButton(label: string): HTMLElement | null {
801
+ return (
802
+ (this.toolbarEl?.shadowRoot?.querySelector(
803
+ `button[aria-label="${label}"]`
804
+ ) as HTMLElement | null) ?? null
805
+ );
806
+ }
807
+
808
+ private updateOverlays(): void {
809
+ if (!this.bubbleMenuEl) return;
810
+ const panelOpen =
811
+ this.linkEditorEl?.open ||
812
+ this.imagePanelEl?.open ||
813
+ this.embedPanelEl?.open ||
814
+ this.bookmarkPanelEl?.open ||
815
+ this.emojiPickerEl?.open;
816
+ const show =
817
+ this.bubbleMenu &&
818
+ this.hasFocus &&
819
+ !!this.toolbarState &&
820
+ !this.toolbarState.selectionEmpty &&
821
+ !this.toolbarState.imageSelected &&
822
+ !this.toolbarState.embedSelected &&
823
+ !panelOpen;
824
+ this.bubbleMenuEl.open = show;
825
+ if (show) {
826
+ this.bubbleMenuEl.toolbarState = this.toolbarState;
827
+ this.bubbleMenuEl.updateComplete.then(() =>
828
+ this.positionOverlay(this.bubbleMenuEl, 'above')
829
+ );
830
+ }
831
+
832
+ if (this.tableMenuEl) {
833
+ const showTable =
834
+ this.hasFocus && !!this.toolbarState && this.toolbarState.inTable && !panelOpen;
835
+ this.tableMenuEl.open = showTable;
836
+ if (showTable) {
837
+ this.tableMenuEl.updateComplete.then(() =>
838
+ this.positionOverlay(this.tableMenuEl, 'below')
839
+ );
840
+ }
841
+ }
842
+
843
+ if (this.codeMenuEl) {
844
+ const showCode =
845
+ this.hasFocus && !!this.toolbarState && this.toolbarState.inCodeBlock && !panelOpen;
846
+ this.codeMenuEl.open = showCode;
847
+ if (showCode) {
848
+ this.codeMenuEl.language = this.toolbarState!.codeBlockLanguage ?? '';
849
+ this.codeMenuEl.updateComplete.then(() =>
850
+ this.positionOverlay(this.codeMenuEl, 'above')
851
+ );
852
+ }
853
+ }
854
+
855
+ if (this.imageMenuEl) {
856
+ const showImage =
857
+ this.hasFocus && !!this.toolbarState && this.toolbarState.imageSelected && !panelOpen;
858
+ this.imageMenuEl.open = showImage;
859
+ if (showImage) {
860
+ this.imageMenuEl.toolbarState = this.toolbarState;
861
+ this.imageMenuEl.updateComplete.then(() =>
862
+ this.positionOverlay(this.imageMenuEl, 'above')
863
+ );
864
+ }
865
+ }
866
+
867
+ if (this.embedMenuEl) {
868
+ const showEmbed =
869
+ this.hasFocus && !!this.toolbarState && this.toolbarState.embedSelected && !panelOpen;
870
+ this.embedMenuEl.open = showEmbed;
871
+ if (showEmbed) {
872
+ this.embedMenuEl.toolbarState = this.toolbarState;
873
+ this.embedMenuEl.updateComplete.then(() =>
874
+ this.positionOverlay(this.embedMenuEl, 'above')
875
+ );
876
+ }
877
+ }
878
+ }
879
+
880
+ // -------------------------------------------------------- special chars
881
+
882
+ private openSpecialChars(): void {
883
+ if (!this.view || !this.specialCharsEl || this.disabled || this.readonly) return;
884
+ this.specialCharsEl.open = true;
885
+ this.specialCharsEl.updateComplete.then(() =>
886
+ this.positionOverlay(this.specialCharsEl, 'below')
887
+ );
888
+ }
889
+
890
+ private closeSpecialChars(refocus = true): void {
891
+ if (this.specialCharsEl) this.specialCharsEl.open = false;
892
+ if (refocus) this.view?.focus();
893
+ }
894
+
895
+ private insertSpecialChar(char: string): void {
896
+ if (!this.view) return;
897
+ this.view.dispatch(this.view.state.tr.insertText(char).scrollIntoView());
898
+ this.view.focus();
899
+ }
900
+
901
+ // ------------------------------------------------ templates & merge fields
902
+
903
+ private openInsertMenu(mode: 'template' | 'merge'): void {
904
+ if (!this.view || !this.insertMenuEl || this.disabled || this.readonly) return;
905
+ this.insertMenuMode = mode;
906
+ const menu = this.insertMenuEl;
907
+ if (mode === 'merge') {
908
+ menu.heading = 'Merge field';
909
+ menu.emptyText = 'No merge fields configured.';
910
+ menu.items = this.mergeFields.map(f => ({ label: f.label, description: `{{${f.id}}}` }));
911
+ } else {
912
+ menu.heading = 'Template';
913
+ menu.emptyText = 'No templates configured.';
914
+ menu.items = this.templates.map(t => ({ label: t.label, description: t.description }));
915
+ }
916
+ menu.open = true;
917
+ if (this.bubbleMenuEl) this.bubbleMenuEl.open = false;
918
+ menu.updateComplete.then(() => this.positionOverlay(menu, 'below'));
919
+ }
920
+
921
+ private closeInsertMenu(refocus = true): void {
922
+ if (this.insertMenuEl) this.insertMenuEl.open = false;
923
+ this.insertMenuMode = null;
924
+ if (refocus) this.view?.focus();
925
+ }
926
+
927
+ private handleInsertSelect(index: number): void {
928
+ const mode = this.insertMenuMode;
929
+ this.closeInsertMenu();
930
+ if (!this.view) return;
931
+ if (mode === 'merge') {
932
+ const field = this.mergeFields[index];
933
+ if (field) this.execCommand('insertMergeField', { id: field.id, label: field.label });
934
+ } else if (mode === 'template') {
935
+ const template = this.templates[index];
936
+ if (template) this.insertHTML(template.html);
937
+ }
938
+ }
939
+
940
+ /** Inserts a merge-field placeholder at the selection. */
941
+ public insertMergeField(id: string, label?: string): void {
942
+ this.execCommand('insertMergeField', { id, label: label ?? null });
943
+ }
944
+
945
+ /** Changes the case of the selected text. */
946
+ public changeCase(mode: 'upper' | 'lower' | 'title' | 'sentence'): void {
947
+ this.execCommand('changeCase', { textCase: mode });
948
+ }
949
+
950
+ // ---------------------------------------------------------------- math
951
+
952
+ private openMathPanel(): void {
953
+ if (!this.view || !this.mathPanelEl || this.disabled || this.readonly) return;
954
+ this.mathPanelEl.reset();
955
+ this.mathPanelEl.open = true;
956
+ if (this.bubbleMenuEl) this.bubbleMenuEl.open = false;
957
+ if (this.tableMenuEl) this.tableMenuEl.open = false;
958
+ this.mathPanelEl.updateComplete.then(() => {
959
+ const anchor = this.toolbarButton('Insert math (LaTeX)');
960
+ if (anchor) this.positionOverlayBelowAnchor(this.mathPanelEl, anchor);
961
+ else this.positionOverlay(this.mathPanelEl, 'below');
962
+ this.mathPanelEl.focusInput();
963
+ });
964
+ }
965
+
966
+ private closeMathPanel(refocus = true): void {
967
+ if (this.mathPanelEl) this.mathPanelEl.open = false;
968
+ if (refocus) this.view?.focus();
969
+ }
970
+
971
+ private openMarkdownPanel(): void {
972
+ if (!this.view || !this.markdownPanelEl || this.disabled || this.readonly) return;
973
+ this.markdownPanelEl.reset();
974
+ this.markdownPanelEl.open = true;
975
+ if (this.bubbleMenuEl) this.bubbleMenuEl.open = false;
976
+ if (this.tableMenuEl) this.tableMenuEl.open = false;
977
+ this.markdownPanelEl.updateComplete.then(() => {
978
+ const anchor = this.toolbarButton('Insert from Markdown');
979
+ if (anchor) this.positionOverlayBelowAnchor(this.markdownPanelEl, anchor);
980
+ else this.positionOverlay(this.markdownPanelEl, 'below');
981
+ this.markdownPanelEl.focusInput();
982
+ });
983
+ }
984
+
985
+ private closeMarkdownPanel(refocus = true): void {
986
+ if (this.markdownPanelEl) this.markdownPanelEl.open = false;
987
+ if (refocus) this.view?.focus();
988
+ }
989
+
990
+ private applyMath(latex: string, display: 'inline' | 'block'): void {
991
+ this.closeMathPanel();
992
+ this.execCommand('insertMath', { latex, mathDisplay: display });
993
+ }
994
+
995
+ /** Inserts a math formula (LaTeX) at the selection. */
996
+ public insertMath(latex: string, display: 'inline' | 'block' = 'inline'): void {
997
+ this.execCommand('insertMath', { latex, mathDisplay: display });
998
+ }
999
+
1000
+ // ----------------------------------------------------------------- emoji
1001
+
1002
+ private openEmojiPicker(): void {
1003
+ if (!this.view || !this.emojiPickerEl || this.disabled || this.readonly) return;
1004
+ this.emojiPickerEl.open = true;
1005
+ if (this.bubbleMenuEl) this.bubbleMenuEl.open = false;
1006
+ if (this.tableMenuEl) this.tableMenuEl.open = false;
1007
+ this.emojiPickerEl.updateComplete.then(() => {
1008
+ this.positionOverlay(this.emojiPickerEl, 'below');
1009
+ this.emojiPickerEl.focusInput();
1010
+ });
1011
+ }
1012
+
1013
+ private closeEmojiPicker(refocus = true): void {
1014
+ if (this.emojiPickerEl) this.emojiPickerEl.open = false;
1015
+ if (refocus) this.view?.focus();
1016
+ }
1017
+
1018
+ // ---------------------------------------------------------------- embeds
1019
+
1020
+ private openEmbedPanel(): void {
1021
+ if (!this.view || !this.embedPanelEl || this.disabled || this.readonly) return;
1022
+ this.embedPanelEl.reset();
1023
+ this.embedPanelEl.open = true;
1024
+ if (this.bubbleMenuEl) this.bubbleMenuEl.open = false;
1025
+ if (this.tableMenuEl) this.tableMenuEl.open = false;
1026
+ this.embedPanelEl.updateComplete.then(() => {
1027
+ // Anchor below the embed toolbar button when available.
1028
+ const anchor = this.toolbarButton('Embed video (YouTube, Vimeo, Loom)');
1029
+ if (anchor) this.positionOverlayBelowAnchor(this.embedPanelEl, anchor);
1030
+ else this.positionOverlay(this.embedPanelEl, 'below');
1031
+ this.embedPanelEl.focusInput();
1032
+ });
1033
+ }
1034
+
1035
+ private closeEmbedPanel(refocus = true): void {
1036
+ if (this.embedPanelEl) this.embedPanelEl.open = false;
1037
+ if (refocus) this.view?.focus();
1038
+ }
1039
+
1040
+ // -------------------------------------------------------------- bookmarks
1041
+
1042
+ private openBookmarkPanel(): void {
1043
+ if (!this.view || !this.bookmarkPanelEl || this.disabled || this.readonly) return;
1044
+ this.bookmarkPanelEl.open = true;
1045
+ if (this.bubbleMenuEl) this.bubbleMenuEl.open = false;
1046
+ if (this.tableMenuEl) this.tableMenuEl.open = false;
1047
+ this.bookmarkPanelEl.updateComplete.then(() => {
1048
+ this.positionOverlay(this.bookmarkPanelEl, 'below');
1049
+ this.bookmarkPanelEl.focusInput();
1050
+ });
1051
+ }
1052
+
1053
+ private closeBookmarkPanel(refocus = true): void {
1054
+ if (this.bookmarkPanelEl) this.bookmarkPanelEl.open = false;
1055
+ if (refocus) this.view?.focus();
1056
+ }
1057
+
1058
+ // ---------------------------------------------------------------- images
1059
+
1060
+ private openImagePanel(): void {
1061
+ if (!this.view || !this.imagePanelEl || this.disabled || this.readonly) return;
1062
+ // Editing an already-selected image/figure pre-fills the fields (and shows
1063
+ // Remove); otherwise open a blank insert panel.
1064
+ const info = selectedImageInfo(this.view.state);
1065
+ if (info) {
1066
+ this.imagePanelEl.prefill({
1067
+ src: info.node.attrs.src,
1068
+ alt: info.node.attrs.alt ?? '',
1069
+ caption: info.isFigure ? info.node.textContent : '',
1070
+ });
1071
+ } else {
1072
+ this.imagePanelEl.reset();
1073
+ }
1074
+ this.imagePanelEl.open = true;
1075
+ if (this.bubbleMenuEl) this.bubbleMenuEl.open = false;
1076
+ if (this.tableMenuEl) this.tableMenuEl.open = false;
1077
+ this.imagePanelEl.updateComplete.then(() => {
1078
+ // Anchor below the image toolbar button when it's available; otherwise
1079
+ // fall back to positioning at the cursor.
1080
+ const anchor = this.toolbarButton('Insert image');
1081
+ if (anchor) this.positionOverlayBelowAnchor(this.imagePanelEl, anchor);
1082
+ else this.positionOverlay(this.imagePanelEl, 'below');
1083
+ this.imagePanelEl.focusInput();
1084
+ });
1085
+ }
1086
+
1087
+ private closeImagePanel(refocus = true): void {
1088
+ if (this.imagePanelEl) this.imagePanelEl.open = false;
1089
+ if (refocus) this.view?.focus();
1090
+ }
1091
+
1092
+ private insertImageAt(
1093
+ src: string,
1094
+ alt: string | null,
1095
+ pos: number | null,
1096
+ caption: string | null = null
1097
+ ): void {
1098
+ if (!this.view) return;
1099
+ if (pos !== null) {
1100
+ const $pos = this.view.state.doc.resolve(
1101
+ Math.min(pos, this.view.state.doc.content.size)
1102
+ );
1103
+ this.view.dispatch(this.view.state.tr.setSelection(TextSelection.near($pos)));
1104
+ }
1105
+ this.execCommand('insertImage', { src, alt, caption });
1106
+ }
1107
+
1108
+ /**
1109
+ * Routes an image file through the upload pipeline: a cancelable
1110
+ * `nile-image-upload-request` first, then `uploadHandler` (with an
1111
+ * uploading placeholder), then the base64 fallback.
1112
+ */
1113
+ public async insertImageFile(
1114
+ file: File,
1115
+ pos: number | null = null,
1116
+ meta?: { alt?: string | null; caption?: string | null }
1117
+ ): Promise<void> {
1118
+ if (!this.view) return;
1119
+ const metaAlt = meta?.alt ?? null;
1120
+ const metaCaption = meta?.caption ?? null;
1121
+
1122
+ const request = this.emit(
1123
+ 'nile-image-upload-request',
1124
+ {
1125
+ file,
1126
+ insert: (src: string, alt?: string) =>
1127
+ this.insertImageAt(src, alt ?? metaAlt, pos, metaCaption),
1128
+ },
1129
+ true,
1130
+ true,
1131
+ true
1132
+ );
1133
+ if (request.defaultPrevented) return;
1134
+
1135
+ if (this.uploadHandler) {
1136
+ uploadCounter += 1;
1137
+ const id = `nile-upload-${uploadCounter}`;
1138
+ const at = pos ?? this.view.state.selection.from;
1139
+ this.view.dispatch(this.view.state.tr.setMeta(imageUploadKey, { add: { id, pos: at } }));
1140
+ try {
1141
+ const { src, alt } = await this.uploadHandler(file);
1142
+ const mappedPos = this.view ? findUploadPos(this.view.state, id) : null;
1143
+ this.view?.dispatch(this.view.state.tr.setMeta(imageUploadKey, { remove: { id } }));
1144
+ this.insertImageAt(src, metaAlt ?? alt ?? null, mappedPos, metaCaption);
1145
+ } catch (error) {
1146
+ this.view?.dispatch(this.view.state.tr.setMeta(imageUploadKey, { remove: { id } }));
1147
+ this.emit('nile-image-upload-error', { file, error });
1148
+ }
1149
+ return;
1150
+ }
1151
+
1152
+ if (this.allowBase64Images) {
1153
+ const src = await readFileAsDataURL(file);
1154
+ this.insertImageAt(src, metaAlt, pos, metaCaption);
1155
+ }
1156
+ }
1157
+
1158
+ // ---------------------------------------------------- mentions & slash
1159
+
1160
+ private dismissKeyFor(
1161
+ source: 'mention' | 'slash' | 'emoji',
1162
+ match: SuggestionMatch
1163
+ ): string {
1164
+ return `${source}:${match.trigger}:${match.from}`;
1165
+ }
1166
+
1167
+ private handleSuggestKey(
1168
+ event: KeyboardEvent,
1169
+ source: 'mention' | 'slash' | 'emoji'
1170
+ ): boolean {
1171
+ if (this.activeSuggestSource !== source || !this.suggestListEl?.open) return false;
1172
+ switch (event.key) {
1173
+ case 'ArrowDown':
1174
+ this.suggestListEl.next();
1175
+ return true;
1176
+ case 'ArrowUp':
1177
+ this.suggestListEl.prev();
1178
+ return true;
1179
+ case 'Enter':
1180
+ case 'Tab':
1181
+ return this.suggestListEl.selectCurrent();
1182
+ case 'Escape': {
1183
+ const match =
1184
+ source === 'mention'
1185
+ ? this.mentionMatch
1186
+ : source === 'slash'
1187
+ ? this.slashMatch
1188
+ : this.emojiMatch;
1189
+ if (match) this.suggestDismissed = this.dismissKeyFor(source, match);
1190
+ this.closeSuggestList();
1191
+ return true;
1192
+ }
1193
+ default:
1194
+ return false;
1195
+ }
1196
+ }
1197
+
1198
+ private closeSuggestList(): void {
1199
+ if (this.suggestListEl) this.suggestListEl.open = false;
1200
+ this.activeSuggestSource = null;
1201
+ }
1202
+
1203
+ private updateSuggestList(): void {
1204
+ if (!this.suggestListEl || !this.view) return;
1205
+ const mention = this.mentionMatch?.active ? this.mentionMatch : null;
1206
+ const slash =
1207
+ !this.noSlashCommands && this.slashMatch?.active ? this.slashMatch : null;
1208
+ const emoji = !this.noEmoji && this.emojiMatch?.active ? this.emojiMatch : null;
1209
+ const source: 'mention' | 'slash' | 'emoji' | null = mention
1210
+ ? 'mention'
1211
+ : slash
1212
+ ? 'slash'
1213
+ : emoji
1214
+ ? 'emoji'
1215
+ : null;
1216
+ const match = mention ?? slash ?? emoji;
1217
+
1218
+ if (!source || !match || this.disabled || this.readonly) {
1219
+ this.closeSuggestList();
1220
+ return;
1221
+ }
1222
+ if (this.suggestDismissed === this.dismissKeyFor(source, match)) return;
1223
+ this.suggestDismissed = null;
1224
+
1225
+ if (source === 'mention') {
1226
+ const all = this.mentions[match.trigger] ?? [];
1227
+ const query = match.query.toLowerCase();
1228
+ this.suggestListEl.items = all.filter(item =>
1229
+ item.label.toLowerCase().includes(query)
1230
+ );
1231
+ // Async override: listeners may provide items for this exact query.
1232
+ const token = `${match.trigger} ${match.query}`;
1233
+ this.mentionQueryToken = token;
1234
+ this.emit('nile-mention-query', {
1235
+ trigger: match.trigger,
1236
+ query: match.query,
1237
+ provide: (items: { key: string; label: string }[]) => {
1238
+ if (this.mentionQueryToken !== token || !Array.isArray(items)) return;
1239
+ this.suggestListEl.items = items
1240
+ .filter(i => i && typeof i.key === 'string' && typeof i.label === 'string')
1241
+ .map(i => ({ key: i.key, label: i.label }));
1242
+ },
1243
+ });
1244
+ } else if (source === 'slash') {
1245
+ const query = match.query.toLowerCase();
1246
+ const snippetItems: SuggestItem[] = this.snippets.map(s => ({
1247
+ key: `snippet:${s.key}`,
1248
+ label: s.label,
1249
+ hint: 'snippet',
1250
+ }));
1251
+ this.suggestListEl.items = [...SLASH_ITEMS, ...snippetItems].filter(item =>
1252
+ item.label.toLowerCase().includes(query)
1253
+ );
1254
+ } else {
1255
+ const query = match.query.toLowerCase();
1256
+ this.suggestListEl.items = EMOJI.filter(e => e.name.includes(query))
1257
+ .slice(0, 12)
1258
+ .map(e => ({ key: e.name, label: `${e.char} ${e.name}` }));
1259
+ if (this.suggestListEl.items.length === 0) {
1260
+ this.closeSuggestList();
1261
+ return;
1262
+ }
1263
+ }
1264
+
1265
+ this.activeSuggestSource = source;
1266
+ this.suggestListEl.resetActive();
1267
+ this.suggestListEl.open = true;
1268
+ this.suggestListEl.updateComplete.then(() =>
1269
+ this.positionOverlay(this.suggestListEl, 'below', match.from)
1270
+ );
1271
+ }
1272
+
1273
+ private handleSuggestSelect(item: SuggestItem): void {
1274
+ if (!this.view) return;
1275
+ if (this.activeSuggestSource === 'mention' && this.mentionMatch?.active) {
1276
+ const match = this.mentionMatch;
1277
+ const { state } = this.view;
1278
+ const mention = state.schema.nodes.mention.create({
1279
+ trigger: match.trigger,
1280
+ key: item.key,
1281
+ label: item.label,
1282
+ });
1283
+ this.view.dispatch(
1284
+ state.tr.replaceWith(match.from, match.to, [mention, state.schema.text(' ')])
1285
+ );
1286
+ this.view.focus();
1287
+ this.emit('nile-mention-select', {
1288
+ trigger: match.trigger,
1289
+ key: item.key,
1290
+ label: item.label,
1291
+ });
1292
+ } else if (this.activeSuggestSource === 'slash' && this.slashMatch?.active) {
1293
+ const match = this.slashMatch;
1294
+ this.view.dispatch(this.view.state.tr.delete(match.from, match.to));
1295
+ this.closeSuggestList();
1296
+ if (item.key.startsWith('snippet:')) {
1297
+ const snippet = this.snippets.find(s => `snippet:${s.key}` === item.key);
1298
+ if (snippet) this.insertHTML(snippet.html);
1299
+ return;
1300
+ }
1301
+ const slashItem = SLASH_ITEMS.find(s => s.key === item.key);
1302
+ if (!slashItem) return;
1303
+ if (slashItem.command === 'openImage') this.openImagePanel();
1304
+ else if (slashItem.command === 'openEmbed') this.openEmbedPanel();
1305
+ else if (slashItem.command === 'openBookmark') this.openBookmarkPanel();
1306
+ else this.execCommand(slashItem.command, slashItem.attrs);
1307
+ } else if (this.activeSuggestSource === 'emoji' && this.emojiMatch?.active) {
1308
+ const match = this.emojiMatch;
1309
+ const emoji = EMOJI.find(e => e.name === item.key);
1310
+ if (emoji) {
1311
+ this.view.dispatch(
1312
+ this.view.state.tr.insertText(emoji.char, match.from, match.to)
1313
+ );
1314
+ this.view.focus();
1315
+ }
1316
+ }
1317
+ this.closeSuggestList();
1318
+ }
1319
+
1320
+ // ---------------------------------------------------------- find & replace
1321
+
1322
+ /** Opens the find & replace bar (also bound to Mod-f inside the editor). */
1323
+ public openFindBar(): void {
1324
+ if (!this.findBarEl || this.sourceMode) return;
1325
+ this.findBarEl.open = true;
1326
+ const find = this.view ? getFindState(this.view.state) : null;
1327
+ this.findBarEl.matchCount = find?.matches.length ?? 0;
1328
+ this.findBarEl.activeIndex = find?.activeIndex ?? 0;
1329
+ this.findBarEl.updateComplete.then(() => {
1330
+ // Anchor below the Find & replace toolbar button when present; otherwise
1331
+ // fall back to the CSS-pinned top-right position.
1332
+ const anchor = this.toolbarButton('Find & replace (Ctrl+F)');
1333
+ if (anchor) this.positionOverlayBelowAnchor(this.findBarEl, anchor);
1334
+ this.findBarEl.focusInput();
1335
+ });
1336
+ }
1337
+
1338
+ public closeFindBar(): void {
1339
+ if (this.view) this.view.dispatch(clearFindTr(this.view.state));
1340
+ if (this.findBarEl) this.findBarEl.open = false;
1341
+ this.view?.focus();
1342
+ }
1343
+
1344
+ public find(query: string): number {
1345
+ if (!this.view) return 0;
1346
+ this.view.dispatch(searchTr(this.view.state, query));
1347
+ this.scrollToActiveMatch();
1348
+ return getFindState(this.view.state).matches.length;
1349
+ }
1350
+
1351
+ public findNext(): void {
1352
+ if (!this.view) return;
1353
+ this.view.dispatch(findStepTr(this.view.state, 'next'));
1354
+ this.scrollToActiveMatch();
1355
+ }
1356
+
1357
+ public findPrev(): void {
1358
+ if (!this.view) return;
1359
+ this.view.dispatch(findStepTr(this.view.state, 'prev'));
1360
+ this.scrollToActiveMatch();
1361
+ }
1362
+
1363
+ public replace(replacement: string): void {
1364
+ if (!this.view) return;
1365
+ const tr = replaceActiveTr(this.view.state, replacement);
1366
+ if (tr) this.view.dispatch(tr);
1367
+ }
1368
+
1369
+ public replaceAll(replacement: string): void {
1370
+ if (!this.view) return;
1371
+ const tr = replaceAllTr(this.view.state, replacement);
1372
+ if (tr) this.view.dispatch(tr);
1373
+ }
1374
+
1375
+ private scrollToActiveMatch(): void {
1376
+ if (!this.view) return;
1377
+ const find = getFindState(this.view.state);
1378
+ const match = find.matches[find.activeIndex];
1379
+ if (!match) return;
1380
+ this.view.dispatch(
1381
+ this.view.state.tr
1382
+ .setSelection(TextSelection.create(this.view.state.doc, match.from, match.to))
1383
+ .scrollIntoView()
1384
+ );
1385
+ }
1386
+
1387
+ // -------------------------------------------------------------- source view
1388
+
1389
+ /** Toggles raw-HTML source editing. Content re-sanitizes on the way back. */
1390
+ public toggleSourceView(): void {
1391
+ if (this.sourceMode) {
1392
+ this.applySourceView(this.sourceViewEl?.textarea?.value ?? null);
1393
+ } else {
1394
+ this.sourceMode = true;
1395
+ this.updateComplete.then(() => {
1396
+ if (this.sourceViewEl) {
1397
+ this.sourceViewEl.value = this.getHTML();
1398
+ this.sourceViewEl.focusInput();
1399
+ }
1400
+ });
1401
+ }
1402
+ }
1403
+
1404
+ private applySourceView(htmlContent: string | null): void {
1405
+ this.sourceMode = false;
1406
+ if (htmlContent !== null) this.setContent(htmlContent, true);
1407
+ this.updateComplete.then(() => this.view?.focus());
1408
+ }
1409
+
1410
+ /** Word and character counts for the current document. */
1411
+ public getWordCount(): { words: number; characters: number } {
1412
+ return this.view ? countWords(this.view.state.doc) : { words: 0, characters: 0 };
1413
+ }
1414
+
1415
+ /**
1416
+ * Arms the format painter (copies the formatting at the selection) or, if
1417
+ * already armed, disarms it. Once armed, the next non-empty selection the
1418
+ * user makes receives the copied formatting.
1419
+ */
1420
+ public toggleFormatPainter(): void {
1421
+ if (!this.view) return;
1422
+ const tr = isFormatPainterArmed(this.view.state)
1423
+ ? disarmFormatPainter(this.view.state)
1424
+ : armFormatPainter(this.view.state);
1425
+ if (tr) this.view.dispatch(tr);
1426
+ this.view.focus();
1427
+ }
1428
+
1429
+ // ---------------------------------------------------------- export & import
1430
+
1431
+ /** The live, styled content element used as the source for exports. */
1432
+ private get contentRoot(): HTMLElement | null {
1433
+ return (this.view?.dom as HTMLElement | undefined) ?? null;
1434
+ }
1435
+
1436
+ /** Sanitizes `this.name` (or a fallback) into a safe download basename. */
1437
+ private exportFilename(extension: string): string {
1438
+ const base =
1439
+ (this.name || 'document').replace(/[^\w.-]+/g, '-').replace(/^-+|-+$/g, '') || 'document';
1440
+ return `${base}.${extension}`;
1441
+ }
1442
+
1443
+ private downloadBlob(content: string, mime: string, filename: string): void {
1444
+ const blob = new Blob([content], { type: mime });
1445
+ const url = URL.createObjectURL(blob);
1446
+ const anchor = document.createElement('a');
1447
+ anchor.href = url;
1448
+ anchor.download = filename;
1449
+ anchor.style.display = 'none';
1450
+ document.body.appendChild(anchor);
1451
+ anchor.click();
1452
+ anchor.remove();
1453
+ // Revoke after the click so the download has a chance to start.
1454
+ setTimeout(() => URL.revokeObjectURL(url), 0);
1455
+ }
1456
+
1457
+ /**
1458
+ * The document as HTML with every style folded into inline `style`
1459
+ * attributes — the form email clients (which strip `<style>` blocks and
1460
+ * classes) can render. Returns the string; does not download.
1461
+ */
1462
+ public getInlineStyledHTML(): string {
1463
+ const root = this.contentRoot;
1464
+ if (!this.view || !root || isDocEmpty(this.view.state.doc)) return '';
1465
+ return toInlineStyledHtml(root);
1466
+ }
1467
+
1468
+ /** Downloads the document as an inline-styled, email-ready `.html` file. */
1469
+ public exportInlineHtml(filename?: string): void {
1470
+ const body = this.getInlineStyledHTML();
1471
+ if (!body) return;
1472
+ const doc = `<!DOCTYPE html>\n<html><head><meta charset="utf-8"></head><body>${body}</body></html>`;
1473
+ this.downloadBlob(doc, 'text/html;charset=utf-8', filename ?? this.exportFilename('html'));
1474
+ }
1475
+
1476
+ /** Downloads the document as a Word-openable `.doc` file. */
1477
+ public exportToWord(filename?: string): void {
1478
+ const body = this.getInlineStyledHTML();
1479
+ if (!body) return;
1480
+ const doc = buildWordDocument(body, this.name || 'Document');
1481
+ this.downloadBlob(doc, 'application/msword', filename ?? this.exportFilename('doc'));
1482
+ }
1483
+
1484
+ /**
1485
+ * Opens the browser print dialog with a self-contained, print-tuned copy of
1486
+ * the document ("Save as PDF"). Honours page breaks, embedded images and the
1487
+ * base font; optional running header/footer text.
1488
+ */
1489
+ public exportToPdf(
1490
+ options: { header?: string; footer?: string; fontFamily?: string } = {}
1491
+ ): void {
1492
+ const body = this.getInlineStyledHTML();
1493
+ if (!body) return;
1494
+ const source = buildPrintDocument(body, { title: this.name || 'Document', ...options });
1495
+
1496
+ const frame = document.createElement('iframe');
1497
+ frame.setAttribute('aria-hidden', 'true');
1498
+ Object.assign(frame.style, {
1499
+ position: 'fixed',
1500
+ right: '0',
1501
+ bottom: '0',
1502
+ width: '0',
1503
+ height: '0',
1504
+ border: '0',
1505
+ });
1506
+ document.body.appendChild(frame);
1507
+
1508
+ const cleanup = (): void => {
1509
+ // Delay removal so the browser's print job keeps its document alive.
1510
+ setTimeout(() => frame.remove(), 1000);
1511
+ };
1512
+
1513
+ frame.onload = (): void => {
1514
+ const win = frame.contentWindow;
1515
+ if (!win) {
1516
+ cleanup();
1517
+ return;
1518
+ }
1519
+ win.focus();
1520
+ win.addEventListener('afterprint', cleanup, { once: true });
1521
+ win.print();
1522
+ };
1523
+
1524
+ const fdoc = frame.contentDocument;
1525
+ if (!fdoc) {
1526
+ frame.remove();
1527
+ return;
1528
+ }
1529
+ fdoc.open();
1530
+ fdoc.write(source);
1531
+ fdoc.close();
1532
+ }
1533
+
1534
+ /**
1535
+ * Imports Word / Google-Docs HTML: normalizes the proprietary markup to
1536
+ * clean semantic HTML, then replaces the document. Fidelity note: comments
1537
+ * and track-changes are not preserved — the editor has no model for them,
1538
+ * and binary `.docx`/`.doc` files are not parsed (export as "Web Page" /
1539
+ * HTML first, or paste directly, which is handled automatically).
1540
+ */
1541
+ public importFromWord(html: string, emitChange = true): void {
1542
+ if (!html) return;
1543
+ const cleaned = isOfficeHtml(html) ? cleanOfficeHtml(html) : html;
1544
+ this.setContent(cleaned, emitChange);
1545
+ }
1546
+
1547
+ /** Opens a file picker for an HTML export and imports it (toolbar action). */
1548
+ private importFromWordFile(): void {
1549
+ const input = document.createElement('input');
1550
+ input.type = 'file';
1551
+ input.accept = '.htm,.html,text/html';
1552
+ input.style.display = 'none';
1553
+ input.addEventListener('change', () => {
1554
+ const file = input.files?.[0];
1555
+ input.remove();
1556
+ if (!file) return;
1557
+ file.text().then(text => this.importFromWord(text));
1558
+ });
1559
+ document.body.appendChild(input);
1560
+ input.click();
1561
+ }
1562
+
1563
+ // -------------------------------------------------------- shortcut help
1564
+
1565
+ /** Opens the keyboard shortcut reference (also bound to Mod-/). */
1566
+ public openShortcutHelp(): void {
1567
+ if (!this.shortcutHelpEl) return;
1568
+ this.shortcutHelpEl.open = true;
1569
+ this.shortcutHelpEl.focusPanel();
1570
+ }
1571
+
1572
+ public closeShortcutHelp(): void {
1573
+ if (this.shortcutHelpEl) this.shortcutHelpEl.open = false;
1574
+ this.view?.focus();
1575
+ }
1576
+
1577
+ // ------------------------------------------------------------ fullscreen
1578
+
1579
+ /** Toggles distraction-free fullscreen mode. Esc exits. */
1580
+ public toggleFullscreen(): void {
1581
+ this.fullscreen = !this.fullscreen;
1582
+ this.view?.focus();
1583
+ }
1584
+
1585
+ private openLinkEditor(): void {
1586
+ if (!this.view || !this.linkEditorEl || this.disabled || this.readonly) return;
1587
+ const ts = this.toolbarState ?? computeToolbarState(this.view.state);
1588
+ this.linkEditorEl.href = ts.linkHref ?? '';
1589
+ this.linkEditorEl.text = '';
1590
+ this.linkEditorEl.allowText = ts.selectionEmpty && !ts.linkActive;
1591
+ this.linkEditorEl.canRemove = ts.linkActive;
1592
+ this.linkEditorEl.open = true;
1593
+ if (this.bubbleMenuEl) this.bubbleMenuEl.open = false;
1594
+ this.linkEditorEl.updateComplete.then(() => {
1595
+ this.positionOverlay(this.linkEditorEl, 'below');
1596
+ this.linkEditorEl.focusInput();
1597
+ });
1598
+ }
1599
+
1600
+ private closeLinkEditor(refocus = true): void {
1601
+ if (this.linkEditorEl) this.linkEditorEl.open = false;
1602
+ if (refocus) this.view?.focus();
1603
+ }
1604
+
1605
+ private applyLink(href: string, text: string): void {
1606
+ if (!this.view) return;
1607
+ const { state } = this.view;
1608
+ const linkType = state.schema.marks.link;
1609
+ if (state.selection.empty && !getMarkRange(state.selection.$from, linkType)) {
1610
+ const label = text || href;
1611
+ const node = state.schema.text(label, [linkType.create({ href })]);
1612
+ this.view.dispatch(state.tr.replaceSelectionWith(node, false).scrollIntoView());
1613
+ } else {
1614
+ this.execCommand('link', { href });
1615
+ }
1616
+ this.closeLinkEditor();
1617
+ }
1618
+
1619
+ // ---------------------------------------------------------------- public
1620
+
1621
+ /** The underlying ProseMirror EditorView. Advanced escape hatch. */
1622
+ public get pmView(): EditorView | undefined {
1623
+ return this.view;
1624
+ }
1625
+
1626
+ /** Document as ProseMirror JSON. */
1627
+ public get json(): Record<string, unknown> | null {
1628
+ return this.view ? docToJson(this.view.state.doc) : null;
1629
+ }
1630
+
1631
+ public set json(value: Record<string, unknown> | null) {
1632
+ if (value) this.setContent(value);
1633
+ }
1634
+
1635
+ public override focus(): void {
1636
+ this.view?.focus();
1637
+ }
1638
+
1639
+ public override blur(): void {
1640
+ (this.view?.dom as HTMLElement | undefined)?.blur();
1641
+ }
1642
+
1643
+ public getHTML(): string {
1644
+ return this.view && !isDocEmpty(this.view.state.doc)
1645
+ ? docToHtml(this.view.state.doc)
1646
+ : '';
1647
+ }
1648
+
1649
+ public getJSON(): Record<string, unknown> | null {
1650
+ return this.json;
1651
+ }
1652
+
1653
+ public getText(): string {
1654
+ return this.view ? docToText(this.view.state.doc) : '';
1655
+ }
1656
+
1657
+ /**
1658
+ * Document as Markdown. Best-effort: structural nodes and Markdown-native
1659
+ * marks are converted; color/font marks are dropped to plain text. Use
1660
+ * getHTML() when a loss-free representation is required.
1661
+ */
1662
+ public getMarkdown(): string {
1663
+ return this.view && !isDocEmpty(this.view.state.doc)
1664
+ ? docToMarkdown(this.view.state.doc)
1665
+ : '';
1666
+ }
1667
+
1668
+ /**
1669
+ * Replaces the document with parsed Markdown. Covers the same subset as
1670
+ * getMarkdown(): headings, lists, code fences, tables, quotes and the
1671
+ * inline marks. The parsed HTML flows through the schema and sanitizer.
1672
+ */
1673
+ public setMarkdown(markdown: string, emitChange = false): void {
1674
+ this.setContent(markdownToHtml(markdown), emitChange);
1675
+ }
1676
+
1677
+ /** Parses and inserts Markdown at the current selection. */
1678
+ public insertMarkdown(markdown: string): void {
1679
+ this.insertHTML(markdownToHtml(markdown));
1680
+ }
1681
+
1682
+ /** Replaces the document. Accepts an HTML string or ProseMirror JSON. */
1683
+ public setContent(content: string | Record<string, unknown>, emitChange = false): void {
1684
+ if (!this.view) {
1685
+ if (typeof content === 'string') this.value = content;
1686
+ return;
1687
+ }
1688
+ let doc =
1689
+ typeof content === 'string'
1690
+ ? htmlToDoc(defaultSchema, content, this.sanitize)
1691
+ : jsonToDoc(defaultSchema, content);
1692
+ if (this.singleLine) doc = flattenToSingleParagraph(doc);
1693
+ this.view.updateState(this.buildState(doc));
1694
+ this.toolbarState = this.snapshotToolbar(this.view.state);
1695
+ this.syncingValue = true;
1696
+ this.value = isDocEmpty(doc) ? '' : docToHtml(doc);
1697
+ this.syncFormValue(doc);
1698
+ if (emitChange) {
1699
+ this.emit('nile-input', this.buildChangeDetail(doc));
1700
+ this.emit('nile-change', this.buildChangeDetail(doc));
1701
+ }
1702
+ }
1703
+
1704
+ public clear(): void {
1705
+ this.setContent('');
1706
+ }
1707
+
1708
+ /** Parses and inserts HTML at the current selection. */
1709
+ public insertHTML(htmlContent: string): void {
1710
+ if (!this.view) return;
1711
+ const slice = htmlToSlice(defaultSchema, htmlContent, this.sanitize);
1712
+ this.view.dispatch(this.view.state.tr.replaceSelection(slice).scrollIntoView());
1713
+ this.view.focus();
1714
+ }
1715
+
1716
+ /** Runs a named editor command. Returns whether the command applied. */
1717
+ public execCommand(name: string, attrs?: CommandAttrs): boolean {
1718
+ if (!this.view) return false;
1719
+ const command = getCommand(this.view.state, name as CommandName, attrs);
1720
+ if (!command) return false;
1721
+ const applied = command(this.view.state, this.view.dispatch, this.view);
1722
+ this.view.focus();
1723
+ return applied;
1724
+ }
1725
+
1726
+ /** True when the given mark/block command is active at the selection. */
1727
+ public isCommandActive(name: string): boolean {
1728
+ const ts = this.toolbarState;
1729
+ if (!ts) return false;
1730
+ switch (name) {
1731
+ case 'bold':
1732
+ return ts.bold;
1733
+ case 'italic':
1734
+ return ts.italic;
1735
+ case 'underline':
1736
+ return ts.underline;
1737
+ case 'strike':
1738
+ return ts.strike;
1739
+ case 'code':
1740
+ return ts.code;
1741
+ case 'link':
1742
+ return ts.linkActive;
1743
+ case 'bulletList':
1744
+ return ts.inBulletList;
1745
+ case 'orderedList':
1746
+ return ts.inOrderedList;
1747
+ case 'blockquote':
1748
+ return ts.inBlockquote;
1749
+ default:
1750
+ return false;
1751
+ }
1752
+ }
1753
+
1754
+ // ---------------------------------------------------------------- render
1755
+
1756
+ private handleCommand(event: CustomEvent<WysiwygCommandDetail>): void {
1757
+ event.stopPropagation();
1758
+ const { name, attrs } = event.detail;
1759
+ if (name === 'openLink') {
1760
+ this.openLinkEditor();
1761
+ return;
1762
+ }
1763
+ if (name === 'openImage') {
1764
+ this.openImagePanel();
1765
+ return;
1766
+ }
1767
+ if (name === 'openEmbed') {
1768
+ this.openEmbedPanel();
1769
+ return;
1770
+ }
1771
+ if (name === 'openBookmark') {
1772
+ this.openBookmarkPanel();
1773
+ return;
1774
+ }
1775
+ if (name === 'toggleShowBlocks') {
1776
+ this.showBlocks = !this.showBlocks;
1777
+ this.view?.focus();
1778
+ return;
1779
+ }
1780
+ if (name === 'toggleSource') {
1781
+ this.toggleSourceView();
1782
+ return;
1783
+ }
1784
+ if (name === 'togglePastePlainText') {
1785
+ this.pasteAsPlainText = !this.pasteAsPlainText;
1786
+ this.view?.focus();
1787
+ return;
1788
+ }
1789
+ if (name === 'toggleFullscreen') {
1790
+ this.toggleFullscreen();
1791
+ return;
1792
+ }
1793
+ if (name === 'openShortcutHelp') {
1794
+ this.openShortcutHelp();
1795
+ return;
1796
+ }
1797
+ if (name === 'openSpecialChars') {
1798
+ this.openSpecialChars();
1799
+ return;
1800
+ }
1801
+ if (name === 'openEmoji') {
1802
+ this.openEmojiPicker();
1803
+ return;
1804
+ }
1805
+ if (name === 'openFind') {
1806
+ this.openFindBar();
1807
+ return;
1808
+ }
1809
+ if (name === 'exportPdf') {
1810
+ this.exportToPdf();
1811
+ return;
1812
+ }
1813
+ if (name === 'exportWord') {
1814
+ this.exportToWord();
1815
+ return;
1816
+ }
1817
+ if (name === 'exportInlineHtml') {
1818
+ this.exportInlineHtml();
1819
+ return;
1820
+ }
1821
+ if (name === 'importWord') {
1822
+ this.importFromWordFile();
1823
+ return;
1824
+ }
1825
+ if (name === 'toggleFormatPainter') {
1826
+ this.toggleFormatPainter();
1827
+ return;
1828
+ }
1829
+ if (name === 'openMergeField') {
1830
+ this.openInsertMenu('merge');
1831
+ return;
1832
+ }
1833
+ if (name === 'openTemplate') {
1834
+ this.openInsertMenu('template');
1835
+ return;
1836
+ }
1837
+ if (name === 'openMarkdown') {
1838
+ this.openMarkdownPanel();
1839
+ return;
1840
+ }
1841
+ if (name === 'openMath') {
1842
+ this.openMathPanel();
1843
+ return;
1844
+ }
1845
+ this.execCommand(name, attrs);
1846
+ }
1847
+
1848
+ private handleBaseKeydown(event: KeyboardEvent): void {
1849
+ if (event.key !== 'Escape' || event.defaultPrevented) return;
1850
+ // The special-chars panel never takes focus, so its Esc lands here.
1851
+ if (this.specialCharsEl?.open) {
1852
+ this.closeSpecialChars();
1853
+ return;
1854
+ }
1855
+ // Esc exits fullscreen — but only when nothing else claimed it (panels
1856
+ // stopPropagation, ProseMirror handlers preventDefault).
1857
+ if (!this.fullscreen) return;
1858
+ const panelOpen =
1859
+ this.linkEditorEl?.open ||
1860
+ this.imagePanelEl?.open ||
1861
+ this.embedPanelEl?.open ||
1862
+ this.bookmarkPanelEl?.open ||
1863
+ this.emojiPickerEl?.open ||
1864
+ this.findBarEl?.open ||
1865
+ this.shortcutHelpEl?.open ||
1866
+ this.suggestListEl?.open;
1867
+ if (!panelOpen) this.fullscreen = false;
1868
+ }
1869
+
1870
+ private handleChromeFocusOut(event: FocusEvent): void {
1871
+ // Focus left the widget entirely (e.g. clicked the page) while it was
1872
+ // parked on chrome like the language picker: close the overlays.
1873
+ const related = event.relatedTarget as Node | null;
1874
+ if (related && (this.shadowRoot?.contains(related) || this.contains(related))) return;
1875
+ if (this.view?.hasFocus()) return;
1876
+ this.hasFocus = false;
1877
+ this.updateOverlays();
1878
+ }
1879
+
1880
+ /** The floating insert panels that should dismiss on outside interaction. */
1881
+ private get floatingPanels(): Array<{ open: boolean } | undefined> {
1882
+ return [
1883
+ this.linkEditorEl,
1884
+ this.imagePanelEl,
1885
+ this.embedPanelEl,
1886
+ this.bookmarkPanelEl,
1887
+ this.specialCharsEl,
1888
+ this.emojiPickerEl,
1889
+ this.insertMenuEl,
1890
+ this.mathPanelEl,
1891
+ this.markdownPanelEl,
1892
+ ];
1893
+ }
1894
+
1895
+ /** Closes every open insert panel. `refocus` returns focus to the editor. */
1896
+ private closeAllPanels(refocus = true): void {
1897
+ if (this.linkEditorEl?.open) this.closeLinkEditor(refocus);
1898
+ if (this.imagePanelEl?.open) this.closeImagePanel(refocus);
1899
+ if (this.embedPanelEl?.open) this.closeEmbedPanel(refocus);
1900
+ if (this.bookmarkPanelEl?.open) this.closeBookmarkPanel(refocus);
1901
+ if (this.specialCharsEl?.open) this.closeSpecialChars(refocus);
1902
+ if (this.emojiPickerEl?.open) this.closeEmojiPicker(refocus);
1903
+ if (this.insertMenuEl?.open) this.closeInsertMenu(refocus);
1904
+ if (this.mathPanelEl?.open) this.closeMathPanel(refocus);
1905
+ if (this.markdownPanelEl?.open) this.closeMarkdownPanel(refocus);
1906
+ }
1907
+
1908
+ /**
1909
+ * Closes any open insert panel when the pointer goes down outside of it —
1910
+ * whether elsewhere in the editor (e.g. the content area) or off the widget.
1911
+ */
1912
+ private onOutsidePointerDown = (event: Event): void => {
1913
+ const openPanels = this.floatingPanels.filter(
1914
+ (p): p is HTMLElement & { open: boolean } => !!p && (p as { open: boolean }).open
1915
+ );
1916
+ if (!openPanels.length) return;
1917
+ const path = event.composedPath();
1918
+ // Click landed inside an open panel — leave it be.
1919
+ if (openPanels.some(panel => path.includes(panel))) return;
1920
+ // Refocus the editor only when the click stayed within the widget.
1921
+ const insideWidget =
1922
+ path.includes(this) || (!!this.shadowRoot && path.includes(this.shadowRoot));
1923
+ this.closeAllPanels(insideWidget);
1924
+ };
1925
+
1926
+ public render(): TemplateResult {
1927
+ const showToolbar = this.toolbar !== 'none' && !this.readonly && !this.sourceMode;
1928
+ return html`
1929
+ <div
1930
+ class="nile-wysiwyg ${this.showBlocks ? 'nile-wysiwyg--show-blocks' : ''}"
1931
+ part="base"
1932
+ @wysiwyg-command=${this.handleCommand}
1933
+ @focusout=${this.handleChromeFocusOut}
1934
+ @keydown=${this.handleBaseKeydown}
1935
+ >
1936
+ ${showToolbar
1937
+ ? html`
1938
+ <nile-wysiwyg-toolbar
1939
+ part="toolbar"
1940
+ exportparts="button, select, color"
1941
+ config=${this.toolbar}
1942
+ ?disabled=${this.disabled}
1943
+ .toolbarState=${this.toolbarState}
1944
+ ></nile-wysiwyg-toolbar>
1945
+ `
1946
+ : nothing}
1947
+ ${this.sourceMode
1948
+ ? html`
1949
+ <nile-wysiwyg-source-view
1950
+ @wysiwyg-source-apply=${(e: CustomEvent) => {
1951
+ e.stopPropagation();
1952
+ this.applySourceView(e.detail.html);
1953
+ }}
1954
+ @wysiwyg-source-cancel=${(e: Event) => {
1955
+ e.stopPropagation();
1956
+ this.applySourceView(null);
1957
+ }}
1958
+ ></nile-wysiwyg-source-view>
1959
+ `
1960
+ : nothing}
1961
+ <div
1962
+ class="nile-wysiwyg__surface"
1963
+ part="surface"
1964
+ style=${this.sourceMode ? 'display: none' : ''}
1965
+ ></div>
1966
+ ${(this.maxLength > 0 || this.showCounter) && !this.sourceMode
1967
+ ? html`
1968
+ <div
1969
+ class="nile-wysiwyg__counter ${this.maxLength > 0 &&
1970
+ this.counter.characters >= this.maxLength
1971
+ ? 'over'
1972
+ : ''}"
1973
+ part="counter"
1974
+ >
1975
+ ${this.maxLength > 0
1976
+ ? `${this.counter.characters} / ${this.maxLength}`
1977
+ : `${this.counter.words} words · ${this.counter.characters} characters`}
1978
+ </div>
1979
+ `
1980
+ : nothing}
1981
+ <nile-wysiwyg-shortcut-help
1982
+ @wysiwyg-shortcut-help-close=${(e: Event) => {
1983
+ e.stopPropagation();
1984
+ this.closeShortcutHelp();
1985
+ }}
1986
+ ></nile-wysiwyg-shortcut-help>
1987
+ <nile-wysiwyg-bubble-menu></nile-wysiwyg-bubble-menu>
1988
+ <nile-wysiwyg-table-menu></nile-wysiwyg-table-menu>
1989
+ <nile-wysiwyg-code-menu></nile-wysiwyg-code-menu>
1990
+ <nile-wysiwyg-image-menu></nile-wysiwyg-image-menu>
1991
+ <nile-wysiwyg-embed-menu></nile-wysiwyg-embed-menu>
1992
+ <nile-wysiwyg-special-chars
1993
+ @wysiwyg-char-select=${(e: CustomEvent) => {
1994
+ e.stopPropagation();
1995
+ this.insertSpecialChar(e.detail.char);
1996
+ }}
1997
+ @wysiwyg-char-cancel=${(e: Event) => {
1998
+ e.stopPropagation();
1999
+ this.closeSpecialChars();
2000
+ }}
2001
+ ></nile-wysiwyg-special-chars>
2002
+ <nile-wysiwyg-emoji-picker
2003
+ @wysiwyg-emoji-select=${(e: CustomEvent) => {
2004
+ e.stopPropagation();
2005
+ this.closeEmojiPicker();
2006
+ this.insertSpecialChar(e.detail.char);
2007
+ }}
2008
+ @wysiwyg-emoji-cancel=${(e: Event) => {
2009
+ e.stopPropagation();
2010
+ this.closeEmojiPicker();
2011
+ }}
2012
+ ></nile-wysiwyg-emoji-picker>
2013
+ <nile-wysiwyg-insert-menu
2014
+ @wysiwyg-insert-select=${(e: CustomEvent) => {
2015
+ e.stopPropagation();
2016
+ this.handleInsertSelect(e.detail.index);
2017
+ }}
2018
+ @wysiwyg-insert-cancel=${(e: Event) => {
2019
+ e.stopPropagation();
2020
+ this.closeInsertMenu();
2021
+ }}
2022
+ ></nile-wysiwyg-insert-menu>
2023
+ <nile-wysiwyg-math-panel
2024
+ @wysiwyg-math-apply=${(e: CustomEvent) => {
2025
+ e.stopPropagation();
2026
+ this.applyMath(e.detail.latex, e.detail.display);
2027
+ }}
2028
+ @wysiwyg-math-cancel=${(e: Event) => {
2029
+ e.stopPropagation();
2030
+ this.closeMathPanel();
2031
+ }}
2032
+ ></nile-wysiwyg-math-panel>
2033
+ <nile-wysiwyg-markdown-panel
2034
+ @wysiwyg-markdown-apply=${(e: CustomEvent) => {
2035
+ e.stopPropagation();
2036
+ this.closeMarkdownPanel();
2037
+ this.insertMarkdown(e.detail.markdown);
2038
+ }}
2039
+ @wysiwyg-markdown-cancel=${(e: Event) => {
2040
+ e.stopPropagation();
2041
+ this.closeMarkdownPanel();
2042
+ }}
2043
+ ></nile-wysiwyg-markdown-panel>
2044
+ <nile-wysiwyg-embed-panel
2045
+ @wysiwyg-embed-apply=${(e: CustomEvent) => {
2046
+ e.stopPropagation();
2047
+ this.closeEmbedPanel();
2048
+ this.execCommand('insertEmbed', {
2049
+ src: e.detail.src,
2050
+ provider: e.detail.provider,
2051
+ });
2052
+ }}
2053
+ @wysiwyg-embed-cancel=${(e: Event) => {
2054
+ e.stopPropagation();
2055
+ this.closeEmbedPanel();
2056
+ }}
2057
+ ></nile-wysiwyg-embed-panel>
2058
+ <nile-wysiwyg-bookmark-panel
2059
+ @wysiwyg-bookmark-apply=${(e: CustomEvent) => {
2060
+ e.stopPropagation();
2061
+ this.closeBookmarkPanel();
2062
+ this.execCommand('insertBookmark', { id: e.detail.id });
2063
+ }}
2064
+ @wysiwyg-bookmark-cancel=${(e: Event) => {
2065
+ e.stopPropagation();
2066
+ this.closeBookmarkPanel();
2067
+ }}
2068
+ ></nile-wysiwyg-bookmark-panel>
2069
+ <nile-wysiwyg-suggest-list
2070
+ @wysiwyg-suggest-select=${(e: CustomEvent) => {
2071
+ e.stopPropagation();
2072
+ this.handleSuggestSelect(e.detail.item);
2073
+ }}
2074
+ ></nile-wysiwyg-suggest-list>
2075
+ <nile-wysiwyg-find-bar
2076
+ @wysiwyg-find=${(e: CustomEvent) => {
2077
+ e.stopPropagation();
2078
+ this.find(e.detail.query);
2079
+ }}
2080
+ @wysiwyg-find-step=${(e: CustomEvent) => {
2081
+ e.stopPropagation();
2082
+ if (e.detail.direction === 'prev') this.findPrev();
2083
+ else this.findNext();
2084
+ }}
2085
+ @wysiwyg-replace=${(e: CustomEvent) => {
2086
+ e.stopPropagation();
2087
+ this.replace(e.detail.value);
2088
+ }}
2089
+ @wysiwyg-replace-all=${(e: CustomEvent) => {
2090
+ e.stopPropagation();
2091
+ this.replaceAll(e.detail.value);
2092
+ }}
2093
+ @wysiwyg-find-close=${(e: Event) => {
2094
+ e.stopPropagation();
2095
+ this.closeFindBar();
2096
+ }}
2097
+ ></nile-wysiwyg-find-bar>
2098
+ <nile-wysiwyg-image-panel
2099
+ ?upload-from-computer=${this.uploadFromComputer}
2100
+ .maxFileSize=${this.imageMaxFileSize}
2101
+ @wysiwyg-image-apply=${(e: CustomEvent) => {
2102
+ e.stopPropagation();
2103
+ const editing = this.imagePanelEl?.editing;
2104
+ this.closeImagePanel();
2105
+ this.execCommand(editing ? 'updateImage' : 'insertImage', {
2106
+ src: e.detail.src,
2107
+ alt: e.detail.alt || null,
2108
+ caption: e.detail.caption || null,
2109
+ });
2110
+ }}
2111
+ @wysiwyg-image-file=${(e: CustomEvent) => {
2112
+ e.stopPropagation();
2113
+ this.closeImagePanel();
2114
+ this.insertImageFile(e.detail.file, null, {
2115
+ alt: e.detail.alt || null,
2116
+ caption: e.detail.caption || null,
2117
+ });
2118
+ }}
2119
+ @wysiwyg-image-remove=${(e: Event) => {
2120
+ e.stopPropagation();
2121
+ this.closeImagePanel();
2122
+ this.execCommand('deleteImage');
2123
+ }}
2124
+ @wysiwyg-image-cancel=${(e: Event) => {
2125
+ e.stopPropagation();
2126
+ this.closeImagePanel();
2127
+ }}
2128
+ ></nile-wysiwyg-image-panel>
2129
+ <nile-wysiwyg-link-editor
2130
+ @wysiwyg-link-apply=${(e: CustomEvent) => {
2131
+ e.stopPropagation();
2132
+ this.applyLink(e.detail.href, e.detail.text);
2133
+ }}
2134
+ @wysiwyg-link-remove=${(e: Event) => {
2135
+ e.stopPropagation();
2136
+ this.execCommand('unlink');
2137
+ this.closeLinkEditor();
2138
+ }}
2139
+ @wysiwyg-link-cancel=${(e: Event) => {
2140
+ e.stopPropagation();
2141
+ this.closeLinkEditor();
2142
+ }}
2143
+ ></nile-wysiwyg-link-editor>
2144
+ </div>
2145
+ `;
2146
+ }
2147
+ }
2148
+
2149
+ export default NileWysiwygEditor;
2150
+
2151
+ declare global {
2152
+ interface HTMLElementTagNameMap {
2153
+ 'nile-wysiwyg-editor': NileWysiwygEditor;
2154
+ }
2155
+ }