@react-x11/components 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/README.md +295 -51
  2. package/dist/charts/node.d.ts +1 -1
  3. package/dist/charts/node.d.ts.map +1 -1
  4. package/dist/charts/node.js +6 -2
  5. package/dist/charts/node.js.map +1 -1
  6. package/dist/color-picker/ColorPicker.d.ts.map +1 -1
  7. package/dist/color-picker/ColorPicker.js +43 -33
  8. package/dist/color-picker/ColorPicker.js.map +1 -1
  9. package/dist/embed/client.d.ts +48 -3
  10. package/dist/embed/client.d.ts.map +1 -1
  11. package/dist/embed/client.js +79 -5
  12. package/dist/embed/client.js.map +1 -1
  13. package/dist/embed/index.d.ts +1 -1
  14. package/dist/embed/index.d.ts.map +1 -1
  15. package/dist/embed/index.js +1 -1
  16. package/dist/embed/index.js.map +1 -1
  17. package/dist/html/node.d.ts +3 -1
  18. package/dist/html/node.d.ts.map +1 -1
  19. package/dist/html/node.js +7 -5
  20. package/dist/html/node.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/{markdown → internal/markdown}/ast.d.ts +12 -0
  26. package/dist/internal/markdown/ast.d.ts.map +1 -0
  27. package/dist/internal/markdown/ast.js +22 -0
  28. package/dist/internal/markdown/ast.js.map +1 -0
  29. package/dist/internal/markdown/parse.d.ts.map +1 -0
  30. package/dist/{markdown → internal/markdown}/parse.js +112 -22
  31. package/dist/internal/markdown/parse.js.map +1 -0
  32. package/dist/internal/markdown/stringify.d.ts +4 -0
  33. package/dist/internal/markdown/stringify.d.ts.map +1 -0
  34. package/dist/internal/markdown/stringify.js +684 -0
  35. package/dist/internal/markdown/stringify.js.map +1 -0
  36. package/dist/internal/markdown/tags.d.ts.map +1 -0
  37. package/dist/internal/markdown/tags.js.map +1 -0
  38. package/dist/internal/scroll.d.ts +3 -2
  39. package/dist/internal/scroll.d.ts.map +1 -1
  40. package/dist/internal/scroll.js +33 -15
  41. package/dist/internal/scroll.js.map +1 -1
  42. package/dist/internal/units.d.ts +2 -0
  43. package/dist/internal/units.d.ts.map +1 -0
  44. package/dist/internal/units.js +35 -0
  45. package/dist/internal/units.js.map +1 -0
  46. package/dist/internal/window.d.ts.map +1 -1
  47. package/dist/internal/window.js +4 -2
  48. package/dist/internal/window.js.map +1 -1
  49. package/dist/maps/anchors.d.ts +98 -0
  50. package/dist/maps/anchors.d.ts.map +1 -0
  51. package/dist/maps/anchors.js +531 -0
  52. package/dist/maps/anchors.js.map +1 -0
  53. package/dist/maps/controller.d.ts +283 -0
  54. package/dist/maps/controller.d.ts.map +1 -0
  55. package/dist/maps/controller.js +743 -0
  56. package/dist/maps/controller.js.map +1 -0
  57. package/dist/maps/gl/buckets.d.ts +95 -0
  58. package/dist/maps/gl/buckets.d.ts.map +1 -0
  59. package/dist/maps/gl/buckets.js +362 -0
  60. package/dist/maps/gl/buckets.js.map +1 -0
  61. package/dist/maps/gl/build-worker.d.ts +13 -0
  62. package/dist/maps/gl/build-worker.d.ts.map +1 -0
  63. package/dist/maps/gl/build-worker.js +44 -0
  64. package/dist/maps/gl/build-worker.js.map +1 -0
  65. package/dist/maps/gl/color.d.ts +8 -0
  66. package/dist/maps/gl/color.d.ts.map +1 -0
  67. package/dist/maps/gl/color.js +57 -0
  68. package/dist/maps/gl/color.js.map +1 -0
  69. package/dist/maps/gl/cover.d.ts +51 -0
  70. package/dist/maps/gl/cover.d.ts.map +1 -0
  71. package/dist/maps/gl/cover.js +124 -0
  72. package/dist/maps/gl/cover.js.map +1 -0
  73. package/dist/maps/gl/index.d.ts +3 -0
  74. package/dist/maps/gl/index.d.ts.map +1 -0
  75. package/dist/maps/gl/index.js +20 -0
  76. package/dist/maps/gl/index.js.map +1 -0
  77. package/dist/maps/gl/markers.d.ts +28 -0
  78. package/dist/maps/gl/markers.d.ts.map +1 -0
  79. package/dist/maps/gl/markers.js +60 -0
  80. package/dist/maps/gl/markers.js.map +1 -0
  81. package/dist/maps/gl/overlays.d.ts +68 -0
  82. package/dist/maps/gl/overlays.d.ts.map +1 -0
  83. package/dist/maps/gl/overlays.js +300 -0
  84. package/dist/maps/gl/overlays.js.map +1 -0
  85. package/dist/maps/gl/pane.d.ts +12 -0
  86. package/dist/maps/gl/pane.d.ts.map +1 -0
  87. package/dist/maps/gl/pane.js +37 -0
  88. package/dist/maps/gl/pane.js.map +1 -0
  89. package/dist/maps/gl/placement.d.ts +111 -0
  90. package/dist/maps/gl/placement.d.ts.map +1 -0
  91. package/dist/maps/gl/placement.js +567 -0
  92. package/dist/maps/gl/placement.js.map +1 -0
  93. package/dist/maps/gl/renderer.d.ts +352 -0
  94. package/dist/maps/gl/renderer.d.ts.map +1 -0
  95. package/dist/maps/gl/renderer.js +1127 -0
  96. package/dist/maps/gl/renderer.js.map +1 -0
  97. package/dist/maps/gl/shaders.d.ts +120 -0
  98. package/dist/maps/gl/shaders.d.ts.map +1 -0
  99. package/dist/maps/gl/shaders.js +451 -0
  100. package/dist/maps/gl/shaders.js.map +1 -0
  101. package/dist/maps/gl/store.d.ts +115 -0
  102. package/dist/maps/gl/store.d.ts.map +1 -0
  103. package/dist/maps/gl/store.js +469 -0
  104. package/dist/maps/gl/store.js.map +1 -0
  105. package/dist/maps/gl/text.d.ts +154 -0
  106. package/dist/maps/gl/text.d.ts.map +1 -0
  107. package/dist/maps/gl/text.js +382 -0
  108. package/dist/maps/gl/text.js.map +1 -0
  109. package/dist/maps/gl/view.d.ts +54 -0
  110. package/dist/maps/gl/view.d.ts.map +1 -0
  111. package/dist/maps/gl/view.js +974 -0
  112. package/dist/maps/gl/view.js.map +1 -0
  113. package/dist/maps/index.d.ts +1 -1
  114. package/dist/maps/index.d.ts.map +1 -1
  115. package/dist/maps/index.js +216 -52
  116. package/dist/maps/index.js.map +1 -1
  117. package/dist/maps/labels.d.ts +37 -16
  118. package/dist/maps/labels.d.ts.map +1 -1
  119. package/dist/maps/labels.js +188 -119
  120. package/dist/maps/labels.js.map +1 -1
  121. package/dist/maps/node.d.ts +162 -56
  122. package/dist/maps/node.d.ts.map +1 -1
  123. package/dist/maps/node.js +607 -515
  124. package/dist/maps/node.js.map +1 -1
  125. package/dist/maps/overlay.d.ts +72 -9
  126. package/dist/maps/overlay.d.ts.map +1 -1
  127. package/dist/maps/overlay.js +108 -28
  128. package/dist/maps/overlay.js.map +1 -1
  129. package/dist/maps/paint.d.ts +3 -0
  130. package/dist/maps/paint.d.ts.map +1 -1
  131. package/dist/maps/paint.js.map +1 -1
  132. package/dist/maps/proj.d.ts +14 -0
  133. package/dist/maps/proj.d.ts.map +1 -1
  134. package/dist/maps/proj.js +28 -0
  135. package/dist/maps/proj.js.map +1 -1
  136. package/dist/maps/renderer.d.ts +69 -0
  137. package/dist/maps/renderer.d.ts.map +1 -0
  138. package/dist/maps/renderer.js +66 -0
  139. package/dist/maps/renderer.js.map +1 -0
  140. package/dist/maps/sources.d.ts +25 -3
  141. package/dist/maps/sources.d.ts.map +1 -1
  142. package/dist/maps/sources.js +17 -0
  143. package/dist/maps/sources.js.map +1 -1
  144. package/dist/maps/theme.d.ts +19 -0
  145. package/dist/maps/theme.d.ts.map +1 -0
  146. package/dist/maps/theme.js +44 -0
  147. package/dist/maps/theme.js.map +1 -0
  148. package/dist/maps/tiles.d.ts +117 -17
  149. package/dist/maps/tiles.d.ts.map +1 -1
  150. package/dist/maps/tiles.js +163 -30
  151. package/dist/maps/tiles.js.map +1 -1
  152. package/dist/maps/types.d.ts +188 -20
  153. package/dist/maps/types.d.ts.map +1 -1
  154. package/dist/markdown/expressions.d.ts +1 -1
  155. package/dist/markdown/expressions.d.ts.map +1 -1
  156. package/dist/markdown/expressions.js.map +1 -1
  157. package/dist/markdown/index.d.ts +7 -6
  158. package/dist/markdown/index.d.ts.map +1 -1
  159. package/dist/markdown/index.js +9 -7
  160. package/dist/markdown/index.js.map +1 -1
  161. package/dist/markdown/spans.d.ts +1 -1
  162. package/dist/markdown/spans.d.ts.map +1 -1
  163. package/dist/media-player/index.d.ts +11 -3
  164. package/dist/media-player/index.d.ts.map +1 -1
  165. package/dist/media-player/index.js +13 -2
  166. package/dist/media-player/index.js.map +1 -1
  167. package/dist/rich-text-editor/clipboard.d.ts +23 -0
  168. package/dist/rich-text-editor/clipboard.d.ts.map +1 -0
  169. package/dist/rich-text-editor/clipboard.js +307 -0
  170. package/dist/rich-text-editor/clipboard.js.map +1 -0
  171. package/dist/rich-text-editor/collab.d.ts +32 -0
  172. package/dist/rich-text-editor/collab.d.ts.map +1 -0
  173. package/dist/rich-text-editor/collab.js +55 -0
  174. package/dist/rich-text-editor/collab.js.map +1 -0
  175. package/dist/rich-text-editor/commands.d.ts +50 -0
  176. package/dist/rich-text-editor/commands.d.ts.map +1 -0
  177. package/dist/rich-text-editor/commands.js +376 -0
  178. package/dist/rich-text-editor/commands.js.map +1 -0
  179. package/dist/rich-text-editor/drag.d.ts +68 -0
  180. package/dist/rich-text-editor/drag.d.ts.map +1 -0
  181. package/dist/rich-text-editor/drag.js +148 -0
  182. package/dist/rich-text-editor/drag.js.map +1 -0
  183. package/dist/rich-text-editor/html.d.ts +145 -0
  184. package/dist/rich-text-editor/html.d.ts.map +1 -0
  185. package/dist/rich-text-editor/html.js +516 -0
  186. package/dist/rich-text-editor/html.js.map +1 -0
  187. package/dist/rich-text-editor/index.d.ts +263 -0
  188. package/dist/rich-text-editor/index.d.ts.map +1 -0
  189. package/dist/rich-text-editor/index.js +822 -0
  190. package/dist/rich-text-editor/index.js.map +1 -0
  191. package/dist/rich-text-editor/inline.d.ts +94 -0
  192. package/dist/rich-text-editor/inline.d.ts.map +1 -0
  193. package/dist/rich-text-editor/inline.js +252 -0
  194. package/dist/rich-text-editor/inline.js.map +1 -0
  195. package/dist/rich-text-editor/keymap.d.ts +71 -0
  196. package/dist/rich-text-editor/keymap.d.ts.map +1 -0
  197. package/dist/rich-text-editor/keymap.js +393 -0
  198. package/dist/rich-text-editor/keymap.js.map +1 -0
  199. package/dist/rich-text-editor/keys.d.ts +29 -0
  200. package/dist/rich-text-editor/keys.d.ts.map +1 -0
  201. package/dist/rich-text-editor/keys.js +93 -0
  202. package/dist/rich-text-editor/keys.js.map +1 -0
  203. package/dist/rich-text-editor/look.d.ts +71 -0
  204. package/dist/rich-text-editor/look.d.ts.map +1 -0
  205. package/dist/rich-text-editor/look.js +342 -0
  206. package/dist/rich-text-editor/look.js.map +1 -0
  207. package/dist/rich-text-editor/markdown.d.ts +18 -0
  208. package/dist/rich-text-editor/markdown.d.ts.map +1 -0
  209. package/dist/rich-text-editor/markdown.js +496 -0
  210. package/dist/rich-text-editor/markdown.js.map +1 -0
  211. package/dist/rich-text-editor/nodes.d.ts +135 -0
  212. package/dist/rich-text-editor/nodes.d.ts.map +1 -0
  213. package/dist/rich-text-editor/nodes.js +397 -0
  214. package/dist/rich-text-editor/nodes.js.map +1 -0
  215. package/dist/rich-text-editor/render.d.ts +68 -0
  216. package/dist/rich-text-editor/render.d.ts.map +1 -0
  217. package/dist/rich-text-editor/render.js +500 -0
  218. package/dist/rich-text-editor/render.js.map +1 -0
  219. package/dist/rich-text-editor/schema.d.ts +16 -0
  220. package/dist/rich-text-editor/schema.d.ts.map +1 -0
  221. package/dist/rich-text-editor/schema.js +386 -0
  222. package/dist/rich-text-editor/schema.js.map +1 -0
  223. package/dist/rich-text-editor/store.d.ts +11 -0
  224. package/dist/rich-text-editor/store.d.ts.map +1 -0
  225. package/dist/rich-text-editor/store.js +43 -0
  226. package/dist/rich-text-editor/store.js.map +1 -0
  227. package/dist/rich-text-editor/suggest.d.ts +127 -0
  228. package/dist/rich-text-editor/suggest.d.ts.map +1 -0
  229. package/dist/rich-text-editor/suggest.js +0 -0
  230. package/dist/rich-text-editor/suggest.js.map +1 -0
  231. package/dist/rich-text-editor/tables.d.ts +41 -0
  232. package/dist/rich-text-editor/tables.d.ts.map +1 -0
  233. package/dist/rich-text-editor/tables.js +204 -0
  234. package/dist/rich-text-editor/tables.js.map +1 -0
  235. package/dist/rich-text-editor/toolbar.d.ts +58 -0
  236. package/dist/rich-text-editor/toolbar.d.ts.map +1 -0
  237. package/dist/rich-text-editor/toolbar.js +548 -0
  238. package/dist/rich-text-editor/toolbar.js.map +1 -0
  239. package/dist/rich-text-editor/view.d.ts +368 -0
  240. package/dist/rich-text-editor/view.d.ts.map +1 -0
  241. package/dist/rich-text-editor/view.js +1968 -0
  242. package/dist/rich-text-editor/view.js.map +1 -0
  243. package/dist/rich-text-editor/virtual.d.ts +54 -0
  244. package/dist/rich-text-editor/virtual.d.ts.map +1 -0
  245. package/dist/rich-text-editor/virtual.js +262 -0
  246. package/dist/rich-text-editor/virtual.js.map +1 -0
  247. package/dist/richtext/node.d.ts +25 -2
  248. package/dist/richtext/node.d.ts.map +1 -1
  249. package/dist/richtext/node.js +46 -24
  250. package/dist/richtext/node.js.map +1 -1
  251. package/dist/table/index.d.ts.map +1 -1
  252. package/dist/table/index.js +14 -5
  253. package/dist/table/index.js.map +1 -1
  254. package/dist/terminal/backends.d.ts +4 -1
  255. package/dist/terminal/backends.d.ts.map +1 -1
  256. package/dist/terminal/backends.js.map +1 -1
  257. package/dist/terminal/index.d.ts +18 -7
  258. package/dist/terminal/index.d.ts.map +1 -1
  259. package/dist/terminal/index.js +28 -11
  260. package/dist/terminal/index.js.map +1 -1
  261. package/dist/terminal/vt/node.d.ts +56 -3
  262. package/dist/terminal/vt/node.d.ts.map +1 -1
  263. package/dist/terminal/vt/node.js +106 -26
  264. package/dist/terminal/vt/node.js.map +1 -1
  265. package/dist/terminal/vt/renderer.d.ts +4 -0
  266. package/dist/terminal/vt/renderer.d.ts.map +1 -1
  267. package/dist/terminal/vt/renderer.js +14 -5
  268. package/dist/terminal/vt/renderer.js.map +1 -1
  269. package/dist/tree/index.d.ts.map +1 -1
  270. package/dist/tree/index.js +6 -4
  271. package/dist/tree/index.js.map +1 -1
  272. package/package.json +23 -4
  273. package/src/charts/node.ts +6 -2
  274. package/src/color-picker/ColorPicker.ts +107 -81
  275. package/src/embed/client.ts +102 -8
  276. package/src/embed/index.ts +2 -0
  277. package/src/html/node.ts +7 -5
  278. package/src/index.ts +2 -0
  279. package/src/{markdown → internal/markdown}/ast.ts +24 -7
  280. package/src/{markdown → internal/markdown}/parse.ts +134 -24
  281. package/src/internal/markdown/stringify.ts +772 -0
  282. package/src/internal/scroll.ts +39 -18
  283. package/src/internal/units.ts +34 -0
  284. package/src/internal/window.ts +4 -2
  285. package/src/maps/anchors.ts +607 -0
  286. package/src/maps/controller.ts +999 -0
  287. package/src/maps/gl/buckets.ts +457 -0
  288. package/src/maps/gl/build-worker.ts +55 -0
  289. package/src/maps/gl/color.ts +62 -0
  290. package/src/maps/gl/cover.ts +170 -0
  291. package/src/maps/gl/index.ts +23 -0
  292. package/src/maps/gl/markers.ts +83 -0
  293. package/src/maps/gl/overlays.ts +379 -0
  294. package/src/maps/gl/pane.ts +42 -0
  295. package/src/maps/gl/placement.ts +816 -0
  296. package/src/maps/gl/renderer.ts +1576 -0
  297. package/src/maps/gl/shaders.ts +486 -0
  298. package/src/maps/gl/store.ts +566 -0
  299. package/src/maps/gl/text.ts +495 -0
  300. package/src/maps/gl/view.ts +1224 -0
  301. package/src/maps/index.ts +270 -81
  302. package/src/maps/labels.ts +214 -121
  303. package/src/maps/node.ts +763 -629
  304. package/src/maps/overlay.ts +148 -43
  305. package/src/maps/paint.ts +3 -0
  306. package/src/maps/proj.ts +31 -0
  307. package/src/maps/renderer.ts +155 -0
  308. package/src/maps/sources.ts +38 -3
  309. package/src/maps/theme.ts +51 -0
  310. package/src/maps/tiles.ts +191 -33
  311. package/src/maps/types.ts +188 -20
  312. package/src/markdown/expressions.ts +5 -1
  313. package/src/markdown/index.ts +14 -11
  314. package/src/markdown/spans.ts +1 -1
  315. package/src/media-player/index.ts +18 -4
  316. package/src/rich-text-editor/clipboard.ts +439 -0
  317. package/src/rich-text-editor/collab.ts +91 -0
  318. package/src/rich-text-editor/commands.ts +448 -0
  319. package/src/rich-text-editor/dom-globals.d.ts +23 -0
  320. package/src/rich-text-editor/drag.ts +211 -0
  321. package/src/rich-text-editor/html.ts +619 -0
  322. package/src/rich-text-editor/index.ts +1331 -0
  323. package/src/rich-text-editor/inline.ts +345 -0
  324. package/src/rich-text-editor/keymap.ts +510 -0
  325. package/src/rich-text-editor/keys.ts +119 -0
  326. package/src/rich-text-editor/look.ts +427 -0
  327. package/src/rich-text-editor/markdown.ts +653 -0
  328. package/src/rich-text-editor/nodes.ts +497 -0
  329. package/src/rich-text-editor/render.ts +800 -0
  330. package/src/rich-text-editor/schema.ts +431 -0
  331. package/src/rich-text-editor/store.ts +39 -0
  332. package/src/rich-text-editor/suggest.ts +597 -0
  333. package/src/rich-text-editor/tables.ts +272 -0
  334. package/src/rich-text-editor/toolbar.ts +724 -0
  335. package/src/rich-text-editor/view.ts +2287 -0
  336. package/src/rich-text-editor/virtual.ts +347 -0
  337. package/src/richtext/node.ts +59 -30
  338. package/src/table/index.ts +14 -5
  339. package/src/terminal/backends.ts +4 -1
  340. package/src/terminal/index.ts +43 -15
  341. package/src/terminal/vt/node.ts +128 -29
  342. package/src/terminal/vt/renderer.ts +27 -5
  343. package/src/tree/index.ts +6 -4
  344. package/dist/markdown/ast.d.ts.map +0 -1
  345. package/dist/markdown/ast.js +0 -17
  346. package/dist/markdown/ast.js.map +0 -1
  347. package/dist/markdown/parse.d.ts.map +0 -1
  348. package/dist/markdown/parse.js.map +0 -1
  349. package/dist/markdown/tags.d.ts.map +0 -1
  350. package/dist/markdown/tags.js.map +0 -1
  351. /package/dist/{markdown → internal/markdown}/parse.d.ts +0 -0
  352. /package/dist/{markdown → internal/markdown}/tags.d.ts +0 -0
  353. /package/dist/{markdown → internal/markdown}/tags.js +0 -0
  354. /package/src/{markdown → internal/markdown}/tags.ts +0 -0
