@esportsplus/ui 0.57.0 → 0.58.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 (490) hide show
  1. package/README.md +123 -39
  2. package/build/components/accordion/group.d.ts +14 -0
  3. package/build/components/accordion/group.js +21 -0
  4. package/build/components/accordion/index.d.ts +9 -0
  5. package/build/components/accordion/index.js +20 -3
  6. package/build/components/accordion/scss/index.scss +1 -1
  7. package/build/components/announcement/scss/index.scss +1 -1
  8. package/build/components/breadcrumb/scss/index.scss +1 -1
  9. package/build/components/button/scss/index.scss +1 -1
  10. package/build/components/button/sidebar.d.ts +3 -0
  11. package/build/components/button/sidebar.js +40 -10
  12. package/build/components/capslock/index.js +1 -1
  13. package/build/components/capslock/scss/index.scss +1 -1
  14. package/build/components/card/scss/index.scss +1 -1
  15. package/build/components/cc/scss/index.scss +1 -1
  16. package/build/components/chat-minimap/scss/index.scss +1 -1
  17. package/build/components/checkbox/scss/index.scss +1 -1
  18. package/build/components/color-picker/index.d.ts +1 -0
  19. package/build/components/color-picker/index.js +166 -136
  20. package/build/components/color-picker/scss/index.scss +1 -1
  21. package/build/components/command/index.d.ts +0 -1
  22. package/build/components/command/index.js +3 -2
  23. package/build/components/command/scss/index.scss +1 -1
  24. package/build/components/copy/scss/index.scss +1 -1
  25. package/build/components/counter/scss/index.scss +1 -1
  26. package/build/components/datalist/index.js +3 -3
  27. package/build/components/datalist/scss/index.scss +1 -1
  28. package/build/components/dock/scss/index.scss +1 -1
  29. package/build/components/editor/code/index.d.ts +0 -1
  30. package/build/components/editor/code/index.js +0 -1
  31. package/build/components/editor/code/minimap.d.ts +2 -1
  32. package/build/components/editor/code/minimap.js +5 -2
  33. package/build/components/editor/code/view.d.ts +3 -1
  34. package/build/components/editor/code/view.js +37 -26
  35. package/build/components/editor/diffs/index.d.ts +0 -1
  36. package/build/components/editor/diffs/index.js +0 -1
  37. package/build/components/editor/diffs/merge.js +7 -3
  38. package/build/components/editor/diffs/view.js +3 -1
  39. package/build/components/editor/index.d.ts +4 -1
  40. package/build/components/editor/index.js +2 -1
  41. package/build/components/editor/markdown/editing.js +22 -3
  42. package/build/components/editor/markdown/erase.d.ts +9 -0
  43. package/build/components/editor/markdown/erase.js +78 -0
  44. package/build/components/editor/markdown/index.d.ts +0 -1
  45. package/build/components/editor/markdown/index.js +0 -1
  46. package/build/components/editor/markdown/layout.d.ts +2 -9
  47. package/build/components/editor/markdown/layout.js +2 -7
  48. package/build/components/editor/markdown/render.d.ts +0 -1
  49. package/build/components/editor/markdown/render.js +287 -235
  50. package/build/components/editor/markdown/view.js +566 -667
  51. package/build/components/editor/viewer/index.d.ts +27 -0
  52. package/build/components/editor/viewer/index.js +250 -0
  53. package/build/components/editor/viewer/model.d.ts +15 -0
  54. package/build/components/editor/viewer/model.js +73 -0
  55. package/build/components/editor/workspace/index.d.ts +6 -2
  56. package/build/components/editor/workspace/index.js +243 -137
  57. package/build/components/editor/workspace/keybindings.d.ts +37 -0
  58. package/build/components/editor/workspace/keybindings.js +288 -0
  59. package/build/components/editor/workspace/model.d.ts +55 -2
  60. package/build/components/editor/workspace/model.js +141 -3
  61. package/build/components/editor/workspace/navigate.d.ts +19 -0
  62. package/build/components/editor/workspace/navigate.js +118 -0
  63. package/build/components/editor/workspace/session.d.ts +29 -0
  64. package/build/components/editor/workspace/session.js +373 -0
  65. package/build/components/editor/workspace/shortcuts.d.ts +19 -0
  66. package/build/components/editor/workspace/shortcuts.js +185 -0
  67. package/build/components/editor/workspace/status.d.ts +34 -0
  68. package/build/components/editor/workspace/status.js +193 -0
  69. package/build/components/editor/workspace/statusbar.d.ts +15 -0
  70. package/build/components/editor/workspace/statusbar.js +190 -0
  71. package/build/components/editor/workspace/strip.d.ts +27 -0
  72. package/build/components/editor/workspace/strip.js +239 -0
  73. package/build/components/editor/workspace/tabs.d.ts +15 -0
  74. package/build/components/editor/workspace/tabs.js +63 -0
  75. package/build/components/error/index.js +1 -1
  76. package/build/components/error/scss/index.scss +1 -1
  77. package/build/components/filter/index.js +5 -3
  78. package/build/components/filter/scss/index.scss +1 -1
  79. package/build/components/frame/scss/index.scss +1 -1
  80. package/build/components/heatmap/index.js +3 -3
  81. package/build/components/heatmap/scss/index.scss +1 -1
  82. package/build/components/highlight/scss/index.scss +1 -1
  83. package/build/components/icon/scss/index.scss +1 -1
  84. package/build/components/image/scss/index.scss +1 -1
  85. package/build/components/index.d.ts +2 -0
  86. package/build/components/index.js +2 -0
  87. package/build/components/inline-edit/scss/index.scss +1 -1
  88. package/build/components/input/scss/index.scss +1 -1
  89. package/build/components/link/scss/index.scss +1 -1
  90. package/build/components/loading/scss/index.scss +1 -1
  91. package/build/components/marquee/scss/index.scss +1 -1
  92. package/build/components/metric/scss/index.scss +1 -1
  93. package/build/components/nav/index.d.ts +1 -1
  94. package/build/components/nav/scss/index.scss +1 -1
  95. package/build/components/nav/tree.d.ts +3 -1
  96. package/build/components/nav/tree.js +30 -60
  97. package/build/components/notification/scss/index.scss +1 -1
  98. package/build/components/overlay/scss/index.scss +1 -1
  99. package/build/components/page/scss/index.scss +1 -1
  100. package/build/components/pagination/arrows.d.ts +15 -0
  101. package/build/components/pagination/arrows.js +65 -0
  102. package/build/components/pagination/cards.d.ts +23 -0
  103. package/build/components/pagination/cards.js +66 -0
  104. package/build/components/pagination/index.d.ts +5 -0
  105. package/build/components/pagination/index.js +3 -1
  106. package/build/components/pagination/scss/index.scss +1 -1
  107. package/build/components/progress/scss/index.scss +1 -1
  108. package/build/components/pull-to-refresh/scss/index.scss +1 -1
  109. package/build/components/range/index.js +1 -1
  110. package/build/components/range/scss/index.scss +1 -1
  111. package/build/components/relative-time/scss/index.scss +1 -1
  112. package/build/components/root/scss/index.scss +1 -1
  113. package/build/components/section/scss/index.scss +2 -0
  114. package/build/components/select/index.d.ts +1 -0
  115. package/build/components/select/index.js +16 -11
  116. package/build/components/select/scss/index.scss +1 -1
  117. package/build/components/shortcut-recorder/scss/index.scss +1 -1
  118. package/build/components/sortable/scss/index.scss +1 -1
  119. package/build/components/sticky-header/index.js +3 -3
  120. package/build/components/sticky-header/scss/index.scss +1 -1
  121. package/build/components/story/scss/index.scss +1 -1
  122. package/build/components/surface-field/scss/index.scss +1 -1
  123. package/build/components/tasklist/scss/index.scss +1 -1
  124. package/build/components/text/scss/index.scss +1 -1
  125. package/build/components/textarea/scss/index.scss +1 -1
  126. package/build/{css-utilities → components}/theme/index.d.ts +1 -1
  127. package/build/{css-utilities → components}/theme/index.js +15 -5
  128. package/build/components/theme/scss/index.scss +2 -0
  129. package/build/components/theme-picker/index.d.ts +52 -0
  130. package/build/components/theme-picker/index.js +167 -0
  131. package/build/components/theme-picker/scss/index.scss +2 -0
  132. package/build/components/toast/index.js +3 -5
  133. package/build/components/toast/scss/index.scss +1 -1
  134. package/build/components/tooltip/menu.js +3 -1
  135. package/build/components/tooltip/scss/index.scss +1 -1
  136. package/build/components/typewriter/scss/index.scss +1 -1
  137. package/build/components/uptime/scss/index.scss +1 -1
  138. package/build/components/usage-meter/scss/index.scss +1 -1
  139. package/build/{css-utilities → modifiers}/glass/index.d.ts +2 -2
  140. package/build/modifiers/glass/index.js +28 -0
  141. package/build/modifiers/glass/scss/index.scss +2 -0
  142. package/build/{css-utilities → modifiers}/index.d.ts +0 -1
  143. package/build/{css-utilities → modifiers}/index.js +0 -1
  144. package/build/modifiers/scrollbar/index.d.ts +72 -0
  145. package/build/{css-utilities → modifiers}/scrollbar/index.js +52 -19
  146. package/build/modifiers/scrollbar/scss/index.scss +2 -0
  147. package/build/normalize/scss/index.scss +1 -2
  148. package/layer.scss +1 -1
  149. package/package.json +11 -18
  150. package/scripts/code-editor-markdown.test.mjs +45 -14
  151. package/scripts/code-viewer.test.mjs +69 -0
  152. package/scripts/workspace-keybindings.test.mjs +273 -0
  153. package/scripts/workspace-session.test.mjs +349 -0
  154. package/scripts/workspace-status.test.mjs +229 -0
  155. package/scripts/workspace-tabs.test.mjs +142 -0
  156. package/src/components/accordion/group.ts +39 -0
  157. package/src/components/accordion/index.ts +32 -6
  158. package/src/components/accordion/scss/index.scss +12 -21
  159. package/src/components/accordion/scss/variables.scss +18 -3
  160. package/src/components/announcement/scss/index.scss +4 -4
  161. package/src/components/announcement/scss/variables.scss +6 -6
  162. package/src/components/banner/scss/index.scss +1 -1
  163. package/src/components/breadcrumb/scss/index.scss +35 -50
  164. package/src/components/breadcrumb/scss/variables.scss +23 -31
  165. package/src/components/button/scss/_fan.scss +39 -43
  166. package/src/components/button/scss/_morph.scss +66 -78
  167. package/src/components/button/scss/_sidebar.scss +41 -30
  168. package/src/components/button/scss/_split.scss +9 -9
  169. package/src/components/button/scss/index.scss +23 -19
  170. package/src/components/button/scss/variables.scss +94 -90
  171. package/src/components/button/sidebar.ts +36 -24
  172. package/src/components/capslock/index.ts +2 -2
  173. package/src/components/capslock/scss/index.scss +11 -6
  174. package/src/components/capslock/scss/variables.scss +3 -5
  175. package/src/components/card/scss/_expand.scss +58 -64
  176. package/src/components/card/scss/index.scss +40 -21
  177. package/src/components/card/scss/variables.scss +40 -31
  178. package/src/components/cc/scss/index.scss +68 -68
  179. package/src/components/cc/scss/variables.scss +13 -14
  180. package/src/components/chat-minimap/scss/index.scss +24 -16
  181. package/src/components/chat-minimap/scss/variables.scss +37 -37
  182. package/src/components/checkbox/scss/index.scss +54 -52
  183. package/src/components/checkbox/scss/variables.scss +37 -39
  184. package/src/components/color-picker/index.ts +157 -120
  185. package/src/components/color-picker/scss/index.scss +159 -96
  186. package/src/components/color-picker/scss/variables.scss +33 -15
  187. package/src/components/command/index.ts +3 -2
  188. package/src/components/command/scss/index.scss +10 -14
  189. package/src/components/command/scss/variables.scss +12 -35
  190. package/src/components/copy/scss/index.scss +6 -6
  191. package/src/components/copy/scss/variables.scss +12 -15
  192. package/src/components/counter/scss/index.scss +14 -14
  193. package/src/components/counter/scss/variables.scss +1 -1
  194. package/src/components/datalist/index.ts +3 -3
  195. package/src/components/datalist/scss/index.scss +7 -6
  196. package/src/components/datalist/scss/variables.scss +7 -3
  197. package/src/components/dock/scss/index.scss +10 -15
  198. package/src/components/dock/scss/variables.scss +29 -25
  199. package/src/components/editor/code/index.ts +0 -1
  200. package/src/components/editor/code/minimap.ts +13 -5
  201. package/src/components/editor/code/scss/_editor.scss +83 -81
  202. package/src/components/editor/code/scss/_features.scss +106 -97
  203. package/src/components/editor/code/scss/_services.scss +27 -25
  204. package/src/components/editor/code/scss/index.scss +0 -1
  205. package/src/components/editor/code/scss/variables.scss +30 -26
  206. package/src/components/editor/code/view.ts +14 -5
  207. package/src/components/editor/diffs/index.ts +0 -1
  208. package/src/components/editor/diffs/merge.ts +2 -1
  209. package/src/components/editor/diffs/scss/_diffs.scss +67 -57
  210. package/src/components/editor/diffs/scss/_merge.scss +17 -14
  211. package/src/components/editor/diffs/scss/index.scss +0 -1
  212. package/src/components/editor/diffs/scss/variables.scss +25 -18
  213. package/src/components/editor/diffs/view.ts +2 -1
  214. package/src/components/editor/index.ts +7 -1
  215. package/src/components/editor/markdown/editing.ts +38 -3
  216. package/src/components/editor/markdown/erase.ts +124 -0
  217. package/src/components/editor/markdown/index.ts +0 -1
  218. package/src/components/editor/markdown/layout.ts +3 -12
  219. package/src/components/editor/markdown/render.ts +65 -37
  220. package/src/components/editor/markdown/scss/index.scss +92 -124
  221. package/src/components/editor/markdown/scss/variables.scss +39 -19
  222. package/src/components/editor/markdown/view.ts +737 -786
  223. package/src/components/editor/tree/scss/index.scss +121 -110
  224. package/src/components/editor/tree/scss/variables.scss +48 -54
  225. package/src/components/editor/viewer/index.ts +321 -0
  226. package/src/components/editor/viewer/model.ts +130 -0
  227. package/src/components/editor/viewer/scss/_viewer.scss +302 -0
  228. package/src/components/editor/viewer/scss/index.scss +2 -0
  229. package/src/components/editor/viewer/scss/variables.scss +69 -0
  230. package/src/components/editor/workspace/index.ts +159 -31
  231. package/src/components/editor/workspace/keybindings.ts +447 -0
  232. package/src/components/editor/workspace/model.ts +233 -3
  233. package/src/components/editor/workspace/navigate.ts +194 -0
  234. package/src/components/editor/workspace/scss/index.scss +72 -59
  235. package/src/components/editor/workspace/scss/keybindings.scss +190 -0
  236. package/src/components/editor/workspace/scss/status.scss +122 -0
  237. package/src/components/editor/workspace/scss/tabs.scss +56 -0
  238. package/src/components/editor/workspace/session.ts +551 -0
  239. package/src/components/editor/workspace/shortcuts.ts +255 -0
  240. package/src/components/editor/workspace/status.ts +259 -0
  241. package/src/components/editor/workspace/statusbar.ts +255 -0
  242. package/src/components/editor/workspace/strip.ts +273 -0
  243. package/src/components/editor/workspace/tabs.ts +106 -0
  244. package/src/components/error/index.ts +1 -1
  245. package/src/components/error/scss/index.scss +19 -19
  246. package/src/components/error/scss/variables.scss +2 -2
  247. package/src/components/filter/index.ts +7 -2
  248. package/src/components/filter/scss/index.scss +1 -1
  249. package/src/components/filter/scss/variables.scss +11 -10
  250. package/src/components/frame/scss/index.scss +12 -12
  251. package/src/components/frame/scss/variables.scss +8 -8
  252. package/src/components/heatmap/index.ts +3 -3
  253. package/src/components/heatmap/scss/index.scss +28 -26
  254. package/src/components/heatmap/scss/variables.scss +18 -17
  255. package/src/components/highlight/scss/index.scss +17 -13
  256. package/src/components/highlight/scss/variables.scss +8 -10
  257. package/src/components/icon/scss/index.scss +10 -0
  258. package/src/components/icon/scss/variables.scss +5 -0
  259. package/src/components/image/scss/index.scss +9 -9
  260. package/src/components/image/scss/variables.scss +7 -2
  261. package/src/components/index.ts +2 -0
  262. package/src/components/inline-edit/scss/index.scss +99 -104
  263. package/src/components/inline-edit/scss/variables.scss +40 -39
  264. package/src/components/input/scss/_field.scss +6 -20
  265. package/src/components/input/scss/_scrub.scss +10 -8
  266. package/src/components/input/scss/_tag.scss +35 -37
  267. package/src/components/input/scss/variables.scss +100 -99
  268. package/src/components/link/scss/index.scss +4 -1
  269. package/src/components/loading/scss/index.scss +1 -1
  270. package/src/components/loading/scss/variables.scss +1 -1
  271. package/src/components/marquee/scss/index.scss +9 -5
  272. package/src/components/marquee/scss/variables.scss +3 -1
  273. package/src/components/metric/scss/_flash.scss +3 -1
  274. package/src/components/metric/scss/index.scss +8 -6
  275. package/src/components/metric/scss/variables.scss +26 -20
  276. package/src/components/nav/index.ts +1 -1
  277. package/src/components/nav/scss/index.scss +12 -40
  278. package/src/components/nav/scss/variables.scss +30 -11
  279. package/src/components/nav/tree.ts +19 -62
  280. package/src/components/notification/scss/index.scss +7 -7
  281. package/src/components/notification/scss/variables.scss +7 -9
  282. package/src/components/overlay/scss/index.scss +181 -170
  283. package/src/components/overlay/scss/variables.scss +26 -57
  284. package/src/components/page/scss/index.scss +12 -11
  285. package/src/components/page/scss/variables.scss +5 -0
  286. package/src/components/pagination/arrows.ts +58 -0
  287. package/src/components/pagination/cards.ts +57 -0
  288. package/src/components/pagination/index.ts +4 -1
  289. package/src/components/pagination/scss/_arrows.scss +69 -0
  290. package/src/components/pagination/scss/_cards.scss +121 -0
  291. package/src/components/pagination/scss/_dots.scss +2 -5
  292. package/src/components/pagination/scss/index.scss +8 -6
  293. package/src/components/pagination/scss/variables.scss +87 -37
  294. package/src/components/progress/scss/_text.scss +21 -18
  295. package/src/components/progress/scss/index.scss +1 -1
  296. package/src/components/progress/scss/variables.scss +12 -12
  297. package/src/components/pull-to-refresh/scss/index.scss +2 -1
  298. package/src/components/pull-to-refresh/scss/variables.scss +11 -6
  299. package/src/components/range/index.ts +1 -1
  300. package/src/components/range/scss/_filter.scss +62 -51
  301. package/src/components/range/scss/_slider.scss +78 -75
  302. package/src/components/range/scss/_vertical.scss +6 -6
  303. package/src/components/range/scss/index.scss +3 -2
  304. package/src/components/range/scss/variables.scss +26 -23
  305. package/src/components/relative-time/scss/index.scss +27 -27
  306. package/src/components/relative-time/scss/variables.scss +10 -12
  307. package/src/components/root/scss/index.scss +71 -43
  308. package/src/components/root/scss/variables.scss +35 -31
  309. package/src/components/section/scss/index.scss +31 -0
  310. package/src/components/section/scss/variables.scss +21 -0
  311. package/src/components/select/index.ts +11 -6
  312. package/src/components/select/scss/index.scss +21 -32
  313. package/src/components/select/scss/variables.scss +25 -18
  314. package/src/components/shortcut-recorder/scss/index.scss +13 -12
  315. package/src/components/shortcut-recorder/scss/variables.scss +12 -13
  316. package/src/components/sortable/scss/index.scss +47 -44
  317. package/src/components/sortable/scss/variables.scss +4 -5
  318. package/src/components/sticky-header/index.ts +4 -4
  319. package/src/components/sticky-header/scss/index.scss +14 -13
  320. package/src/components/sticky-header/scss/variables.scss +9 -9
  321. package/src/components/story/scss/index.scss +26 -30
  322. package/src/components/story/scss/variables.scss +16 -9
  323. package/src/components/surface-field/scss/index.scss +45 -40
  324. package/src/components/surface-field/scss/variables.scss +5 -6
  325. package/src/components/tasklist/scss/index.scss +19 -19
  326. package/src/components/tasklist/scss/variables.scss +26 -29
  327. package/src/components/text/scss/index.scss +2 -1
  328. package/src/components/textarea/scss/variables.scss +1 -1
  329. package/src/{css-utilities → components}/theme/index.ts +17 -3
  330. package/src/components/theme/scss/index.scss +1 -0
  331. package/src/components/theme/scss/variables.scss +78 -0
  332. package/src/components/theme-picker/index.ts +231 -0
  333. package/src/components/theme-picker/scss/index.scss +264 -0
  334. package/src/components/toast/index.ts +28 -30
  335. package/src/components/toast/scss/index.scss +87 -87
  336. package/src/components/toast/scss/variables.scss +39 -39
  337. package/src/components/tooltip/menu.ts +4 -1
  338. package/src/components/tooltip/scss/_east.scss +10 -10
  339. package/src/components/tooltip/scss/_menu.scss +284 -269
  340. package/src/components/tooltip/scss/_north.scss +10 -10
  341. package/src/components/tooltip/scss/_shared.scss +7 -7
  342. package/src/components/tooltip/scss/_south.scss +10 -10
  343. package/src/components/tooltip/scss/_west.scss +10 -10
  344. package/src/components/tooltip/scss/index.scss +21 -20
  345. package/src/components/tooltip/scss/variables.scss +104 -96
  346. package/src/components/typewriter/scss/_retype.scss +12 -11
  347. package/src/components/typewriter/scss/_rotate.scss +2 -1
  348. package/src/components/typewriter/scss/variables.scss +41 -39
  349. package/src/components/uptime/scss/index.scss +15 -15
  350. package/src/components/uptime/scss/variables.scss +8 -8
  351. package/src/components/usage-meter/scss/index.scss +3 -1
  352. package/src/components/usage-meter/scss/variables.scss +5 -5
  353. package/src/css-utilities/absolute.scss +64 -0
  354. package/src/css-utilities/background.scss +43 -0
  355. package/src/css-utilities/border.scss +45 -0
  356. package/src/css-utilities/box-shadow.scss +29 -0
  357. package/src/css-utilities/color.scss +41 -0
  358. package/src/css-utilities/flex.scss +64 -0
  359. package/src/css-utilities/font/geist/{scss/index.scss → index.scss} +12 -4
  360. package/src/css-utilities/font/montserrat/Montserrat-Italic-Variable.woff2 +0 -0
  361. package/src/css-utilities/font/montserrat/Montserrat-Variable.woff2 +0 -0
  362. package/src/css-utilities/font/montserrat/OFL.txt +93 -0
  363. package/src/css-utilities/font/montserrat/index.scss +38 -0
  364. package/src/{shared/scss → css-utilities}/icon.scss +1 -1
  365. package/src/css-utilities/{inline/scss/index.scss → inline.scss} +1 -1
  366. package/src/css-utilities/{skeleton/scss/index.scss → skeleton.scss} +11 -11
  367. package/src/css-utilities/surface.scss +266 -0
  368. package/src/css-utilities/text.scss +191 -0
  369. package/src/modifiers/glass/index.ts +44 -0
  370. package/src/{css-utilities → modifiers}/glass/scss/index.scss +6 -6
  371. package/src/{css-utilities → modifiers}/glass/scss/variables.scss +1 -1
  372. package/src/{css-utilities → modifiers}/index.ts +0 -1
  373. package/src/{css-utilities → modifiers}/scrollbar/index.ts +92 -17
  374. package/src/{css-utilities → modifiers}/scrollbar/scss/index.scss +178 -177
  375. package/src/{css-utilities → modifiers}/scrollbar/scss/variables.scss +28 -21
  376. package/src/normalize/scss/index.scss +4 -2
  377. package/src/shared/scss/color-weights.scss +19 -0
  378. package/src/shared/scss/index.scss +1 -12
  379. package/src/tokens/scss/blur.scss +5 -0
  380. package/src/tokens/scss/color.scss +74 -50
  381. package/src/tokens/scss/index.scss +1 -2
  382. package/src/tokens/scss/size.scss +0 -2
  383. package/src/tokens/scss/state.scss +3 -2
  384. package/src/tokens/scss/transition.scss +19 -2
  385. package/storage/svg/monitor.svg +1 -0
  386. package/storage/svg/moon.svg +1 -0
  387. package/storage/svg/pin.svg +1 -0
  388. package/storage/svg/sun.svg +1 -0
  389. package/vite.config.ts +33 -8
  390. package/build/css-utilities/background/scss/index.scss +0 -2
  391. package/build/css-utilities/border/scss/index.scss +0 -2
  392. package/build/css-utilities/color/scss/index.scss +0 -2
  393. package/build/css-utilities/flex/scss/index.scss +0 -2
  394. package/build/css-utilities/font/geist/index.d.ts +0 -1
  395. package/build/css-utilities/font/geist/index.js +0 -1
  396. package/build/css-utilities/font/geist/scss/index.scss +0 -2
  397. package/build/css-utilities/font/montserrat/index.d.ts +0 -1
  398. package/build/css-utilities/font/montserrat/index.js +0 -1
  399. package/build/css-utilities/font/montserrat/scss/Montserrat-Bold.woff +0 -0
  400. package/build/css-utilities/font/montserrat/scss/Montserrat-Bold.woff2 +0 -0
  401. package/build/css-utilities/font/montserrat/scss/Montserrat-BoldItalic.woff +0 -0
  402. package/build/css-utilities/font/montserrat/scss/Montserrat-BoldItalic.woff2 +0 -0
  403. package/build/css-utilities/font/montserrat/scss/Montserrat-Italic.woff +0 -0
  404. package/build/css-utilities/font/montserrat/scss/Montserrat-Italic.woff2 +0 -0
  405. package/build/css-utilities/font/montserrat/scss/Montserrat-Medium.woff +0 -0
  406. package/build/css-utilities/font/montserrat/scss/Montserrat-Medium.woff2 +0 -0
  407. package/build/css-utilities/font/montserrat/scss/Montserrat-MediumItalic.woff +0 -0
  408. package/build/css-utilities/font/montserrat/scss/Montserrat-MediumItalic.woff2 +0 -0
  409. package/build/css-utilities/font/montserrat/scss/Montserrat-Regular.woff +0 -0
  410. package/build/css-utilities/font/montserrat/scss/Montserrat-Regular.woff2 +0 -0
  411. package/build/css-utilities/font/montserrat/scss/Montserrat-SemiBold.woff +0 -0
  412. package/build/css-utilities/font/montserrat/scss/Montserrat-SemiBold.woff2 +0 -0
  413. package/build/css-utilities/font/montserrat/scss/Montserrat-SemiBoldItalic.woff +0 -0
  414. package/build/css-utilities/font/montserrat/scss/Montserrat-SemiBoldItalic.woff2 +0 -0
  415. package/build/css-utilities/font/montserrat/scss/index.scss +0 -2
  416. package/build/css-utilities/glass/index.js +0 -26
  417. package/build/css-utilities/glass/scss/index.scss +0 -2
  418. package/build/css-utilities/index.scss +0 -2
  419. package/build/css-utilities/inline/scss/index.scss +0 -2
  420. package/build/css-utilities/scrollbar/index.d.ts +0 -42
  421. package/build/css-utilities/scrollbar/scss/index.scss +0 -2
  422. package/build/css-utilities/skeleton/scss/index.scss +0 -2
  423. package/build/css-utilities/text/scss/index.scss +0 -2
  424. package/build/themes/dark/index.d.ts +0 -3
  425. package/build/themes/dark/index.js +0 -3
  426. package/build/themes/dark/input.d.ts +0 -3
  427. package/build/themes/dark/input.js +0 -12
  428. package/build/themes/dark/select.d.ts +0 -3
  429. package/build/themes/dark/select.js +0 -23
  430. package/build/themes/dark/textarea.d.ts +0 -2186
  431. package/build/themes/dark/textarea.js +0 -11
  432. package/src/css-utilities/background/scss/index.scss +0 -1
  433. package/src/css-utilities/background/scss/variables.scss +0 -18
  434. package/src/css-utilities/border/scss/index.scss +0 -1
  435. package/src/css-utilities/border/scss/variables.scss +0 -23
  436. package/src/css-utilities/color/scss/index.scss +0 -1
  437. package/src/css-utilities/color/scss/variables.scss +0 -17
  438. package/src/css-utilities/flex/scss/index.scss +0 -57
  439. package/src/css-utilities/flex/scss/variables.scss +0 -16
  440. package/src/css-utilities/font/geist/index.ts +0 -1
  441. package/src/css-utilities/font/montserrat/index.ts +0 -1
  442. package/src/css-utilities/font/montserrat/scss/Montserrat-Bold.woff +0 -0
  443. package/src/css-utilities/font/montserrat/scss/Montserrat-Bold.woff2 +0 -0
  444. package/src/css-utilities/font/montserrat/scss/Montserrat-BoldItalic.woff +0 -0
  445. package/src/css-utilities/font/montserrat/scss/Montserrat-BoldItalic.woff2 +0 -0
  446. package/src/css-utilities/font/montserrat/scss/Montserrat-Italic.woff +0 -0
  447. package/src/css-utilities/font/montserrat/scss/Montserrat-Italic.woff2 +0 -0
  448. package/src/css-utilities/font/montserrat/scss/Montserrat-Medium.woff +0 -0
  449. package/src/css-utilities/font/montserrat/scss/Montserrat-Medium.woff2 +0 -0
  450. package/src/css-utilities/font/montserrat/scss/Montserrat-MediumItalic.woff +0 -0
  451. package/src/css-utilities/font/montserrat/scss/Montserrat-MediumItalic.woff2 +0 -0
  452. package/src/css-utilities/font/montserrat/scss/Montserrat-Regular.woff +0 -0
  453. package/src/css-utilities/font/montserrat/scss/Montserrat-Regular.woff2 +0 -0
  454. package/src/css-utilities/font/montserrat/scss/Montserrat-SemiBold.woff +0 -0
  455. package/src/css-utilities/font/montserrat/scss/Montserrat-SemiBold.woff2 +0 -0
  456. package/src/css-utilities/font/montserrat/scss/Montserrat-SemiBoldItalic.woff +0 -0
  457. package/src/css-utilities/font/montserrat/scss/Montserrat-SemiBoldItalic.woff2 +0 -0
  458. package/src/css-utilities/font/montserrat/scss/index.scss +0 -94
  459. package/src/css-utilities/glass/index.ts +0 -42
  460. package/src/css-utilities/index.scss +0 -13
  461. package/src/css-utilities/skeleton/scss/variables.scss +0 -3
  462. package/src/css-utilities/text/scss/index.scss +0 -152
  463. package/src/css-utilities/text/scss/variables.scss +0 -22
  464. package/src/shared/scss/absolute.scss +0 -28
  465. package/src/shared/scss/background.scss +0 -21
  466. package/src/shared/scss/border.scss +0 -21
  467. package/src/shared/scss/color.scss +0 -21
  468. package/src/shared/scss/position.scss +0 -55
  469. package/src/shared/scss/text.scss +0 -31
  470. package/src/themes/dark/index.ts +0 -3
  471. package/src/themes/dark/input.ts +0 -16
  472. package/src/themes/dark/select.ts +0 -28
  473. package/src/themes/dark/textarea.ts +0 -13
  474. package/src/tokens/scss/box-shadow.scss +0 -23
  475. package/src/tokens/scss/font-family.scss +0 -5
  476. package/storage/code.md +0 -180
  477. package/storage/editor-features.md +0 -227
  478. package/build/css-utilities/font/geist/{scss/Geist-Italic-Variable.woff2 → Geist-Italic-Variable.woff2} +0 -0
  479. package/build/css-utilities/font/geist/{scss/Geist-Variable.woff2 → Geist-Variable.woff2} +0 -0
  480. package/build/css-utilities/font/geist/{scss/GeistMono-Italic-Variable.woff2 → GeistMono-Italic-Variable.woff2} +0 -0
  481. package/build/css-utilities/font/geist/{scss/GeistMono-Variable.woff2 → GeistMono-Variable.woff2} +0 -0
  482. package/src/{shared/scss → css-utilities}/anchor.scss +6 -6
  483. package/src/{shared/scss → css-utilities}/drag.scss +0 -0
  484. package/src/css-utilities/font/geist/{scss/Geist-Italic-Variable.woff2 → Geist-Italic-Variable.woff2} +0 -0
  485. package/src/css-utilities/font/geist/{scss/Geist-Variable.woff2 → Geist-Variable.woff2} +0 -0
  486. package/src/css-utilities/font/geist/{scss/GeistMono-Italic-Variable.woff2 → GeistMono-Italic-Variable.woff2} +0 -0
  487. package/src/css-utilities/font/geist/{scss/GeistMono-Variable.woff2 → GeistMono-Variable.woff2} +0 -0
  488. package/src/{shared/scss → css-utilities}/hidden.scss +11 -11
  489. /package/src/{shared/scss → css-utilities}/layer.scss +0 -0
  490. /package/src/{shared/scss → css-utilities}/reset.scss +0 -0
