@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,1968 @@
1
+ // The editor view: ProseMirror's `EditorView` contract, drawn with react-x11.
2
+ //
3
+ // ProseMirror splits an editor in two. The *state* — the document, the
4
+ // selection, the plugins' state fields and every transformation of them — is
5
+ // plain data and runs anywhere. The *view* turns it into DOM, reads the DOM's
6
+ // geometry and events back, and is the only half that assumes a browser. So
7
+ // this file is a second view: the same contract, the same props, the same
8
+ // plugin hooks, over react-x11 elements instead of a DOM. A plugin written
9
+ // for ProseMirror — prosemirror-history, -inputrules, -keymap, -commands,
10
+ // y-prosemirror's sync — is written against the state and that contract, and
11
+ // runs here unchanged. What it cannot have is `view.dom` as a DOM node; the
12
+ // ledger of what is and is not answered is in docs/prd-rich-text-editor.md.
13
+ //
14
+ // How the work divides:
15
+ //
16
+ // - **dispatch → state → keys → render.** A transaction is applied, the
17
+ // block keys are carried through its mapping (`./keys.ts`), and React is
18
+ // asked to render only when the document changed. A selection change
19
+ // renders nothing: the caret and the selection bands belong to the text
20
+ // nodes and are set from here (`syncBlock`).
21
+ // - **Geometry is asked of the nodes.** `posAtCoords`, `coordsAtPos`, line
22
+ // motion and hit testing read the mounted text blocks' layouts through
23
+ // core's text accessors — device pixels, divided on the way out, because a
24
+ // pointer event and everything ProseMirror's API speaks are logical pixels
25
+ // (react-x11 docs/scale.md; AGENTS.md "know which unit you are in").
26
+ // - **What a browser does natively is done here**: caret motion, deleting a
27
+ // character or a word, inserting typed text, the clipboard. It runs only
28
+ // when no plugin's `handleKeyDown` claimed the key first — the same order a
29
+ // contenteditable gives ProseMirror.
30
+ import { splitBlock } from 'prosemirror-commands';
31
+ import { redo, redoDepth, undo, undoDepth } from 'prosemirror-history';
32
+ import { Slice } from 'prosemirror-model';
33
+ import { AllSelection, NodeSelection, Selection, TextSelection, } from 'prosemirror-state';
34
+ import { Mapping } from 'prosemirror-transform';
35
+ import { closeEditMenu, editMenuOpen, openEditMenu } from 'react-x11';
36
+ import { CARET_BLINK_MS } from 'react-x11/node';
37
+ import { startInterval, stopInterval } from '../code-language/timers.js';
38
+ import { afterLayout, cancelAfterLayout } from '../internal/timers.js';
39
+ import { scaleOf } from '../internal/units.js';
40
+ import { parseFromClipboard, serializeForClipboard } from './clipboard.js';
41
+ import { caretOf } from './collab.js';
42
+ import { domDrop, dropTransaction, filesSlice, SLICE_TYPE, textOf, } from './drag.js';
43
+ import { BlockKeys } from './keys.js';
44
+ import { primaryModifierOf, toDomKeyEvent } from './keymap.js';
45
+ import { styleFromCSS } from './look.js';
46
+ import { KeyedStore } from './store.js';
47
+ /** Motions made from where the caret is drawn — a line, a page, a
48
+ * line's ends — which wait for its block when a long document has it
49
+ * scrolled out of the window. */
50
+ const LAID_OUT = new Set([
51
+ 'up',
52
+ 'down',
53
+ 'pageUp',
54
+ 'pageDown',
55
+ 'lineStart',
56
+ 'lineEnd',
57
+ ]);
58
+ const XK_TAB = 0xff09;
59
+ const XK_ISO_LEFT_TAB = 0xfe20;
60
+ const XK_ESCAPE = 0xff1b;
61
+ const NO_CARETS = [];
62
+ // --- text helpers ----------------------------------------------------------
63
+ /** A textblock's text with every inline leaf as one character — so offsets
64
+ * into it are ProseMirror's offsets into the block. */
65
+ function blockText(node) {
66
+ return node.textBetween(0, node.content.size, undefined, '');
67
+ }
68
+ let graphemes = null;
69
+ let words = null;
70
+ function segmenter(kind) {
71
+ if (typeof Intl === 'undefined' || typeof Intl.Segmenter !== 'function')
72
+ return null;
73
+ if (kind === 'grapheme')
74
+ return (graphemes ??= new Intl.Segmenter(undefined, {
75
+ granularity: 'grapheme',
76
+ }));
77
+ return (words ??= new Intl.Segmenter(undefined, { granularity: 'word' }));
78
+ }
79
+ /** One user-perceived character from `offset`: a flag, a family emoji, an
80
+ * accent on its letter — never half of any of them. */
81
+ function graphemeStep(text, offset, dir) {
82
+ const seg = segmenter('grapheme');
83
+ if (seg) {
84
+ if (dir > 0) {
85
+ const s = seg.segment(text).containing(offset);
86
+ return s ? s.index + s.segment.length : text.length;
87
+ }
88
+ if (offset <= 0)
89
+ return 0;
90
+ const s = seg.segment(text).containing(offset - 1);
91
+ return s ? s.index : 0;
92
+ }
93
+ if (dir > 0)
94
+ return Math.min(text.length, offset + ((text.codePointAt(offset) ?? 0) > 0xffff ? 2 : 1));
95
+ const low = text.charCodeAt(offset - 1);
96
+ return Math.max(0, offset - (low >= 0xdc00 && low <= 0xdfff ? 2 : 1));
97
+ }
98
+ function wordSegments(text) {
99
+ const seg = segmenter('word');
100
+ if (seg) {
101
+ return [...seg.segment(text)].map((s) => ({
102
+ start: s.index,
103
+ end: s.index + s.segment.length,
104
+ word: !!s.isWordLike,
105
+ }));
106
+ }
107
+ const out = [];
108
+ for (const m of text.matchAll(/[\p{L}\p{N}_]+|\s+|[^\p{L}\p{N}_\s]/gu)) {
109
+ out.push({
110
+ start: m.index,
111
+ end: m.index + m[0].length,
112
+ word: /[\p{L}\p{N}_]/u.test(m[0]),
113
+ });
114
+ }
115
+ return out;
116
+ }
117
+ /** Ctrl+Arrow: over any run of spaces and punctuation, then the word. */
118
+ function wordStep(text, offset, dir) {
119
+ const segs = wordSegments(text);
120
+ if (dir < 0) {
121
+ let i = segs.length - 1;
122
+ while (i >= 0 && segs[i].start >= offset)
123
+ i--;
124
+ while (i >= 0 && !segs[i].word)
125
+ i--;
126
+ return i >= 0 ? segs[i].start : 0;
127
+ }
128
+ let i = 0;
129
+ while (i < segs.length && segs[i].end <= offset)
130
+ i++;
131
+ while (i < segs.length && !segs[i].word)
132
+ i++;
133
+ return i < segs.length ? segs[i].end : text.length;
134
+ }
135
+ /** The word a double click lands on, or the run of spaces or punctuation
136
+ * it lands on when that is what is there. */
137
+ function wordRange(text, offset) {
138
+ const segs = wordSegments(text);
139
+ const at = offset < text.length ? offset : offset - 1;
140
+ for (const s of segs)
141
+ if (at >= s.start && at < s.end)
142
+ return [s.start, s.end];
143
+ return [offset, offset];
144
+ }
145
+ function isEmptyDoc(doc) {
146
+ return (doc.childCount === 1 &&
147
+ !!doc.firstChild?.isTextblock &&
148
+ doc.firstChild.content.size === 0);
149
+ }
150
+ /** What an assistive technology reads a leaf as. */
151
+ function leafText(node) {
152
+ const own = node.type.spec.leafText?.(node);
153
+ if (own !== undefined)
154
+ return own;
155
+ const name = node.type.name;
156
+ if (name === 'hard_break' || name === 'hardBreak')
157
+ return '\n';
158
+ if (name === 'image')
159
+ return String(node.attrs.alt ?? '') || '';
160
+ return '';
161
+ }
162
+ function mappingOf(trs, doc) {
163
+ if (trs.length === 0 || trs[0].before !== doc)
164
+ return null;
165
+ const mapping = new Mapping();
166
+ for (const tr of trs)
167
+ mapping.appendMapping(tr.mapping);
168
+ return mapping;
169
+ }
170
+ function collectDecorations(source, out) {
171
+ const s = source;
172
+ if (typeof s.find === 'function')
173
+ out.push(...s.find());
174
+ else if (Array.isArray(s.members))
175
+ for (const m of s.members)
176
+ collectDecorations(m, out);
177
+ }
178
+ function sliceSingleNode(slice) {
179
+ return slice.openStart === 0 &&
180
+ slice.openEnd === 0 &&
181
+ slice.content.childCount === 1
182
+ ? slice.content.firstChild
183
+ : null;
184
+ }
185
+ // --- the view ----------------------------------------------------------------
186
+ export class RichEditorView {
187
+ state;
188
+ direct;
189
+ config;
190
+ /** The block keys React renders under — see `./keys.ts`. */
191
+ keys;
192
+ /** Per-block decorations, for the block's own renderer to subscribe to. */
193
+ decorations = new KeyedStore();
194
+ /** Which block a node selection has selected, for its renderer. */
195
+ selectedBlocks = new KeyedStore();
196
+ root = null;
197
+ texts = new Map();
198
+ /** Block atoms — a rule, an image block, an atom node view — by key: what
199
+ * a press can land on that is not text. */
200
+ boxes = new Map();
201
+ scroller = null;
202
+ /** A long document's window (./virtual.ts): which top-level blocks are
203
+ * drawn, and how one that is not gets drawn. Null while all of them
204
+ * are. */
205
+ blockWindow = null;
206
+ pluginViews = [];
207
+ /** Whether the component has asked for plugin views
208
+ * (`mountPluginViews`), and whether they are made now: they live while
209
+ * the root element does. */
210
+ pluginViewsWanted = false;
211
+ pluginViewsLive = false;
212
+ /** Collaborators' carets by block key, from widgets `remoteCaret`
213
+ * built (./collab.ts). */
214
+ carets = new Map();
215
+ focused = false;
216
+ preedit = null;
217
+ goalX = null;
218
+ keepGoal = false;
219
+ drag = null;
220
+ dragScroll = null;
221
+ pointer = null;
222
+ /** A press on the selection, until the pointer either comes up there —
223
+ * a click, which places the caret — or core starts a drag from it. */
224
+ dragCandidate = null;
225
+ /** What is being dragged out of this editor — prosemirror-view's
226
+ * `dragging` — the range it came from, and its clipboard form once
227
+ * something asked for it. */
228
+ draggingNow = null;
229
+ /** The block the drop caret is drawn in. */
230
+ dropKey = null;
231
+ tabEscapes = false;
232
+ /** Above zero while user input is being handled — a read-only editor
233
+ * drops the document changes that input would make, and only those. */
234
+ userInput = 0;
235
+ pending = [];
236
+ caretKey = null;
237
+ bandKeys = new Set();
238
+ selectedKey = null;
239
+ blinkTimer = null;
240
+ scrollTick = null;
241
+ lastCopy = null;
242
+ a11yCache = null;
243
+ destroyedFlag = false;
244
+ primary = 'ctrl';
245
+ constructor(state, props, config) {
246
+ this.state = state;
247
+ this.direct = props;
248
+ this.config = config;
249
+ this.keys = new BlockKeys(state.doc);
250
+ this.recomputeDecorations();
251
+ // Plugin views wait for `mountPluginViews`: a browser's EditorView has
252
+ // its DOM from its constructor, and plugin views are written to find
253
+ // `view.dom` there — y-prosemirror's cursor plugin listens on it at once.
254
+ // Here the root element does not exist until React has created it.
255
+ }
256
+ // --- the EditorView surface ----------------------------------------------
257
+ /** This object, typed as what a plugin's callbacks are handed. */
258
+ get asEditorView() {
259
+ return this;
260
+ }
261
+ get props() {
262
+ return { ...this.direct, state: this.state };
263
+ }
264
+ get editable() {
265
+ return !this.someProp('editable', (f) => f(this.state) === false);
266
+ }
267
+ get composing() {
268
+ return this.preedit !== null;
269
+ }
270
+ get isDestroyed() {
271
+ return this.destroyedFlag;
272
+ }
273
+ /** The root element — not a DOM node, which is the one thing a plugin
274
+ * written for a browser may not be given here. */
275
+ get dom() {
276
+ return this.root;
277
+ }
278
+ /** prosemirror-view's `dragging`: the slice a drag out of this editor
279
+ * carries, and whether a drop back in here moves it — while there is
280
+ * one. */
281
+ get dragging() {
282
+ const d = this.draggingNow;
283
+ return d && { slice: d.slice, move: d.move };
284
+ }
285
+ /** prosemirror-view's own view description, internal there, which a
286
+ * plugin reads as "is the view drawn?": y-prosemirror's cursor plugin
287
+ * publishes no collaborator's move without it. The root element,
288
+ * while there is one. */
289
+ get docView() {
290
+ return this.root;
291
+ }
292
+ /** The backend's shortcut modifier — Cmd on macOS, Ctrl on X11. */
293
+ get primaryModifier() {
294
+ return this.primary;
295
+ }
296
+ hasFocus() {
297
+ return this.focused;
298
+ }
299
+ focus() {
300
+ this.root?.focus();
301
+ }
302
+ setProps(props) {
303
+ this.update({ ...this.direct, ...props });
304
+ }
305
+ update(props) {
306
+ const pluginsChanged = props.plugins !== this.direct.plugins;
307
+ this.direct = props;
308
+ if (pluginsChanged)
309
+ this.resetPluginViews();
310
+ this.recomputeDecorations();
311
+ this.syncSelection();
312
+ }
313
+ /** The component's half: callbacks, the clipboard, the colours. Called on
314
+ * every render, so it only acts on what changed. */
315
+ configure(config) {
316
+ const prev = this.config;
317
+ this.config = config;
318
+ const c = config.colors;
319
+ const p = prev.colors;
320
+ const redraw = config.placeholder !== prev.placeholder ||
321
+ config.decorationClasses !== prev.decorationClasses ||
322
+ c.placeholder !== p.placeholder ||
323
+ c.preedit !== p.preedit;
324
+ if (redraw)
325
+ this.recomputeDecorations();
326
+ if (redraw ||
327
+ c.selection !== p.selection ||
328
+ c.selectionBlurred !== p.selectionBlurred ||
329
+ c.caret !== p.caret) {
330
+ this.syncSelection();
331
+ }
332
+ }
333
+ /**
334
+ * ProseMirror's prop lookup, in its order: the view's own props, then its
335
+ * direct plugins, then the state's plugins — the first to give a truthy
336
+ * answer wins.
337
+ */
338
+ someProp(name, f) {
339
+ const own = this.direct[name];
340
+ if (own != null) {
341
+ const r = f(own);
342
+ if (r)
343
+ return r;
344
+ }
345
+ for (const plugin of this.direct.plugins ?? []) {
346
+ const prop = plugin.props[name];
347
+ if (prop != null) {
348
+ const r = f(prop);
349
+ if (r)
350
+ return r;
351
+ }
352
+ }
353
+ for (const plugin of this.state.plugins) {
354
+ const prop = plugin.props[name];
355
+ if (prop != null) {
356
+ const r = f(prop);
357
+ if (r)
358
+ return r;
359
+ }
360
+ }
361
+ return undefined;
362
+ }
363
+ dispatch = (tr) => {
364
+ if (this.destroyedFlag)
365
+ return;
366
+ if (this.userInput > 0 && tr.docChanged && !this.editable)
367
+ return;
368
+ const own = this.direct.dispatchTransaction;
369
+ if (own) {
370
+ // the app applies it, and hands the state back through updateState
371
+ this.pending.push(tr);
372
+ own.call(this.asEditorView, tr);
373
+ return;
374
+ }
375
+ const { state, transactions } = this.state.applyTransaction(tr);
376
+ this.apply(state, transactions);
377
+ };
378
+ /** Replace the state — what an app owning it calls with the result of a
379
+ * transaction this view dispatched. */
380
+ updateState(state) {
381
+ const trs = this.pending;
382
+ this.pending = [];
383
+ this.apply(state, trs);
384
+ }
385
+ /** Run a command against the current state. */
386
+ run(command) {
387
+ return command(this.state, this.dispatch, this.asEditorView);
388
+ }
389
+ apply(state, trs) {
390
+ const prev = this.state;
391
+ if (state === prev)
392
+ return;
393
+ this.state = state;
394
+ const docChanged = prev.doc !== state.doc;
395
+ if (docChanged)
396
+ this.keys.update(state.doc, mappingOf(trs, prev.doc));
397
+ if (prev.plugins !== state.plugins)
398
+ this.resetPluginViews();
399
+ if (!this.keepGoal)
400
+ this.goalX = null;
401
+ this.keepGoal = false;
402
+ this.recomputeDecorations();
403
+ if (docChanged)
404
+ this.config.onRender();
405
+ this.syncSelection();
406
+ for (const view of this.pluginViews)
407
+ view.update?.(this.asEditorView, prev);
408
+ if (docChanged &&
409
+ this.root &&
410
+ editMenuOpen(this.root)) {
411
+ closeEditMenu(this.root);
412
+ }
413
+ this.a11yCache = null;
414
+ this.root?.notifyA11yTextChanged();
415
+ this.config.onUpdate?.(prev, trs);
416
+ if (trs.some((tr) => tr.scrolledIntoView))
417
+ this.scheduleScroll();
418
+ }
419
+ /**
420
+ * Make the plugins' views, once the root element exists: the component
421
+ * calls this after its first commit. A browser's EditorView makes them
422
+ * in its constructor, where it already has its DOM, and a plugin view
423
+ * is written to find `view.dom` there; here that is the root element.
424
+ */
425
+ mountPluginViews() {
426
+ this.pluginViewsWanted = true;
427
+ if (this.root)
428
+ this.startPluginViews();
429
+ }
430
+ startPluginViews() {
431
+ if (this.pluginViewsLive || this.destroyedFlag)
432
+ return;
433
+ this.pluginViewsLive = true;
434
+ this.createPluginViews();
435
+ }
436
+ stopPluginViews() {
437
+ for (const view of this.pluginViews)
438
+ view.destroy?.();
439
+ this.pluginViews = [];
440
+ this.pluginViewsLive = false;
441
+ }
442
+ createPluginViews() {
443
+ for (const plugin of [
444
+ ...(this.direct.plugins ?? []),
445
+ ...this.state.plugins,
446
+ ]) {
447
+ const make = plugin.spec.view;
448
+ if (make)
449
+ this.pluginViews.push(make(this.asEditorView));
450
+ }
451
+ }
452
+ resetPluginViews() {
453
+ const live = this.pluginViewsLive;
454
+ this.stopPluginViews();
455
+ if (live)
456
+ this.startPluginViews();
457
+ }
458
+ destroy() {
459
+ if (this.destroyedFlag)
460
+ return;
461
+ this.destroyedFlag = true;
462
+ for (const view of this.pluginViews)
463
+ view.destroy?.();
464
+ this.pluginViews = [];
465
+ this.stopBlink();
466
+ stopInterval(this.dragScroll);
467
+ this.dragScroll = null;
468
+ cancelAfterLayout(this.scrollTick);
469
+ }
470
+ // --- the elements ----------------------------------------------------------
471
+ attachRoot(node) {
472
+ this.root = node;
473
+ this.primary = primaryModifierOf(node.app);
474
+ if (this.pluginViewsWanted)
475
+ this.startPluginViews();
476
+ }
477
+ detachRoot(node) {
478
+ if (this.root !== node)
479
+ return;
480
+ // the plugin views were made for this element, so they go before it
481
+ // does: a plugin view's `destroy` reads `view.dom` too
482
+ this.stopPluginViews();
483
+ this.root = null;
484
+ this.stopBlink();
485
+ }
486
+ attachText(key, node) {
487
+ this.texts.set(key, node);
488
+ this.syncBlock(key);
489
+ }
490
+ detachText(key, node) {
491
+ if (this.texts.get(key) !== node)
492
+ return;
493
+ this.texts.delete(key);
494
+ this.bandKeys.delete(key);
495
+ if (this.caretKey === key) {
496
+ this.caretKey = null;
497
+ this.stopBlink();
498
+ }
499
+ }
500
+ textUpdated(key, node) {
501
+ if (this.texts.get(key) === node)
502
+ this.syncBlock(key);
503
+ }
504
+ /** A block atom's box, or null when it unmounts. */
505
+ attachBox(key, node) {
506
+ if (node)
507
+ this.boxes.set(key, node);
508
+ else
509
+ this.boxes.delete(key);
510
+ }
511
+ /** The scrolling viewport the document is in. */
512
+ attachScroller(node) {
513
+ this.scroller = node;
514
+ }
515
+ /** A long document's window, while it draws only some blocks. */
516
+ attachWindow(window) {
517
+ this.blockWindow = window;
518
+ }
519
+ // --- selection, painted ------------------------------------------------------
520
+ selectionColor() {
521
+ const menu = this.root && editMenuOpen(this.root);
522
+ return this.focused || menu
523
+ ? this.config.colors.selection
524
+ : this.config.colors.selectionBlurred;
525
+ }
526
+ /** The block the caret is drawn in, if there is one to draw. */
527
+ caretBlock() {
528
+ const sel = this.state.selection;
529
+ if (!this.focused || !sel.empty || !(sel instanceof TextSelection))
530
+ return null;
531
+ const $head = sel.$head;
532
+ if (!$head.parent.isTextblock || $head.depth === 0)
533
+ return null;
534
+ return this.keys.keyAt($head.before()) ?? null;
535
+ }
536
+ /**
537
+ * Place whatever of the selection falls in one block: its band, and the
538
+ * caret if the caret is there. A block whose element still shows an older
539
+ * node is skipped — the render bringing the new one calls back.
540
+ */
541
+ syncBlock(key) {
542
+ const text = this.texts.get(key);
543
+ const node = this.keys.nodeOf(key);
544
+ const pos = this.keys.posOf(key);
545
+ if (!text || !node || pos === undefined)
546
+ return;
547
+ const shown = text.props.node;
548
+ if (shown !== undefined && shown !== node)
549
+ return;
550
+ const map = text.map;
551
+ if (!map)
552
+ return;
553
+ const sel = this.state.selection;
554
+ const start = pos + 1;
555
+ const end = start + node.content.size;
556
+ let band = null;
557
+ const blockSelected = sel instanceof NodeSelection && !sel.node.isInline;
558
+ if (!sel.empty && !blockSelected && sel.from <= end && sel.to >= start) {
559
+ const a = map.toDisplay(Math.max(sel.from, start) - start);
560
+ const b = map.toDisplay(Math.min(sel.to, end) - start);
561
+ if (b > a)
562
+ band = { start: a, end: b };
563
+ else if (sel.from < start && sel.to > end)
564
+ band = { start: 0, end: map.length }; // an empty block the selection runs through
565
+ }
566
+ text.setBand(band, this.selectionColor());
567
+ if (band)
568
+ this.bandKeys.add(key);
569
+ else
570
+ this.bandKeys.delete(key);
571
+ if (this.caretBlock() === key) {
572
+ text.setCaret(map.toDisplay(sel.head - start), this.config.colors.caret);
573
+ this.caretKey = key;
574
+ this.restartBlink();
575
+ }
576
+ else {
577
+ text.setCaret(null, '');
578
+ if (this.caretKey === key) {
579
+ this.caretKey = null;
580
+ this.stopBlink();
581
+ }
582
+ }
583
+ const carets = this.carets.get(key);
584
+ text.setRemoteCarets(carets
585
+ ? carets.map((c) => ({ index: map.toDisplay(c.pos), color: c.color }))
586
+ : NO_CARETS);
587
+ }
588
+ /** Place the whole selection: every block it touches, every block it
589
+ * touched before, the caret, and a selected block's highlight. */
590
+ syncSelection() {
591
+ const keys = new Set(this.bandKeys);
592
+ if (this.caretKey)
593
+ keys.add(this.caretKey);
594
+ const sel = this.state.selection;
595
+ if (!sel.empty) {
596
+ this.state.doc.nodesBetween(sel.from, sel.to, (node, pos) => {
597
+ if (!node.isTextblock)
598
+ return true;
599
+ const key = this.keys.keyAt(pos);
600
+ if (key)
601
+ keys.add(key);
602
+ return false;
603
+ });
604
+ }
605
+ const caret = this.caretBlock();
606
+ if (caret)
607
+ keys.add(caret);
608
+ for (const key of keys)
609
+ this.syncBlock(key);
610
+ if (!caret && this.caretKey) {
611
+ this.texts.get(this.caretKey)?.setCaret(null, '');
612
+ this.caretKey = null;
613
+ this.stopBlink();
614
+ }
615
+ const selected = sel instanceof NodeSelection && !sel.node.isInline
616
+ ? (this.keys.keyAt(sel.from) ?? null)
617
+ : null;
618
+ if (selected !== this.selectedKey) {
619
+ if (this.selectedKey)
620
+ this.selectedBlocks.set(this.selectedKey, undefined);
621
+ if (selected)
622
+ this.selectedBlocks.set(selected, true);
623
+ this.selectedKey = selected;
624
+ }
625
+ }
626
+ restartBlink() {
627
+ stopInterval(this.blinkTimer);
628
+ this.blinkTimer = null;
629
+ if (!this.caretKey || !this.focused || this.destroyedFlag)
630
+ return;
631
+ let on = true;
632
+ this.blinkTimer = startInterval(() => {
633
+ on = !on;
634
+ const key = this.caretKey;
635
+ if (key)
636
+ this.texts.get(key)?.blink(on);
637
+ }, CARET_BLINK_MS);
638
+ }
639
+ stopBlink() {
640
+ stopInterval(this.blinkTimer);
641
+ this.blinkTimer = null;
642
+ }
643
+ // --- decorations -------------------------------------------------------------
644
+ decorationStyle(deco) {
645
+ const attrs = deco.type
646
+ ?.attrs ?? {};
647
+ const spec = deco.spec;
648
+ let style = {};
649
+ if (attrs.class) {
650
+ for (const cls of attrs.class.split(/\s+/)) {
651
+ const s = this.config.decorationClasses?.[cls];
652
+ if (s)
653
+ style = { ...style, ...s };
654
+ }
655
+ }
656
+ if (attrs.style)
657
+ style = { ...style, ...styleFromCSS(attrs.style, {}) };
658
+ if (spec?.run)
659
+ style = { ...style, ...spec.run };
660
+ return Object.keys(style).length ? style : null;
661
+ }
662
+ /**
663
+ * Every plugin's decorations, bucketed by the block they fall in, plus the
664
+ * view's own two widgets — the placeholder and a composition's preedit.
665
+ * Only a block whose bucket changed is told; returns whether any was.
666
+ *
667
+ * What a DOM view would draw that this one cannot: a widget whose only
668
+ * description is a `toDOM` function. A widget draws here when its spec
669
+ * carries `text` (and, optionally, a `run` style) — see the docs page.
670
+ */
671
+ recomputeDecorations() {
672
+ const { doc } = this.state;
673
+ const buckets = new Map();
674
+ const bucket = (key) => {
675
+ let b = buckets.get(key);
676
+ if (!b)
677
+ buckets.set(key, (b = { inline: [], widgets: [], box: null }));
678
+ return b;
679
+ };
680
+ const found = [];
681
+ const carets = new Map();
682
+ this.someProp('decorations', (f) => {
683
+ const source = f(this.state);
684
+ if (source)
685
+ collectDecorations(source, found);
686
+ return undefined;
687
+ });
688
+ for (const deco of found) {
689
+ const typed = deco;
690
+ if (typed.widget) {
691
+ const spec = deco.spec;
692
+ if (typeof spec?.text !== 'string' || spec.text === '') {
693
+ // no text to draw it with — unless it is a collaborator's caret
694
+ const caret = caretOf(deco, this.asEditorView);
695
+ const $at = caret ? doc.resolve(deco.from) : null;
696
+ const key = $at && $at.parent.isTextblock && $at.depth > 0
697
+ ? this.keys.keyAt($at.before())
698
+ : undefined;
699
+ if (caret && $at && key) {
700
+ let list = carets.get(key);
701
+ if (!list)
702
+ carets.set(key, (list = []));
703
+ list.push({ pos: $at.parentOffset, color: caret.color });
704
+ }
705
+ continue;
706
+ }
707
+ const $pos = doc.resolve(deco.from);
708
+ if (!$pos.parent.isTextblock || $pos.depth === 0)
709
+ continue;
710
+ const key = this.keys.keyAt($pos.before());
711
+ if (!key)
712
+ continue;
713
+ bucket(key).widgets.push({
714
+ pos: $pos.parentOffset,
715
+ side: typed.type?.side ?? spec.side ?? 0,
716
+ text: spec.text,
717
+ ...(spec.run ? { style: spec.run } : null),
718
+ });
719
+ }
720
+ else if (typed.inline) {
721
+ const style = this.decorationStyle(deco);
722
+ if (!style)
723
+ continue;
724
+ doc.nodesBetween(deco.from, deco.to, (node, pos) => {
725
+ if (!node.isTextblock)
726
+ return true;
727
+ const key = this.keys.keyAt(pos);
728
+ if (key) {
729
+ const start = pos + 1;
730
+ bucket(key).inline.push({
731
+ from: Math.max(0, deco.from - start),
732
+ to: Math.min(node.content.size, deco.to - start),
733
+ style,
734
+ });
735
+ }
736
+ return false;
737
+ });
738
+ }
739
+ else {
740
+ const key = this.keys.keyAt(deco.from);
741
+ if (!key)
742
+ continue;
743
+ const style = this.decorationStyle(deco);
744
+ if (style?.bg)
745
+ bucket(key).box = { background: style.bg };
746
+ }
747
+ }
748
+ const placeholder = this.config.placeholder;
749
+ if (placeholder && isEmptyDoc(doc)) {
750
+ const key = this.keys.keyAt(0);
751
+ if (key) {
752
+ bucket(key).widgets.push({
753
+ pos: 0,
754
+ side: 0,
755
+ text: placeholder,
756
+ style: { color: this.config.colors.placeholder },
757
+ });
758
+ }
759
+ }
760
+ if (this.preedit) {
761
+ const $head = this.state.selection.$head;
762
+ if ($head.parent.isTextblock && $head.depth > 0) {
763
+ const key = this.keys.keyAt($head.before());
764
+ if (key) {
765
+ bucket(key).widgets.push({
766
+ pos: $head.parentOffset,
767
+ side: -1,
768
+ text: this.preedit,
769
+ style: { underline: this.config.colors.preedit },
770
+ });
771
+ }
772
+ }
773
+ }
774
+ let changed = false;
775
+ for (const [key, b] of buckets) {
776
+ const sig = JSON.stringify([b.inline, b.widgets, b.box]);
777
+ if (this.decorations.get(key)?.sig === sig)
778
+ continue;
779
+ this.decorations.set(key, { ...b, sig });
780
+ changed = true;
781
+ }
782
+ for (const key of [...this.decorations.keys()]) {
783
+ if (!buckets.has(key)) {
784
+ this.decorations.set(key, undefined);
785
+ changed = true;
786
+ }
787
+ }
788
+ // collaborators' carets go to their blocks' elements the way the
789
+ // editor's own caret does: set on the node, nothing re-rendered
790
+ const moved = [];
791
+ for (const key of new Set([...this.carets.keys(), ...carets.keys()])) {
792
+ if (JSON.stringify(this.carets.get(key) ?? []) !==
793
+ JSON.stringify(carets.get(key) ?? []))
794
+ moved.push(key);
795
+ }
796
+ this.carets = carets;
797
+ for (const key of moved)
798
+ this.syncBlock(key);
799
+ return changed;
800
+ }
801
+ // --- geometry ----------------------------------------------------------------
802
+ /** The logical rect of a node — `abs`, already divided (AGENTS.md: ask for
803
+ * the rect, and the arithmetic stays in one unit). */
804
+ rectOf(node) {
805
+ const r = node.getClientRects()[0];
806
+ return r && (r.width > 0 || r.height > 0) ? r : null;
807
+ }
808
+ /** The top-level block `pos` is in, by index — what a long
809
+ * document's window draws or not. */
810
+ topIndexOf(pos) {
811
+ const { doc } = this.state;
812
+ const $pos = doc.resolve(Math.max(0, Math.min(pos, doc.content.size)));
813
+ return Math.max(0, Math.min($pos.index(0), doc.childCount - 1));
814
+ }
815
+ /** Whether the block that draws `pos` is laid out: always, but in a
816
+ * long document scrolled away from it (./virtual.ts). */
817
+ isDrawnAt(pos) {
818
+ return !this.blockWindow || this.blockWindow.drawn(this.topIndexOf(pos));
819
+ }
820
+ /** The nearest block to a point: the one it is in, or the one closest
821
+ * vertically, then horizontally. Text blocks win ties with atoms. */
822
+ hit(x, y) {
823
+ let best = null;
824
+ let bestScore = Infinity;
825
+ const consider = (key, node, text) => {
826
+ const r = this.rectOf(node);
827
+ if (!r)
828
+ return;
829
+ const dy = y < r.y
830
+ ? r.y - y
831
+ : y >= r.y + r.height
832
+ ? y - (r.y + r.height) + 0.001
833
+ : 0;
834
+ const dx = x < r.x ? r.x - x : x >= r.x + r.width ? x - (r.x + r.width) : 0;
835
+ const score = dy * 1e6 + dx;
836
+ if (score < bestScore || (score === bestScore && text && !best?.text)) {
837
+ bestScore = score;
838
+ best = { key, node, text, r };
839
+ }
840
+ };
841
+ for (const [key, node] of this.texts)
842
+ consider(key, node, true);
843
+ for (const [key, node] of this.boxes)
844
+ consider(key, node, false);
845
+ return best ? this.hitIn(best, x, y) : null;
846
+ }
847
+ hitIn(best, x, y) {
848
+ const pos = this.keys.posOf(best.key);
849
+ if (pos === undefined)
850
+ return null;
851
+ if (!best.text)
852
+ return { pos, inside: pos, atom: true };
853
+ const text = best.node;
854
+ const map = text.map;
855
+ if (!map)
856
+ return null;
857
+ const s = scaleOf(text);
858
+ const { r } = best;
859
+ const cx = Math.min(Math.max(x, r.x), r.x + r.width - 0.5);
860
+ const cy = Math.min(Math.max(y, r.y), r.y + r.height - 0.5);
861
+ const cp = text.textIndexAt(cx * s, cy * s);
862
+ return { pos: pos + 1 + map.toDoc(cp), inside: pos, atom: false };
863
+ }
864
+ /** ProseMirror's `posAtCoords`, in window logical pixels — the space a
865
+ * pointer event's `x`/`y` are in. */
866
+ posAtCoords(coords) {
867
+ const hit = this.hit(coords.left, coords.top);
868
+ return hit && { pos: hit.pos, inside: hit.inside };
869
+ }
870
+ /** ProseMirror's `coordsAtPos`: where a caret at `pos` is drawn, in window
871
+ * logical pixels. */
872
+ coordsAtPos(pos, _side = 1) {
873
+ const { doc } = this.state;
874
+ let $pos = doc.resolve(Math.max(0, Math.min(pos, doc.content.size)));
875
+ if (!$pos.parent.isTextblock) {
876
+ const near = Selection.near($pos);
877
+ if (near instanceof NodeSelection && !near.node.isInline) {
878
+ const key = this.keys.keyAt(near.from);
879
+ const box = key ? this.boxes.get(key) : undefined;
880
+ const r = box && this.rectOf(box);
881
+ if (r)
882
+ return {
883
+ left: r.x,
884
+ right: r.x + r.width,
885
+ top: r.y,
886
+ bottom: r.y + r.height,
887
+ };
888
+ }
889
+ $pos = near.$head;
890
+ }
891
+ if ($pos.parent.isTextblock && $pos.depth > 0) {
892
+ const key = this.keys.keyAt($pos.before());
893
+ const text = key ? this.texts.get(key) : undefined;
894
+ if (text?.map) {
895
+ const r = text.textCaretRect(text.map.toDisplay($pos.parentOffset));
896
+ if (r) {
897
+ const s = scaleOf(text);
898
+ return {
899
+ left: r.x / s,
900
+ right: r.x / s,
901
+ top: r.y / s,
902
+ bottom: (r.y + r.height) / s,
903
+ };
904
+ }
905
+ }
906
+ }
907
+ const r = this.root?.getClientRects()[0];
908
+ return r
909
+ ? { left: r.x, right: r.x, top: r.y, bottom: r.y }
910
+ : { left: 0, right: 0, top: 0, bottom: 0 };
911
+ }
912
+ /**
913
+ * The textblock element that draws `pos`, and where a caret there is in
914
+ * that element's own logical coordinates — what a `<popup anchor>` hangs
915
+ * off, so it moves with the text when the document scrolls and unmaps
916
+ * when the text scrolls out of view. Null when no mounted block draws it.
917
+ */
918
+ anchorAt(pos) {
919
+ const { doc } = this.state;
920
+ const $pos = doc.resolve(Math.max(0, Math.min(pos, doc.content.size)));
921
+ if (!$pos.parent.isTextblock || $pos.depth === 0)
922
+ return null;
923
+ const key = this.keys.keyAt($pos.before());
924
+ const text = key ? this.texts.get(key) : undefined;
925
+ const box = text?.map ? this.rectOf(text) : null;
926
+ if (!text || !box)
927
+ return null;
928
+ const caret = this.coordsAtPos(pos);
929
+ return {
930
+ node: text,
931
+ at: {
932
+ x: caret.left - box.x,
933
+ y: caret.top - box.y,
934
+ width: 1,
935
+ height: Math.max(1, caret.bottom - caret.top),
936
+ },
937
+ };
938
+ }
939
+ /** ProseMirror's `endOfTextblock`: would moving `dir` from the selection
940
+ * leave its textblock? Up and down ask the layout which line it is on. */
941
+ endOfTextblock(dir, state = this.state) {
942
+ const $head = state.selection.$head;
943
+ if (!$head.parent.isTextblock)
944
+ return true;
945
+ const atStart = $head.parentOffset === 0;
946
+ const atEnd = $head.parentOffset === $head.parent.content.size;
947
+ if (dir === 'backward' || dir === 'left')
948
+ return atStart;
949
+ if (dir === 'forward' || dir === 'right')
950
+ return atEnd;
951
+ const key = state === this.state && $head.depth > 0
952
+ ? this.keys.keyAt($head.before())
953
+ : undefined;
954
+ const text = key ? this.texts.get(key) : undefined;
955
+ const line = text?.map
956
+ ? text.lineRange(text.map.toDisplay($head.parentOffset))
957
+ : null;
958
+ if (!line)
959
+ return dir === 'up' ? atStart : atEnd;
960
+ return dir === 'up' ? line.start === 0 : line.last;
961
+ }
962
+ /** The nearest line that way: blocks whose band holds `y` first, then the
963
+ * first block past the caret's line. For line motion and paging. */
964
+ near(x, y, dir, from) {
965
+ let inBand = null;
966
+ let inBandDx = Infinity;
967
+ let beyond = null;
968
+ let beyondScore = Infinity;
969
+ const consider = (key, node, text) => {
970
+ const r = this.rectOf(node);
971
+ if (!r)
972
+ return;
973
+ const dx = x < r.x ? r.x - x : x >= r.x + r.width ? x - (r.x + r.width) : 0;
974
+ if (y >= r.y && y < r.y + r.height) {
975
+ if (dx < inBandDx) {
976
+ inBandDx = dx;
977
+ inBand = { key, node, text, r };
978
+ }
979
+ return;
980
+ }
981
+ const gap = dir < 0 ? from.top - (r.y + r.height) : r.y - from.bottom;
982
+ if (gap < -0.5)
983
+ return;
984
+ const reach = dir < 0 ? y - (r.y + r.height) : r.y - y;
985
+ const score = Math.abs(reach) * 1e6 + dx;
986
+ if (score < beyondScore) {
987
+ beyondScore = score;
988
+ beyond = { key, node, text, r };
989
+ }
990
+ };
991
+ for (const [key, node] of this.texts)
992
+ consider(key, node, true);
993
+ for (const [key, node] of this.boxes)
994
+ consider(key, node, false);
995
+ const pick = (inBand ?? beyond);
996
+ if (!pick)
997
+ return null;
998
+ const ty = inBand
999
+ ? y
1000
+ : dir < 0
1001
+ ? pick.r.y + pick.r.height - 1
1002
+ : pick.r.y + 1;
1003
+ return this.hitIn(pick, x, ty);
1004
+ }
1005
+ viewportHeight() {
1006
+ return this.scroller?.getClientRects()[0]?.height ?? 400;
1007
+ }
1008
+ // --- motion ----------------------------------------------------------------------
1009
+ /**
1010
+ * Move the caret — or, with `extend`, the head of the selection — by a
1011
+ * unit. Always answers true: a motion key that could not move (Up on the
1012
+ * first line of the document) is still the editor's key.
1013
+ */
1014
+ move(unit, extend) {
1015
+ const { doc, selection: sel } = this.state;
1016
+ // the caret's block scrolled out of a long document's window: it is
1017
+ // drawn again first, and the motion made from it once it is laid out
1018
+ if (LAID_OUT.has(unit) && !this.isDrawnAt(sel.head)) {
1019
+ const head = sel.head;
1020
+ this.blockWindow?.reveal(this.topIndexOf(head), () => {
1021
+ if (this.state.selection.head === head)
1022
+ this.move(unit, extend);
1023
+ });
1024
+ return true;
1025
+ }
1026
+ let target = null;
1027
+ let head = null;
1028
+ let atom = false;
1029
+ switch (unit) {
1030
+ case 'left':
1031
+ case 'right':
1032
+ target = this.horizontal(unit === 'left' ? -1 : 1, extend);
1033
+ break;
1034
+ case 'wordBackward':
1035
+ case 'wordForward':
1036
+ head = this.wordTarget(unit === 'wordBackward' ? -1 : 1);
1037
+ break;
1038
+ case 'up':
1039
+ case 'down':
1040
+ case 'pageUp':
1041
+ case 'pageDown': {
1042
+ const dir = unit === 'up' || unit === 'pageUp' ? -1 : 1;
1043
+ const page = unit === 'pageUp' || unit === 'pageDown'
1044
+ ? this.viewportHeight() * 0.9
1045
+ : 0;
1046
+ const coords = this.coordsAtPos(sel.head);
1047
+ if (this.goalX === null)
1048
+ this.goalX = coords.left;
1049
+ const lineHeight = Math.max(1, coords.bottom - coords.top);
1050
+ const y = page
1051
+ ? dir < 0
1052
+ ? coords.top - page
1053
+ : coords.bottom + page
1054
+ : dir < 0
1055
+ ? coords.top - lineHeight / 2
1056
+ : coords.bottom + lineHeight / 2;
1057
+ const hit = this.near(this.goalX, y, dir, coords);
1058
+ if (hit) {
1059
+ head = hit.pos;
1060
+ atom = hit.atom;
1061
+ }
1062
+ else {
1063
+ // no line that way: the start (or the end) of the document
1064
+ head =
1065
+ dir < 0 ? Selection.atStart(doc).head : Selection.atEnd(doc).head;
1066
+ }
1067
+ this.keepGoal = true;
1068
+ break;
1069
+ }
1070
+ case 'lineStart':
1071
+ case 'lineEnd':
1072
+ head = this.lineEdge(sel.head, unit === 'lineStart' ? -1 : 1);
1073
+ break;
1074
+ case 'docStart':
1075
+ head = Selection.atStart(doc).head;
1076
+ break;
1077
+ case 'docEnd':
1078
+ head = Selection.atEnd(doc).head;
1079
+ break;
1080
+ }
1081
+ if (head !== null) {
1082
+ if (atom &&
1083
+ !extend &&
1084
+ NodeSelection.isSelectable(doc.nodeAt(head) ?? doc)) {
1085
+ target = NodeSelection.create(doc, head);
1086
+ }
1087
+ else if (extend) {
1088
+ target = TextSelection.between(sel.$anchor, doc.resolve(head));
1089
+ }
1090
+ else {
1091
+ target = Selection.near(doc.resolve(head));
1092
+ }
1093
+ }
1094
+ if (target && !target.eq(sel))
1095
+ this.dispatch(this.state.tr.setSelection(target).scrollIntoView());
1096
+ else if (target)
1097
+ this.scheduleScroll();
1098
+ return true;
1099
+ }
1100
+ horizontal(dir, extend) {
1101
+ const { doc, selection: sel } = this.state;
1102
+ if (!extend && !sel.empty) {
1103
+ if (sel instanceof NodeSelection) {
1104
+ return (Selection.findFrom(doc.resolve(dir < 0 ? sel.from : sel.to), dir) ??
1105
+ sel);
1106
+ }
1107
+ return TextSelection.create(doc, dir < 0 ? sel.from : sel.to);
1108
+ }
1109
+ const $head = sel.$head;
1110
+ if ($head.parent.isTextblock) {
1111
+ const offset = $head.parentOffset;
1112
+ if (dir < 0 ? offset > 0 : offset < $head.parent.content.size) {
1113
+ const pos = $head.start() + graphemeStep(blockText($head.parent), offset, dir);
1114
+ return extend
1115
+ ? TextSelection.create(doc, sel.anchor, pos)
1116
+ : TextSelection.create(doc, pos);
1117
+ }
1118
+ if ($head.depth === 0)
1119
+ return null;
1120
+ // out of the block: to the next place a caret can be — or, when not
1121
+ // extending, onto a block atom beside it (a rule), selected whole
1122
+ const next = Selection.findFrom(doc.resolve(dir < 0 ? $head.before() : $head.after()), dir, extend);
1123
+ if (!next)
1124
+ return null;
1125
+ return extend ? TextSelection.create(doc, sel.anchor, next.head) : next;
1126
+ }
1127
+ const next = Selection.findFrom($head, dir, extend);
1128
+ if (!next)
1129
+ return null;
1130
+ return extend ? TextSelection.create(doc, sel.anchor, next.head) : next;
1131
+ }
1132
+ wordTarget(dir) {
1133
+ const { doc, selection: sel } = this.state;
1134
+ const $head = sel.$head;
1135
+ if (!$head.parent.isTextblock || $head.depth === 0)
1136
+ return sel.head;
1137
+ const offset = $head.parentOffset;
1138
+ if (dir < 0 ? offset > 0 : offset < $head.parent.content.size) {
1139
+ return $head.start() + wordStep(blockText($head.parent), offset, dir);
1140
+ }
1141
+ const next = Selection.findFrom(doc.resolve(dir < 0 ? $head.before() : $head.after()), dir, true);
1142
+ return next ? next.head : sel.head;
1143
+ }
1144
+ /** Where Home or End goes from `pos`: the visual line's edge, read off the
1145
+ * layout — so a wrapped paragraph's End stops where the line does. */
1146
+ lineEdge(pos, dir) {
1147
+ const $pos = this.state.doc.resolve(pos);
1148
+ if (!$pos.parent.isTextblock || $pos.depth === 0)
1149
+ return pos;
1150
+ const key = this.keys.keyAt($pos.before());
1151
+ const text = key ? this.texts.get(key) : undefined;
1152
+ const map = text?.map;
1153
+ const line = text && map ? text.lineRange(map.toDisplay($pos.parentOffset)) : null;
1154
+ if (!map || !line)
1155
+ return dir < 0 ? $pos.start() : $pos.end();
1156
+ return $pos.start() + map.toDoc(dir < 0 ? line.start : line.end);
1157
+ }
1158
+ deleteBy(dir, unit) {
1159
+ const { state } = this;
1160
+ const sel = state.selection;
1161
+ if (!sel.empty) {
1162
+ this.dispatch(state.tr.deleteSelection().scrollIntoView());
1163
+ return true;
1164
+ }
1165
+ const $head = sel.$head;
1166
+ if (!$head.parent.isTextblock)
1167
+ return true;
1168
+ const offset = $head.parentOffset;
1169
+ // at the block's edge a join is the keymap's (joinBackward), and it
1170
+ // declined — nothing is left to delete here
1171
+ if (dir < 0 ? offset === 0 : offset === $head.parent.content.size)
1172
+ return true;
1173
+ let target;
1174
+ if (unit === 'line')
1175
+ target = this.lineEdge($head.pos, dir);
1176
+ else {
1177
+ const text = blockText($head.parent);
1178
+ target =
1179
+ $head.start() +
1180
+ (unit === 'word'
1181
+ ? wordStep(text, offset, dir)
1182
+ : graphemeStep(text, offset, dir));
1183
+ }
1184
+ const from = Math.min(target, $head.pos);
1185
+ const to = Math.max(target, $head.pos);
1186
+ if (from !== to)
1187
+ this.dispatch(state.tr.delete(from, to).scrollIntoView());
1188
+ return true;
1189
+ }
1190
+ /** Typed text, as ProseMirror's DOM view inserts it: `handleTextInput`
1191
+ * first (input rules live there), then the insertion. */
1192
+ insertText(text) {
1193
+ const { from, to } = this.state.selection;
1194
+ const insert = () => this.state.tr.insertText(text, from, to);
1195
+ if (this.someProp('handleTextInput', (f) => f(this.asEditorView, from, to, text, insert)))
1196
+ return;
1197
+ this.dispatch(insert().scrollIntoView());
1198
+ }
1199
+ // --- keys ----------------------------------------------------------------------
1200
+ keyDown(ev) {
1201
+ if (ev.composing || this.destroyedFlag)
1202
+ return;
1203
+ const k = ev.keysym;
1204
+ // Escape arms one Tab that leaves: an editor that keeps Tab for lists,
1205
+ // tables and code owes the keyboard user a way out (react-x11
1206
+ // docs/extending.md, "Behaviour of your own")
1207
+ if ((k === XK_TAB || k === XK_ISO_LEFT_TAB) && this.tabEscapes) {
1208
+ this.tabEscapes = false;
1209
+ return;
1210
+ }
1211
+ this.tabEscapes = false;
1212
+ const dom = toDomKeyEvent(ev, this.primary);
1213
+ this.userInput++;
1214
+ try {
1215
+ if (this.someProp('handleKeyDown', (f) => f(this.asEditorView, dom))) {
1216
+ ev.preventDefault();
1217
+ return;
1218
+ }
1219
+ // only an Escape nothing claimed: one a plugin took — a suggestion
1220
+ // list closing — was spent, and the next Escape arms the Tab instead
1221
+ this.tabEscapes = k === XK_ESCAPE;
1222
+ if (this.fallbackKey(ev, dom))
1223
+ ev.preventDefault();
1224
+ }
1225
+ finally {
1226
+ this.userInput--;
1227
+ }
1228
+ }
1229
+ /** What a browser's contenteditable does with a key no plugin claimed. */
1230
+ fallbackKey(ev, dom) {
1231
+ const mac = this.primary === 'meta';
1232
+ const shift = ev.shiftKey;
1233
+ const word = mac ? ev.altKey : ev.ctrlKey;
1234
+ const line = mac && ev.metaKey;
1235
+ const primaryDown = mac ? ev.metaKey : ev.ctrlKey;
1236
+ switch (dom.key) {
1237
+ case 'ArrowLeft':
1238
+ return this.move(line ? 'lineStart' : word ? 'wordBackward' : 'left', shift);
1239
+ case 'ArrowRight':
1240
+ return this.move(line ? 'lineEnd' : word ? 'wordForward' : 'right', shift);
1241
+ case 'ArrowUp':
1242
+ return this.move(line ? 'docStart' : 'up', shift);
1243
+ case 'ArrowDown':
1244
+ return this.move(line ? 'docEnd' : 'down', shift);
1245
+ case 'Home':
1246
+ return this.move(primaryDown ? 'docStart' : 'lineStart', shift);
1247
+ case 'End':
1248
+ return this.move(primaryDown ? 'docEnd' : 'lineEnd', shift);
1249
+ case 'PageUp':
1250
+ return this.move('pageUp', shift);
1251
+ case 'PageDown':
1252
+ return this.move('pageDown', shift);
1253
+ case 'Backspace':
1254
+ return this.deleteBy(-1, line ? 'line' : word ? 'word' : 'char');
1255
+ case 'Delete':
1256
+ if (shift && !mac) {
1257
+ this.copy(true);
1258
+ return true;
1259
+ }
1260
+ return this.deleteBy(1, line ? 'line' : word ? 'word' : 'char');
1261
+ case 'Insert':
1262
+ if (shift) {
1263
+ void this.paste(false);
1264
+ return true;
1265
+ }
1266
+ if (ev.ctrlKey) {
1267
+ this.copy(false);
1268
+ return true;
1269
+ }
1270
+ return false;
1271
+ case 'Enter':
1272
+ // nothing claimed Enter: a state without the base keymap still splits
1273
+ return this.run(splitBlock);
1274
+ }
1275
+ if (primaryDown && !ev.altKey) {
1276
+ const letter = ev.keysym !== undefined && ev.keysym < 0x80
1277
+ ? String.fromCharCode(ev.keysym).toLowerCase()
1278
+ : '';
1279
+ switch (letter) {
1280
+ case 'c':
1281
+ this.copy(false);
1282
+ return true;
1283
+ case 'x':
1284
+ this.copy(true);
1285
+ return true;
1286
+ case 'v':
1287
+ void this.paste(shift);
1288
+ return true;
1289
+ case 'a':
1290
+ this.dispatch(this.state.tr.setSelection(new AllSelection(this.state.doc)));
1291
+ return true;
1292
+ }
1293
+ return false;
1294
+ }
1295
+ const text = ev.key;
1296
+ const cp = ev.codepoint;
1297
+ if (text &&
1298
+ cp !== undefined &&
1299
+ cp >= 0x20 &&
1300
+ cp !== 0x7f &&
1301
+ !ev.ctrlKey &&
1302
+ !ev.metaKey &&
1303
+ (mac || !ev.altKey)) {
1304
+ if (this.someProp('handleKeyPress', (f) => f(this.asEditorView, dom))) {
1305
+ return true;
1306
+ }
1307
+ this.insertText(text);
1308
+ return true;
1309
+ }
1310
+ return false;
1311
+ }
1312
+ composition(ev) {
1313
+ if (ev.type === 'compositionEnd') {
1314
+ this.setPreedit(null);
1315
+ if (ev.data) {
1316
+ this.userInput++;
1317
+ try {
1318
+ this.insertText(ev.data);
1319
+ }
1320
+ finally {
1321
+ this.userInput--;
1322
+ }
1323
+ }
1324
+ return;
1325
+ }
1326
+ this.setPreedit(ev.data ?? '');
1327
+ }
1328
+ setPreedit(text) {
1329
+ if (text === this.preedit)
1330
+ return;
1331
+ this.preedit = text;
1332
+ this.recomputeDecorations();
1333
+ this.syncSelection();
1334
+ }
1335
+ focusChanged(focused) {
1336
+ if (focused === this.focused)
1337
+ return;
1338
+ this.focused = focused;
1339
+ if (!focused) {
1340
+ this.drag = null;
1341
+ this.tabEscapes = false;
1342
+ if (this.preedit !== null)
1343
+ this.setPreedit(null);
1344
+ }
1345
+ this.syncSelection();
1346
+ if (!focused)
1347
+ this.stopBlink();
1348
+ this.config.onFocusChange?.(focused);
1349
+ }
1350
+ // --- the pointer -----------------------------------------------------------------
1351
+ /** A press, from the root's `onMouseDown`. Returns whether the editor
1352
+ * took it — the component then prevents core's default action. */
1353
+ mouseDown(ev) {
1354
+ this.goalX = null;
1355
+ this.dragCandidate = null;
1356
+ const hit = this.hit(ev.x, ev.y);
1357
+ if (!hit)
1358
+ return false;
1359
+ const { doc } = this.state;
1360
+ const sel = this.state.selection;
1361
+ if (ev.button === 3) {
1362
+ // a right press outside the selection moves the caret there first, so
1363
+ // the menu it opens acts on what is under the pointer
1364
+ if (hit.pos < sel.from || hit.pos > sel.to) {
1365
+ this.dispatch(this.state.tr.setSelection(Selection.near(doc.resolve(hit.pos))));
1366
+ }
1367
+ return false;
1368
+ }
1369
+ if (ev.button === 2) {
1370
+ this.dispatch(this.state.tr.setSelection(Selection.near(doc.resolve(hit.pos))));
1371
+ void this.paste(false, 'PRIMARY');
1372
+ return true;
1373
+ }
1374
+ if (ev.button !== 1)
1375
+ return false;
1376
+ const detail = ev.detail || 1;
1377
+ if (this.clickHandlers(hit, ev, detail))
1378
+ return true;
1379
+ // a press on the selection may be the start of dragging it: it is
1380
+ // left to core, which arms a drag on the root, and settled when the
1381
+ // pointer comes up — a click — or when core starts the drag
1382
+ if (detail === 1 && !ev.shiftKey && this.onSelection(hit)) {
1383
+ this.dragCandidate = {
1384
+ pos: hit.pos,
1385
+ collapse: !(sel instanceof NodeSelection),
1386
+ };
1387
+ return false;
1388
+ }
1389
+ if (hit.atom) {
1390
+ const node = doc.nodeAt(hit.pos);
1391
+ if (node && NodeSelection.isSelectable(node)) {
1392
+ this.dispatch(this.state.tr.setSelection(NodeSelection.create(doc, hit.pos)));
1393
+ }
1394
+ return true;
1395
+ }
1396
+ let anchor = hit.pos;
1397
+ let head = hit.pos;
1398
+ let unit = 'char';
1399
+ if (detail >= 3) {
1400
+ unit = 'block';
1401
+ [anchor, head] = this.blockAround(hit.pos);
1402
+ }
1403
+ else if (detail === 2) {
1404
+ unit = 'word';
1405
+ [anchor, head] = this.wordAround(hit.pos);
1406
+ }
1407
+ else if (ev.shiftKey) {
1408
+ anchor = sel.anchor;
1409
+ }
1410
+ this.drag = { anchor, unit, origin: { from: anchor, to: head } };
1411
+ this.dispatch(this.state.tr.setSelection(TextSelection.between(doc.resolve(anchor), doc.resolve(head))));
1412
+ ev.capturePointer?.();
1413
+ return true;
1414
+ }
1415
+ /** ProseMirror's click props, innermost node first: `handleClickOn` for
1416
+ * each node around the position, then `handleClick`. */
1417
+ clickHandlers(hit, ev, detail) {
1418
+ const [on, plain] = detail >= 3
1419
+ ? ['handleTripleClickOn', 'handleTripleClick']
1420
+ : detail === 2
1421
+ ? ['handleDoubleClickOn', 'handleDoubleClick']
1422
+ : ['handleClickOn', 'handleClick'];
1423
+ const event = {
1424
+ clientX: ev.x,
1425
+ clientY: ev.y,
1426
+ button: ev.button - 1,
1427
+ detail,
1428
+ shiftKey: ev.shiftKey,
1429
+ ctrlKey: ev.ctrlKey,
1430
+ altKey: ev.altKey,
1431
+ metaKey: ev.metaKey,
1432
+ preventDefault() { },
1433
+ stopPropagation() { },
1434
+ };
1435
+ const $pos = this.state.doc.resolve(hit.pos);
1436
+ for (let d = $pos.depth; d >= 0; d--) {
1437
+ const node = $pos.node(d);
1438
+ const nodePos = d > 0 ? $pos.before(d) : -1;
1439
+ const direct = d === $pos.depth;
1440
+ const handled = this.someProp(on, (f) => f(this.asEditorView, hit.pos, node, nodePos, event, direct));
1441
+ if (handled)
1442
+ return true;
1443
+ }
1444
+ return !!this.someProp(plain, (f) => f(this.asEditorView, hit.pos, event));
1445
+ }
1446
+ /** Motion while pressed, from the root's `onMouseMove`. */
1447
+ mouseMove(ev) {
1448
+ if (!this.drag)
1449
+ return false;
1450
+ this.pointer = { x: ev.x, y: ev.y };
1451
+ this.extendDrag(ev.x, ev.y);
1452
+ this.autoScroll();
1453
+ return true;
1454
+ }
1455
+ mouseUp() {
1456
+ // a press on the selection that never became a drag was a click: the
1457
+ // caret goes where it was — a selected block stays selected
1458
+ const press = this.dragCandidate;
1459
+ this.dragCandidate = null;
1460
+ if (press?.collapse) {
1461
+ const { doc } = this.state;
1462
+ this.dispatch(this.state.tr.setSelection(Selection.near(doc.resolve(Math.min(press.pos, doc.content.size)))));
1463
+ }
1464
+ if (!this.drag)
1465
+ return;
1466
+ this.drag = null;
1467
+ stopInterval(this.dragScroll);
1468
+ this.dragScroll = null;
1469
+ this.ownPrimary();
1470
+ }
1471
+ extendDrag(x, y) {
1472
+ const drag = this.drag;
1473
+ const hit = drag && this.hit(x, y);
1474
+ if (!drag || !hit || hit.atom)
1475
+ return;
1476
+ const { doc } = this.state;
1477
+ let anchor = drag.anchor;
1478
+ let head = hit.pos;
1479
+ if (drag.unit !== 'char') {
1480
+ const [a, b] = drag.unit === 'word'
1481
+ ? this.wordAround(hit.pos)
1482
+ : this.blockAround(hit.pos);
1483
+ if (hit.pos < drag.origin.from) {
1484
+ anchor = drag.origin.to;
1485
+ head = a;
1486
+ }
1487
+ else {
1488
+ anchor = drag.origin.from;
1489
+ head = b;
1490
+ }
1491
+ }
1492
+ const next = TextSelection.between(doc.resolve(anchor), doc.resolve(head));
1493
+ if (!next.eq(this.state.selection))
1494
+ this.dispatch(this.state.tr.setSelection(next));
1495
+ }
1496
+ /** A drag held past the viewport's edge keeps scrolling, and keeps
1497
+ * extending, until the pointer comes back or the button comes up. */
1498
+ autoScroll() {
1499
+ const outside = () => {
1500
+ const p = this.pointer;
1501
+ const box = this.scroller?.getClientRects()[0];
1502
+ if (!p || !box)
1503
+ return 0;
1504
+ if (p.y < box.y)
1505
+ return p.y - box.y;
1506
+ if (p.y > box.y + box.height)
1507
+ return p.y - (box.y + box.height);
1508
+ return 0;
1509
+ };
1510
+ if (!outside()) {
1511
+ stopInterval(this.dragScroll);
1512
+ this.dragScroll = null;
1513
+ return;
1514
+ }
1515
+ if (this.dragScroll !== null)
1516
+ return;
1517
+ this.dragScroll = startInterval(() => {
1518
+ const o = outside();
1519
+ if (!o || !this.drag || !this.scroller)
1520
+ return;
1521
+ this.scroller.scrollBy({
1522
+ y: Math.sign(o) * Math.min(40, Math.max(4, Math.abs(o))),
1523
+ });
1524
+ const p = this.pointer;
1525
+ if (p)
1526
+ this.extendDrag(p.x, p.y);
1527
+ }, 50);
1528
+ }
1529
+ wordAround(pos) {
1530
+ const $pos = this.state.doc.resolve(pos);
1531
+ if (!$pos.parent.isTextblock)
1532
+ return [pos, pos];
1533
+ const [a, b] = wordRange(blockText($pos.parent), $pos.parentOffset);
1534
+ return [$pos.start() + a, $pos.start() + b];
1535
+ }
1536
+ blockAround(pos) {
1537
+ const $pos = this.state.doc.resolve(pos);
1538
+ if (!$pos.parent.isTextblock)
1539
+ return [pos, pos];
1540
+ return [$pos.start(), $pos.end()];
1541
+ }
1542
+ // --- drag and drop --------------------------------------------------------------
1543
+ //
1544
+ // The gesture is core's; these answer its events on the root, and
1545
+ // ./drag.ts has the document's half. A drag starts only from a press on the
1546
+ // selection — `mouseDown` leaves that press to core, which arms a drag on
1547
+ // the draggable root — and a drop is read the way a paste is.
1548
+ /** Whether a press lands on what is selected: inside a text selection, or
1549
+ * on the block a node selection holds. */
1550
+ onSelection(hit) {
1551
+ const sel = this.state.selection;
1552
+ if (sel.empty)
1553
+ return false;
1554
+ if (sel instanceof NodeSelection)
1555
+ return hit.pos === sel.from;
1556
+ return hit.pos > sel.from && hit.pos < sel.to;
1557
+ }
1558
+ /** Core is starting a drag from the root. It is the selection's when the
1559
+ * press was on the selection; false has the drag cancelled, and the
1560
+ * gesture goes on as mouse events. */
1561
+ dragStart() {
1562
+ const press = this.dragCandidate;
1563
+ this.dragCandidate = null;
1564
+ const sel = this.state.selection;
1565
+ if (!press || sel.empty)
1566
+ return false;
1567
+ this.draggingNow = {
1568
+ slice: sel.content(),
1569
+ move: this.editable,
1570
+ from: sel.from,
1571
+ to: sel.to,
1572
+ content: null,
1573
+ copy: false,
1574
+ };
1575
+ return true;
1576
+ }
1577
+ dragContent() {
1578
+ const d = this.draggingNow;
1579
+ return d ? (d.content ??= serializeForClipboard(this, d.slice)) : null;
1580
+ }
1581
+ /** What a drag out of the editor offers another application: what a copy
1582
+ * would — its text, and its HTML. */
1583
+ dragText() {
1584
+ return this.dragContent()?.text ?? '';
1585
+ }
1586
+ dragHTML() {
1587
+ return this.dragContent()?.html ?? '';
1588
+ }
1589
+ /** …and a drop in this app, the slice itself. */
1590
+ dragPayload() {
1591
+ const d = this.draggingNow;
1592
+ return d ? { slice: d.slice, view: this, copy: d.copy } : null;
1593
+ }
1594
+ /** Each motion of a drag out of this editor: whether the copy
1595
+ * modifier — Ctrl, or Option on the Mac backend — is down. Core's drop
1596
+ * in this app arrives with no modifiers, so the drag carries them. */
1597
+ dragMoved(ctrlKey, altKey) {
1598
+ if (this.draggingNow)
1599
+ this.draggingNow.copy = this.primary === 'meta' ? altKey : ctrlKey;
1600
+ }
1601
+ /** The drag out of this editor ended. A move that another editor or
1602
+ * application took takes the content out of here; a drop back in here
1603
+ * has done that already. */
1604
+ dragEnd(action, dropped) {
1605
+ const d = this.draggingNow;
1606
+ this.draggingNow = null;
1607
+ this.setDropAt(null);
1608
+ if (!d || !dropped || action !== 'move' || !this.editable)
1609
+ return;
1610
+ const sel = this.state.selection;
1611
+ // only what was dragged: a document that has moved on keeps it
1612
+ if (sel.from !== d.from || sel.to !== d.to)
1613
+ return;
1614
+ this.dispatch(this.state.tr
1615
+ .deleteSelection()
1616
+ .scrollIntoView()
1617
+ .setMeta('uiEvent', 'drop'));
1618
+ }
1619
+ /** A drag over the editor at logical window `x`, `y`: a caret drawn where
1620
+ * it would go in. False when nothing is taken here. */
1621
+ dragOver(x, y) {
1622
+ if (!this.editable) {
1623
+ this.setDropAt(null);
1624
+ return false;
1625
+ }
1626
+ const hit = this.hit(x, y);
1627
+ this.setDropAt(hit && !hit.atom ? hit.pos : null);
1628
+ return true;
1629
+ }
1630
+ dragLeave() {
1631
+ this.setDropAt(null);
1632
+ }
1633
+ /** The drop caret, on the block a drop would go into: its text element
1634
+ * draws it the way it draws the caret, re-rendering nothing. */
1635
+ setDropAt(pos) {
1636
+ let key = null;
1637
+ let index = 0;
1638
+ if (pos !== null) {
1639
+ const { doc } = this.state;
1640
+ const $pos = doc.resolve(Math.max(0, Math.min(pos, doc.content.size)));
1641
+ const at = $pos.parent.isTextblock && $pos.depth > 0
1642
+ ? this.keys.keyAt($pos.before())
1643
+ : undefined;
1644
+ const map = at ? this.texts.get(at)?.map : null;
1645
+ if (at && map) {
1646
+ key = at;
1647
+ index = map.toDisplay($pos.parentOffset);
1648
+ }
1649
+ }
1650
+ if (this.dropKey && this.dropKey !== key)
1651
+ this.texts.get(this.dropKey)?.setDropCaret(null, '');
1652
+ this.dropKey = key;
1653
+ if (key)
1654
+ this.texts.get(key)?.setDropCaret(index, this.config.colors.caret);
1655
+ }
1656
+ /**
1657
+ * A drop on the editor: read, offered to `handleDrop`, and put in where it
1658
+ * fits — prosemirror-view's drop. Answers the action to report to the
1659
+ * source, or null to refuse it: at once for a drag from this app, which
1660
+ * core settles before the source's `onDragEnd` runs, and after a read for
1661
+ * HTML from another application.
1662
+ */
1663
+ drop(ev) {
1664
+ this.setDropAt(null);
1665
+ if (!this.editable || this.destroyedFlag)
1666
+ return null;
1667
+ const payload = ev.items?.[SLICE_TYPE];
1668
+ if (payload?.slice)
1669
+ return this.dropSlice(ev, payload);
1670
+ if (!ev.files.length && ev.types.includes('text/html')) {
1671
+ return ev.getData('text/html').then((got) => this.dropRead(ev, textOf(got)), () => this.dropRead(ev, null));
1672
+ }
1673
+ return this.dropRead(ev, null);
1674
+ }
1675
+ /** A slice out of an editor in this app — this one, or another. */
1676
+ dropSlice(ev, payload) {
1677
+ const hit = this.hit(ev.x, ev.y);
1678
+ if (!hit)
1679
+ return null;
1680
+ const copy = payload.copy ?? (this.primary === 'meta' ? ev.altKey : ev.ctrlKey);
1681
+ const own = payload.view === this;
1682
+ const moved = own && !copy && !!this.draggingNow?.move;
1683
+ let slice = payload.slice;
1684
+ this.someProp('transformPasted', (f) => {
1685
+ slice = f(slice, this.asEditorView, false);
1686
+ });
1687
+ // a drop back in here is the whole of the move: the drag's end has
1688
+ // nothing left to take out
1689
+ if (own)
1690
+ this.draggingNow = null;
1691
+ const answer = copy ? 'copy' : 'move';
1692
+ if (this.someProp('handleDrop', (f) => f(this.asEditorView, domDrop(ev, null, null), slice, moved)))
1693
+ return answer;
1694
+ this.insertDrop(slice, hit.pos, moved);
1695
+ return answer;
1696
+ }
1697
+ /** Text, HTML, links or files from another application — or from an
1698
+ * element of this one that is not an editor. */
1699
+ dropRead(ev, html) {
1700
+ if (this.destroyedFlag || !this.editable)
1701
+ return null;
1702
+ const hit = this.hit(ev.x, ev.y);
1703
+ if (!hit)
1704
+ return null;
1705
+ const $at = this.state.doc.resolve(hit.pos);
1706
+ const files = ev.files;
1707
+ // with files on offer, `text` is the URI list itself
1708
+ const text = files.length ? null : (ev.text ?? null);
1709
+ const slice = files.length && !html
1710
+ ? filesSlice(this.state.schema, files, $at)
1711
+ : parseFromClipboard(this, text, html, false, $at);
1712
+ if (this.someProp('handleDrop', (f) => f(this.asEditorView, domDrop(ev, html, text), slice ?? Slice.empty, false)))
1713
+ return 'copy';
1714
+ if (!slice)
1715
+ return null;
1716
+ return this.insertDrop(slice, hit.pos, false) ? 'copy' : null;
1717
+ }
1718
+ insertDrop(slice, at, moved) {
1719
+ const tr = dropTransaction(this.state, slice, at, moved);
1720
+ if (!tr)
1721
+ return false;
1722
+ this.userInput++;
1723
+ try {
1724
+ this.dispatch(tr.scrollIntoView());
1725
+ }
1726
+ finally {
1727
+ this.userInput--;
1728
+ }
1729
+ this.focus();
1730
+ return true;
1731
+ }
1732
+ /** Right-click: core's standard edit menu, with the verbs this editor has.
1733
+ * A read-only editor offers Copy and Select All and nothing else — a verb
1734
+ * left out is a row that is not there (react-x11 docs/extending.md). */
1735
+ contextMenu(ev) {
1736
+ const root = this.root;
1737
+ if (!root)
1738
+ return;
1739
+ const sel = this.state.selection;
1740
+ const editable = this.editable;
1741
+ openEditMenu(root, { x: ev.x, y: ev.y }, {
1742
+ hasSelection: !sel.empty,
1743
+ ...(editable
1744
+ ? {
1745
+ canUndo: undoDepth(this.state) > 0,
1746
+ undo: () => this.run(undo),
1747
+ canRedo: redoDepth(this.state) > 0,
1748
+ redo: () => this.run(redo),
1749
+ cut: () => this.copy(true),
1750
+ paste: () => void this.paste(false),
1751
+ }
1752
+ : null),
1753
+ copy: () => this.copy(false),
1754
+ canSelectAll: !(sel instanceof AllSelection),
1755
+ selectAll: () => this.dispatch(this.state.tr.setSelection(new AllSelection(this.state.doc))),
1756
+ });
1757
+ }
1758
+ // --- the clipboard -----------------------------------------------------------------
1759
+ /** Copy (or cut) the selection: HTML, plain text, and the slice kept in
1760
+ * this process for a paste back — see `./clipboard.ts`. */
1761
+ copy(cut) {
1762
+ const sel = this.state.selection;
1763
+ if (sel.empty)
1764
+ return;
1765
+ const { html, text, slice } = serializeForClipboard(this, sel.content());
1766
+ this.lastCopy = { text, slice };
1767
+ this.config.clipboard
1768
+ ?.write({
1769
+ 'text/html': html,
1770
+ 'text/plain;charset=utf-8': text,
1771
+ 'text/plain': text,
1772
+ UTF8_STRING: text,
1773
+ STRING: text,
1774
+ })
1775
+ .catch(() => { });
1776
+ if (cut && this.editable) {
1777
+ this.dispatch(this.state.tr
1778
+ .deleteSelection()
1779
+ .scrollIntoView()
1780
+ .setMeta('uiEvent', 'cut'));
1781
+ }
1782
+ }
1783
+ /** Paste from the clipboard (or from X11's PRIMARY): its HTML when it has
1784
+ * some and the paste is not plain, its text otherwise. */
1785
+ async paste(plain, selection) {
1786
+ const clipboard = this.config.clipboard;
1787
+ if (!clipboard || !this.editable)
1788
+ return;
1789
+ const options = selection ? { selection } : undefined;
1790
+ const [html, text] = await Promise.all([
1791
+ plain ? null : clipboard.read('text/html', options).catch(() => null),
1792
+ clipboard.read('text', options).catch(() => null),
1793
+ ]);
1794
+ if (this.destroyedFlag)
1795
+ return;
1796
+ this.pasteContent(typeof text === 'string' ? text : null, typeof html === 'string' && html ? html : null, plain);
1797
+ }
1798
+ /** ProseMirror's `pasteText`. */
1799
+ pasteText(text) {
1800
+ return this.pasteContent(text, null, true);
1801
+ }
1802
+ /** ProseMirror's `pasteHTML`. */
1803
+ pasteHTML(html) {
1804
+ return this.pasteContent(null, html, false);
1805
+ }
1806
+ pasteContent(text, html, plain) {
1807
+ const own = this.lastCopy;
1808
+ const slice = own && text !== null && text === own.text && !plain
1809
+ ? own.slice
1810
+ : parseFromClipboard(this, text, html, plain, this.state.selection.$from);
1811
+ if (!slice)
1812
+ return false;
1813
+ const event = {
1814
+ clipboardData: {
1815
+ getData: (type) => type === 'text/html'
1816
+ ? (html ?? '')
1817
+ : type === 'text/plain' || type === 'text'
1818
+ ? (text ?? '')
1819
+ : '',
1820
+ types: [
1821
+ ...(html ? ['text/html'] : []),
1822
+ ...(text !== null ? ['text/plain'] : []),
1823
+ ],
1824
+ },
1825
+ preventDefault() { },
1826
+ };
1827
+ this.userInput++;
1828
+ try {
1829
+ if (this.someProp('handlePaste', (f) => f(this.asEditorView, event, slice)))
1830
+ return true;
1831
+ const single = sliceSingleNode(slice);
1832
+ const tr = single
1833
+ ? this.state.tr.replaceSelectionWith(single, plain)
1834
+ : this.state.tr.replaceSelection(slice);
1835
+ this.dispatch(tr.scrollIntoView().setMeta('paste', true).setMeta('uiEvent', 'paste'));
1836
+ }
1837
+ finally {
1838
+ this.userInput--;
1839
+ }
1840
+ return true;
1841
+ }
1842
+ /** X11's PRIMARY: whatever is selected is pasteable with the middle
1843
+ * button, anywhere. */
1844
+ ownPrimary() {
1845
+ const sel = this.state.selection;
1846
+ if (sel.empty || !this.config.clipboard)
1847
+ return;
1848
+ const { text } = serializeForClipboard(this, sel.content());
1849
+ this.config.clipboard.write(text, { selection: 'PRIMARY' }).catch(() => { });
1850
+ }
1851
+ // --- scrolling -------------------------------------------------------------------
1852
+ scheduleScroll() {
1853
+ cancelAfterLayout(this.scrollTick);
1854
+ this.scrollTick = afterLayout(() => {
1855
+ this.scrollTick = null;
1856
+ this.scrollToSelection();
1857
+ });
1858
+ }
1859
+ /** Bring the selection's head into the viewport — after the layout the
1860
+ * change caused, which is the first moment it has a place. */
1861
+ scrollToSelection() {
1862
+ if (this.destroyedFlag)
1863
+ return;
1864
+ if (this.someProp('handleScrollToSelection', (f) => f(this.asEditorView)))
1865
+ return;
1866
+ const box = this.scroller;
1867
+ const viewport = box?.getClientRects()[0];
1868
+ if (!box || !viewport)
1869
+ return;
1870
+ const head = this.state.selection.head;
1871
+ if (!this.isDrawnAt(head)) {
1872
+ // scrolled out of a long document's window: its block is drawn
1873
+ // first, and the caret brought into view once it is laid out
1874
+ this.blockWindow?.reveal(this.topIndexOf(head), () => this.scrollToSelection());
1875
+ return;
1876
+ }
1877
+ const r = this.coordsAtPos(head);
1878
+ const margin = 8;
1879
+ let dy = 0;
1880
+ if (r.top < viewport.y + margin)
1881
+ dy = r.top - viewport.y - margin;
1882
+ else if (r.bottom > viewport.y + viewport.height - margin) {
1883
+ dy = r.bottom - (viewport.y + viewport.height) + margin;
1884
+ }
1885
+ if (dy)
1886
+ box.scrollBy({ y: dy });
1887
+ }
1888
+ // --- accessibility -----------------------------------------------------------------
1889
+ textOffset(doc, pos) {
1890
+ return [
1891
+ ...doc.textBetween(0, Math.max(0, Math.min(pos, doc.content.size)), '\n', leafText),
1892
+ ].length;
1893
+ }
1894
+ /** The document as a screen reader reads it: one text, a line per block,
1895
+ * the caret and selection in code points of it. */
1896
+ a11yText() {
1897
+ const { state } = this;
1898
+ if (this.a11yCache?.state === state)
1899
+ return this.a11yCache.value;
1900
+ const { doc, selection } = state;
1901
+ const value = {
1902
+ value: doc.textBetween(0, doc.content.size, '\n', leafText),
1903
+ caret: this.textOffset(doc, selection.head),
1904
+ selectionStart: this.textOffset(doc, selection.from),
1905
+ selectionEnd: this.textOffset(doc, selection.to),
1906
+ editable: this.editable,
1907
+ multiline: true,
1908
+ };
1909
+ this.a11yCache = { state, value };
1910
+ return value;
1911
+ }
1912
+ /** The document position at a code-point offset of `a11yText().value`. */
1913
+ posAtTextOffset(offset) {
1914
+ const { doc } = this.state;
1915
+ let remaining = offset;
1916
+ let result = null;
1917
+ let first = true;
1918
+ doc.descendants((node, pos) => {
1919
+ if (result !== null)
1920
+ return false;
1921
+ if (!node.isTextblock)
1922
+ return true;
1923
+ if (!first) {
1924
+ if (remaining === 0) {
1925
+ result = pos - 1 >= 0 ? pos - 1 : pos;
1926
+ return false;
1927
+ }
1928
+ remaining -= 1; // the line break between blocks
1929
+ }
1930
+ first = false;
1931
+ let inner = pos + 1;
1932
+ node.forEach((child) => {
1933
+ if (result !== null)
1934
+ return;
1935
+ const text = child.isText ? (child.text ?? '') : leafText(child);
1936
+ const cps = [...text];
1937
+ if (remaining <= cps.length) {
1938
+ result = child.isText
1939
+ ? inner + cps.slice(0, remaining).join('').length
1940
+ : inner + (remaining > 0 ? child.nodeSize : 0);
1941
+ return;
1942
+ }
1943
+ remaining -= cps.length;
1944
+ inner += child.nodeSize;
1945
+ });
1946
+ if (result === null && remaining === 0)
1947
+ result = pos + 1 + node.content.size;
1948
+ return false;
1949
+ });
1950
+ return result ?? doc.content.size;
1951
+ }
1952
+ a11ySelect(start, end) {
1953
+ const { doc } = this.state;
1954
+ const from = this.posAtTextOffset(start);
1955
+ const to = this.posAtTextOffset(end);
1956
+ this.dispatch(this.state.tr.setSelection(TextSelection.between(doc.resolve(from), doc.resolve(to))));
1957
+ return true;
1958
+ }
1959
+ a11yReplace(start, end, text) {
1960
+ if (!this.editable)
1961
+ return false;
1962
+ const from = this.posAtTextOffset(start);
1963
+ const to = this.posAtTextOffset(end);
1964
+ this.dispatch(this.state.tr.insertText(text, from, to).scrollIntoView());
1965
+ return true;
1966
+ }
1967
+ }
1968
+ //# sourceMappingURL=view.js.map