@@ -0,0 +1,597 @@
1
+ // Suggestions: a list that opens at a trigger character as it is typed — `@`
2
+ // for people, `#` for issues, `:` for emoji, `/` for a block menu — kept by a
3
+ // plugin and drawn by `<RichTextEditor>`.
4
+ //
5
+ // The division of labour is ProseMirror's own, and it is what keeps the list
6
+ // working on every rung of the ladder (docs/prd-rich-text-editor.md):
7
+ //
8
+ // - **The plugin holds the state**: which trigger is open, what has been
9
+ // typed after it, the rows that answer it and the highlighted one — all of
10
+ // it plugin state, changed only by transactions. An app that owns the
11
+ // EditorState sees the list change like anything else, and a re-render
12
+ // cannot lose it, because a render is not where it lives.
13
+ // - **The plugin takes the keys**, through `handleKeyDown`, and only while
14
+ // rows are showing: Up and Down move the highlight, Enter and Tab take the
15
+ // row, Escape closes the list. It sits ahead of the default keymap, so
16
+ // Enter takes a row instead of splitting the paragraph.
17
+ // - **The plugin asks for the rows**, from its plugin view: a list is
18
+ // filtered here, a function is called, and an answer to a query the user
19
+ // has since typed past is dropped — `<CodeEditor>`'s completion rule, for
20
+ // the same reason.
21
+ // - **The component draws**, from nothing but this state: a `<popup>` hung
22
+ // off the trigger, whose rows run `acceptSuggestion`.
23
+ //
24
+ // When a list opens is the decision with a user-visible edge: **as the
25
+ // trigger's word is typed, never as the caret moves.** The value is markdown,
26
+ // so a mention stays text (`@ada`), and a document is soon full of words that
27
+ // start with a trigger. Opening whenever the caret visits one would pop a list
28
+ // up at every click; opening on an edit at the caret is the rule GitHub's
29
+ // comment box keeps, and `<CodeEditor>`'s completion.
30
+ import type { ReactNode } from 'react';
31
+ import { closeHistory } from 'prosemirror-history';
32
+ import type { Node as PMNode } from 'prosemirror-model';
33
+ import { Plugin, PluginKey, Selection, TextSelection } from 'prosemirror-state';
34
+ import type { Command, EditorState, Transaction } from 'prosemirror-state';
35
+ import { StepMap } from 'prosemirror-transform';
36
+ import { Decoration, DecorationSet } from 'prosemirror-view';
37
+ import type { EditorView } from 'prosemirror-view';
38
+
39
+ /** One row of a suggestion list. */
40
+ export interface SuggestionItem {
41
+ /** What the row shows — and, in a list the editor filters, what the query
42
+ * is matched against. */
43
+ label: string;
44
+ /** Muted text after the label: a handle, a description, a shortcut. */
45
+ detail?: string;
46
+ /**
47
+ * What replaces the trigger and the query: text, or a node of the
48
+ * document's schema — text carrying a link mark, say, or a mention node of
49
+ * an app's own schema. Default: the trigger and the label, `@Ada`. A space
50
+ * follows it, unless one already does.
51
+ */
52
+ insert?: string | PMNode;
53
+ /**
54
+ * Run instead of inserting anything, on the state the trigger and the
55
+ * query have been deleted from — a block menu's "Heading 1". What it
56
+ * dispatches joins the deletion, so the choice is one undo step.
57
+ */
58
+ command?: Command;
59
+ }
60
+
61
+ /** What a suggester's `items` function is asked with. */
62
+ export interface SuggestionQuery {
63
+ /** What has been typed after the trigger. */
64
+ query: string;
65
+ /** The trigger. */
66
+ char: string;
67
+ /** The state the query was read from — for its schema, to build a node. */
68
+ state: EditorState;
69
+ }
70
+
71
+ /** What `renderItem` is handed beside its row. */
72
+ export interface SuggestionRow {
73
+ /** The highlighted row — the one Enter takes — drawn on the accent. */
74
+ selected: boolean;
75
+ /** What has been typed after the trigger. */
76
+ query: string;
77
+ }
78
+
79
+ /**
80
+ * A trigger character, and where the rows it opens come from. `Item` is the
81
+ * app's own row type when a row carries more than a label — an avatar, a
82
+ * presence — for `renderItem` to draw.
83
+ */
84
+ export interface Suggester<Item extends SuggestionItem = SuggestionItem> {
85
+ /** What opens the list: `'@'`, `'#'`, `':'`, `'/'`. */
86
+ char: string;
87
+ /**
88
+ * The rows. An array is filtered by what has been typed — labels that
89
+ * start with it first, then labels with a word that does, then any that
90
+ * contain it. A function is called with each new query and its rows are
91
+ * shown as given; it may answer with a promise, and an answer that comes
92
+ * after the query has moved on is dropped.
93
+ */
94
+ items:
95
+ | readonly Item[]
96
+ | ((query: SuggestionQuery) => readonly Item[] | Promise<readonly Item[]>);
97
+ /**
98
+ * A row of the app's own, in place of the label and its detail: an avatar,
99
+ * a presence dot, a shortcut drawn as keys. The editor still draws the
100
+ * highlight behind it and takes a press on it, and an array is still
101
+ * filtered on the label.
102
+ */
103
+ renderItem?(item: Item, row: SuggestionRow): ReactNode;
104
+ /** Only at the very start of a textblock — a block menu. Default false:
105
+ * at the start of any word. */
106
+ startOfLine?: boolean;
107
+ /** The query may have spaces in it — a full name. Default false: a space
108
+ * ends it. Two spaces in a row end it either way. */
109
+ allowSpaces?: boolean;
110
+ }
111
+
112
+ /** An open list — what `suggestionState` answers and the component draws. */
113
+ export interface SuggestionState {
114
+ /** Which suggester opened it: an index into the plugin's list. */
115
+ readonly index: number;
116
+ readonly char: string;
117
+ /** What has been typed after the trigger — the whole word the caret is
118
+ * in, so a row taken mid-word replaces all of it. */
119
+ readonly query: string;
120
+ /** The trigger and the query, in the document: what a row replaces. */
121
+ readonly from: number;
122
+ readonly to: number;
123
+ /** The rows — null until the first answer comes. */
124
+ readonly items: readonly SuggestionItem[] | null;
125
+ /** The highlighted row. */
126
+ readonly selected: number;
127
+ }
128
+
129
+ interface PluginState {
130
+ active: SuggestionState | null;
131
+ /** Where a list was closed — by Escape, or by a choice. A trigger at one
132
+ * of these positions does not open again while its character is there. */
133
+ closed: readonly number[];
134
+ }
135
+
136
+ type Meta =
137
+ | {
138
+ type: 'items';
139
+ index: number;
140
+ from: number;
141
+ query: string;
142
+ items: readonly SuggestionItem[];
143
+ }
144
+ | { type: 'select'; index: number }
145
+ | { type: 'close' };
146
+
147
+ const key = new PluginKey<PluginState>('suggestions');
148
+
149
+ /** Rows to a page: PageUp and PageDown move by it, and the list shows it. */
150
+ export const SUGGESTION_PAGE = 8;
151
+ /** The most rows a list keeps. */
152
+ const MAX_ITEMS = 100;
153
+ /** How many closed triggers are remembered. */
154
+ const MAX_CLOSED = 16;
155
+
156
+ // --- where a list opens --------------------------------------------------------
157
+
158
+ /** An inline leaf — an image, a line break — in a textblock's text. */
159
+ const LEAF = '';
160
+ /** What a trigger may follow: the start of the block, a space, an inline
161
+ * leaf, or opening punctuation — so `ada@example.com` is an address. */
162
+ const BEFORE_TRIGGER = /[\s([{<"'“‘«]/u;
163
+ /** What ends a query that may not hold spaces. */
164
+ const WORD_END = /[\s]/u;
165
+
166
+ interface Match {
167
+ index: number;
168
+ char: string;
169
+ query: string;
170
+ from: number;
171
+ to: number;
172
+ }
173
+
174
+ /** Has what was typed after a trigger stopped being a query? */
175
+ function queryEnded(typed: string, spaces: boolean): boolean {
176
+ if (typed.includes(LEAF)) return true;
177
+ if (!spaces) return WORD_END.test(typed);
178
+ return /^\s|\s\s|\n/u.test(typed);
179
+ }
180
+
181
+ /**
182
+ * The trigger whose word the caret is in, if any: for each suggester, the
183
+ * nearest occurrence of its character before the caret, where a word can
184
+ * start, with nothing between it and the caret that ends a query — and
185
+ * outside code, where `@` is a character. The nearest of those wins.
186
+ */
187
+ function findMatch(
188
+ state: EditorState,
189
+ suggesters: readonly Suggester[],
190
+ ): Match | null {
191
+ const sel = state.selection;
192
+ if (!(sel instanceof TextSelection) || !sel.empty) return null;
193
+ const $head = sel.$head;
194
+ const block = $head.parent;
195
+ if (!block.isTextblock || block.type.spec.code) return null;
196
+ const text = block.textBetween(0, block.content.size, undefined, LEAF);
197
+ const caret = $head.parentOffset;
198
+ const start = $head.start();
199
+ let best: Match | null = null;
200
+ for (let index = 0; index < suggesters.length; index++) {
201
+ const s = suggesters[index];
202
+ const char = s.char;
203
+ if (!char || caret < char.length) continue;
204
+ const at = text.lastIndexOf(char, caret - char.length);
205
+ if (at < 0 || (best && start + at <= best.from)) continue;
206
+ if (s.startOfLine ? at !== 0 : at > 0 && !BEFORE_TRIGGER.test(text[at - 1]))
207
+ continue;
208
+ const spaces = !!s.allowSpaces;
209
+ if (queryEnded(text.slice(at + char.length, caret), spaces)) continue;
210
+ if (block.childAfter(at).node?.marks.some((m) => m.type.spec.code))
211
+ continue;
212
+ // without spaces the query is the whole word, the part past the caret
213
+ // too; with them it is what has been typed so far
214
+ let end = caret;
215
+ if (!spaces) {
216
+ while (
217
+ end < text.length &&
218
+ !WORD_END.test(text[end]) &&
219
+ !text.startsWith(char, end)
220
+ )
221
+ end++;
222
+ }
223
+ best = {
224
+ index,
225
+ char,
226
+ query: text.slice(at + char.length, end),
227
+ from: start + at,
228
+ to: start + end,
229
+ };
230
+ }
231
+ return best;
232
+ }
233
+
234
+ /** Did `tr` change the document between `from` and `to` — positions in the
235
+ * document it made? A deletion there counts: it leaves an empty range. */
236
+ function editedBetween(tr: Transaction, from: number, to: number): boolean {
237
+ if (!tr.docChanged) return false;
238
+ const { maps } = tr.mapping;
239
+ for (let i = 0; i < maps.length; i++) {
240
+ const later = tr.mapping.slice(i + 1);
241
+ let hit = false;
242
+ maps[i].forEach((_oldStart, _oldEnd, newStart, newEnd) => {
243
+ if (later.map(newStart, -1) <= to && later.map(newEnd, 1) >= from)
244
+ hit = true;
245
+ });
246
+ if (hit) return true;
247
+ }
248
+ return false;
249
+ }
250
+
251
+ function nextState(
252
+ tr: Transaction,
253
+ prev: PluginState,
254
+ state: EditorState,
255
+ suggesters: readonly Suggester[],
256
+ ): PluginState {
257
+ const meta = tr.getMeta(key) as Meta | undefined;
258
+ let closed = prev.closed;
259
+ let active = prev.active;
260
+ if (tr.docChanged) {
261
+ // a closed trigger goes with its character; the open one follows its own
262
+ closed = closed.flatMap((pos) => {
263
+ const r = tr.mapping.mapResult(pos);
264
+ return r.deletedAfter ? [] : [r.pos];
265
+ });
266
+ if (active) {
267
+ const r = tr.mapping.mapResult(active.from);
268
+ active = r.deletedAfter ? null : { ...active, from: r.pos };
269
+ }
270
+ }
271
+ if (meta?.type === 'close') {
272
+ // where the trigger was — or, when a choice replaced it, where what
273
+ // replaced it starts, so a mention that begins with its own trigger does
274
+ // not reopen on itself
275
+ if (prev.active) {
276
+ closed = [...closed, tr.mapping.map(prev.active.from, -1)].slice(
277
+ -MAX_CLOSED,
278
+ );
279
+ }
280
+ return { active: null, closed };
281
+ }
282
+ const match = findMatch(state, suggesters);
283
+ if (!match || closed.includes(match.from)) {
284
+ return !prev.active && closed === prev.closed
285
+ ? prev
286
+ : { active: null, closed };
287
+ }
288
+ const same =
289
+ !!active && active.from === match.from && active.index === match.index;
290
+ // a new list opens on an edit in its word, and not on an edit that is not
291
+ // the user's — a reset, a collaborator's change
292
+ if (
293
+ !same &&
294
+ (tr.getMeta('addToHistory') === false ||
295
+ !editedBetween(tr, match.from, match.to))
296
+ ) {
297
+ return { active: null, closed };
298
+ }
299
+ let items = same ? active!.items : null;
300
+ let selected = same && active!.query === match.query ? active!.selected : 0;
301
+ if (
302
+ meta?.type === 'items' &&
303
+ meta.index === match.index &&
304
+ meta.from === match.from &&
305
+ meta.query === match.query
306
+ ) {
307
+ items = meta.items;
308
+ selected = 0;
309
+ } else if (meta?.type === 'select') {
310
+ selected = meta.index;
311
+ }
312
+ if (items) selected = Math.max(0, Math.min(selected, items.length - 1));
313
+ return { active: { ...match, items, selected }, closed };
314
+ }
315
+
316
+ // --- the rows ------------------------------------------------------------------
317
+
318
+ /**
319
+ * A list, by what has been typed: labels that start with it (as typed, then
320
+ * in any case), then labels with a later word that does, then labels that
321
+ * merely contain it — in the list's own order within each. What the editor
322
+ * does with a suggester whose `items` is an array; exported for an app that
323
+ * filters its own.
324
+ */
325
+ export function filterSuggestions<Item extends SuggestionItem>(
326
+ items: readonly Item[],
327
+ query: string,
328
+ ): Item[] {
329
+ if (!query) return items.slice(0, MAX_ITEMS);
330
+ const folded = query.toLowerCase();
331
+ const ranked: Array<{ item: Item; rank: number; order: number }> = [];
332
+ items.forEach((item, order) => {
333
+ const label = item.label;
334
+ const lower = label.toLowerCase();
335
+ if (!lower.includes(folded)) return;
336
+ const rank = label.startsWith(query)
337
+ ? 0
338
+ : lower.startsWith(folded)
339
+ ? 1
340
+ : lower.split(/[\s\-_./@#]+/u).some((word) => word.startsWith(folded))
341
+ ? 2
342
+ : 3;
343
+ ranked.push({ item, rank, order });
344
+ });
345
+ ranked.sort((a, b) => a.rank - b.rank || a.order - b.order);
346
+ return ranked.slice(0, MAX_ITEMS).map((r) => r.item);
347
+ }
348
+
349
+ /** The plugin view: asks for the rows whenever the query changes, and hands
350
+ * back the answer to the query that is still open — no other. */
351
+ function asker(
352
+ view: EditorView,
353
+ get: () => readonly Suggester[],
354
+ ): { update(): void; destroy(): void } {
355
+ let asked = '';
356
+ let serial = 0;
357
+ const ask = (): void => {
358
+ const active = key.getState(view.state)?.active ?? null;
359
+ if (!active) {
360
+ asked = '';
361
+ return;
362
+ }
363
+ const stamp = `${active.index}${active.from}${active.query}`;
364
+ if (stamp === asked) return;
365
+ asked = stamp;
366
+ const id = ++serial;
367
+ const suggester = get()[active.index];
368
+ if (!suggester) return;
369
+ const { index, from, query, char } = active;
370
+ const state = view.state;
371
+ // always a turn later, answered or not: rows land in a transaction of
372
+ // their own, never inside the one being applied
373
+ Promise.resolve()
374
+ .then(() =>
375
+ // a query typed past before its turn came is never asked at all
376
+ id !== serial
377
+ ? null
378
+ : typeof suggester.items === 'function'
379
+ ? suggester.items({ query, char, state })
380
+ : filterSuggestions(suggester.items, query),
381
+ )
382
+ .then((items) => {
383
+ if (!items || id !== serial || view.isDestroyed) return;
384
+ const now = key.getState(view.state)?.active;
385
+ if (
386
+ !now ||
387
+ now.index !== index ||
388
+ now.from !== from ||
389
+ now.query !== query
390
+ )
391
+ return;
392
+ const meta: Meta = {
393
+ type: 'items',
394
+ index,
395
+ from,
396
+ query,
397
+ items: items.slice(0, MAX_ITEMS),
398
+ };
399
+ view.dispatch(view.state.tr.setMeta(key, meta));
400
+ })
401
+ // a source that throws has nothing to suggest
402
+ .catch(() => {});
403
+ };
404
+ ask();
405
+ return {
406
+ update: ask,
407
+ destroy: () => {
408
+ serial++;
409
+ },
410
+ };
411
+ }
412
+
413
+ // --- the commands ----------------------------------------------------------------
414
+
415
+ /** The open list, or null — what `<RichTextEditor>` draws its popup from,
416
+ * and what an app drawing its own reads. */
417
+ export function suggestionState(state: EditorState): SuggestionState | null {
418
+ return key.getState(state)?.active ?? null;
419
+ }
420
+
421
+ /** The suggester whose list is open, or null — for its `renderItem`. */
422
+ export function suggesterFor(state: EditorState): Suggester | null {
423
+ const active = suggestionState(state);
424
+ if (!active) return null;
425
+ const get = key.get(state)?.spec.suggesters as
426
+ (() => readonly Suggester[]) | undefined;
427
+ return get?.()[active.index] ?? null;
428
+ }
429
+
430
+ /** Close the list. It does not open again for this trigger while the
431
+ * trigger is there; typing a new one opens a new list. */
432
+ export const dismissSuggestion: Command = (state, dispatch) => {
433
+ if (!suggestionState(state)) return false;
434
+ dispatch?.(state.tr.setMeta(key, { type: 'close' } satisfies Meta));
435
+ return true;
436
+ };
437
+
438
+ /** Highlight row `index`, held to the rows there are. */
439
+ export function selectSuggestion(index: number): Command {
440
+ return (state, dispatch) => {
441
+ const active = suggestionState(state);
442
+ if (!active?.items?.length) return false;
443
+ const to = Math.max(0, Math.min(index, active.items.length - 1));
444
+ if (to !== active.selected)
445
+ dispatch?.(state.tr.setMeta(key, { type: 'select', index: to }));
446
+ return true;
447
+ };
448
+ }
449
+
450
+ /** A command's transaction, made on the state after `tr`, as part of `tr`. */
451
+ function join(tr: Transaction, inner: Transaction): void {
452
+ for (const step of inner.steps) tr.step(step);
453
+ if (inner.selectionSet)
454
+ tr.setSelection(inner.selection.map(tr.doc, StepMap.empty));
455
+ if (inner.storedMarksSet) tr.setStoredMarks(inner.storedMarks);
456
+ if (inner.scrolledIntoView) tr.scrollIntoView();
457
+ }
458
+
459
+ /**
460
+ * Take a row — the highlighted one, unless `index` says which. Its `insert`
461
+ * (or the trigger and its label) replaces the trigger and the query, with a
462
+ * space after it; or its `command` runs where they were.
463
+ */
464
+ export function acceptSuggestion(index?: number): Command {
465
+ return (state, dispatch, view) => {
466
+ const active = suggestionState(state);
467
+ const item = active?.items?.[index ?? active.selected];
468
+ if (!active || !item) return false;
469
+ if (!dispatch) return true;
470
+ const { from, to } = active;
471
+ // its own undo step: undoing a choice gives back what was typed, however
472
+ // soon after the typing it came
473
+ const tr = closeHistory(
474
+ state.tr.setMeta(key, { type: 'close' } satisfies Meta),
475
+ );
476
+ if (item.command) {
477
+ tr.delete(from, to);
478
+ const run = item.command;
479
+ const { state: after, transactions } = state.applyTransaction(tr);
480
+ if (transactions.length > 1) {
481
+ // another plugin appended to the deletion, so the command's state is
482
+ // not `tr`'s document and its steps cannot join: two steps, then.
483
+ // `after` is what applying `tr` makes — the view's next state, or
484
+ // the one an app that owns the state hands back — so the command's
485
+ // transaction applies after `tr` on either.
486
+ dispatch(tr);
487
+ run(after, dispatch, view);
488
+ return true;
489
+ }
490
+ let joined = false;
491
+ run(
492
+ after,
493
+ (inner) => {
494
+ if (joined || inner.before !== tr.doc) return;
495
+ joined = true;
496
+ join(tr, inner);
497
+ },
498
+ view,
499
+ );
500
+ dispatch(tr.scrollIntoView());
501
+ return true;
502
+ }
503
+ const insert = item.insert ?? active.char + item.label;
504
+ if (typeof insert === 'string') tr.insertText(insert, from, to);
505
+ else tr.replaceRangeWith(from, to, insert);
506
+ const end = tr.mapping.map(to);
507
+ const $end = tr.doc.resolve(end);
508
+ if ($end.parent.inlineContent) {
509
+ // the way a word ends: with a space, or the one already after it
510
+ const next = $end.nodeAfter;
511
+ if (!(next?.isText && /^\s/u.test(next.text ?? '')))
512
+ tr.insertText(' ', end);
513
+ tr.setSelection(TextSelection.create(tr.doc, end + 1));
514
+ } else {
515
+ tr.setSelection(Selection.near($end));
516
+ }
517
+ dispatch(tr.scrollIntoView());
518
+ return true;
519
+ };
520
+ }
521
+
522
+ // --- the plugin --------------------------------------------------------------------
523
+
524
+ /** Plain keys only, and only while rows are showing: a chord, or Shift with
525
+ * an arrow, is still the editor's. */
526
+ function keyDown(view: EditorView, event: KeyboardEvent): boolean {
527
+ const active = suggestionState(view.state);
528
+ if (!active?.items?.length || !view.editable) return false;
529
+ const e = event as unknown as {
530
+ key: string;
531
+ shiftKey: boolean;
532
+ ctrlKey: boolean;
533
+ altKey: boolean;
534
+ metaKey: boolean;
535
+ };
536
+ if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return false;
537
+ const move = (by: number): boolean =>
538
+ selectSuggestion(active.selected + by)(view.state, view.dispatch);
539
+ switch (e.key) {
540
+ case 'ArrowDown':
541
+ return move(1);
542
+ case 'ArrowUp':
543
+ return move(-1);
544
+ case 'PageDown':
545
+ return move(SUGGESTION_PAGE);
546
+ case 'PageUp':
547
+ return move(-SUGGESTION_PAGE);
548
+ case 'Enter':
549
+ case 'Tab':
550
+ return acceptSuggestion()(view.state, view.dispatch, view);
551
+ case 'Escape':
552
+ return dismissSuggestion(view.state, view.dispatch);
553
+ }
554
+ return false;
555
+ }
556
+
557
+ /**
558
+ * The suggestion plugin — one per editor, holding every trigger. It goes
559
+ * ahead of the default plugins, so its keys come before the keymap's:
560
+ * `<RichTextEditor suggestions>` puts it there itself, and an app that owns
561
+ * the state writes `plugins: [suggestions([…]), ...defaultPlugins(schema)]`.
562
+ *
563
+ * `suggesters` may be a function that returns them, read whenever they are
564
+ * needed — how the component hands over a prop that may be a new array on
565
+ * every render without the plugin, and the list that is open, being rebuilt.
566
+ * While a trigger's word is being typed its text carries the decoration
567
+ * class `suggestion`, which the editor draws in the accent colour.
568
+ */
569
+ export function suggestions(
570
+ suggesters: readonly Suggester[] | (() => readonly Suggester[]),
571
+ ): Plugin {
572
+ const get = typeof suggesters === 'function' ? suggesters : () => suggesters;
573
+ return new Plugin<PluginState>({
574
+ key,
575
+ // read back by `suggesterFor`, so a row is drawn by its suggester's
576
+ // `renderItem` in an app-owned state too
577
+ suggesters: get,
578
+ state: {
579
+ init: () => ({ active: null, closed: [] }),
580
+ apply: (tr, prev, _old, state) => nextState(tr, prev, state, get()),
581
+ },
582
+ props: {
583
+ handleKeyDown: keyDown,
584
+ decorations(state) {
585
+ const active = suggestionState(state);
586
+ return active
587
+ ? DecorationSet.create(state.doc, [
588
+ Decoration.inline(active.from, active.to, {
589
+ class: 'suggestion',
590
+ }),
591
+ ])
592
+ : null;
593
+ },
594
+ },
595
+ view: (view) => asker(view, get),
596
+ });
597
+ }