@@ -1,13 +1,12 @@
1
1
  import { flush, reactive } from '@esportsplus/reactivity';
2
2
  import { html, type Attributes } from '@esportsplus/template';
3
- import icon from '~/components/icon';
3
+ import scrollbar from '~/modifiers/scrollbar';
4
4
  import { clamp } from '~/shared/clamp';
5
5
  import { mac } from '~/shared/platform';
6
6
  import { observer, observeSize, type Observer } from '~/shared/resize';
7
7
  import { copied, read, write } from '../code/clipboard';
8
8
  import { addNextOccurrence, deleteCharacter, indent, insertText, lineCommand, toggleComment } from '../code/commands';
9
9
  import { composition } from '../code/composition';
10
- import { write as writeField } from '../code/field';
11
10
  import { find } from '../code/find';
12
11
  import { mapFolds, outerFolds, type FoldRange } from '../code/folding';
13
12
  import { goto } from '../code/goto';
@@ -16,17 +15,19 @@ import { EditorLayout, type Rect } from '../code/layout';
16
15
  import { minimap, type Source } from '../code/minimap';
17
16
  import { move } from '../code/navigation';
18
17
  import { pointer } from '../code/pointer';
19
- import { inputEdit, NativeText, type Before } from '../code/projection';
18
+ import { NativeText } from '../code/projection';
20
19
  import { syntaxCache, type Token } from '../code/syntax';
21
20
  import { markdownCommand } from './editing';
21
+ import { reach, widen, type Span } from './erase';
22
22
  import { parseInline, type Inline, type References } from './inline';
23
- import { arrange, cache, headingScale, MarkdownLayout, SCALED_LINE, unitStart, type Unit } from './layout';
23
+ import { arrange, cache, MarkdownLayout, unitStart, type Unit } from './layout';
24
24
  import {
25
25
  blockAt,
26
26
  contentEnd,
27
27
  markdownBackspace,
28
28
  markdownReferences,
29
29
  parseMarkdown,
30
+ quotePrefix,
30
31
  reparse,
31
32
  toggleTask,
32
33
  type Kind,
@@ -34,15 +35,18 @@ import {
34
35
  } from './model';
35
36
  import { renderer } from './render';
36
37
  import { foldable, foldAt, markdownFolds } from './structure';
37
- import type { Change, Edit, EditorDocument, Selection, Snapshot } from '../code/document';
38
+ import type { Change, EditorDocument, Selection, Snapshot } from '../code/document';
38
39
  import type { Callbacks, Controller, Options } from '../code/view';
39
- import chevron from '@esportsplus/ui/svg/chevron-down.svg';
40
+ import tooltip from '~/components/tooltip';
40
41
 
41
42
 
42
- type Active = { first: number; from: number; last: number; to: number };
43
-
44
43
  type Decoration = { kind: string; style: string };
45
44
 
45
+ // Space standing in for the units left undrawn between drawn ones.
46
+ type Gap = { height: number };
47
+
48
+ type Item = Gap | Unit;
49
+
46
50
  type MarkdownController = Omit<Controller, 'setDocument' | 'setOptions'> & {
47
51
  bold(): boolean;
48
52
  italic(): boolean;
@@ -69,15 +73,34 @@ type Metrics = {
69
73
  width: number;
70
74
  };
71
75
 
76
+ type Point = { node: Node; offset: number };
77
+
72
78
 
73
79
  const DECORATIONS = 1000;
74
80
 
81
+ // A list item whose text so far is '[]' or '[ ]': a space turns it into a task.
82
+ const ITEM_TASK = /^[ \t]*(?:[-+*]|\d+[.)])[ \t]+\[ ?\]$/;
83
+
84
+ // Kinds whose text joins a neighbour's when the break between them is deleted.
85
+ const JOINABLE = new Set<Kind>(['heading', 'list', 'paragraph', 'quote']);
86
+
87
+ const LINE_BREAK = /[\r\n]/;
88
+
89
+ // Long enough that the hint only shows once the pointer rests on a link, not as it passes over text.
90
+ const LINK_DELAY = 700;
91
+
92
+ // Source the editor keeps literal, so typing there never turns into markup.
93
+ const LITERAL = new Set<Kind>(['code', 'fence', 'frontmatter', 'html']);
94
+
75
95
  const NAVIGATION = /^(ArrowDown|ArrowLeft|ArrowRight|ArrowUp|End|Home|PageDown|PageUp)$/;
76
96
 
77
97
  const NONE: readonly number[] = [];
78
98
 
79
99
  const NO_TOKENS: readonly Token[] = [];
80
100
 
101
+ // A line whose text so far is '[]' or '[ ]': a space turns it into a task item.
102
+ const TASK = /^\[ ?\]$/;
103
+
81
104
  const TOKEN_COLORS = ['comment', 'keyword', 'operator', 'property', 'string', 'type', 'variable'] as const;
82
105
 
83
106
  const VERTICAL = /^(ArrowDown|ArrowUp|PageDown|PageUp)$/;
@@ -86,9 +109,9 @@ const VERTICAL = /^(ArrowDown|ArrowUp|PageDown|PageUp)$/;
86
109
  let uid = 0;
87
110
 
88
111
 
89
- // Brings a reactive list to 'next' with the fewest splices, so units that stay keep their DOM. Both are ordered
90
- // runs of the same unit list.
91
- function assign(list: Unit[], next: readonly Unit[]) {
112
+ // Brings a reactive list to 'next' with the fewest splices, so items that stay keep their DOM. Both are ordered runs
113
+ // of the same items.
114
+ function assign(list: Item[], next: readonly Item[]) {
92
115
  if (list.length === next.length) {
93
116
  let same = true;
94
117
 
@@ -151,46 +174,37 @@ function pixels(value: string) {
151
174
  return parseFloat(value) || 0;
152
175
  }
153
176
 
154
- function prevent(e: Event) {
155
- e.preventDefault();
156
- }
157
-
158
177
 
159
- // The markdown editor's view: blocks render formatted until the caret enters one, which then edits as source in a
160
- // native textarea standing in its place. Only units near the viewport are drawn, keyed by block, so edits redraw the
161
- // block they touched. Returns its template and controller.
178
+ // The markdown editor's view: blocks stay formatted while they edit. The rendered text itself is editable; every input
179
+ // is turned into an edit of the markdown source, and the blocks it touched draw again with the caret put back. Markup
180
+ // never shows: deletions skip over it, and a delimiter left with nothing to wrap goes with what it wrapped. Only units
181
+ // near the viewport are drawn, plus the ones holding the selection's ends. Returns its template and controller.
162
182
  const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller: MarkdownController) => void) => {
163
- let active: Active | null = null,
164
- activeIndex = -1,
165
- apple = mac(),
166
- before: Before | null = null,
167
- beforeRanges: readonly Selection[] = [],
168
- beforeSelection: Selection | undefined,
183
+ let apple = mac(),
169
184
  blocks = parseMarkdown(model),
185
+ // The final text of the composition ending, and the selection it replaces.
186
+ composed = '',
187
+ composing: Selection | null = null,
188
+ content: HTMLElement | undefined,
170
189
  decorations = reactive([] as Decoration[]),
171
190
  definitions: References = new Map(),
172
191
  disposed = false,
173
- drag: { anchor: number; head: number } | null = null,
174
192
  // Content height the minimap last drew.
175
193
  drawn = 0,
176
194
  dropAt: number | null = null,
177
- editing = false,
195
+ elements = new Map<Unit, HTMLElement>(),
178
196
  escapeTab = false,
179
- estimated = false,
180
- fieldSource = '',
181
197
  folds: FoldRange[] = [],
182
198
  frame = 0,
183
- geometry: EditorLayout | null = null,
199
+ gaps: Gap[] = [],
184
200
  goals: number[] = [],
185
201
  help = `markdown-editor-help-${++uid}`,
186
202
  host: HTMLElement | undefined,
187
203
  inlines = new WeakMap<MarkdownBlock, { definitions: References; tokens: readonly Inline[] }>(),
188
- inputData: string | null = null,
189
- inputType = '',
204
+ items = reactive([] as Item[]),
190
205
  keys = keymap(apple),
191
206
  layout = new MarkdownLayout(),
192
207
  length = model.value.length,
193
- listed = { after: reactive([] as Unit[]), before: reactive([] as Unit[]) },
194
208
  metrics: Metrics = {
195
209
  charWidth: 7.8,
196
210
  clientHeight: 320,
@@ -208,9 +222,9 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
208
222
  palette: Record<string, string> = {},
209
223
  parsed = model.revision,
210
224
  pendingValue: string | undefined,
211
- pinned = false,
212
- pointerId = -1,
213
- projection = new NativeText(''),
225
+ // The native selection this view last set, which maps to the document's but needn't map back to it exactly.
226
+ placed: { anchor: Point; focus: Point } | null = null,
227
+ rendered: Unit[] = [],
214
228
  resize: Observer | undefined = observer(resized),
215
229
  revealing = false,
216
230
  ruler: HTMLElement | undefined,
@@ -222,46 +236,27 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
222
236
  selections: model.selections.map((range) => ({ ...range }))
223
237
  }),
224
238
  surface: HTMLElement | undefined,
225
- textarea: HTMLTextAreaElement | undefined,
226
239
  ui = reactive({
227
- between: 0,
228
- bottom: 0,
229
- editing: false,
230
- foldable: false,
231
- height: 20,
232
- input: '',
240
+ empty: !model.value,
241
+ fold: false,
233
242
  label: 'Markdown editor',
243
+ // The platform's modifier is held, so links show they open on a click.
244
+ linking: false,
234
245
  minimap: false,
235
- name: '',
236
- next: 0,
237
246
  placeholder: '',
238
247
  readonly: false,
239
248
  spellcheck: true,
240
- style: '',
241
249
  tabSize: 4,
242
- top: 0,
243
250
  wrap: true
244
251
  }),
245
252
  units = cache(),
246
253
  unitsDirty = true,
254
+ // The document's selection hasn't reached the native one yet, so the native one is stale.
255
+ unplaced = false,
247
256
  unsubscribe: VoidFunction | undefined,
248
257
  // Where each document this editor showed was left.
249
258
  visited = new WeakMap<EditorDocument, Memo>(),
250
- whole: { layout: EditorLayout; projection: NativeText; revision: number } | null = null,
251
- writing = false,
252
- written = '';
253
-
254
- let activeUnit: Unit = {
255
- active: true,
256
- block: blocks[0],
257
- continuation: false,
258
- continues: false,
259
- estimate: 0,
260
- estimated: -1,
261
- height: 0,
262
- measured: -1,
263
- raw: false
264
- };
259
+ whole: { layout: EditorLayout; projection: NativeText; revision: number } | null = null;
265
260
 
266
261
  let draw = renderer({
267
262
  fold: (unit) => {
@@ -269,18 +264,23 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
269
264
  },
270
265
  foldable: opens,
271
266
  inline: inlineOf,
272
- press,
273
267
  readonly: () => ui.readonly,
274
268
  shown: (element, unit) => {
275
269
  if (unit) {
270
+ elements.set(unit, element);
276
271
  shown.set(element, unit);
277
-
278
272
  resize?.observe(element);
273
+ return;
279
274
  }
280
- else {
281
- shown.delete(element);
282
- resize?.unobserve(element);
275
+
276
+ let previous = shown.get(element);
277
+
278
+ if (previous && elements.get(previous) === element) {
279
+ elements.delete(previous);
283
280
  }
281
+
282
+ shown.delete(element);
283
+ resize?.unobserve(element);
284
284
  },
285
285
  task: (block) => {
286
286
  if (!disposed && !options.readonly && !ime.busy() && block.task) {
@@ -293,22 +293,30 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
293
293
  }
294
294
  });
295
295
 
296
+ // The input method writes into the drawn text itself; once it ends, its text replaces what it was typed over and
297
+ // the block draws again, so none of the nodes it made stay behind.
296
298
  let ime = composition({
297
299
  begin: () => {
298
300
  capture();
299
- beforeRanges = model.selections;
300
- beforeSelection = model.selection;
301
+ composed = '';
302
+ composing = { ...model.selection };
301
303
  model.breakHistory();
302
304
  },
303
305
  commit: (conflict) => {
304
- if (conflict) {
305
- reset();
306
- resync();
307
- }
308
- else {
309
- accept(true);
306
+ let range = composing;
307
+
308
+ composing = null;
309
+
310
+ if (range && !conflict && composed && !options.readonly) {
311
+ model.replace(range.start, range.end, composed, {
312
+ selection: { start: range.start + composed.length },
313
+ source: 'composition'
314
+ });
310
315
  }
311
316
 
317
+ composed = '';
318
+ invalidate(range?.start ?? model.selection.start);
319
+
312
320
  if (pendingValue !== undefined) {
313
321
  let value = pendingValue;
314
322
 
@@ -351,11 +359,7 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
351
359
  });
352
360
 
353
361
  let map = minimap({
354
- focus: () => {
355
- if (editing) {
356
- textarea?.focus({ preventScroll: true });
357
- }
358
- },
362
+ focus: () => content?.focus({ preventScroll: true }),
359
363
  scroll: (top, relative) => {
360
364
  scrollTo(relative ? metrics.scrollTop + top : top);
361
365
  }
@@ -381,6 +385,9 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
381
385
 
382
386
  let size = observeSize(() => measure());
383
387
 
388
+ // Links place the caret on a plain click, so resting on one says how to open it.
389
+ let tip = tooltip.shared({ delay: { open: LINK_DELAY } });
390
+
384
391
  let source: Source = {
385
392
  breaks: () => NONE,
386
393
  get count() {
@@ -426,115 +433,29 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
426
433
  }
427
434
  };
428
435
 
429
- // Applies a native input to the document. 'beforeinput' recorded the field's selection and length, so most inputs
430
- // resolve without a diff; the rest diff the field's value.
431
- function accept(composed = false) {
432
- if (disposed || !textarea || !active) {
433
- return;
434
- }
435
-
436
- if (options.readonly) {
437
- reset();
438
- resync();
439
- return;
440
- }
441
-
442
- let after = { end: textarea.selectionEnd, length: textarea.textLength, start: textarea.selectionStart },
443
- base = active.from,
444
- direction = textarea.selectionDirection,
445
- native = before && !composed ? inputEdit(before, after, inputType, inputData) : null,
446
- ranges = beforeRanges.length ? beforeRanges : model.selections,
447
- value = textarea.value,
448
- edit: Edit;
449
-
450
- if (native) {
451
- edit = projection.toSourceEdit(model.value, native.from, native.to, native.insert);
452
- }
453
- else if (value === projection.value) {
454
- reset();
455
- capture();
456
- return;
457
- }
458
- else {
459
- let selection = beforeSelection
460
- ? { end: clamp(beforeSelection.end - base, 0, fieldSource.length), start: clamp(beforeSelection.start - base, 0, fieldSource.length) }
461
- : undefined;
462
-
463
- edit = projection.edit(model.value, value, selection, inputType);
464
- }
465
-
466
- // A native edit the projection can't map would remove text the field doesn't show; the field goes back.
467
- if (edit.from === edit.to && !edit.insert) {
468
- reset();
469
- resync();
470
- return;
471
- }
472
-
473
- let group = !composed && /^(deleteContentBackward|deleteContentForward|insertText)$/.test(inputType) ? inputType : undefined,
474
- origin = composed ? 'composition' : 'input';
475
-
476
- written = value;
477
-
478
- if (ranges.length > 1) {
479
- reset();
480
- insertText(model, edit.insert, origin);
481
- return;
482
- }
483
-
484
- let from = base + edit.from,
485
- caret = native
486
- ? { direction, end: from + edit.insert.length, start: from + edit.insert.length }
487
- : caretAfter(edit, after, direction);
488
-
489
- reset();
490
- model.replace(from, base + edit.to, edit.insert, { group, selection: caret, source: origin });
491
- }
492
-
493
- // Shows the field for the selection.
494
- function activate(focus: boolean) {
495
- if (disposed) {
496
- return;
497
- }
498
-
499
- editing = true;
500
- ui.editing = true;
501
- refresh();
502
- schedule();
503
-
504
- if (focus && textarea && host?.ownerDocument.activeElement !== textarea) {
505
- // The field must be displayed before it can take focus.
506
- flush();
507
- textarea.focus({ preventScroll: true });
508
- }
509
- }
510
-
511
436
  // Takes over from the previous document: whatever was derived from it goes, and this one's view comes back as
512
437
  // this editor left it, folds only while its text is unchanged since.
513
438
  function adopt() {
514
439
  let memo = visited.get(model),
515
440
  { selections, ...single } = model.state;
516
441
 
517
- reset();
518
- active = null;
519
- activeIndex = -1;
520
442
  blocks = parseMarkdown(model);
521
- activeUnit.block = blocks[0];
522
- drag = null;
443
+ composed = '';
444
+ composing = null;
523
445
  dropAt = null;
524
446
  escapeTab = false;
525
- fieldSource = '';
526
447
  folds = memo?.revision === model.revision ? memo.folds : [];
527
- geometry = null;
528
448
  goals = [];
529
449
  length = model.value.length;
530
450
  metrics.scrollLeft = memo?.left ?? 0;
531
451
  metrics.scrollTop = memo?.top ?? 0;
532
452
  parsed = model.revision;
533
453
  pendingValue = undefined;
534
- projection = new NativeText('');
454
+ placed = null;
535
455
  secondary = selections.length > 1;
456
+ ui.empty = !model.value;
457
+ unitsDirty = true;
536
458
  whole = null;
537
- written = '';
538
459
  state.selections.splice(0, state.selections.length, ...selections.map((range) => ({ ...range })));
539
460
  Object.assign(state, single, { selection: { ...single.selection } });
540
461
  gestures.reset();
@@ -549,21 +470,45 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
549
470
  search.result();
550
471
  }
551
472
 
473
+ // Reads the native selection back into the document, unless it is the one this view placed or the document's is
474
+ // still waiting to be placed.
552
475
  function capture() {
553
- if (disposed || !textarea || !active || !editing || ime.busy() || writing || textarea.value !== written) {
476
+ if (disposed || !content || ime.busy() || unplaced) {
477
+ return;
478
+ }
479
+
480
+ let selection = content.ownerDocument.getSelection(),
481
+ anchorNode = selection?.anchorNode,
482
+ focusNode = selection?.focusNode;
483
+
484
+ if (!selection || !anchorNode || !focusNode || !content.contains(anchorNode) || !content.contains(focusNode)) {
485
+ return;
486
+ }
487
+
488
+ if (
489
+ placed &&
490
+ placed.anchor.node === anchorNode &&
491
+ placed.anchor.offset === selection.anchorOffset &&
492
+ placed.focus.node === focusNode &&
493
+ placed.focus.offset === selection.focusOffset
494
+ ) {
495
+ return;
496
+ }
497
+
498
+ let anchor = sourceAt(anchorNode, selection.anchorOffset),
499
+ head = sourceAt(focusNode, selection.focusOffset);
500
+
501
+ if (anchor === null || head === null) {
554
502
  return;
555
503
  }
556
504
 
557
- let base = active.from,
505
+ let primary = model.selection,
558
506
  next: Selection = {
559
- direction: textarea.selectionDirection,
560
- end: base + projection.toSource(textarea.selectionEnd),
561
- start: base + projection.toSource(textarea.selectionStart)
507
+ direction: head < anchor ? 'backward' : 'forward',
508
+ end: Math.max(anchor, head),
509
+ start: Math.min(anchor, head)
562
510
  };
563
511
 
564
- let primary = model.selection;
565
-
566
- // A field reports a direction even for a caret, and 'forward' where the document has none.
567
512
  if (
568
513
  next.start === primary.start &&
569
514
  next.end === primary.end &&
@@ -575,15 +520,7 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
575
520
  model.selectMany([next, ...model.selections.slice(1)]);
576
521
  }
577
522
 
578
- function caretAfter(edit: Edit, after: Before, direction: Selection['direction']) {
579
- let base = active!.from,
580
- next = new NativeText(fieldSource.slice(0, edit.from) + edit.insert + fieldSource.slice(edit.to));
581
-
582
- return { direction, end: base + next.toSource(after.end), start: base + next.toSource(after.start) };
583
- }
584
-
585
- // Client rectangle of a source offset: the field's geometry inside it, the drawn text elsewhere, or else the
586
- // unit's top.
523
+ // Client rectangle of a source offset: the drawn text there, or else the unit's top.
587
524
  function clientRect(offset: number): Rect | null {
588
525
  if (!surface) {
589
526
  return null;
@@ -591,21 +528,6 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
591
528
 
592
529
  current();
593
530
 
594
- let box = surface.getBoundingClientRect(),
595
- originX = box.left + surface.clientLeft - surface.scrollLeft,
596
- originY = box.top + surface.clientTop - surface.scrollTop;
597
-
598
- if (active && editing && geometry && offset >= active.from && offset <= active.to) {
599
- let rect = geometry.rect(projection.toNative(offset - active.from));
600
-
601
- return {
602
- height: rect.height,
603
- left: originX + fieldLeft() + rect.left,
604
- top: originY + metrics.padTop + layout.top(activeIndex) + rect.top,
605
- width: rect.width
606
- };
607
- }
608
-
609
531
  let index = layout.indexOf(offset),
610
532
  unit = layout.units[index];
611
533
 
@@ -619,7 +541,14 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
619
541
  return rects[0];
620
542
  }
621
543
 
622
- return { height: metrics.lineHeight, left: originX + metrics.padLeft, top: originY + metrics.padTop + layout.top(index), width: 1 };
544
+ let box = surface.getBoundingClientRect();
545
+
546
+ return {
547
+ height: metrics.lineHeight,
548
+ left: box.left + surface.clientLeft - surface.scrollLeft + metrics.padLeft,
549
+ top: box.top + surface.clientTop - surface.scrollTop + metrics.padTop + layout.top(index),
550
+ width: 1
551
+ };
623
552
  }
624
553
 
625
554
  function clip(e: ClipboardEvent, cut: boolean) {
@@ -641,13 +570,12 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
641
570
  }
642
571
 
643
572
  function connect() {
644
- if (disposed || !host || !ruler || !surface || !textarea || unsubscribe) {
573
+ if (disposed || !content || !host || !ruler || !surface || unsubscribe) {
645
574
  return;
646
575
  }
647
576
 
648
577
  measure();
649
578
  subscribe();
650
- refresh();
651
579
  unitsDirty = true;
652
580
  schedule();
653
581
  void host.ownerDocument.fonts?.ready.then(() => {
@@ -670,27 +598,12 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
670
598
  }
671
599
  }
672
600
 
673
- // Hides the field; every block shows formatted again.
674
- function deactivate() {
675
- if (!editing) {
676
- return;
677
- }
678
-
679
- ime.flush();
680
- editing = false;
681
- drag = null;
682
- ui.editing = false;
683
- gestures.reset();
684
- refresh();
685
- schedule();
686
- }
687
-
688
601
  function decorate() {
689
602
  let items: Decoration[] = [],
690
603
  ranges = model.selections,
691
604
  matches = search.state.open || search.state.query ? search.result() : null;
692
605
 
693
- if (ranges.length < 2 && !drag && dropAt === null && !matches?.matches.length) {
606
+ if (ranges.length < 2 && dropAt === null && !matches?.matches.length) {
694
607
  if (decorations.length) {
695
608
  decorations.splice(0, decorations.length);
696
609
  }
@@ -743,10 +656,6 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
743
656
  mark(head, head, 'markdown-editor-mark markdown-editor-mark--caret');
744
657
  }
745
658
 
746
- if (drag) {
747
- mark(Math.min(drag.anchor, drag.head), Math.max(drag.anchor, drag.head), 'markdown-editor-mark markdown-editor-mark--selection');
748
- }
749
-
750
659
  if (dropAt !== null) {
751
660
  mark(dropAt, dropAt, 'markdown-editor-mark markdown-editor-mark--caret');
752
661
  }
@@ -765,102 +674,152 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
765
674
  }
766
675
  }
767
676
 
768
- function editable(run: () => boolean) {
769
- if (disposed || options.readonly || ime.busy()) {
770
- return false;
677
+ // The native point standing for a source offset: in the drawn text holding it, or after the text before it when it
678
+ // falls on markup. Offsets in units that aren't drawn go to the nearest drawn edge. Null while the unit's element
679
+ // has yet to connect.
680
+ function domAt(offset: number): Point | null {
681
+ current();
682
+
683
+ let unit = layout.units[layout.indexOf(offset)];
684
+
685
+ if (!unit || !draw.segments(unit)) {
686
+ let first = rendered[0],
687
+ last = rendered[rendered.length - 1];
688
+
689
+ if (!first || !last) {
690
+ return null;
691
+ }
692
+
693
+ if (offset < unitStart(first)) {
694
+ unit = first;
695
+ offset = contentStart(first);
696
+ }
697
+ else {
698
+ unit = last;
699
+ offset = contentEnd(model.value, last.block);
700
+ }
771
701
  }
772
702
 
773
- capture();
774
- revealFolds(true);
703
+ let element = elements.get(unit),
704
+ segments = draw.segments(unit) ?? [],
705
+ after: Point | null = null,
706
+ before: Point | null = null;
775
707
 
776
- let changed = run();
708
+ for (let i = 0, n = segments.length; i < n; i++) {
709
+ let segment = segments[i],
710
+ at = segment.block.from + segment.from,
711
+ end = at + segment.node.length;
777
712
 
778
- start(true);
713
+ if (at <= offset && offset <= end) {
714
+ return { node: segment.node, offset: offset - at };
715
+ }
779
716
 
780
- return changed;
781
- }
717
+ if (end < offset) {
718
+ before = { node: segment.node, offset: segment.node.length };
719
+ }
720
+ else {
721
+ after = { node: segment.node, offset: 0 };
722
+ break;
723
+ }
724
+ }
782
725
 
783
- // Lays out the field's text and sizes it; the field's styles must match what this assumes.
784
- function fieldGeometry() {
785
- if (!active) {
786
- geometry = null;
787
- return;
726
+ if (before || after) {
727
+ return before ?? after;
788
728
  }
789
729
 
790
- let block = blocks[active.first],
791
- scale = headingScale(block.level),
792
- fontSize = metrics.fontSize * scale,
793
- lineHeight = block.level && block.level <= 2 ? fontSize * SCALED_LINE : metrics.lineHeight,
794
- wrap = options.wrap !== false,
795
- width = Math.max(1, metrics.width - block.quoteDepth * metrics.quoteIndent);
730
+ if (!element) {
731
+ return null;
732
+ }
796
733
 
797
- geometry = new EditorLayout(projection, fieldSource, wrap ? width : Number.MAX_SAFE_INTEGER, lineHeight, metrics.charWidth * scale, options.tabSize ?? 4, wrap);
798
- estimated = false;
734
+ // No text: on the line break that gives the caret its line.
735
+ let children = element.childNodes;
799
736
 
800
- if (wrap) {
801
- for (let i = 0, n = geometry.count; i < n && !estimated; i++) {
802
- estimated = geometry.estimated(i);
737
+ for (let i = 0, n = children.length; i < n; i++) {
738
+ if (children[i].nodeName === 'BR') {
739
+ return { node: element, offset: i };
803
740
  }
804
741
  }
805
742
 
806
- let height = Math.max(lineHeight, geometry.height),
807
- widest = 0;
743
+ return { node: element, offset: children.length };
744
+ }
808
745
 
809
- if (!wrap) {
810
- for (let i = 0, n = geometry.count; i < n; i++) {
811
- widest = Math.max(widest, geometry.end(i) - geometry.lineFrom(i));
812
- }
746
+ // Source offset at the drawn units' edge nearest a point outside them: on the content root or a spacer.
747
+ function edgeAt(node: Node, offset: number) {
748
+ if (!content) {
749
+ return null;
813
750
  }
814
751
 
815
- ui.height = height;
816
- ui.input = `markdown-editor-input markdown-editor-input--${block.kind}${block.level ? ` markdown-editor-input--h${block.level}` : ''}${block.quoteDepth ? ' markdown-editor-input--quoted' : ''}`;
817
- ui.style = `--depth: ${block.quoteDepth};${wrap ? '' : ` --field-width: ${Math.ceil((widest + 1) * metrics.charWidth * scale)}px;`}`;
752
+ let child: Node | null = node === content ? content.childNodes[offset] ?? null : node;
818
753
 
819
- layout.measure(activeUnit, height);
820
- }
754
+ while (child && child.parentNode !== content) {
755
+ child = child.parentNode;
756
+ }
821
757
 
822
- // Left edge of the field's text in the surface's padding box.
823
- function fieldLeft() {
824
- return metrics.padLeft + (active ? blocks[active.first].quoteDepth * metrics.quoteIndent : 0);
825
- }
758
+ for (let at = child; at; at = at.nextSibling) {
759
+ let unit = shown.get(at as Element);
826
760
 
827
- // Client rectangles of [from, to) inside the field, one per row, from its geometry.
828
- function fieldRects(from: number, to: number, out: Rect[]) {
829
- if (!active || !geometry || !surface) {
830
- return;
761
+ if (unit) {
762
+ return contentStart(unit);
763
+ }
831
764
  }
832
765
 
833
- let box = surface.getBoundingClientRect(),
834
- x = box.left + surface.clientLeft - surface.scrollLeft + fieldLeft(),
835
- y = box.top + surface.clientTop - surface.scrollTop + metrics.padTop + layout.top(activeIndex),
836
- end = geometry.rect(projection.toNative(clamp(to, active.from, active.to) - active.from)),
837
- start = geometry.rect(projection.toNative(clamp(from, active.from, active.to) - active.from)),
838
- lineHeight = geometry.lineHeight;
766
+ for (let at = child ? child.previousSibling : content.lastChild; at; at = at.previousSibling) {
767
+ let unit = shown.get(at as Element);
839
768
 
840
- if (from === to) {
841
- out.push({ height: lineHeight, left: x + start.left, top: y + start.top, width: 1 });
842
- return;
769
+ if (unit) {
770
+ return contentEnd(model.value, unit.block);
771
+ }
843
772
  }
844
773
 
845
- for (let top = start.top; top <= end.top && out.length < DECORATIONS; top += lineHeight) {
846
- let left = top === start.top ? start.left : 0,
847
- right = top === end.top ? end.left : geometry.width;
774
+ return null;
775
+ }
848
776
 
849
- out.push({ height: lineHeight, left: x + left, top: y + top, width: Math.max(1, right - left) });
777
+ function editable(run: () => boolean) {
778
+ if (disposed || options.readonly || ime.busy()) {
779
+ return false;
850
780
  }
781
+
782
+ capture();
783
+ revealFolds(true);
784
+ revealing = true;
785
+
786
+ let changed = run();
787
+
788
+ schedule();
789
+
790
+ return changed;
851
791
  }
852
792
 
853
- // Applies a drag or click on the drawn text once the pointer comes back up.
854
- function finish() {
855
- if (!drag) {
856
- return;
793
+ // Backspace or Delete at a single selection: a selection goes, a caret takes the visible character beside it,
794
+ // and at a block's edge the blocks join, or the empty line or rule beside it goes.
795
+ function erase(backward: boolean) {
796
+ if (model.selections.length > 1) {
797
+ return backward ? markdownCommand(model, 'backspace', kindAt) : deleteCharacter(model, false);
798
+ }
799
+
800
+ current();
801
+
802
+ let { end, start } = model.selection;
803
+
804
+ if (start !== end) {
805
+ return remove({ from: start, to: end });
857
806
  }
858
807
 
859
- let { anchor, head } = drag;
808
+ let index = blockAt(blocks, start),
809
+ block = blocks[index],
810
+ unit = layout.units[layout.indexOf(start)],
811
+ span = unit && draw.segments(unit) ? reach(model.value, spans(unit), start, backward) : null;
812
+
813
+ if (span) {
814
+ // The info string and the code under it never merge, nor does code run into a fence.
815
+ if ((block.kind === 'fence' || block.kind === 'frontmatter') && span.from < block.contentFrom && span.to >= block.contentFrom) {
816
+ return false;
817
+ }
818
+
819
+ return remove(span);
820
+ }
860
821
 
861
- drag = null;
862
- model.select({ direction: head < anchor ? 'backward' : 'forward', end: Math.max(anchor, head), start: Math.min(anchor, head) });
863
- activate(true);
822
+ return backward ? joinBackward(index, start) : joinForward(index, start);
864
823
  }
865
824
 
866
825
  function inlineOf(block: MarkdownBlock) {
@@ -880,34 +839,79 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
880
839
  return entry.tokens;
881
840
  }
882
841
 
883
- // The block kind at a source offset, for commands that leave literal source alone.
884
- function kindAt(offset: number): Kind | undefined {
885
- return blocks[blockAt(blocks, offset)]?.kind;
842
+ // Drops a block's drawing, for when something other than this view wrote into it.
843
+ function invalidate(offset: number) {
844
+ let block = blocks[blockAt(blocks, offset)];
845
+
846
+ if (block) {
847
+ units.folded.delete(block);
848
+ units.plain.delete(block);
849
+ units.raw.delete(block);
850
+ }
851
+
852
+ unitsDirty = true;
853
+ schedule();
886
854
  }
887
855
 
888
- // Whether a plain arrow key at the field's edge leaves it for the neighbouring block.
889
- function leaves(key: string) {
890
- if (!active || !geometry || !textarea) {
856
+ // Backspace at the start of a block's text: a list or quote marker goes first, then a heading's, then the break to
857
+ // the block before.
858
+ function joinBackward(index: number, offset: number) {
859
+ if (markdownBackspace(model)) {
860
+ return true;
861
+ }
862
+
863
+ let block = blocks[index],
864
+ previous = blocks[index - 1];
865
+
866
+ if (block.kind === 'heading' && block.contentFrom > block.from && offset <= block.contentFrom) {
867
+ let from = block.from + quotePrefix(model.lineText(model.lineAt(block.from))).length;
868
+
869
+ return model.replace(from, block.contentFrom, '', { selection: { start: from }, source: 'markdown-unwrap' });
870
+ }
871
+
872
+ if (!previous) {
891
873
  return false;
892
874
  }
893
875
 
894
- let head = textarea.selectionDirection === 'backward' ? textarea.selectionStart : textarea.selectionEnd;
876
+ if (previous.kind === 'blank' || previous.kind === 'rule') {
877
+ return model.replace(previous.from, previous.to, '', {
878
+ selection: { start: offset - (previous.to - previous.from) },
879
+ source: 'delete'
880
+ });
881
+ }
895
882
 
896
- if (key === 'ArrowLeft') {
897
- return head === 0 && active.from > 0;
883
+ if (JOINABLE.has(previous.kind) && (JOINABLE.has(block.kind) || block.kind === 'blank')) {
884
+ return model.replace(previous.contentTo, offset, '', { selection: { start: previous.contentTo }, source: 'delete' });
898
885
  }
899
886
 
900
- if (key === 'ArrowRight') {
901
- return head === textarea.textLength && active.to < model.value.length;
887
+ model.select({ start: previous.contentTo });
888
+
889
+ return false;
890
+ }
891
+
892
+ // Delete at the end of a block's text: the empty line or rule after it goes, or the next block's text joins it.
893
+ function joinForward(index: number, offset: number) {
894
+ let block = blocks[index],
895
+ next = blocks[index + 1];
896
+
897
+ if (!next) {
898
+ return false;
902
899
  }
903
900
 
904
- let rect = geometry.rect(head);
901
+ if (next.kind === 'blank' || next.kind === 'rule') {
902
+ return model.replace(next.from, next.to, '', { selection: { start: offset }, source: 'delete' });
903
+ }
905
904
 
906
- if (key === 'ArrowUp') {
907
- return rect.top < geometry.lineHeight / 2 && active.from > 0;
905
+ if (JOINABLE.has(next.kind) && (JOINABLE.has(block.kind) || block.kind === 'blank')) {
906
+ return model.replace(offset, next.contentFrom, '', { selection: { start: offset }, source: 'delete' });
908
907
  }
909
908
 
910
- return rect.top + geometry.lineHeight >= geometry.height - 0.5 && active.to < model.value.length;
909
+ return false;
910
+ }
911
+
912
+ // The block kind at a source offset, for commands that leave literal source alone.
913
+ function kindAt(offset: number): Kind | undefined {
914
+ return blocks[blockAt(blocks, offset)]?.kind;
911
915
  }
912
916
 
913
917
  // Source lines [first, last] unit 'index' covers.
@@ -919,12 +923,18 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
919
923
  return { first: model.lineAt(from), last: model.lineAt(Math.max(from, to)) };
920
924
  }
921
925
 
922
- // Marks which drawn units in [from, to) show a fold marker; only neighbouring blocks are looked at.
923
- function markFoldable(from: number, to: number) {
924
- let units = layout.units;
926
+ function linking(e: KeyboardEvent | PointerEvent) {
927
+ let next = apple ? e.metaKey : e.ctrlKey;
928
+
929
+ if (ui.linking !== next) {
930
+ ui.linking = next;
931
+ }
932
+ }
925
933
 
926
- for (let i = from; i < to; i++) {
927
- let unit = units[i],
934
+ // Marks which drawn units show a fold marker; only neighbouring blocks are looked at.
935
+ function markFoldable() {
936
+ for (let i = 0, n = rendered.length; i < n; i++) {
937
+ let unit = rendered[i],
928
938
  state = draw.state(unit);
929
939
 
930
940
  if (!state) {
@@ -941,7 +951,7 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
941
951
 
942
952
  // Reads computed geometry and colors on connect, resize, font load or refresh; never per frame.
943
953
  function measure() {
944
- if (disposed || !host || !ruler || !surface || !textarea) {
954
+ if (disposed || !host || !ruler || !surface) {
945
955
  return;
946
956
  }
947
957
 
@@ -980,7 +990,6 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
980
990
  width: metrics.width
981
991
  })) {
982
992
  whole = null;
983
- fieldGeometry();
984
993
  map.invalidate();
985
994
  unitsDirty = true;
986
995
  }
@@ -988,17 +997,39 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
988
997
  schedule();
989
998
  }
990
999
 
1000
+ // A single caret moves through the drawn text the way the browser lays it out; several move by source.
991
1001
  function moveSelection(key: string, extend: boolean, word = false, add = false, boundary = false) {
1002
+ let native = content?.ownerDocument.getSelection();
1003
+
1004
+ if (
1005
+ native &&
1006
+ model.selections.length === 1 &&
1007
+ !add &&
1008
+ !key.startsWith('Page') &&
1009
+ content?.ownerDocument.activeElement === content
1010
+ ) {
1011
+ let vertical = VERTICAL.test(key);
1012
+
1013
+ native.modify(
1014
+ extend ? 'extend' : 'move',
1015
+ key === 'ArrowLeft' || key === 'ArrowUp' || key === 'Home' ? 'backward' : 'forward',
1016
+ vertical ? 'line' : key === 'Home' || key === 'End' ? (boundary ? 'documentboundary' : 'lineboundary') : word ? 'word' : 'character'
1017
+ );
1018
+ capture();
1019
+ start(true);
1020
+ return;
1021
+ }
1022
+
992
1023
  if (!VERTICAL.test(key) || boundary || add) {
993
1024
  goals = [];
994
1025
  }
995
1026
 
996
1027
  if (!whole || whole.revision !== model.revision) {
997
- let native = new NativeText(model.value);
1028
+ let text = new NativeText(model.value);
998
1029
 
999
1030
  whole = {
1000
- layout: new EditorLayout(native, model.value, Number.MAX_SAFE_INTEGER, metrics.lineHeight, metrics.charWidth, options.tabSize ?? 4, false),
1001
- projection: native,
1031
+ layout: new EditorLayout(text, model.value, Number.MAX_SAFE_INTEGER, metrics.lineHeight, metrics.charWidth, options.tabSize ?? 4, false),
1032
+ projection: text,
1002
1033
  revision: model.revision
1003
1034
  };
1004
1035
  }
@@ -1018,7 +1049,7 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1018
1049
  }
1019
1050
 
1020
1051
  function offsetAt(clientX: number, clientY: number) {
1021
- if (disposed || !surface) {
1052
+ if (disposed || !surface || !host) {
1022
1053
  return null;
1023
1054
  }
1024
1055
 
@@ -1030,22 +1061,22 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1030
1061
  return null;
1031
1062
  }
1032
1063
 
1033
- if (active && editing && textarea && geometry) {
1034
- let field = textarea.getBoundingClientRect();
1035
-
1036
- if (clientY >= field.top && clientY < field.bottom) {
1037
- return active.from + projection.toSource(geometry.offset(clientX - field.left + textarea.scrollLeft, clientY - field.top));
1038
- }
1039
- }
1064
+ let doc = host.ownerDocument as Document & {
1065
+ caretPositionFromPoint?: (x: number, y: number) => { offset: number; offsetNode: Node } | null;
1066
+ },
1067
+ point = doc.caretPositionFromPoint?.(clientX, clientY),
1068
+ range = point ? null : doc.caretRangeFromPoint?.(clientX, clientY),
1069
+ node = point?.offsetNode ?? range?.startContainer ?? null;
1040
1070
 
1041
- let point = pointAt(clientX, clientY);
1071
+ if (node && content?.contains(node)) {
1072
+ let offset = sourceAt(node, point?.offset ?? range?.startOffset ?? 0);
1042
1073
 
1043
- if (point !== null) {
1044
- return point;
1074
+ if (offset !== null) {
1075
+ return offset;
1076
+ }
1045
1077
  }
1046
1078
 
1047
- // Off the text (a marker, padding, an image): the unit under the point.
1048
- let under = host!.ownerDocument.elementFromPoint(clientX, clientY)?.closest('.markdown-editor-block'),
1079
+ let under = doc.elementFromPoint(clientX, clientY)?.closest('.markdown-editor-block'),
1049
1080
  unit = under && shown.get(under);
1050
1081
 
1051
1082
  if (unit) {
@@ -1059,7 +1090,7 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1059
1090
 
1060
1091
  // Whether a drawn unit shows a fold marker; only neighbouring blocks are looked at.
1061
1092
  function opens(unit: Unit) {
1062
- return options.fold !== false && !unit.continuation && !unit.raw && !unit.fold && foldable(blocks, blockAt(blocks, unit.block.from), model.value);
1093
+ return !!options.fold && !unit.continuation && !unit.raw && !unit.fold && foldable(blocks, blockAt(blocks, unit.block.from), model.value);
1063
1094
  }
1064
1095
 
1065
1096
  function paint() {
@@ -1071,52 +1102,17 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1071
1102
 
1072
1103
  current();
1073
1104
 
1074
- if (estimated && textarea && editing) {
1075
- // The arithmetic only estimated some of the field's rows; it takes the height the DOM drew instead.
1076
- flush();
1077
-
1078
- let height = textarea.scrollHeight;
1079
-
1080
- if (height > ui.height + 0.5) {
1081
- ui.height = height;
1082
- layout.measure(activeUnit, height);
1083
- }
1084
-
1085
- estimated = false;
1086
- }
1087
-
1088
1105
  if (revealing) {
1089
1106
  revealing = false;
1090
1107
  reveal();
1091
1108
  }
1092
1109
 
1093
- let n = layout.units.length,
1094
- a = activeIndex,
1095
- view = layout.window(metrics.scrollTop - metrics.padTop, metrics.clientHeight),
1096
- split = a < 0 ? n : a,
1097
- s1 = Math.min(view.start, split),
1098
- e1 = Math.min(view.end, split),
1099
- s2 = Math.max(view.start, split + 1),
1100
- e2 = Math.max(view.end, s2);
1101
-
1102
- if (a < 0) {
1103
- s2 = e2 = n;
1104
- }
1105
-
1106
- if (view.start > split) {
1107
- s1 = e1 = split;
1110
+ // An input method owns the drawn text until it commits.
1111
+ if (!ime.busy()) {
1112
+ present();
1108
1113
  }
1109
1114
 
1110
- assign(listed.before, layout.units.slice(s1, e1));
1111
- assign(listed.after, layout.units.slice(s2, e2));
1112
- ui.top = layout.top(s1);
1113
- ui.between = a < 0 ? 0 : layout.top(split) - layout.top(e1);
1114
- ui.next = a < 0 ? 0 : layout.top(s2) - layout.top(a + 1);
1115
- ui.bottom = layout.total - layout.top(a < 0 ? e1 : e2);
1116
- ui.foldable = !!active && options.fold !== false && foldable(blocks, active.first, model.value);
1117
-
1118
- markFoldable(s1, e1);
1119
- markFoldable(s2, e2);
1115
+ markFoldable();
1120
1116
  // Rectangles come from the drawn text; units this frame added draw in the list's own pass, queued before.
1121
1117
  queueMicrotask(() => {
1122
1118
  if (!disposed) {
@@ -1140,67 +1136,104 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1140
1136
  }
1141
1137
  }
1142
1138
 
1143
- // Source offset under a client point from the text node there.
1144
- function pointAt(clientX: number, clientY: number) {
1145
- let doc = host!.ownerDocument as Document & {
1146
- caretPositionFromPoint?: (x: number, y: number) => { offset: number; offsetNode: Node } | null;
1147
- },
1148
- point = doc.caretPositionFromPoint?.(clientX, clientY),
1149
- range = point ? null : doc.caretRangeFromPoint?.(clientX, clientY);
1139
+ // Puts the native selection where the document's is, unless it already maps there: moving it anyway would lose
1140
+ // the column the browser keeps for vertical moves.
1141
+ function place() {
1142
+ unplaced = false;
1150
1143
 
1151
- return draw.offset(point?.offsetNode ?? range?.startContainer ?? null, point?.offset ?? range?.startOffset ?? 0);
1152
- }
1153
-
1154
- // Draws the window at the scroll position now rather than next frame, so the surface is as tall as its content
1155
- // before the position applies.
1156
- function present() {
1157
- if (disposed || !surface) {
1144
+ if (disposed || !content || ime.busy() || content.ownerDocument.activeElement !== content) {
1158
1145
  return;
1159
1146
  }
1160
1147
 
1161
- if (frame) {
1162
- cancelAnimationFrame(frame);
1148
+ let native = content.ownerDocument.getSelection();
1149
+
1150
+ if (!native) {
1151
+ return;
1163
1152
  }
1164
1153
 
1165
- paint();
1166
- flush();
1167
- surface.scrollLeft = metrics.scrollLeft;
1168
- surface.scrollTop = metrics.scrollTop;
1169
- }
1154
+ let selection = model.selection,
1155
+ backward = selection.direction === 'backward',
1156
+ anchorAt = backward ? selection.end : selection.start,
1157
+ headAt = backward ? selection.start : selection.end;
1170
1158
 
1171
- // A press on a drawn unit places the caret where it lands, in the same gesture: nothing redraws until the
1172
- // pointer comes back up, so the press and release meet the same nodes.
1173
- function press(e: MouseEvent, unit: Unit | null) {
1174
- if (disposed || ime.busy() || e.button !== 0 || e.altKey || e.defaultPrevented) {
1159
+ if (
1160
+ native.anchorNode &&
1161
+ native.focusNode &&
1162
+ content.contains(native.anchorNode) &&
1163
+ content.contains(native.focusNode) &&
1164
+ sourceAt(native.anchorNode, native.anchorOffset) === anchorAt &&
1165
+ sourceAt(native.focusNode, native.focusOffset) === headAt
1166
+ ) {
1175
1167
  return;
1176
1168
  }
1177
1169
 
1178
- let target = e.target as Element;
1170
+ let anchor = domAt(anchorAt),
1171
+ focus = anchorAt === headAt ? anchor : domAt(headAt);
1179
1172
 
1180
- if ((e.ctrlKey || e.metaKey) && target.closest('a')) {
1173
+ // A unit drawn this frame connects after it; the selection goes there next frame.
1174
+ if (!anchor || !focus) {
1175
+ unplaced = true;
1176
+ schedule();
1181
1177
  return;
1182
1178
  }
1183
1179
 
1184
- e.preventDefault();
1180
+ native.setBaseAndExtent(anchor.node, anchor.offset, focus.node, focus.offset);
1181
+ placed = { anchor, focus };
1182
+ unplaced = false;
1183
+ }
1184
+
1185
+ // Draws the units near the viewport and the ones holding the selection's ends, with spacers for the rest, then
1186
+ // puts the native selection back.
1187
+ function present() {
1188
+ let n = layout.units.length,
1189
+ view = layout.window(metrics.scrollTop - metrics.padTop, metrics.clientHeight),
1190
+ selection = model.selection,
1191
+ runs: Span[] = [];
1192
+
1193
+ if (n) {
1194
+ let anchor = layout.indexOf(selection.direction === 'backward' ? selection.end : selection.start),
1195
+ head = layout.indexOf(selection.direction === 'backward' ? selection.start : selection.end);
1196
+
1197
+ runs.push({ from: anchor, to: anchor + 1 }, { from: head, to: head + 1 }, { from: view.start, to: view.end });
1198
+ runs.sort((a, b) => a.from - b.from);
1199
+ }
1200
+
1201
+ let cursor = 0,
1202
+ gap = 0,
1203
+ next: Item[] = [];
1185
1204
 
1186
- let offset = pointAt(e.clientX, e.clientY) ?? (unit ? contentStart(unit) : offsetAt(e.clientX, e.clientY) ?? 0),
1187
- selection = model.selection;
1205
+ let space = (height: number) => {
1206
+ let item = gaps[gap++] ??= reactive({ height: 0 });
1188
1207
 
1189
- drag = {
1190
- anchor: e.shiftKey ? (selection.direction === 'backward' ? selection.end : selection.start) : offset,
1191
- head: offset
1208
+ item.height = height;
1209
+ next.push(item);
1192
1210
  };
1193
1211
 
1194
- if (pointerId >= 0 && surface?.isConnected) {
1195
- try {
1196
- surface.setPointerCapture(pointerId);
1212
+ for (let i = 0, m = runs.length; i < m; i++) {
1213
+ let { from, to } = runs[i];
1214
+
1215
+ from = Math.max(from, cursor);
1216
+
1217
+ if (to <= from) {
1218
+ continue;
1219
+ }
1220
+
1221
+ if (from > cursor || !next.length) {
1222
+ space(layout.top(from) - layout.top(cursor));
1197
1223
  }
1198
- catch {
1199
- // The pointer may already be gone.
1224
+
1225
+ for (let j = from; j < to; j++) {
1226
+ next.push(layout.units[j]);
1200
1227
  }
1228
+
1229
+ cursor = to;
1201
1230
  }
1202
1231
 
1203
- schedule();
1232
+ space(layout.total - layout.top(cursor));
1233
+ assign(items, next);
1234
+ rendered = next.filter((item): item is Unit => 'block' in item);
1235
+ flush();
1236
+ place();
1204
1237
  }
1205
1238
 
1206
1239
  function rangeRects(from: number, to: number) {
@@ -1210,17 +1243,10 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1210
1243
  last = layout.indexOf(to);
1211
1244
 
1212
1245
  for (let i = first; i <= last && out.length < DECORATIONS; i++) {
1213
- let unit = units[i];
1246
+ let rects = textRects(units[i], from, to);
1214
1247
 
1215
- if (unit === activeUnit) {
1216
- fieldRects(from, to, out);
1217
- }
1218
- else {
1219
- let rects = textRects(unit, from, to);
1220
-
1221
- for (let j = 0, n = rects.length; j < n; j++) {
1222
- out.push(rects[j]);
1223
- }
1248
+ for (let j = 0, n = rects.length; j < n; j++) {
1249
+ out.push(rects[j]);
1224
1250
  }
1225
1251
  }
1226
1252
 
@@ -1241,28 +1267,7 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1241
1267
  }
1242
1268
  }
1243
1269
 
1244
- if (active) {
1245
- activeUnit.block = blocks[active.first];
1246
- }
1247
-
1248
- let list = arrange(
1249
- blocks,
1250
- model.value,
1251
- options.fold === false ? [] : folds,
1252
- units,
1253
- active && editing ? { first: active.first, last: active.last, unit: activeUnit } : null,
1254
- raw ? (index) => raw.has(index) : null
1255
- );
1256
-
1257
- layout.set(list, model.value);
1258
- activeIndex = -1;
1259
-
1260
- if (active && editing) {
1261
- let index = layout.indexOf(active.from);
1262
-
1263
- activeIndex = list[index] === activeUnit ? index : list.indexOf(activeUnit);
1264
- }
1265
-
1270
+ layout.set(arrange(blocks, model.value, options.fold ? folds : [], units, raw ? (index) => raw.has(index) : null), model.value);
1266
1271
  unitsDirty = false;
1267
1272
  }
1268
1273
 
@@ -1289,85 +1294,18 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1289
1294
  unitsDirty = true;
1290
1295
  }
1291
1296
 
1292
- // Brings the field to the primary selection: which blocks it covers, its text, its selection and its size.
1293
- function refresh() {
1294
- if (disposed) {
1295
- return;
1296
- }
1297
+ // Removes source, widened to take any delimiters it leaves empty.
1298
+ function remove(span: Span) {
1299
+ let block = blocks[blockAt(blocks, span.from)];
1297
1300
 
1298
- if (!editing || !blocks.length) {
1299
- if (active) {
1300
- active = null;
1301
- geometry = null;
1302
- unitsDirty = true;
1303
- }
1304
-
1305
- return;
1306
- }
1307
-
1308
- let selection = model.selection,
1309
- first = blockAt(blocks, selection.start),
1310
- last = blockAt(blocks, selection.end) + 1;
1311
-
1312
- // A drag inside the field never shrinks it under the pointer.
1313
- if (pinned && active && active.last <= blocks.length) {
1314
- first = Math.min(first, active.first);
1315
- last = Math.max(last, active.last);
1316
- }
1317
-
1318
- let from = blocks[first].from,
1319
- to = Math.max(selection.end, contentEnd(model.value, blocks[last - 1]));
1320
-
1321
- if (!active || active.first !== first || active.last !== last) {
1322
- unitsDirty = true;
1323
- }
1324
-
1325
- active = { first, from, last, to };
1326
-
1327
- let text = model.value.slice(from, to),
1328
- shaped = fieldSource !== text || !geometry;
1329
-
1330
- if (fieldSource !== text) {
1331
- fieldSource = text;
1332
- projection = new NativeText(text);
1333
- }
1334
-
1335
- if (shaped || activeUnit.block !== blocks[first]) {
1336
- activeUnit.block = blocks[first];
1337
- fieldGeometry();
1301
+ if (block && JOINABLE.has(block.kind) && span.to <= block.to) {
1302
+ span = widen(model.value, inlineOf(block), block.from, span);
1338
1303
  }
1339
1304
 
1340
- if (!textarea || ime.busy()) {
1341
- return;
1342
- }
1343
-
1344
- if (written !== projection.value) {
1345
- writing = true;
1346
-
1347
- try {
1348
- writeField(textarea, written, projection.value, null);
1349
- }
1350
- finally {
1351
- writing = false;
1352
- }
1353
-
1354
- written = projection.value;
1355
- }
1356
-
1357
- let nativeEnd = projection.toNative(clamp(selection.end - from, 0, text.length)),
1358
- nativeStart = projection.toNative(clamp(selection.start - from, 0, text.length));
1359
-
1360
- if (
1361
- textarea.selectionStart !== nativeStart ||
1362
- textarea.selectionEnd !== nativeEnd ||
1363
- (nativeStart !== nativeEnd && selection.direction !== 'none' && textarea.selectionDirection !== selection.direction)
1364
- ) {
1365
- textarea.setSelectionRange(nativeStart, nativeEnd, selection.direction);
1366
- }
1305
+ return model.replace(span.from, span.to, '', { group: 'delete', selection: { start: span.from }, source: 'delete' });
1367
1306
  }
1368
1307
 
1369
- // Brings the blocks up to the document: only from the edited block until block boundaries line up again. True
1370
- // when only blocks the field stands for changed, so the units stay as they are.
1308
+ // Brings the blocks up to the document: only from the edited block until block boundaries line up again.
1371
1309
  function reparseBlocks() {
1372
1310
  let deltas = model.deltas(parsed),
1373
1311
  previous = length;
@@ -1379,11 +1317,11 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1379
1317
  blocks = parseMarkdown(model);
1380
1318
  map.invalidate();
1381
1319
  references();
1382
- return false;
1320
+ return;
1383
1321
  }
1384
1322
 
1385
1323
  if (!deltas.length) {
1386
- return true;
1324
+ return;
1387
1325
  }
1388
1326
 
1389
1327
  let end = -1,
@@ -1429,7 +1367,7 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1429
1367
  if (changed) {
1430
1368
  map.invalidate();
1431
1369
  references();
1432
- return false;
1370
+ return;
1433
1371
  }
1434
1372
 
1435
1373
  // Edits that kept the line count only redraw their lines in the minimap.
@@ -1439,20 +1377,6 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1439
1377
  else {
1440
1378
  map.invalidate(start, end);
1441
1379
  }
1442
-
1443
- return !!active &&
1444
- editing &&
1445
- splice.dropped.length === splice.inserted &&
1446
- splice.start >= active.first &&
1447
- splice.start + splice.inserted <= active.last;
1448
- }
1449
-
1450
- function reset() {
1451
- before = null;
1452
- beforeRanges = [];
1453
- beforeSelection = undefined;
1454
- inputData = null;
1455
- inputType = '';
1456
1380
  }
1457
1381
 
1458
1382
  // Heights the DOM drew for units. Units above the viewport that changed move the view by as much, so it stays on
@@ -1497,28 +1421,29 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1497
1421
  }
1498
1422
  }
1499
1423
 
1500
- // Writes the field again whatever it holds.
1501
- function resync() {
1502
- written = '';
1503
- refresh();
1504
- schedule();
1505
- }
1506
-
1507
1424
  // Scrolls the primary selection's head into view.
1508
1425
  function reveal() {
1509
1426
  let selection = model.selection,
1510
1427
  head = selection.direction === 'backward' ? selection.start : selection.end,
1511
- height = metrics.lineHeight,
1512
- y: number;
1428
+ index = layout.indexOf(head),
1429
+ unit = layout.units[index];
1430
+
1431
+ if (!unit || !surface) {
1432
+ return;
1433
+ }
1513
1434
 
1514
- if (active && editing && geometry && head >= active.from && head <= active.to) {
1515
- let rect = geometry.rect(projection.toNative(head - active.from));
1435
+ let height = Math.min(layout.height(unit), metrics.clientHeight),
1436
+ y = metrics.padTop + layout.top(index),
1437
+ rect = draw.segments(unit) ? textRects(unit, head, head)[0] : undefined;
1438
+
1439
+ if (rect) {
1440
+ let box = surface.getBoundingClientRect();
1516
1441
 
1517
1442
  height = rect.height;
1518
- y = metrics.padTop + layout.top(activeIndex) + rect.top;
1443
+ y = rect.top - box.top - surface.clientTop + surface.scrollTop;
1519
1444
 
1520
- if (options.wrap === false && surface) {
1521
- let x = fieldLeft() + rect.left;
1445
+ if (options.wrap === false) {
1446
+ let x = rect.left - box.left - surface.clientLeft + surface.scrollLeft;
1522
1447
 
1523
1448
  if (x < metrics.scrollLeft + metrics.padLeft) {
1524
1449
  surface.scrollLeft = metrics.scrollLeft = Math.max(0, x - metrics.padLeft);
@@ -1528,17 +1453,6 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1528
1453
  }
1529
1454
  }
1530
1455
  }
1531
- else {
1532
- let index = layout.indexOf(head),
1533
- unit = layout.units[index];
1534
-
1535
- if (!unit) {
1536
- return;
1537
- }
1538
-
1539
- y = metrics.padTop + layout.top(index);
1540
- height = Math.min(layout.height(unit), metrics.clientHeight);
1541
- }
1542
1456
 
1543
1457
  if (y < metrics.scrollTop) {
1544
1458
  scrollTo(y - metrics.padTop);
@@ -1610,12 +1524,92 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1610
1524
  schedule();
1611
1525
  }
1612
1526
 
1613
- // Shows the selection once a gesture or command set it, revealing it when asked.
1614
- function start(reveal: boolean) {
1615
- if (!editing) {
1616
- activate(false);
1527
+ // A space after '[]' at the start of a line or a list item's text makes it a task, as '- ' makes a list item.
1528
+ function shortcut(text: string) {
1529
+ let { end, start } = model.selection;
1530
+
1531
+ if (text !== ' ' || start !== end || model.selections.length > 1 || LITERAL.has(kindAt(start)!)) {
1532
+ return false;
1533
+ }
1534
+
1535
+ let from = model.lineStart(model.lineAt(start)),
1536
+ before = model.value.slice(from, start),
1537
+ at = from + quotePrefix(before).length,
1538
+ rest = model.value.slice(at, start);
1539
+
1540
+ if (TASK.test(rest)) {
1541
+ return model.replace(at, start, '- [ ] ', { selection: { start: at + 6 }, source: 'markdown-task' });
1542
+ }
1543
+
1544
+ if (ITEM_TASK.test(rest)) {
1545
+ let bracket = at + rest.lastIndexOf('[');
1546
+
1547
+ return model.replace(bracket, start, '[ ] ', { selection: { start: bracket + 4 }, source: 'markdown-task' });
1548
+ }
1549
+
1550
+ return false;
1551
+ }
1552
+
1553
+ // Source offset of a native point: in a run of drawn text directly, otherwise from the runs of its unit around it.
1554
+ function sourceAt(node: Node, offset: number): number | null {
1555
+ let direct = draw.offset(node, offset);
1556
+
1557
+ if (direct !== null) {
1558
+ return direct;
1559
+ }
1560
+
1561
+ let element = (node.nodeType === 1 ? node as Element : node.parentElement)?.closest('.markdown-editor-block'),
1562
+ unit = element ? shown.get(element) : undefined;
1563
+
1564
+ if (!unit) {
1565
+ return edgeAt(node, offset);
1617
1566
  }
1618
1567
 
1568
+ let range = node.ownerDocument!.createRange(),
1569
+ segments = draw.segments(unit) ?? [],
1570
+ after: number | null = null,
1571
+ before: number | null = null;
1572
+
1573
+ range.setStart(node, offset);
1574
+
1575
+ for (let i = 0, n = segments.length; i < n; i++) {
1576
+ let segment = segments[i];
1577
+
1578
+ if (range.comparePoint(segment.node, segment.node.length) <= 0) {
1579
+ before = segment.block.from + segment.from + segment.node.length;
1580
+ }
1581
+ else if (range.comparePoint(segment.node, 0) >= 0) {
1582
+ after = segment.block.from + segment.from;
1583
+ break;
1584
+ }
1585
+ }
1586
+
1587
+ // Just past a line break the caret is on the next row.
1588
+ let broken = node.nodeType === 3 && offset > 0 && LINE_BREAK.test(node.textContent![offset - 1]);
1589
+
1590
+ if (after !== null && (broken || before === null)) {
1591
+ return after;
1592
+ }
1593
+
1594
+ return before ?? contentStart(unit);
1595
+ }
1596
+
1597
+ // Absolute source runs of a drawn unit's text, in order.
1598
+ function spans(unit: Unit) {
1599
+ let out: Span[] = [],
1600
+ segments = draw.segments(unit) ?? [];
1601
+
1602
+ for (let i = 0, n = segments.length; i < n; i++) {
1603
+ let from = segments[i].block.from + segments[i].from;
1604
+
1605
+ out.push({ from, to: from + segments[i].node.length });
1606
+ }
1607
+
1608
+ return out;
1609
+ }
1610
+
1611
+ // Shows the selection once a gesture or command set it, revealing it when asked.
1612
+ function start(reveal: boolean) {
1619
1613
  revealing ||= reveal;
1620
1614
  schedule();
1621
1615
  }
@@ -1635,6 +1629,7 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1635
1629
  if (change.textChanged) {
1636
1630
  ime.conflict();
1637
1631
  textChanged(change);
1632
+ ui.empty = !snapshot.value;
1638
1633
  }
1639
1634
 
1640
1635
  if (change.selectionChanged) {
@@ -1644,8 +1639,12 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1644
1639
  secondary = selections.length > 1;
1645
1640
  }
1646
1641
 
1647
- refresh();
1648
- schedule();
1642
+ // Drawn at once, so the text under the caret is current before the next input reads the selection.
1643
+ if (frame) {
1644
+ cancelAnimationFrame(frame);
1645
+ }
1646
+
1647
+ paint();
1649
1648
 
1650
1649
  if (change.selectionChanged) {
1651
1650
  callbacks.onSelection?.(snapshot.selection, model.position());
@@ -1658,15 +1657,14 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1658
1657
  }
1659
1658
 
1660
1659
  function textChanged(change: Change) {
1661
- let local = reparseBlocks();
1660
+ reparseBlocks();
1662
1661
 
1663
1662
  if (folds.length) {
1664
1663
  folds = outerFolds(mapFolds(folds, change.editBatches ?? [], model));
1665
- local = false;
1666
1664
  }
1667
1665
 
1668
1666
  whole = null;
1669
- unitsDirty ||= !local || secondary;
1667
+ unitsDirty = true;
1670
1668
  }
1671
1669
 
1672
1670
  // Client rectangles of [from, to) in a drawn unit's text; a collapsed range gives its caret.
@@ -1706,6 +1704,11 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1706
1704
  return out;
1707
1705
  }
1708
1706
 
1707
+ // Typed text, unless it completes a shortcut.
1708
+ function typed(text: string) {
1709
+ return editable(() => shortcut(text) || insertText(model, text, 'input'));
1710
+ }
1711
+
1709
1712
  let controller: MarkdownController = {
1710
1713
  addNextOccurrence: (all = false) => {
1711
1714
  if (disposed || ime.busy()) {
@@ -1753,12 +1756,16 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1753
1756
  findNext: () => search.next(),
1754
1757
  findPrevious: () => search.previous(),
1755
1758
  focus: () => {
1756
- if (!disposed) {
1757
- activate(true);
1759
+ if (disposed || !content) {
1760
+ return;
1758
1761
  }
1762
+
1763
+ content.focus({ preventScroll: true });
1764
+ place();
1765
+ schedule();
1759
1766
  },
1760
1767
  fold: (line) => {
1761
- if (disposed || ime.busy() || options.fold === false) {
1768
+ if (disposed || ime.busy() || !options.fold) {
1762
1769
  return false;
1763
1770
  }
1764
1771
 
@@ -1776,26 +1783,18 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1776
1783
  model.select({ start: range.open ?? range.from });
1777
1784
  }
1778
1785
 
1779
- // A field showing the fold's own lines would hide nothing; the block shows folded instead.
1780
- if (active && range.from < blocks[active.last - 1].to && range.to > active.from) {
1781
- deactivate();
1782
- textarea?.blur();
1783
- }
1784
-
1785
1786
  unitsDirty = true;
1786
1787
  schedule();
1787
1788
 
1788
1789
  return true;
1789
1790
  },
1790
1791
  foldAll: () => {
1791
- if (disposed || ime.busy() || options.fold === false) {
1792
+ if (disposed || ime.busy() || !options.fold) {
1792
1793
  return;
1793
1794
  }
1794
1795
 
1795
1796
  model.select({ start: 0 });
1796
1797
  folds = outerFolds(markdownFolds(model, blocks));
1797
- deactivate();
1798
- textarea?.blur();
1799
1798
  unitsDirty = true;
1800
1799
  schedule();
1801
1800
  },
@@ -1858,7 +1857,6 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1858
1857
  refresh: () => {
1859
1858
  if (!disposed) {
1860
1859
  measure();
1861
- fieldGeometry();
1862
1860
  unitsDirty = true;
1863
1861
  schedule();
1864
1862
  }
@@ -1896,10 +1894,10 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1896
1894
  return;
1897
1895
  }
1898
1896
 
1899
- let focused = editing && !!textarea && host?.ownerDocument.activeElement === textarea;
1897
+ let focused = !!content && host?.ownerDocument.activeElement === content;
1900
1898
 
1901
- // Hiding the field commits a composition to the document it started in.
1902
- deactivate();
1899
+ // A composition commits to the document it started in.
1900
+ ime.flush();
1903
1901
 
1904
1902
  if (surface) {
1905
1903
  visited.set(model, { folds, left: metrics.scrollLeft, revision: model.revision, top: metrics.scrollTop });
@@ -1908,10 +1906,20 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1908
1906
  model = next;
1909
1907
  adopt();
1910
1908
  controller.setOptions(settings ?? {}, !!settings);
1911
- present();
1909
+
1910
+ if (frame) {
1911
+ cancelAnimationFrame(frame);
1912
+ }
1913
+
1914
+ paint();
1915
+
1916
+ if (surface) {
1917
+ surface.scrollLeft = metrics.scrollLeft;
1918
+ surface.scrollTop = metrics.scrollTop;
1919
+ }
1912
1920
 
1913
1921
  if (focused) {
1914
- activate(true);
1922
+ controller.focus();
1915
1923
  }
1916
1924
  },
1917
1925
  setOptions: (next, replace = false) => {
@@ -1926,9 +1934,9 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1926
1934
  options.indent = ' ';
1927
1935
  }
1928
1936
 
1937
+ ui.fold = !!options.fold;
1929
1938
  ui.label = options.label ?? 'Markdown editor';
1930
1939
  ui.minimap = !!options.minimap;
1931
- ui.name = options.name ?? '';
1932
1940
  ui.placeholder = options.placeholder ?? '';
1933
1941
  ui.readonly = !!options.readonly;
1934
1942
  ui.spellcheck = options.spellcheck ?? true;
@@ -1936,12 +1944,11 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1936
1944
  ui.wrap = options.wrap !== false;
1937
1945
  search.status();
1938
1946
 
1939
- if (options.fold === false) {
1947
+ if (!options.fold) {
1940
1948
  folds = [];
1941
1949
  }
1942
1950
 
1943
1951
  whole = null;
1944
- fieldGeometry();
1945
1952
  map.invalidate();
1946
1953
  unitsDirty = true;
1947
1954
  schedule();
@@ -1964,9 +1971,6 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
1964
1971
  get state() {
1965
1972
  return state as Snapshot;
1966
1973
  },
1967
- get textarea() {
1968
- return textarea!;
1969
- },
1970
1974
  toggleComment: () => editable(() => toggleComment(model, options.lineComment ?? false, options.blockComment ?? ['<!--', '-->'])),
1971
1975
  undo: () => editable(() => model.undo()),
1972
1976
  undoSelection: () => {
@@ -2012,7 +2016,7 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
2012
2016
  cache: () => syntaxCache(model, 'markdown'),
2013
2017
  capture,
2014
2018
  controller: controller as Controller,
2015
- deleteVisible: (backwards, word) => deleteCharacter(model, backwards, word),
2019
+ deleteVisible: (backwards, word) => word ? deleteCharacter(model, backwards, true) : erase(backwards),
2016
2020
  get document() {
2017
2021
  return model;
2018
2022
  },
@@ -2026,97 +2030,123 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
2026
2030
  }
2027
2031
  });
2028
2032
 
2033
+ // The editable text. The browser never edits it: each input is read as an edit of the source and prevented, the
2034
+ // blocks it touched draw again, and the selection is put back. Only an input method writes into it, until it ends.
2029
2035
  let field: Attributes = {
2030
2036
  'aria-describedby': help,
2031
2037
  'aria-label': () => ui.label,
2032
- class: () => ui.input,
2033
- name: () => ui.name,
2038
+ 'aria-multiline': 'true',
2039
+ 'aria-placeholder': () => ui.placeholder,
2040
+ 'aria-readonly': () => ui.readonly ? 'true' : 'false',
2041
+ autocapitalize: 'off',
2042
+ class: 'markdown-editor-content',
2043
+ contenteditable: () => ui.readonly ? 'false' : 'true',
2044
+ 'data-placeholder': () => ui.placeholder,
2034
2045
  onbeforeinput: (e: InputEvent) => {
2035
- if (writing || ime.busy() || e.isComposing || !textarea) {
2046
+ let type = e.inputType;
2047
+
2048
+ if (ime.busy() || e.isComposing || type === 'insertCompositionText' || type === 'deleteCompositionText' || type === 'insertFromComposition') {
2036
2049
  return;
2037
2050
  }
2038
2051
 
2039
- capture();
2040
- revealFolds(!e.inputType.startsWith('history') && !options.readonly);
2041
-
2042
- before = { end: textarea.selectionEnd, length: textarea.textLength, start: textarea.selectionStart };
2043
- beforeRanges = model.selections;
2044
- beforeSelection = model.selection;
2045
- inputData = e.data;
2046
- inputType = e.inputType;
2047
-
2048
- if (e.inputType === 'historyUndo' || e.inputType === 'historyRedo') {
2049
- if (e.cancelable) {
2050
- e.preventDefault();
2051
- e.inputType === 'historyUndo' ? controller.undo() : controller.redo();
2052
- }
2053
-
2052
+ // One the browser applies regardless is drawn over from the document once it lands.
2053
+ if (!e.cancelable) {
2054
2054
  return;
2055
2055
  }
2056
2056
 
2057
- if (!e.cancelable || options.readonly) {
2057
+ e.preventDefault();
2058
+
2059
+ if (options.readonly) {
2058
2060
  return;
2059
2061
  }
2060
2062
 
2061
- if (model.selections.length > 1) {
2062
- if (e.inputType === 'insertText' && e.data !== null) {
2063
- e.preventDefault();
2064
- editable(() => insertText(model, e.data!, 'input'));
2065
- return;
2066
- }
2063
+ capture();
2064
+ revealFolds(!type.startsWith('history'));
2067
2065
 
2068
- if (/^delete(?:Content|Word)(?:Backward|Forward)$/.test(e.inputType)) {
2069
- e.preventDefault();
2070
- editable(() => e.inputType === 'deleteContentBackward'
2071
- ? markdownCommand(model, 'backspace', kindAt)
2072
- : deleteCharacter(model, e.inputType.endsWith('Backward'), e.inputType.startsWith('deleteWord')));
2066
+ switch (type) {
2067
+ case 'deleteContentBackward':
2068
+ case 'deleteContentForward':
2069
+ editable(() => erase(type === 'deleteContentBackward'));
2070
+ return;
2071
+ case 'deleteWordBackward':
2072
+ case 'deleteWordForward':
2073
+ editable(() => deleteCharacter(model, type === 'deleteWordBackward', true));
2074
+ return;
2075
+ case 'formatBold':
2076
+ controller.bold();
2077
+ return;
2078
+ case 'formatItalic':
2079
+ controller.italic();
2080
+ return;
2081
+ case 'historyRedo':
2082
+ controller.redo();
2083
+ return;
2084
+ case 'historyUndo':
2085
+ controller.undo();
2086
+ return;
2087
+ case 'insertLineBreak':
2088
+ editable(() => insertText(model, model.eol, 'input'));
2073
2089
  return;
2090
+ case 'insertParagraph':
2091
+ controller.newline();
2092
+ return;
2093
+ case 'insertReplacementText': {
2094
+ let range = e.getTargetRanges()[0],
2095
+ from = range ? sourceAt(range.startContainer, range.startOffset) : null,
2096
+ to = range ? sourceAt(range.endContainer, range.endOffset) : null;
2097
+
2098
+ if (from !== null && to !== null) {
2099
+ model.select({ end: Math.max(from, to), start: Math.min(from, to) });
2100
+ }
2101
+
2102
+ break;
2074
2103
  }
2075
2104
  }
2076
2105
 
2077
- if (e.inputType === 'insertLineBreak' || e.inputType === 'insertParagraph') {
2078
- e.preventDefault();
2079
- controller.newline();
2106
+ if (type.startsWith('delete')) {
2107
+ // Deletions to a line's edge cover what the browser says they do.
2108
+ let range = e.getTargetRanges()[0],
2109
+ from = range ? sourceAt(range.startContainer, range.startOffset) : null,
2110
+ to = range ? sourceAt(range.endContainer, range.endOffset) : null;
2111
+
2112
+ if (from !== null && to !== null && from !== to) {
2113
+ editable(() => remove({ from: Math.min(from, to), to: Math.max(from, to) }));
2114
+ }
2115
+
2116
+ return;
2080
2117
  }
2081
- else if (e.inputType === 'deleteContentBackward' && editable(() => markdownBackspace(model))) {
2082
- e.preventDefault();
2118
+
2119
+ let text = e.data ?? e.dataTransfer?.getData('text/plain') ?? '';
2120
+
2121
+ if (text) {
2122
+ typed(text);
2083
2123
  }
2084
2124
  },
2085
- oncompositionend: ime.attributes.oncompositionend,
2125
+ oncompositionend: (e: CompositionEvent) => {
2126
+ composed = e.data ?? '';
2127
+ ime.attributes.oncompositionend();
2128
+ },
2086
2129
  oncompositionstart: ime.attributes.oncompositionstart,
2087
2130
  oncopy: (e: ClipboardEvent) => clip(e, false),
2088
2131
  oncut: (e: ClipboardEvent) => clip(e, true),
2089
2132
  ondragstart: gestures.ondragstart,
2090
2133
  onfocus: () => {
2091
- if (!editing) {
2092
- activate(false);
2093
- }
2134
+ let native = content?.ownerDocument.getSelection();
2094
2135
 
2095
- capture();
2136
+ if (!native?.anchorNode || !content?.contains(native.anchorNode)) {
2137
+ place();
2138
+ }
2096
2139
  },
2097
2140
  oninput: (e: InputEvent) => {
2098
- if (writing) {
2099
- return;
2100
- }
2101
-
2102
2141
  if (ime.input(e)) {
2103
- // The field grows with a composition the document doesn't hold yet.
2104
- if (textarea && textarea.scrollHeight > ui.height) {
2105
- ui.height = textarea.scrollHeight;
2106
- }
2107
-
2108
- return;
2109
- }
2110
-
2111
- if (e.inputType === 'historyUndo' || e.inputType === 'historyRedo') {
2112
- e.inputType === 'historyUndo' ? controller.undo() : controller.redo();
2113
2142
  return;
2114
2143
  }
2115
2144
 
2116
- accept();
2145
+ invalidate(model.selection.start);
2117
2146
  },
2118
2147
  onkeydown: (e: KeyboardEvent) => {
2119
2148
  gestures.alt(e);
2149
+ linking(e);
2120
2150
 
2121
2151
  if (ime.busy() || e.isComposing || e.keyCode === 229) {
2122
2152
  return;
@@ -2163,62 +2193,25 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
2163
2193
  return;
2164
2194
  }
2165
2195
 
2166
- if (mod) {
2167
- if (e.altKey || !NAVIGATION.test(e.key)) {
2168
- return;
2169
- }
2170
-
2171
- if (/^(End|Home)$/.test(e.key) || (apple && /^Arrow(Down|Up)$/.test(e.key))) {
2172
- e.preventDefault();
2173
- moveSelection(e.key, e.shiftKey, false, false, true);
2174
- }
2175
- else if (model.selections.length > 1 || (/^Arrow(Left|Right)$/.test(e.key) && leaves(e.key))) {
2176
- e.preventDefault();
2177
- moveSelection(e.key, e.shiftKey, true);
2178
- }
2179
-
2196
+ if (NAVIGATION.test(e.key) && !e.altKey && model.selections.length > 1) {
2197
+ e.preventDefault();
2198
+ moveSelection(e.key, e.shiftKey, mod);
2180
2199
  return;
2181
2200
  }
2182
2201
 
2183
- if (e.key === 'Tab' && !e.altKey && options.captureTab !== false && !options.readonly) {
2184
- e.preventDefault();
2185
- e.shiftKey ? controller.outdent() : controller.indent();
2186
- }
2187
- else if (e.key === 'Enter' && !e.altKey && !options.readonly) {
2188
- e.preventDefault();
2189
- controller.newline();
2190
- }
2191
- else if ((e.key === 'Backspace' || e.key === 'Delete') && !options.readonly && model.selections.length > 1) {
2192
- e.preventDefault();
2193
- editable(() => e.key === 'Backspace' && !e.altKey
2194
- ? markdownCommand(model, 'backspace', kindAt)
2195
- : deleteCharacter(model, e.key === 'Backspace', e.altKey));
2196
- }
2197
- else if ((e.key === 'Backspace' || e.key === 'Delete') && !options.readonly && active) {
2198
- let selection = model.selection,
2199
- collapsed = selection.start === selection.end;
2202
+ // Tab nests list items and indents code; in prose, where indentation would make code, it moves focus.
2203
+ if (e.key === 'Tab' && !e.altKey && !mod && options.captureTab !== false && !options.readonly) {
2204
+ let kind = kindAt(model.selection.start);
2200
2205
 
2201
- if (e.key === 'Backspace' && collapsed && editable(() => markdownBackspace(model))) {
2206
+ if (kind === 'code' || kind === 'fence' || kind === 'list') {
2202
2207
  e.preventDefault();
2203
- }
2204
- else if (e.key === 'Backspace' && collapsed && selection.start === active.from && active.from > 0) {
2205
- // The line break before the field isn't in it; joining lines takes the document.
2206
- e.preventDefault();
2207
- editable(() => deleteCharacter(model, true, e.altKey));
2208
- }
2209
- else if (e.key === 'Delete' && collapsed && selection.end === active.to && active.to < model.value.length) {
2210
- e.preventDefault();
2211
- editable(() => deleteCharacter(model, false, e.altKey));
2208
+ e.shiftKey ? controller.outdent() : controller.indent();
2212
2209
  }
2213
2210
  }
2214
- else if (NAVIGATION.test(e.key) && (model.selections.length > 1 || e.key.startsWith('Page') || leaves(e.key))) {
2215
- e.preventDefault();
2216
- moveSelection(e.key, e.shiftKey, e.altKey);
2217
- }
2218
2211
  },
2219
2212
  onkeyup: (e: KeyboardEvent) => {
2220
2213
  gestures.alt(e);
2221
- capture();
2214
+ linking(e);
2222
2215
  },
2223
2216
  onpaste: (e: ClipboardEvent) => {
2224
2217
  if (ime.busy() || options.readonly || !e.clipboardData) {
@@ -2231,30 +2224,24 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
2231
2224
 
2232
2225
  editable(() => insertText(model, text, 'paste'));
2233
2226
  },
2234
- onpointerdown: () => {
2235
- goals = [];
2236
- pinned = true;
2237
- },
2238
- onpointerup: () => {
2239
- pinned = false;
2240
- capture();
2241
- },
2242
- onselect: capture,
2243
- onselectionchange: capture,
2244
- placeholder: () => ui.placeholder,
2245
- readOnly: () => ui.readonly,
2227
+ role: 'textbox',
2246
2228
  spellcheck: () => ui.spellcheck,
2247
- style: () => `${ui.style} height: ${ui.height}px;`,
2248
- wrap: () => ui.wrap ? 'soft' : 'off'
2229
+ tabindex: '0'
2249
2230
  };
2250
2231
 
2251
-
2252
2232
  let body: Attributes = {
2253
- 'aria-label': () => ui.label,
2254
2233
  onclick: (e: MouseEvent) => {
2255
- // Links edit in place on a plain click and open with the platform's modifier.
2256
- if (!(e.ctrlKey || e.metaKey) && (e.target as Element).closest('a')) {
2257
- e.preventDefault();
2234
+ let link = (e.target as Element).closest('a');
2235
+
2236
+ if (!link) {
2237
+ return;
2238
+ }
2239
+
2240
+ // Links place the caret on a plain click and open with the platform's modifier.
2241
+ e.preventDefault();
2242
+
2243
+ if ((e.ctrlKey || e.metaKey) && link.href) {
2244
+ window.open(link.href, '_blank', 'noopener');
2258
2245
  }
2259
2246
  },
2260
2247
  onconnect: (element: HTMLElement) => {
@@ -2265,56 +2252,43 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
2265
2252
  ondragleave: gestures.ondragleave,
2266
2253
  ondragover: gestures.ondragover,
2267
2254
  ondrop: gestures.ondrop,
2268
- onfocus: (e: FocusEvent) => {
2269
- if (e.target === surface) {
2270
- controller.focus();
2271
- }
2272
- },
2273
- onlostpointercapture: () => {
2274
- gestures.end();
2275
- finish();
2276
- },
2255
+ onlostpointercapture: gestures.end,
2256
+ // A press beside the text, in the padding or past the end, still puts the caret on the nearest text.
2277
2257
  onmousedown: (e: MouseEvent) => {
2278
- if (e.target === textarea || (e.target as Element).closest('.markdown-editor-block, .markdown-editor-active')) {
2258
+ if (e.button !== 0 || ime.busy() || (e.target !== surface && !(e.target as Element).classList.contains('markdown-editor-spacer'))) {
2279
2259
  return;
2280
2260
  }
2281
2261
 
2282
- press(e, null);
2283
- },
2284
- onpointercancel: () => {
2285
- gestures.end();
2286
- finish();
2262
+ e.preventDefault();
2263
+ controller.focus();
2264
+ model.select({ start: offsetAt(e.clientX, e.clientY) ?? model.value.length });
2287
2265
  },
2266
+ onpointercancel: gestures.end,
2288
2267
  onpointerdown: (e: PointerEvent) => {
2289
- pointerId = e.pointerId;
2268
+ tip.release();
2290
2269
  gestures.onpointerdown(e);
2291
2270
  },
2271
+ onpointerleave: () => tip.release(),
2292
2272
  onpointermove: (e: PointerEvent) => {
2293
2273
  gestures.onpointermove(e);
2274
+ linking(e);
2275
+ },
2276
+ onpointerover: (e: PointerEvent) => {
2277
+ let link = (e.target as Element).closest<HTMLElement>('a.markdown-editor-link[href]');
2294
2278
 
2295
- if (!drag) {
2296
- return;
2279
+ if (link && e.pointerType !== 'touch') {
2280
+ tip.request(link, apple ? '⌘+Click to open' : 'Ctrl+Click to open');
2297
2281
  }
2298
-
2299
- let offset = offsetAt(e.clientX, e.clientY);
2300
-
2301
- if (offset !== null && offset !== drag.head) {
2302
- drag.head = offset;
2303
- schedule();
2282
+ else {
2283
+ tip.release();
2304
2284
  }
2305
2285
  },
2306
- onpointerup: () => {
2307
- pinned = false;
2308
- pointerId = -1;
2309
- gestures.end();
2310
- finish();
2311
- },
2286
+ onpointerup: gestures.end,
2312
2287
  onscroll: () => {
2313
2288
  metrics.scrollLeft = surface!.scrollLeft;
2314
2289
  metrics.scrollTop = surface!.scrollTop;
2315
2290
  schedule();
2316
- },
2317
- tabindex: () => ui.editing ? -1 : 0
2291
+ }
2318
2292
  };
2319
2293
 
2320
2294
  let template = (attributes?: Attributes) => html`
@@ -2325,6 +2299,9 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
2325
2299
  class: [
2326
2300
  () => ime.state.composing && 'markdown-editor--composing',
2327
2301
  () => gestures.state.crosshair && 'markdown-editor--crosshair',
2302
+ () => ui.empty && 'markdown-editor--empty',
2303
+ () => ui.linking && 'markdown-editor--linking',
2304
+ () => ui.fold && 'markdown-editor--fold',
2328
2305
  () => ui.minimap && 'markdown-editor--minimap',
2329
2306
  () => !ui.wrap && 'markdown-editor--nowrap',
2330
2307
  () => ui.readonly && 'markdown-editor--readonly'
@@ -2334,13 +2311,10 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
2334
2311
  connect();
2335
2312
  },
2336
2313
  ondisconnect: () => controller.dispose(),
2337
- onfocusout: (e: FocusEvent) => {
2338
- let next = e.relatedTarget as Node | null;
2339
-
2340
- if (!next || !host?.contains(next)) {
2341
- capture();
2342
- deactivate();
2343
- }
2314
+ ondocumentselectionchange: capture,
2315
+ // A modifier released in another window never reaches this one.
2316
+ onwindowblur: () => {
2317
+ ui.linking = false;
2344
2318
  },
2345
2319
  style: () => `--tab-size: ${ui.tabSize};`
2346
2320
  }}
@@ -2348,50 +2322,26 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
2348
2322
  ${search.template()}
2349
2323
  ${jump.template()}
2350
2324
  <div class='markdown-editor-body' ${size}>
2351
- <div class='markdown-editor-surface --scrollbar' ${body}>
2325
+ <div class='markdown-editor-surface' ${body} ${scrollbar()}>
2352
2326
  <div aria-hidden='true' class='markdown-editor-overlay'>
2353
2327
  ${html.reactive(decorations, (decoration) => html`
2354
2328
  <div ${{ class: () => decoration.kind, style: () => decoration.style }}></div>
2355
2329
  `)}
2356
2330
  </div>
2357
- <div aria-hidden='true' class='markdown-editor-spacer' ${{ style: () => `height: ${ui.top}px;` }}></div>
2358
- ${html.reactive(listed.before, draw.template)}
2359
- <div aria-hidden='true' class='markdown-editor-spacer' ${{ style: () => `height: ${ui.between}px;` }}></div>
2360
- <div class='markdown-editor-active' ${{ class: () => ui.editing && '--active' }}>
2361
- <button
2362
- aria-label='Fold block'
2363
- class='markdown-editor-fold'
2364
- tabindex='-1'
2365
- type='button'
2366
- ${{
2367
- class: () => !ui.foldable && 'markdown-editor-fold--none',
2368
- onclick: () => {
2369
- if (active) {
2370
- controller.fold(model.lineAt(blocks[active.first].from) + 1);
2371
- }
2372
- },
2373
- onmousedown: prevent
2374
- }}
2375
- >
2376
- ${icon({ 'aria-hidden': 'true', class: 'markdown-editor-fold-icon' }, chevron)}
2377
- </button>
2378
- <textarea
2379
- autocapitalize='off'
2380
- autocomplete='off'
2381
- autocorrect='off'
2382
- dir='ltr'
2383
- ${field}
2384
- ${{
2385
- onconnect: (element: HTMLTextAreaElement) => {
2386
- textarea = element;
2387
- connect();
2388
- }
2389
- }}
2390
- ></textarea>
2331
+ <div
2332
+ ${field}
2333
+ ${{
2334
+ onconnect: (element: HTMLElement) => {
2335
+ content = element;
2336
+ connect();
2337
+ }
2338
+ }}
2339
+ >
2340
+ ${html.reactive(items, (item) => 'block' in item
2341
+ ? draw.template(item)
2342
+ : html`<div aria-hidden='true' class='markdown-editor-spacer' contenteditable='false' ${{ style: () => `height: ${item.height}px;` }}></div>`
2343
+ )}
2391
2344
  </div>
2392
- <div aria-hidden='true' class='markdown-editor-spacer' ${{ style: () => `height: ${ui.next}px;` }}></div>
2393
- ${html.reactive(listed.after, draw.template)}
2394
- <div aria-hidden='true' class='markdown-editor-spacer' ${{ style: () => `height: ${ui.bottom}px;` }}></div>
2395
2345
  </div>
2396
2346
  ${map.template()}
2397
2347
  <span
@@ -2406,6 +2356,7 @@ const view = (model: EditorDocument, callbacks: Callbacks, receive?: (controller
2406
2356
  >0000000000000000000000000000000000000000000000000000000000000000</span>
2407
2357
  </div>
2408
2358
  <span class='markdown-editor-help' id='${help}'>Press Escape then Tab to move focus out of the editor.</span>
2359
+ ${tip.render()}
2409
2360
  </div>
2410
2361
  `;
2411
2362