edytor 0.0.11 → 0.1.0-next.16

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 (371) hide show
  1. package/README.md +77 -197
  2. package/dist/block/block.svelte.d.ts +156 -69
  3. package/dist/block/block.svelte.js +324 -355
  4. package/dist/block/block.utils.d.ts +191 -24
  5. package/dist/block/block.utils.js +327 -460
  6. package/dist/block/contentRange.d.ts +4 -0
  7. package/dist/block/contentRange.js +20 -0
  8. package/dist/block/inlineBlock.svelte.d.ts +26 -19
  9. package/dist/block/inlineBlock.svelte.js +91 -39
  10. package/dist/clipboard/clipboard.d.ts +4 -0
  11. package/dist/clipboard/clipboard.js +4 -0
  12. package/dist/clipboard/createClipboardFragment.d.ts +3 -0
  13. package/dist/clipboard/createClipboardFragment.js +103 -0
  14. package/dist/clipboard/fragmentData.d.ts +6 -0
  15. package/dist/clipboard/fragmentData.js +74 -0
  16. package/dist/clipboard/htmlFlow.d.ts +22 -0
  17. package/dist/clipboard/htmlFlow.js +167 -0
  18. package/dist/clipboard/insertClipboardFragment.d.ts +18 -0
  19. package/dist/clipboard/insertClipboardFragment.js +48 -0
  20. package/dist/clipboard/serializeClipboardFragment.d.ts +7 -0
  21. package/dist/clipboard/serializeClipboardFragment.js +53 -0
  22. package/dist/clipboard/textBlocks.d.ts +23 -0
  23. package/dist/clipboard/textBlocks.js +103 -0
  24. package/dist/clipboard/types.d.ts +20 -0
  25. package/dist/clipboard/types.js +2 -0
  26. package/dist/cloudflare/DocumentRoom.d.ts +586 -0
  27. package/dist/cloudflare/DocumentRoom.js +1706 -0
  28. package/dist/cloudflare/index.d.ts +8 -0
  29. package/dist/cloudflare/index.js +8 -0
  30. package/dist/cloudflare/routeDocumentSocket.d.ts +33 -0
  31. package/dist/cloudflare/routeDocumentSocket.js +51 -0
  32. package/dist/collaboration/RemoteSelections.svelte +94 -0
  33. package/dist/collaboration/RemoteSelections.svelte.d.ts +7 -0
  34. package/dist/collaboration/awarenessSelection.d.ts +127 -0
  35. package/dist/collaboration/awarenessSelection.js +199 -0
  36. package/dist/collaboration/documentSync.d.ts +20 -0
  37. package/dist/collaboration/documentSync.js +13 -0
  38. package/dist/collaboration/index.d.ts +3 -0
  39. package/dist/collaboration/index.js +3 -0
  40. package/dist/collaboration/providers.d.ts +13 -0
  41. package/dist/collaboration/providers.js +21 -0
  42. package/dist/collaboration/remoteSelection.d.ts +21 -0
  43. package/dist/collaboration/remoteSelection.js +159 -0
  44. package/dist/components/Block.svelte +223 -15
  45. package/dist/components/Block.svelte.d.ts +4 -2
  46. package/dist/components/Content.svelte +128 -32
  47. package/dist/components/Content.svelte.d.ts +6 -2
  48. package/dist/components/Edytor.svelte +308 -110
  49. package/dist/components/Edytor.svelte.d.ts +64 -20
  50. package/dist/components/InlineBlock.svelte +38 -7
  51. package/dist/components/InlineBlock.svelte.d.ts +5 -2
  52. package/dist/components/Mark.svelte +29 -13
  53. package/dist/components/Mark.svelte.d.ts +4 -4
  54. package/dist/components/Suggestion.svelte +48 -0
  55. package/dist/components/Suggestion.svelte.d.ts +7 -0
  56. package/dist/components/SuggestionRanges.svelte +58 -0
  57. package/dist/components/SuggestionRanges.svelte.d.ts +7 -0
  58. package/dist/components/Text.svelte +107 -14
  59. package/dist/components/Text.svelte.d.ts +8 -1
  60. package/dist/constants.d.ts +0 -2
  61. package/dist/constants.js +0 -2
  62. package/dist/crdt/admission.d.ts +141 -0
  63. package/dist/crdt/admission.js +147 -0
  64. package/dist/crdt/anchors.d.ts +47 -0
  65. package/dist/crdt/anchors.js +42 -0
  66. package/dist/crdt/attribution/attribution.d.ts +169 -0
  67. package/dist/crdt/attribution/attribution.js +136 -0
  68. package/dist/crdt/attribution/block.d.ts +200 -0
  69. package/dist/crdt/attribution/block.js +327 -0
  70. package/dist/crdt/attribution/index.d.ts +9 -0
  71. package/dist/crdt/attribution/index.js +9 -0
  72. package/dist/crdt/data.d.ts +81 -0
  73. package/dist/crdt/data.js +383 -0
  74. package/dist/crdt/document.d.ts +590 -0
  75. package/dist/crdt/document.js +1160 -0
  76. package/dist/crdt/edytor-doc.d.ts +752 -0
  77. package/dist/crdt/edytor-doc.js +2382 -0
  78. package/dist/crdt/engine-api.d.ts +153 -0
  79. package/dist/crdt/engine.d.ts +37 -0
  80. package/dist/crdt/engine.js +50 -0
  81. package/dist/crdt/flow.d.ts +104 -0
  82. package/dist/crdt/flow.js +229 -0
  83. package/dist/crdt/index.d.ts +630 -0
  84. package/dist/crdt/index.js +168 -0
  85. package/dist/crdt/migration/legacy-schema.d.ts +61 -0
  86. package/dist/crdt/migration/legacy-schema.js +149 -0
  87. package/dist/crdt/migration/migrate.d.ts +67 -0
  88. package/dist/crdt/migration/migrate.js +219 -0
  89. package/dist/crdt/nodes.d.ts +120 -0
  90. package/dist/crdt/nodes.js +56 -0
  91. package/dist/crdt/placement/model.d.ts +383 -0
  92. package/dist/crdt/placement/model.js +811 -0
  93. package/dist/crdt/placement/rank.d.ts +57 -0
  94. package/dist/crdt/placement/rank.js +209 -0
  95. package/dist/crdt/protocols/auth.d.ts +28 -0
  96. package/dist/crdt/protocols/auth.js +43 -0
  97. package/dist/crdt/protocols/awareness.d.ts +87 -0
  98. package/dist/crdt/protocols/awareness.js +271 -0
  99. package/dist/crdt/protocols/envelope.d.ts +75 -0
  100. package/dist/crdt/protocols/envelope.js +89 -0
  101. package/dist/crdt/protocols/observable.d.ts +19 -0
  102. package/dist/crdt/protocols/observable.js +26 -0
  103. package/dist/crdt/protocols/sync.d.ts +82 -0
  104. package/dist/crdt/protocols/sync.js +288 -0
  105. package/dist/crdt/providers/container.d.ts +23 -0
  106. package/dist/crdt/providers/container.js +64 -0
  107. package/dist/crdt/providers/index.d.ts +282 -0
  108. package/dist/crdt/providers/index.js +118 -0
  109. package/dist/crdt/providers/indexeddb.d.ts +196 -0
  110. package/dist/crdt/providers/indexeddb.js +332 -0
  111. package/dist/crdt/providers/room.d.ts +193 -0
  112. package/dist/crdt/providers/room.js +444 -0
  113. package/dist/crdt/providers/websocket.d.ts +223 -0
  114. package/dist/crdt/providers/websocket.js +601 -0
  115. package/dist/crdt/rand.d.ts +22 -0
  116. package/dist/crdt/rand.js +22 -0
  117. package/dist/crdt/rangeDelete.d.ts +85 -0
  118. package/dist/crdt/rangeDelete.js +126 -0
  119. package/dist/crdt/schema.d.ts +96 -0
  120. package/dist/crdt/schema.js +79 -0
  121. package/dist/crdt/semantics.d.ts +81 -0
  122. package/dist/crdt/semantics.js +57 -0
  123. package/dist/crdt/structs.d.ts +106 -0
  124. package/dist/crdt/structs.js +119 -0
  125. package/dist/crdt/text/deletes.d.ts +23 -0
  126. package/dist/crdt/text/deletes.js +608 -0
  127. package/dist/crdt/text/model.d.ts +280 -0
  128. package/dist/crdt/text/model.js +554 -0
  129. package/dist/crdt/text/runs.d.ts +212 -0
  130. package/dist/crdt/text/runs.js +1142 -0
  131. package/dist/crdt/vendor/yjs/API-NOTES.md +59 -0
  132. package/dist/crdt/vendor/yjs/LICENSE +23 -0
  133. package/dist/crdt/vendor/yjs/UPSTREAM.md +653 -0
  134. package/dist/crdt/vendor/yjs/dts/global.d.ts +52 -0
  135. package/dist/crdt/vendor/yjs/dts/index.d.ts +25 -0
  136. package/dist/crdt/vendor/yjs/dts/structs/AbstractStruct.d.ts +38 -0
  137. package/dist/crdt/vendor/yjs/dts/structs/GC.d.ts +25 -0
  138. package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +748 -0
  139. package/dist/crdt/vendor/yjs/dts/structs/Skip.d.ts +30 -0
  140. package/dist/crdt/vendor/yjs/dts/utils/BlockSet.d.ts +34 -0
  141. package/dist/crdt/vendor/yjs/dts/utils/Doc.d.ts +198 -0
  142. package/dist/crdt/vendor/yjs/dts/utils/EventHandler.d.ts +17 -0
  143. package/dist/crdt/vendor/yjs/dts/utils/ID.d.ts +20 -0
  144. package/dist/crdt/vendor/yjs/dts/utils/RangeCursor.d.ts +198 -0
  145. package/dist/crdt/vendor/yjs/dts/utils/RelativePosition.d.ts +80 -0
  146. package/dist/crdt/vendor/yjs/dts/utils/StructStore.d.ts +61 -0
  147. package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +129 -0
  148. package/dist/crdt/vendor/yjs/dts/utils/UndoManager.d.ts +205 -0
  149. package/dist/crdt/vendor/yjs/dts/utils/UpdateDecoder.d.ts +170 -0
  150. package/dist/crdt/vendor/yjs/dts/utils/UpdateEncoder.d.ts +164 -0
  151. package/dist/crdt/vendor/yjs/dts/utils/YEvent.d.ts +93 -0
  152. package/dist/crdt/vendor/yjs/dts/utils/content-helper.d.ts +1 -0
  153. package/dist/crdt/vendor/yjs/dts/utils/encoding-helpers.d.ts +5 -0
  154. package/dist/crdt/vendor/yjs/dts/utils/encoding.d.ts +26 -0
  155. package/dist/crdt/vendor/yjs/dts/utils/ids.d.ts +407 -0
  156. package/dist/crdt/vendor/yjs/dts/utils/isParentOf.d.ts +1 -0
  157. package/dist/crdt/vendor/yjs/dts/utils/meta.d.ts +25 -0
  158. package/dist/crdt/vendor/yjs/dts/utils/renderer-helpers.d.ts +98 -0
  159. package/dist/crdt/vendor/yjs/dts/utils/schemas.d.ts +2 -0
  160. package/dist/crdt/vendor/yjs/dts/utils/transaction-helpers.d.ts +15 -0
  161. package/dist/crdt/vendor/yjs/dts/utils/updates.d.ts +70 -0
  162. package/dist/crdt/vendor/yjs/dts/ynode.d.ts +655 -0
  163. package/dist/crdt/vendor/yjs/global.d.ts +52 -0
  164. package/dist/crdt/vendor/yjs/src/index.js +64 -0
  165. package/dist/crdt/vendor/yjs/src/structs/AbstractStruct.js +64 -0
  166. package/dist/crdt/vendor/yjs/src/structs/GC.js +75 -0
  167. package/dist/crdt/vendor/yjs/src/structs/Item.js +1538 -0
  168. package/dist/crdt/vendor/yjs/src/structs/Skip.js +72 -0
  169. package/dist/crdt/vendor/yjs/src/utils/BlockSet.js +291 -0
  170. package/dist/crdt/vendor/yjs/src/utils/Doc.js +255 -0
  171. package/dist/crdt/vendor/yjs/src/utils/EventHandler.js +75 -0
  172. package/dist/crdt/vendor/yjs/src/utils/ID.js +59 -0
  173. package/dist/crdt/vendor/yjs/src/utils/RangeCursor.js +407 -0
  174. package/dist/crdt/vendor/yjs/src/utils/RelativePosition.js +301 -0
  175. package/dist/crdt/vendor/yjs/src/utils/StructStore.js +134 -0
  176. package/dist/crdt/vendor/yjs/src/utils/Transaction.js +458 -0
  177. package/dist/crdt/vendor/yjs/src/utils/UndoManager.js +554 -0
  178. package/dist/crdt/vendor/yjs/src/utils/UpdateDecoder.js +284 -0
  179. package/dist/crdt/vendor/yjs/src/utils/UpdateEncoder.js +323 -0
  180. package/dist/crdt/vendor/yjs/src/utils/YEvent.js +158 -0
  181. package/dist/crdt/vendor/yjs/src/utils/content-helper.js +0 -0
  182. package/dist/crdt/vendor/yjs/src/utils/encoding-helpers.js +110 -0
  183. package/dist/crdt/vendor/yjs/src/utils/encoding.js +662 -0
  184. package/dist/crdt/vendor/yjs/src/utils/ids.js +1504 -0
  185. package/dist/crdt/vendor/yjs/src/utils/isParentOf.js +19 -0
  186. package/dist/crdt/vendor/yjs/src/utils/meta.js +74 -0
  187. package/dist/crdt/vendor/yjs/src/utils/renderer-helpers.js +186 -0
  188. package/dist/crdt/vendor/yjs/src/utils/schemas.js +3 -0
  189. package/dist/crdt/vendor/yjs/src/utils/transaction-helpers.js +391 -0
  190. package/dist/crdt/vendor/yjs/src/utils/updates.js +264 -0
  191. package/dist/crdt/vendor/yjs/src/ynode.js +2897 -0
  192. package/dist/dnd/pragmatic.d.ts +22 -0
  193. package/dist/dnd/pragmatic.js +26 -0
  194. package/dist/edytor.svelte.d.ts +323 -54
  195. package/dist/edytor.svelte.js +771 -197
  196. package/dist/edytor.utils.d.ts +49 -2
  197. package/dist/edytor.utils.js +65 -76
  198. package/dist/events/beforeInputCommands.d.ts +21 -0
  199. package/dist/events/beforeInputCommands.js +328 -0
  200. package/dist/events/beforeInputDeleteCommands.d.ts +3 -0
  201. package/dist/events/beforeInputDeleteCommands.js +245 -0
  202. package/dist/events/dataTransferPayload.d.ts +6 -0
  203. package/dist/events/dataTransferPayload.js +9 -0
  204. package/dist/events/domTextOffset.d.ts +12 -0
  205. package/dist/events/domTextOffset.js +52 -0
  206. package/dist/events/events.utils.d.ts +3 -0
  207. package/dist/events/events.utils.js +13 -0
  208. package/dist/events/nativeInteractiveControl.d.ts +31 -0
  209. package/dist/events/nativeInteractiveControl.js +98 -0
  210. package/dist/events/onBeforeInput.d.ts +10 -1
  211. package/dist/events/onBeforeInput.js +449 -330
  212. package/dist/events/onCopy.d.ts +7 -0
  213. package/dist/events/onCopy.js +30 -0
  214. package/dist/events/onCut.d.ts +8 -0
  215. package/dist/events/onCut.js +13 -0
  216. package/dist/events/onDrop.d.ts +16 -0
  217. package/dist/events/onDrop.js +158 -0
  218. package/dist/events/onFocus.d.ts +4 -0
  219. package/dist/events/onFocus.js +166 -0
  220. package/dist/events/onInput.d.ts +20 -0
  221. package/dist/events/onInput.js +249 -0
  222. package/dist/events/onKeyDown.js +191 -11
  223. package/dist/events/onPaste.d.ts +3 -0
  224. package/dist/events/onPaste.js +108 -0
  225. package/dist/events/wordBoundary.d.ts +2 -0
  226. package/dist/events/wordBoundary.js +58 -0
  227. package/dist/index.d.ts +38 -1
  228. package/dist/index.js +36 -1
  229. package/dist/kinds.d.ts +100 -0
  230. package/dist/kinds.js +198 -0
  231. package/dist/plugins/arrowMove/arrowMove.d.ts +1 -1
  232. package/dist/plugins/arrowMove/arrowMove.js +30 -65
  233. package/dist/plugins/blockHandles/BlockHandle.svelte +152 -0
  234. package/dist/plugins/blockHandles/BlockHandle.svelte.d.ts +12 -0
  235. package/dist/plugins/blockHandles/BlockHandleController.svelte.d.ts +208 -0
  236. package/dist/plugins/blockHandles/BlockHandleController.svelte.js +823 -0
  237. package/dist/plugins/blockHandles/BlockHandles.svelte +151 -0
  238. package/dist/plugins/blockHandles/BlockHandles.svelte.d.ts +20 -0
  239. package/dist/plugins/blockHandles/autoScroll.d.ts +8 -0
  240. package/dist/plugins/blockHandles/autoScroll.js +25 -0
  241. package/dist/plugins/blockHandles/blockHandlesPlugin.d.ts +37 -0
  242. package/dist/plugins/blockHandles/blockHandlesPlugin.js +100 -0
  243. package/dist/plugins/blockHandles/dragPreview.d.ts +33 -0
  244. package/dist/plugins/blockHandles/dragPreview.js +228 -0
  245. package/dist/plugins/blockMenu/BlockMenu.svelte +301 -0
  246. package/dist/plugins/blockMenu/BlockMenu.svelte.d.ts +9 -0
  247. package/dist/plugins/blockMenu/BlockMenuController.svelte.d.ts +102 -0
  248. package/dist/plugins/blockMenu/BlockMenuController.svelte.js +245 -0
  249. package/dist/plugins/blockMenu/blockMenuPlugin.d.ts +11 -0
  250. package/dist/plugins/blockMenu/blockMenuPlugin.js +89 -0
  251. package/dist/plugins/code/CodePlugin.svelte +210 -163
  252. package/dist/plugins/code/CodePlugin.svelte.d.ts +1 -3
  253. package/dist/plugins/code/code.css +92 -0
  254. package/dist/plugins/icons.d.ts +2 -0
  255. package/dist/plugins/icons.js +31 -0
  256. package/dist/plugins/image/ImageEmpty.svelte +69 -0
  257. package/dist/plugins/image/ImageEmpty.svelte.d.ts +8 -0
  258. package/dist/plugins/image/ImagePlugin.svelte +74 -12
  259. package/dist/plugins/image/ImagePlugin.svelte.d.ts +15 -0
  260. package/dist/plugins/image/image.d.ts +2 -0
  261. package/dist/plugins/image/image.js +7 -0
  262. package/dist/plugins/index.d.ts +14 -1
  263. package/dist/plugins/index.js +10 -1
  264. package/dist/plugins/keepInView.d.ts +4 -0
  265. package/dist/plugins/keepInView.js +6 -0
  266. package/dist/plugins/markdownShortcuts.d.ts +3 -0
  267. package/dist/plugins/markdownShortcuts.js +104 -0
  268. package/dist/plugins/mention/MentionPlugin.svelte +28 -31
  269. package/dist/plugins/richtext/RichTextPlugin.svelte +411 -200
  270. package/dist/plugins/richtext/RichTextPlugin.svelte.d.ts +8 -7
  271. package/dist/plugins/richtext/richTextOperations.d.ts +53 -0
  272. package/dist/plugins/richtext/richTextOperations.js +200 -0
  273. package/dist/plugins/slashMenu/SlashMenu.svelte +297 -0
  274. package/dist/plugins/slashMenu/SlashMenu.svelte.d.ts +11 -0
  275. package/dist/plugins/slashMenu/SlashMenuController.svelte.d.ts +56 -0
  276. package/dist/plugins/slashMenu/SlashMenuController.svelte.js +275 -0
  277. package/dist/plugins/slashMenu/slashMenuPlugin.d.ts +25 -0
  278. package/dist/plugins/slashMenu/slashMenuPlugin.js +105 -0
  279. package/dist/plugins/suggestions/SuggestionBars.svelte +142 -0
  280. package/dist/plugins/suggestions/SuggestionBars.svelte.d.ts +10 -0
  281. package/dist/plugins/suggestions/suggestionsPlugin.d.ts +28 -0
  282. package/dist/plugins/suggestions/suggestionsPlugin.js +53 -0
  283. package/dist/plugins/toolbar/Toolbar.svelte +341 -0
  284. package/dist/plugins/toolbar/Toolbar.svelte.d.ts +9 -0
  285. package/dist/plugins/toolbar/ToolbarController.svelte.d.ts +85 -0
  286. package/dist/plugins/toolbar/ToolbarController.svelte.js +150 -0
  287. package/dist/plugins/toolbar/toolbarPlugin.d.ts +16 -0
  288. package/dist/plugins/toolbar/toolbarPlugin.js +40 -0
  289. package/dist/plugins.d.ts +249 -40
  290. package/dist/plugins.js +11 -5
  291. package/dist/selection/domSelection.d.ts +29 -0
  292. package/dist/selection/domSelection.js +230 -0
  293. package/dist/selection/replaceSelection.d.ts +143 -0
  294. package/dist/selection/replaceSelection.js +316 -0
  295. package/dist/selection/selection.svelte.d.ts +275 -37
  296. package/dist/selection/selection.svelte.js +1162 -331
  297. package/dist/selection/selection.utils.d.ts +35 -4
  298. package/dist/selection/selection.utils.js +309 -100
  299. package/dist/selection/visibility.d.ts +88 -0
  300. package/dist/selection/visibility.js +140 -0
  301. package/dist/session/attempt.d.ts +190 -0
  302. package/dist/session/attempt.js +188 -0
  303. package/dist/session/bindings.d.ts +16 -0
  304. package/dist/session/bindings.js +262 -0
  305. package/dist/session/commands.d.ts +158 -0
  306. package/dist/session/commands.js +601 -0
  307. package/dist/session/composition.svelte.d.ts +114 -0
  308. package/dist/session/composition.svelte.js +489 -0
  309. package/dist/session/editing/text.d.ts +33 -0
  310. package/dist/session/editing/text.js +36 -0
  311. package/dist/session/handles.d.ts +63 -0
  312. package/dist/session/handles.js +123 -0
  313. package/dist/session/history.d.ts +49 -0
  314. package/dist/session/history.js +131 -0
  315. package/dist/session/keymap.d.ts +42 -0
  316. package/dist/session/keymap.js +144 -0
  317. package/dist/session/moves.d.ts +32 -0
  318. package/dist/session/moves.js +116 -0
  319. package/dist/session/navigation.d.ts +51 -0
  320. package/dist/session/navigation.js +312 -0
  321. package/dist/session/props.d.ts +28 -0
  322. package/dist/session/props.js +162 -0
  323. package/dist/session/selection.d.ts +126 -0
  324. package/dist/session/selection.js +227 -0
  325. package/dist/session/suggestions.svelte.d.ts +125 -0
  326. package/dist/session/suggestions.svelte.js +329 -0
  327. package/dist/session/virtual.d.ts +33 -0
  328. package/dist/session/virtual.js +182 -0
  329. package/dist/surface/attributes.d.ts +36 -0
  330. package/dist/surface/attributes.js +143 -0
  331. package/dist/surface/cells.d.ts +103 -0
  332. package/dist/surface/cells.js +215 -0
  333. package/dist/surface/observer.svelte.d.ts +70 -0
  334. package/dist/surface/observer.svelte.js +969 -0
  335. package/dist/surface/overlay.d.ts +33 -0
  336. package/dist/surface/overlay.js +91 -0
  337. package/dist/surface/pin.svelte.d.ts +56 -0
  338. package/dist/surface/pin.svelte.js +108 -0
  339. package/dist/surface/projector.svelte.d.ts +69 -0
  340. package/dist/surface/projector.svelte.js +486 -0
  341. package/dist/text/deltas.d.ts +14 -3
  342. package/dist/text/deltas.js +27 -90
  343. package/dist/text/text.svelte.d.ts +43 -31
  344. package/dist/text/text.svelte.js +88 -69
  345. package/dist/text/text.utils.d.ts +7 -2
  346. package/dist/text/text.utils.js +121 -97
  347. package/dist/themes/notion.css +473 -0
  348. package/dist/utils/diffText.d.ts +15 -0
  349. package/dist/utils/diffText.js +26 -0
  350. package/dist/utils/json.d.ts +90 -3
  351. package/dist/utils/json.js +369 -2
  352. package/dist/utils.d.ts +3 -1
  353. package/dist/utils.js +2 -0
  354. package/package.json +112 -50
  355. package/dist/block/content.svelte.d.ts +0 -14
  356. package/dist/block/content.svelte.js +0 -40
  357. package/dist/components/ReadonlyEditor.svelte +0 -46
  358. package/dist/components/ReadonlyEditor.svelte.d.ts +0 -4
  359. package/dist/components/readonlyElements.svelte.d.ts +0 -82
  360. package/dist/components/readonlyElements.svelte.js +0 -144
  361. package/dist/dnd.svelte.d.ts +0 -17
  362. package/dist/dnd.svelte.js +0 -125
  363. package/dist/hotkeys.d.ts +0 -27
  364. package/dist/hotkeys.js +0 -282
  365. package/dist/localProvider.d.ts +0 -82
  366. package/dist/localProvider.js +0 -267
  367. package/dist/plugins/code/prism.css +0 -141
  368. package/dist/utils/jsx.d.ts +0 -25
  369. package/dist/utils/jsx.js +0 -66
  370. package/dist/utils/serialize.js +0 -1
  371. /package/dist/{utils/serialize.d.ts → crdt/engine-api.js} +0 -0
@@ -1,391 +1,1222 @@
1
- import {} from 'yjs';
2
- import * as Y from 'yjs';
3
1
  import { Text } from '../text/text.svelte.js';
4
- import { climb, climbDom, getRangesFromSelection, getTextOfNode, getTextsInSelection, getYIndex } from './selection.utils.js';
2
+ import { climbDom, getElementFromNode, getInlineBlockOfNode, getInlineBlockInSelectedRange, getRangesFromSelection, getTextOfNode, getTextsInSelection, getYIndex, getMarkEdgeSide, isTextBoundSelectionPoint, normalizeUtf16Boundary, SYNTHETIC_TEXT_OVERLAY_SELECTOR, SUGGESTION } from './selection.utils.js';
3
+ import { clearDomSelection, getActiveElement, getDomSelectionSnapshot } from './domSelection.js';
4
+ import { domPointOf } from '../surface/projector.svelte.js';
5
5
  import { Block } from '../block/block.svelte.js';
6
6
  import { SvelteSet } from 'svelte/reactivity';
7
- import { tick } from 'svelte';
8
7
  import { InlineBlock } from '../block/inlineBlock.svelte.js';
8
+ import { isNativeFormControl, isNativeInteractiveEvent, isNestedForeignEditableTarget } from '../events/nativeInteractiveControl.js';
9
+ import { atomSelection, blockSelection, noSelection, project, sameValue, segmentsOf, serialize, textSelection } from '../session/selection.js';
10
+ import { seam } from '../crdt/anchors.js';
11
+ import { getTextPath } from '../events/events.utils.js';
12
+ import { landed } from '../session/navigation.js';
13
+ import * as visibility from './visibility.js';
14
+ import { caretBeside, shownText } from './replaceSelection.js';
15
+ /** The compatibility state of no (or an unresolvable) selection. */
16
+ const EMPTY_STATE = Object.freeze({
17
+ yStart: 0,
18
+ yEnd: 0,
19
+ isCollapsed: true,
20
+ isReversed: false,
21
+ texts: [],
22
+ blocks: [],
23
+ startText: null,
24
+ endText: null,
25
+ startBlock: null,
26
+ endBlock: null,
27
+ isBlockSpanning: false,
28
+ isVoidEditableElement: false
29
+ });
30
+ /**
31
+ * Whether a DOM selection runs backward (focus before anchor in document
32
+ * order), comparing boundary points — a focus on an ancestor element of the
33
+ * anchor (`(textElement, childCount)`) is after it (F-S6).
34
+ */
35
+ export const isBackward = (selection) => {
36
+ const { anchorNode, focusNode } = selection;
37
+ if (!anchorNode || !focusNode)
38
+ return false;
39
+ if (anchorNode === focusNode)
40
+ return selection.focusOffset < selection.anchorOffset;
41
+ try {
42
+ const range = (anchorNode.ownerDocument ?? document).createRange();
43
+ range.setStart(anchorNode, selection.anchorOffset);
44
+ return range.comparePoint(focusNode, selection.focusOffset) < 0;
45
+ }
46
+ catch {
47
+ return Boolean(anchorNode.compareDocumentPosition(focusNode) & Node.DOCUMENT_POSITION_PRECEDING);
48
+ }
49
+ };
50
+ const getSyntheticTextOverlayElement = (node, offset) => {
51
+ let syntheticOverlay = null;
52
+ climbDom(node, (currentNode) => {
53
+ if (currentNode instanceof HTMLElement &&
54
+ currentNode.matches(SYNTHETIC_TEXT_OVERLAY_SELECTOR)) {
55
+ syntheticOverlay = currentNode;
56
+ return true;
57
+ }
58
+ });
59
+ if (syntheticOverlay || !(node instanceof Element)) {
60
+ return syntheticOverlay;
61
+ }
62
+ for (const boundaryNode of [node.childNodes[offset], node.childNodes[offset - 1]]) {
63
+ if (!(boundaryNode instanceof HTMLElement)) {
64
+ continue;
65
+ }
66
+ if (boundaryNode.matches(SYNTHETIC_TEXT_OVERLAY_SELECTOR)) {
67
+ return boundaryNode;
68
+ }
69
+ const descendant = boundaryNode.querySelector(SYNTHETIC_TEXT_OVERLAY_SELECTOR);
70
+ if (descendant) {
71
+ return descendant;
72
+ }
73
+ }
74
+ return null;
75
+ };
76
+ const getInlineBlockBetweenBoundaryTexts = (startText, endText, yStart, yEnd) => {
77
+ if (!endText || startText.parent !== endText.parent) {
78
+ return null;
79
+ }
80
+ if (yStart !== startText.length || yEnd !== 0) {
81
+ return null;
82
+ }
83
+ const content = startText.parent.content;
84
+ const startIndex = content.indexOf(startText);
85
+ const endIndex = content.indexOf(endText);
86
+ if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
87
+ return null;
88
+ }
89
+ const selectedParts = content.slice(startIndex + 1, endIndex);
90
+ return selectedParts.length === 1 && selectedParts[0] instanceof InlineBlock
91
+ ? selectedParts[0]
92
+ : null;
93
+ };
94
+ /**
95
+ * Where a range over `block` starts (`first`) or ends (`last`): its first or
96
+ * last shown line (a list's items, a code block's lines, GX-03). A block that
97
+ * shows none (a divider) passes to its neighbours as a caret does, the next
98
+ * line first; to `cover` it, a start passes to the line before it and an end
99
+ * to the line after it, so the range still holds it — when a line lies on
100
+ * that side: a divider with none beyond it (it starts or ends the
101
+ * document, or only dividers follow to that edge) is left out.
102
+ */
103
+ const rangeEdge = (block, edge, cover = false) => {
104
+ const text = shownText(block, edge);
105
+ if (text)
106
+ return { text, offset: edge === 'first' ? 0 : text.length };
107
+ const before = cover && edge === 'first';
108
+ return (caretBeside(block, before ? 'blockBefore' : 'blockAfter') ??
109
+ caretBeside(block, before ? 'blockAfter' : 'blockBefore'));
110
+ };
9
111
  export class EdytorSelection {
10
112
  edytorOnSelectionChange;
11
113
  edytor;
12
114
  focusedBlocks = new SvelteSet();
115
+ /** The selected blocks as clicked: a grip-selected list is one block. */
13
116
  selectedBlocks = new SvelteSet();
14
- hasSelectedAll = $state(false);
15
- state = $state({
16
- selection: null,
17
- start: 0,
18
- end: 0,
19
- yStart: 0,
20
- yEnd: 0,
21
- length: 0,
22
- content: '',
23
- isCollapsed: true,
24
- ranges: [],
25
- texts: [],
26
- blocks: [],
27
- contentParts: [],
28
- isAtStartOfBlock: false,
29
- isAtEndOfBlock: false,
30
- isAtStartOfText: false,
31
- isAtEndOfText: false,
32
- startNode: null,
33
- endNode: null,
34
- startText: null,
35
- endText: null,
36
- startBlock: null,
37
- endBlock: null,
38
- isTextSpanning: false,
39
- isBlockSpanning: false,
40
- isVoid: false,
41
- isIsland: false,
42
- islandRoot: null,
43
- voidRoot: null,
44
- relativePosition: null,
45
- // TOREMOVE
46
- yTextContent: ''
47
- });
117
+ /**
118
+ * The blocks a command over the block selection acts on, in document
119
+ * order: the selected blocks, a selected list or code block with its
120
+ * whole subtree (`sel.blocks.exact`). Delete, cut, copy and Turn into read it.
121
+ */
122
+ get selectedMembers() {
123
+ return visibility.selectedMembers(this.edytor);
124
+ }
125
+ selectedInlineBlock = new SvelteSet();
126
+ inlineBlockDeletionTarget = null;
127
+ /**
128
+ * Set for the synchronous window of THIS view's history command
129
+ * (`session/history`): the replay commits under the history's origin, so
130
+ * the remote-apply and repair restores stand aside — the view's recorded
131
+ * value is selected right after.
132
+ */
133
+ expectHistoryRestore = false;
134
+ pointerDragStart = null;
135
+ selectionDocument = null;
136
+ shouldKeepModelSelectionForNextTextInsertion = false;
137
+ modelSelectionPreservationBlock = null;
138
+ /**
139
+ * The selection (R9, L4): a value — none, a text range of two anchors,
140
+ * one inline atom, or a set of block ids. Only `select()` replaces it.
141
+ */
142
+ value = $state.raw(noSelection);
143
+ /** Advanced by every `select()`. */
144
+ epoch = 0;
145
+ /**
146
+ * Advanced by every `select()` that did not come from the DOM: the
147
+ * projector displays the current value after the flush (R10).
148
+ */
149
+ request = $state(0);
150
+ /** The last display request came from a user-input frame: the display may scroll (O54). */
151
+ scrollOnDisplay = false;
152
+ /** The intent serial (gestures but `input`) when the last display was requested. */
153
+ requestSerial = 0;
154
+ /** Why the last `select()` ran. */
155
+ cause = 'model';
156
+ /** DOM fields (Surface) observed with the value they describe. */
157
+ /** The mark-edge side a display or a DOM derive observed the value with. */
158
+ #surface = null;
159
+ #compat = new WeakMap();
48
160
  constructor(edytor, edytorOnSelectionChange) {
49
161
  this.edytorOnSelectionChange = edytorOnSelectionChange;
50
162
  this.edytor = edytor;
51
163
  }
164
+ /** The projection of the current value at the current document version. */
165
+ get projection() {
166
+ void this.edytor.valueRevision;
167
+ return project(this.value, this.edytor.facade);
168
+ }
169
+ /**
170
+ * The read-only wrapper view of the projection (D3): every field is a
171
+ * projection of (value, document version); `edge` comes from the Surface
172
+ * for the value it was observed with.
173
+ */
174
+ get state() {
175
+ const projection = this.projection;
176
+ const surface = this.#surface?.value === this.value ? this.#surface : null;
177
+ const hit = this.#compat.get(projection);
178
+ if (hit && hit.surface === surface)
179
+ return hit.state;
180
+ const state = this.#view(this.value, projection, surface?.edge);
181
+ this.#compat.set(projection, { surface, state });
182
+ return state;
183
+ }
184
+ #textOf = (block, segment) => {
185
+ if (segment?.kind !== 'text')
186
+ return null;
187
+ return this.edytor.idToBlock.text(block.id, segment.segOrd);
188
+ };
189
+ #view = (value, projection, edge) => {
190
+ const { start, end } = projection;
191
+ const blockOf = (id) => this.edytor.idToBlock.get(id) ?? null;
192
+ const startBlock = start && blockOf(start.block);
193
+ const endBlock = end && blockOf(end.block);
194
+ if (!start || !end || !startBlock || !endBlock)
195
+ return EMPTY_STATE;
196
+ let parts = projection.segments;
197
+ if (value.kind === 'atom') {
198
+ const all = segmentsOf(this.edytor.facade, start.block);
199
+ const index = all.findIndex((part) => part.kind === 'inline' && part.id === value.atomId);
200
+ parts = all.slice(index - 1, index + 2);
201
+ }
202
+ const segments = parts.filter((part) => part.kind === 'text');
203
+ const [startSegment, endSegment] = [segments[0], segments.at(-1)];
204
+ const startText = this.#textOf(startBlock, startSegment);
205
+ const endText = this.#textOf(endBlock, endSegment);
206
+ if (!startText || !endText || !startSegment || !endSegment)
207
+ return EMPTY_STATE;
208
+ const texts = segments.flatMap((part) => {
209
+ const block = blockOf(part.block);
210
+ return (block && this.#textOf(block, part)) ?? [];
211
+ });
212
+ const blocks = value.kind === 'blocks'
213
+ ? value.ids
214
+ .filter((id) => this.edytor.facade.isVisibleBlock(id))
215
+ .sort((a, b) => this.edytor.facade.compare(a, b))
216
+ .flatMap((id) => blockOf(id) ?? [])
217
+ : projection.blocks.flatMap((id) => blockOf(id) ?? []);
218
+ const node = this.edytor.node;
219
+ return Object.defineProperty({
220
+ yStart: start.offset - startSegment.start,
221
+ yEnd: end.offset - endSegment.start,
222
+ isCollapsed: projection.isCollapsed,
223
+ isReversed: projection.isReversed,
224
+ isBlockSpanning: projection.isBlockSpanning,
225
+ texts,
226
+ blocks,
227
+ edge,
228
+ startText,
229
+ endText,
230
+ startBlock,
231
+ endBlock
232
+ }, 'isVoidEditableElement', {
233
+ enumerable: true,
234
+ get: () => projection.voidRoot !== null &&
235
+ ['INPUT', 'TEXTAREA'].includes(getActiveElement(node)?.tagName.toUpperCase() ?? '')
236
+ });
237
+ };
238
+ /**
239
+ * The one commit point (R9): replaces the value, advances the epoch and
240
+ * applies every side effect once — the selected, atom and focused sets
241
+ * (hooks and attributes), and, when the value changed, presence and `onSelectionChange`.
242
+ * `surface` carries the mark-edge side the value was observed with.
243
+ */
244
+ select = (next, cause = 'model', surface) => {
245
+ next = this.#keepPending(this.#shown(next));
246
+ const changed = !sameValue(this.value, next);
247
+ if (changed)
248
+ this.value = next;
249
+ const value = this.value;
250
+ this.epoch++;
251
+ // A repair is a background display (it never takes focus); a command,
252
+ // history or host code asks for one (R10).
253
+ if (cause === 'repair')
254
+ this.edytor.surface.update();
255
+ else if (cause !== 'dom')
256
+ this.display();
257
+ this.cause = cause;
258
+ if (surface)
259
+ this.#surface = { value, ...surface };
260
+ const projection = project(value, this.edytor.facade);
261
+ const blockOf = (id) => this.edytor.idToBlock.get(id);
262
+ const selected = value.kind === 'blocks'
263
+ ? value.ids
264
+ .filter((id) => this.edytor.facade.isVisibleBlock(id))
265
+ .flatMap((id) => blockOf(id) ?? [])
266
+ : [];
267
+ const atom = value.kind === 'atom'
268
+ ? blockOf(value.blockId)?.content.find((part) => part instanceof InlineBlock && part.id === value.atomId)
269
+ : undefined;
270
+ const focused = value.kind === 'text'
271
+ ? projection.blocks.flatMap((id) => blockOf(id) ?? [])
272
+ : value.kind === 'atom'
273
+ ? [blockOf(value.blockId) ?? []].flat()
274
+ : [];
275
+ this.#sync(this.selectedBlocks, selected, 'selected');
276
+ this.#sync(this.focusedBlocks, focused, 'focused');
277
+ if (atom !== this.inlineBlockDeletionTarget || (!atom && this.selectedInlineBlock.size)) {
278
+ this.selectedInlineBlock.clear();
279
+ if (atom)
280
+ this.selectedInlineBlock.add(atom);
281
+ this.inlineBlockDeletionTarget = atom ?? null;
282
+ }
283
+ const state = this.state;
284
+ this.edytor.history?.selected(value);
285
+ if (state.startText) {
286
+ this.#lastText = state.startText;
287
+ this.#lastBlock = state.startBlock?.id ?? null;
288
+ }
289
+ if (!changed)
290
+ return;
291
+ // A dead view never publishes: its entry went with its teardown.
292
+ if (!this.edytor.destroyed) {
293
+ const focus = projection.isReversed ? projection.start : projection.end;
294
+ this.edytor.presence.write(serialize(value, projection), focus?.block ?? null);
295
+ }
296
+ this.edytorOnSelectionChange?.(this);
297
+ this.edytor.plugins.forEach((plugin) => {
298
+ plugin.onSelectionChange?.(this);
299
+ });
300
+ };
301
+ /** The marks the next insertion at the caret takes (L4, values kept). */
302
+ get pending() {
303
+ return this.value.kind === 'text' ? this.value.pending : undefined;
304
+ }
305
+ /** Stage (or, with `undefined`, clear) the caret's pending marks: a new value, same anchors. */
306
+ stage = (pending) => {
307
+ const value = this.value;
308
+ if (value.kind === 'text')
309
+ this.select(Object.freeze({ ...value, pending }));
310
+ };
311
+ /**
312
+ * No text endpoint rests in a block that renders no content (FX-01): its
313
+ * slot never mounts, so typing there is saved and never shown. Such a
314
+ * caret moves to the nearest shown line — a container's first item, else
315
+ * the next line, else the end of the one before (a line just created is
316
+ * displayed once it mounts). A range's start moves to the block's first
317
+ * shown line and its end to its last (a list's items, `rangeEdge`, GX-03);
318
+ * a void's passes on as a caret does. With none, the value stays as it
319
+ * was. Every caret write passes here, and so does the repair after a
320
+ * change this view did not make.
321
+ */
322
+ #shown = (next) => {
323
+ if (next.kind !== 'text')
324
+ return next;
325
+ const { start, end, isReversed } = project(next, this.edytor.facade);
326
+ const caret = next.focus === next.anchor;
327
+ const moved = (at, anchor, edge) => {
328
+ const block = at && this.edytor.idToBlock.get(at.block);
329
+ if (!block || block.rendersContent)
330
+ return anchor;
331
+ const to = caret
332
+ ? (caretBeside(block, 'blockAfter') ?? caretBeside(block, 'blockBefore'))
333
+ : rangeEdge(block, edge);
334
+ return to && this.createTextAnchor(to.text, to.offset);
335
+ };
336
+ const [first, last] = isReversed ? ['last', 'first'] : ['first', 'last'];
337
+ const anchor = moved(isReversed ? end : start, next.anchor, first);
338
+ const focus = caret ? anchor : moved(isReversed ? start : end, next.focus, last);
339
+ if (anchor === next.anchor && focus === next.focus)
340
+ return next;
341
+ return anchor && focus ? textSelection(anchor, focus, next.pending) : this.value;
342
+ };
343
+ /** A caret that did not move keeps its pending marks (L4); a value that names them wins. */
344
+ #keepPending = (next) => {
345
+ const prev = this.value;
346
+ if (prev.kind !== 'text' || !prev.pending || next.kind !== 'text' || 'pending' in next)
347
+ return next;
348
+ const [a, b] = [project(prev, this.edytor.facade), project(next, this.edytor.facade)];
349
+ const at = (p, q) => p !== null && q !== null && p.block === q.block && p.offset === q.offset;
350
+ return at(a.start, b.start) && at(a.end, b.end)
351
+ ? textSelection(next.anchor, next.focus, prev.pending)
352
+ : next;
353
+ };
354
+ /** Diff one block set against its new members: hooks only for changes (the renderer draws the attributes). */
355
+ #sync = (set, blocks, kind) => {
356
+ const next = new Set(blocks);
357
+ for (const block of set) {
358
+ if (next.has(block))
359
+ continue;
360
+ set.delete(block);
361
+ // `onDeselect` pairs with `onSelect`; `onBlur` keeps firing on both
362
+ // (it has historically doubled as the selection-loss hook).
363
+ if (kind === 'selected')
364
+ block.definition.onDeselect?.({ block });
365
+ block.definition.onBlur?.({ block });
366
+ }
367
+ for (const block of next) {
368
+ if (set.has(block))
369
+ continue;
370
+ set.add(block);
371
+ if (kind === 'selected')
372
+ block.definition.onSelect?.({ block });
373
+ else
374
+ block.definition.onFocus?.({ block });
375
+ }
376
+ };
377
+ /**
378
+ * A text value from wrapper endpoints (offsets inside text segments):
379
+ * the start binds right (boundary inserts stay outside), the end and a
380
+ * caret bind left; `none` when the start text is not bound to the document.
381
+ */
382
+ textValue = (startText, yStart, endText = startText, yEnd = yStart, isReversed = false, startAffinity, mint = this.createTextAnchor) => {
383
+ const collapsed = startText === endText && yStart === yEnd;
384
+ const start = mint(startText, yStart, startAffinity ?? (collapsed ? 'left' : 'right'));
385
+ if (!start)
386
+ return noSelection;
387
+ if (collapsed)
388
+ return textSelection(start);
389
+ const end = mint(endText, yEnd, 'left') ?? start;
390
+ return isReversed ? textSelection(end, start) : textSelection(start, end);
391
+ };
392
+ /**
393
+ * A write's intent (R4): `textValue` minted from each text's block record,
394
+ * live or not — the atoms of a text that died to a merge live on in the
395
+ * block that claimed them, so resolution follows them before any seam.
396
+ */
397
+ #intent = (startText, yStart, endText = startText, yEnd = yStart, isReversed = false) => this.textValue(startText, yStart, endText, yEnd, isReversed, undefined, (text, at, side) => {
398
+ return text.parent.isRoot
399
+ ? null
400
+ : this.edytor.facade.anchorAt(text.blockId, text.segStart + at, side);
401
+ });
402
+ /** Ask the projector to display the current value after the flush (R10). */
403
+ display = () => {
404
+ this.scrollOnDisplay =
405
+ this.edytor.isHandlingUserInput && this.edytor.suppressCaretScrollDepth === 0;
406
+ this.requestSerial = this.edytor.intentSerial;
407
+ this.request++;
408
+ };
409
+ /** The mark-edge side a display showed the current value with. */
410
+ observed = (surface) => {
411
+ this.#surface = { value: this.value, ...surface };
412
+ };
413
+ /** A pointer drag is in progress: the projector does not display under it (O57). */
414
+ get dragging() {
415
+ return this.pointerDragStart !== null;
416
+ }
417
+ getBlockByPath = (path) => {
418
+ if (!path?.length) {
419
+ return null;
420
+ }
421
+ let block = this.edytor.root;
422
+ for (const index of path) {
423
+ block = block?.children[index];
424
+ }
425
+ return block ?? null;
426
+ };
52
427
  destroy = () => {
53
- document?.removeEventListener('selectionchange', this.onSelectionChange);
428
+ if (this.selectionDocument) {
429
+ this.selectionDocument.removeEventListener('selectionchange', this.onSelectionChange);
430
+ this.selectionDocument = null;
431
+ }
432
+ };
433
+ /**
434
+ * `selectedInlineBlock` and `inlineBlockDeletionTarget` are one
435
+ * selection — every mutation must go through these two helpers so the
436
+ * armed Backspace/delete target can never outlive the cleared set
437
+ * (a stale target feeds `deleteSelectedInlineBlock` and makes
438
+ * `restoreSelectionAfterRepair` bail forever).
439
+ */
440
+ clearInlineBlockSelection = () => {
441
+ const value = this.value;
442
+ if (value.kind !== 'atom')
443
+ return;
444
+ // Leaving the atom: a caret at its start (the end of the text before it).
445
+ const atom = this.edytor.idToBlock
446
+ .get(value.blockId)
447
+ ?.content.find((part) => part instanceof InlineBlock && part.id === value.atomId);
448
+ const before = atom?.parent.content[atom.parent.content.indexOf(atom) - 1];
449
+ this.select(before instanceof Text ? this.textValue(before, before.length) : noSelection);
450
+ };
451
+ selectInlineBlock = (inlineBlock, from = 'before') => {
452
+ this.select(atomSelection(inlineBlock.parent.id, inlineBlock.id, from));
453
+ };
454
+ clearModelSelectionPreservation = () => {
455
+ this.shouldKeepModelSelectionForNextTextInsertion = false;
456
+ this.modelSelectionPreservationBlock = null;
457
+ };
458
+ isStateBlockContentRange = (block) => Boolean(!this.state.isCollapsed &&
459
+ this.state.startBlock === block &&
460
+ this.state.endBlock === block &&
461
+ this.state.startText === block.firstText &&
462
+ this.state.endText === block.lastText &&
463
+ this.state.yStart === 0 &&
464
+ this.state.yEnd === block.lastText.length);
465
+ consumeModelSelectionPreservationForTextInsertion = () => {
466
+ const preservedBlock = this.modelSelectionPreservationBlock;
467
+ const shouldKeepPreservedBlock = this.shouldKeepModelSelectionForNextTextInsertion &&
468
+ preservedBlock instanceof Block &&
469
+ this.getBlockByPath(preservedBlock.path) === preservedBlock;
470
+ const shouldKeepSelection = shouldKeepPreservedBlock ||
471
+ (this.shouldKeepModelSelectionForNextTextInsertion &&
472
+ this.state.startBlock instanceof Block &&
473
+ this.isStateBlockContentRange(this.state.startBlock));
474
+ if (shouldKeepPreservedBlock) {
475
+ this.setStateFromBlockContentRange(preservedBlock);
476
+ }
477
+ this.shouldKeepModelSelectionForNextTextInsertion = false;
478
+ this.modelSelectionPreservationBlock = null;
479
+ return shouldKeepSelection;
54
480
  };
55
481
  getTextOfNode = getTextOfNode.bind(this);
56
482
  getTextsInSelection = getTextsInSelection.bind(this);
483
+ getInlineBlockOfNode = getInlineBlockOfNode.bind(this);
484
+ getInlineBlockInSelectedRange = getInlineBlockInSelectedRange.bind(this);
485
+ getBlockOfNode = (node) => {
486
+ const element = getElementFromNode(node);
487
+ const blockElement = element?.closest('[data-edytor-block]');
488
+ if (!(blockElement instanceof HTMLElement) || !this.edytor.node?.contains(blockElement)) {
489
+ return null;
490
+ }
491
+ const id = blockElement.dataset.edytorId;
492
+ return id ? (this.edytor.idToBlock.get(id) ?? null) : null;
493
+ };
494
+ isInsideEditableText = (node) => {
495
+ const element = getElementFromNode(node);
496
+ return Boolean(element?.closest('[data-edytor-text]'));
497
+ };
498
+ getNonNativeEditableIslandBlock = (node) => {
499
+ const element = getElementFromNode(node);
500
+ const island = element?.closest('[contenteditable="false"]');
501
+ if (!(island instanceof HTMLElement) || !this.edytor.node?.contains(island)) {
502
+ return null;
503
+ }
504
+ if (island.closest('input, textarea, select, button, a[href]')) {
505
+ return null;
506
+ }
507
+ return this.getBlockOfNode(island);
508
+ };
509
+ getNonNativeEditableBlockChromeBlock = (node) => {
510
+ const element = getElementFromNode(node);
511
+ const nonEditableElement = element?.closest('[contenteditable="false"]');
512
+ if (!(nonEditableElement instanceof HTMLElement) ||
513
+ !this.edytor.node?.contains(nonEditableElement)) {
514
+ return null;
515
+ }
516
+ // A suggestion's preview is no block's chrome: a press there is its own (cancelled).
517
+ if (nonEditableElement.closest(`input, textarea, select, button, a[href], ${SUGGESTION}`)) {
518
+ return null;
519
+ }
520
+ const block = this.getBlockOfNode(nonEditableElement);
521
+ if (nonEditableElement.closest('[data-edytor-text], [data-edytor-inline-block]') || !block) {
522
+ return null;
523
+ }
524
+ return block;
525
+ };
526
+ handleNonNativeEditableBlockChromePointerDown = (event) => {
527
+ const targetNode = event.target instanceof Node ? event.target : null;
528
+ const targetBlock = this.getNonNativeEditableBlockChromeBlock(targetNode);
529
+ const targetText = targetBlock?.firstText;
530
+ // A native control inside the chrome (a void block's input) owns its press.
531
+ if (!targetText || isNativeInteractiveEvent(event)) {
532
+ return;
533
+ }
534
+ // The model answers this press: the browser's own caret placement on
535
+ // the chrome is cancelled (a canceled pointerdown moves no selection).
536
+ event.preventDefault();
537
+ this.edytor.expectInternalFocus();
538
+ this.edytor.node?.focus({ preventScroll: true });
539
+ this.setAtTextOffset(targetText, 0);
540
+ };
541
+ normalizeTextRangePoints = (startText, startOffset, endText, endOffset, isReversed) => {
542
+ const clampedStart = Math.min(Math.max(startOffset, 0), startText.length);
543
+ const clampedEnd = Math.min(Math.max(endOffset, 0), endText.length);
544
+ const order = startText === endText
545
+ ? clampedStart - clampedEnd
546
+ : startText.parent === endText.parent
547
+ ? startText.index - endText.index
548
+ : this.edytor.compareBlocks(startText.parent, endText.parent);
549
+ if (order <= 0) {
550
+ return {
551
+ startText,
552
+ startOffset: clampedStart,
553
+ endText,
554
+ endOffset: clampedEnd,
555
+ isReversed: order === 0 ? false : isReversed
556
+ };
557
+ }
558
+ return {
559
+ startText: endText,
560
+ startOffset: clampedEnd,
561
+ endText: startText,
562
+ endOffset: clampedStart,
563
+ // Text atoms never reorder inside one text. Undo can nevertheless
564
+ // resolve the two deleted-boundary anchors on opposite sides of
565
+ // their restored atoms. Sort those offsets without changing the
566
+ // selection direction stored independently in the snapshot.
567
+ isReversed: startText === endText ? isReversed : !isReversed
568
+ };
569
+ };
57
570
  init = () => {
58
- this.edytor.undoManager.on('stack-item-added', (event) => {
59
- event.stackItem.meta.set('cursor-location', {
60
- id: this.state.startText?.id,
61
- offset: this.state.yEnd
62
- });
63
- });
64
- this.edytor.undoManager.on('stack-item-popped', (event) => {
65
- const cursorLocation = event.stackItem.meta.get('cursor-location');
66
- const text = this.edytor.getTextById(cursorLocation.id);
67
- text && this.setAtTextOffset(text, cursorLocation.offset);
68
- });
69
- document?.addEventListener('selectionchange', this.onSelectionChange);
571
+ if (typeof document !== 'undefined') {
572
+ this.selectionDocument = this.edytor.node?.ownerDocument ?? document;
573
+ this.selectionDocument.addEventListener('selectionchange', this.onSelectionChange);
574
+ }
70
575
  };
71
- handleTripleClick = async (e) => {
576
+ /**
577
+ * A triple click selects the block's content in the model; the browser's
578
+ * own multi-click selection is cancelled at its `mousedown`
579
+ * (`preventNativeTripleClick`), so no native selection competes with the display.
580
+ */
581
+ handleTripleClick = (e) => {
72
582
  if (e.detail < 3)
73
583
  return;
74
- await tick();
75
- const { startText } = this.state;
76
- startText && this.setAtTextRange(startText, 0, startText.yText.length);
584
+ const targetNode = e.target instanceof Node ? e.target : null;
585
+ const clickedBlock = this.getBlockOfNode(targetNode);
586
+ if (clickedBlock?.definition.void && !this.isInsideEditableText(targetNode)) {
587
+ e.preventDefault();
588
+ this.selectBlocks(clickedBlock);
589
+ return;
590
+ }
591
+ const targetBlock = targetNode instanceof Node
592
+ ? (this.getTextOfNode(targetNode)?.parent ?? this.getBlockOfNode(targetNode))
593
+ : this.state.startText?.parent;
594
+ if (targetBlock) {
595
+ this.setStateFromBlockContentRange(targetBlock);
596
+ this.shouldKeepModelSelectionForNextTextInsertion = true;
597
+ this.modelSelectionPreservationBlock = targetBlock;
598
+ }
599
+ };
600
+ preventNativeTripleClick = (e) => {
601
+ if (e.detail >= 3)
602
+ e.preventDefault();
603
+ };
604
+ getTextPointFromClientPoint = (clientX, clientY) => {
605
+ const ownerDocument = (this.edytor.node?.ownerDocument ?? document);
606
+ const caretPosition = ownerDocument.caretPositionFromPoint?.(clientX, clientY);
607
+ const caretRange = caretPosition ? null : ownerDocument.caretRangeFromPoint?.(clientX, clientY);
608
+ const node = caretPosition?.offsetNode ?? caretRange?.startContainer ?? null;
609
+ const domOffset = caretPosition?.offset ?? caretRange?.startOffset ?? null;
610
+ const text = this.getTextOfNode(node, domOffset ?? undefined);
611
+ if (!text || typeof domOffset !== 'number') {
612
+ return null;
613
+ }
614
+ return {
615
+ text,
616
+ offset: Math.min(Math.max(getYIndex(text, node, domOffset), 0), text.length),
617
+ clientX,
618
+ clientY
619
+ };
620
+ };
621
+ getTextOffsetFromClientPoint = (text, clientX, clientY) => {
622
+ const ownerDocument = text.node?.ownerDocument;
623
+ if (!text.node || !ownerDocument) {
624
+ return 0;
625
+ }
626
+ const caretPoint = this.getTextPointFromClientPoint(clientX, clientY);
627
+ if (caretPoint?.text === text) {
628
+ return caretPoint.offset;
629
+ }
630
+ let closestOffset = 0;
631
+ let closestDistance = Number.POSITIVE_INFINITY;
632
+ for (let offset = 0; offset <= text.length; offset++) {
633
+ const [textNode, nodeOffset] = domPointOf(text.node, offset);
634
+ const range = ownerDocument.createRange();
635
+ range.setStart(textNode, nodeOffset);
636
+ range.collapse(true);
637
+ const rect = range.getBoundingClientRect();
638
+ const fallbackRect = text.node.getBoundingClientRect();
639
+ const x = rect.width || rect.height ? rect.left : fallbackRect.left;
640
+ const y = rect.width || rect.height ? rect.top + rect.height / 2 : fallbackRect.top;
641
+ const distance = Math.hypot(clientX - x, clientY - y);
642
+ if (distance < closestDistance) {
643
+ closestDistance = distance;
644
+ closestOffset = offset;
645
+ }
646
+ }
647
+ return closestOffset;
77
648
  };
78
- shift = async (length) => {
79
- this.state.yStart += length;
80
- this.state.yEnd += length;
81
- this.state.start += length;
82
- this.state.end += length;
83
- this.state.yTextContent = this.state.startText?.yText.toJSON();
649
+ setTextSelectionFromPointer = (text, clientX, clientY) => {
650
+ this.setAtTextOffset(text, this.getTextOffsetFromClientPoint(text, clientX, clientY));
84
651
  };
652
+ capturePointerDragStart = (event) => {
653
+ if (event.button !== 0) {
654
+ this.pointerDragStart = null;
655
+ return;
656
+ }
657
+ this.pointerDragStart = this.getTextPointFromClientPoint(event.clientX, event.clientY);
658
+ };
659
+ clearPointerDragStart = () => {
660
+ this.pointerDragStart = null;
661
+ this.#dropped();
662
+ };
663
+ /** A normalization a pointer drag held back (the gesture is the user's, O57). */
664
+ #held = false;
665
+ /** The drag ended: the DOM selection it left is derived again, now normalized. */
666
+ #dropped = () => {
667
+ if (!this.#held)
668
+ return;
669
+ this.#held = false;
670
+ this.applySelectionSnapshot(getDomSelectionSnapshot(this.edytor.node));
671
+ };
672
+ collapseSelectedBlocksAtPointer = (event) => {
673
+ if (event.button !== 0 || this.selectedBlocks.size === 0) {
674
+ return;
675
+ }
676
+ const point = this.getTextPointFromClientPoint(event.clientX, event.clientY);
677
+ if (!point) {
678
+ return;
679
+ }
680
+ this.setAtTextOffset(point.text, point.offset);
681
+ };
682
+ restoreInlineAtomDragRange = (event) => {
683
+ const dragStart = this.pointerDragStart;
684
+ this.pointerDragStart = null;
685
+ this.#dropped();
686
+ if (!dragStart || event.button !== 0) {
687
+ return;
688
+ }
689
+ const dragEnd = this.getTextPointFromClientPoint(event.clientX, event.clientY);
690
+ if (!dragEnd) {
691
+ return;
692
+ }
693
+ const movement = Math.hypot(event.clientX - dragStart.clientX, event.clientY - dragStart.clientY);
694
+ if (movement < 4) {
695
+ return;
696
+ }
697
+ window.setTimeout(() => {
698
+ if (dragStart.text.parent !== dragEnd.text.parent) {
699
+ return;
700
+ }
701
+ const content = dragStart.text.parent.content;
702
+ const startIndex = content.indexOf(dragStart.text);
703
+ const endIndex = content.indexOf(dragEnd.text);
704
+ const firstTextIndex = Math.min(startIndex, endIndex);
705
+ const lastTextIndex = Math.max(startIndex, endIndex);
706
+ const hasInlineAtomBetweenTextPoints = content.some((part, index) => part instanceof InlineBlock && index > firstTextIndex && index < lastTextIndex);
707
+ if (startIndex === -1 || endIndex === -1 || !hasInlineAtomBetweenTextPoints) {
708
+ return;
709
+ }
710
+ const isReversed = startIndex > endIndex;
711
+ const rangeStart = isReversed ? dragEnd : dragStart;
712
+ const rangeEnd = isReversed ? dragStart : dragEnd;
713
+ this.clearInlineBlockSelection();
714
+ this.setAtRange(rangeStart.text, rangeStart.offset, rangeEnd.text, rangeEnd.offset, {
715
+ isReversed
716
+ });
717
+ this.clearInlineBlockSelection();
718
+ });
719
+ };
720
+ /**
721
+ * `selectstart` is the only event fired before a drag-selection begins.
722
+ * A drag starting on `contenteditable=false` chrome (void/island
723
+ * chrome, inline atoms, placeholders, block handles, the render
724
+ * anchor) would anchor the DOM selection on nodes the model cannot
725
+ * represent — block-level selection owns that surface instead.
726
+ *
727
+ * The NEAREST `[contenteditable]` boundary decides: `false` means
728
+ * non-editable chrome, while a nested `true` (void-block captions
729
+ * re-enable editing on their text spans) keeps native selection.
730
+ * The root itself is excluded so readonly mode stays selectable.
731
+ */
732
+ onSelectStart = (event) => {
733
+ // Native controls (todo checkboxes, plugin inputs…) keep their own
734
+ // selection behavior.
735
+ if (isNativeInteractiveEvent(event)) {
736
+ return;
737
+ }
738
+ const element = getElementFromNode(event.target instanceof Node ? event.target : null);
739
+ if (!element || !this.edytor.node?.contains(element)) {
740
+ return;
741
+ }
742
+ // Walk ancestors to the nearest contenteditable boundary. Both the
743
+ // attribute and the `contentEditable` property are checked — the
744
+ // attach hooks of inline/void blocks set the property, which jsdom
745
+ // does not reflect onto the attribute.
746
+ let boundary = element instanceof HTMLElement ? element : null;
747
+ while (boundary && boundary !== this.edytor.node) {
748
+ const state = boundary.getAttribute('contenteditable') ?? boundary.contentEditable;
749
+ if (state === 'false') {
750
+ event.preventDefault();
751
+ return;
752
+ }
753
+ if (state === 'true' || state === 'plaintext-only' || state === '') {
754
+ // Nested editable region (e.g. void-block captions re-enabling
755
+ // editing on their text spans) keeps native selection.
756
+ return;
757
+ }
758
+ boundary = boundary.parentElement;
759
+ }
760
+ };
761
+ /**
762
+ * One classifier (R10, the projector): an echo or a DOM state older than a
763
+ * display still to land is ignored, and so is a move while a composition
764
+ * session is live (the IME's; the session's end displays); drift is
765
+ * displayed again; a foreign write or intent is adopted.
766
+ */
85
767
  onSelectionChange = () => {
86
- const selection = window.getSelection();
87
- if (!selection?.anchorNode || !this.edytor.container?.contains(selection.anchorNode)) {
768
+ const selection = getDomSelectionSnapshot(this.edytor.node);
769
+ const observation = this.edytor.projector.classify(selection);
770
+ if (observation === 'echo' || observation === 'composition')
771
+ return;
772
+ if (observation === 'drift')
773
+ return this.display();
774
+ this.applySelectionSnapshot(selection);
775
+ };
776
+ applySelectionSnapshot = (selection, options = {}) => {
777
+ if (this.selectedBlocks.size > 0)
778
+ return;
779
+ const container = this.edytor.node;
780
+ if (!selection?.anchorNode ||
781
+ !selection.focusNode ||
782
+ !container?.contains(selection.anchorNode) ||
783
+ !container.contains(selection.focusNode)) {
784
+ return;
785
+ }
786
+ const activeElement = getActiveElement(this.edytor.node);
787
+ if (activeElement instanceof Element &&
788
+ this.edytor.node?.contains(activeElement) &&
789
+ isNativeFormControl(activeElement)) {
88
790
  return;
89
791
  }
90
- const { anchorNode, focusNode, anchorOffset, focusOffset, isCollapsed, direction, type } = selection;
792
+ // A selection inside a nested foreign editable belongs to the
793
+ // island, not the model — deriving it would map the caret into the
794
+ // island's host text and corrupt the next model-routed write. Both
795
+ // endpoints are checked: a range anchored in host text but focused
796
+ // inside an island must not resolve either.
797
+ if (isNestedForeignEditableTarget(this.edytor.node, selection.anchorNode) ||
798
+ isNestedForeignEditableTarget(this.edytor.node, selection.focusNode)) {
799
+ return;
800
+ }
801
+ const { anchorNode, focusNode, anchorOffset, focusOffset } = selection;
802
+ const isCollapsed = selection.isCollapsed;
803
+ // A native vertical move that landed on no caret stop takes the key's line stop (R9).
804
+ if (isCollapsed &&
805
+ !isTextBoundSelectionPoint(anchorNode) &&
806
+ !this.getInlineBlockOfNode(anchorNode) &&
807
+ landed(this.edytor))
808
+ return;
91
809
  const ranges = getRangesFromSelection(selection);
92
- const isReversed = focusNode === anchorNode
93
- ? focusOffset < anchorOffset
94
- : focusNode
95
- ? anchorNode.compareDocumentPosition(focusNode) === Node.DOCUMENT_POSITION_PRECEDING
96
- : false;
97
- const content = selection?.toString() || '';
810
+ const isReversed = isBackward(selection);
98
811
  const startNode = isReversed ? focusNode : anchorNode;
99
812
  const endNode = isReversed ? anchorNode : focusNode;
100
813
  const start = isReversed ? focusOffset : anchorOffset;
101
814
  const end = isReversed ? anchorOffset : focusOffset;
102
- const { startText, endText, texts } = this.getTextsInSelection(startNode, endNode, ranges);
103
- this.selectBlocks();
104
- this.focusBlocks(...texts.map((text) => text.parent));
105
- let yStart = getYIndex(startText, startNode, start);
106
- let yEnd = isCollapsed ? yStart : getYIndex(endText, endNode, end);
107
- if (startText?.parent.isEmpty && yStart > startText.length) {
108
- // In case the placeholder or suggestion is not absolutely positioned, we need to set the selection to the start of the text because caret may be placed after the placeholder which is deceptive
109
- return this.setAtTextOffset(startText, startText.length);
110
- }
111
- let isIsland = false;
112
- let islandRoot = null;
113
- climb(startText?.parent, (block) => {
114
- if (block instanceof Block && block.definition.island) {
115
- isIsland = true;
116
- islandRoot = block;
117
- return true;
815
+ const selectedInlineBlock = this.getInlineBlockInSelectedRange(ranges[0]);
816
+ const selectionParts = selectedInlineBlock
817
+ ? { startText: null, endText: null, inlineBlock: selectedInlineBlock }
818
+ : this.getTextsInSelection(startNode, endNode, start, end);
819
+ let { startText, endText, inlineBlock } = selectionParts;
820
+ if (!startText) {
821
+ const islandBlock = this.getNonNativeEditableIslandBlock(startNode);
822
+ if (islandBlock?.firstText) {
823
+ return this.setAtTextOffset(islandBlock.firstText, 0);
118
824
  }
119
- });
120
- let isVoid = false;
121
- let voidRoot = null;
122
- climb(startText?.parent, (block) => {
123
- if (block instanceof Block && block.definition.void) {
124
- isVoid = true;
125
- voidRoot = block;
126
- return true;
825
+ // A point on a white space node: the model caret is displayed again.
826
+ if (this.state.startText && !inlineBlock) {
827
+ this.display();
828
+ return;
127
829
  }
128
- });
129
- if (!isVoid) {
130
- climbDom(startNode, (node) => {
131
- if (node instanceof HTMLElement && node.dataset.edytorBlock && node.dataset.edytorVoid) {
132
- const id = node.dataset.edytorId;
133
- const block = id && this.edytor.idToBlock.get(id);
134
- if (block) {
135
- isVoid = true;
136
- voidRoot = block;
137
- return true;
138
- }
830
+ else {
831
+ clearDomSelection(this.edytor.node);
832
+ if (inlineBlock) {
833
+ this.selectInlineBlock(inlineBlock);
139
834
  }
140
- });
141
- }
142
- const isAtStartOfText = yStart === 0;
143
- const isAtEndOfText = yEnd === endText?.yText.length;
144
- const isAtStartOfBlock = (startText && startText === startText.parent.firstText && isAtStartOfText) || false;
145
- const isAtEndOfBlock = (endText && endText === endText.parent.lastText && isAtEndOfText) || false;
146
- const startBlock = startText?.parent || null;
147
- const endBlock = endText?.parent || null;
148
- const { startBlock: previousStartBlock, endBlock: previousEndBlock } = this.state;
149
- if ((previousStartBlock !== startBlock || previousEndBlock !== endBlock) &&
150
- [previousStartBlock, previousEndBlock].some((block) => block?.suggestions)) {
151
- [previousStartBlock, previousEndBlock].forEach((block) => {
152
- if (block?.suggestions) {
153
- block.suggestions = null;
835
+ else {
836
+ this.clearInlineBlockSelection();
154
837
  }
155
- });
838
+ return;
839
+ }
156
840
  }
157
- const isTextSpanning = startText !== endText;
158
- const isBlockSpanning = startBlock !== endBlock;
159
- const blocks = startBlock ? [startBlock] : [];
160
- let currentBlock = startBlock;
161
- // Collect all blocks between the start and end blocks
162
- while (currentBlock && currentBlock !== endBlock) {
163
- const nextBlock = currentBlock.closestNextBlock;
164
- currentBlock = nextBlock;
165
- if (nextBlock) {
166
- blocks.push(nextBlock);
841
+ const syntheticOverlay = isCollapsed ? getSyntheticTextOverlayElement(startNode, start) : null;
842
+ if (syntheticOverlay) {
843
+ const overlayBlock = this.getBlockOfNode(syntheticOverlay);
844
+ const targetText = overlayBlock?.lastText ?? startText;
845
+ return this.setAtTextOffset(targetText, targetText.length);
846
+ }
847
+ const rawYStart = getYIndex(startText, startNode, start);
848
+ const rawYEnd = isCollapsed ? rawYStart : getYIndex(endText, endNode, end);
849
+ let yStart = normalizeUtf16Boundary(startText.stringContent, rawYStart, isCollapsed ? 'forward' : 'backward');
850
+ let yEnd = isCollapsed
851
+ ? yStart
852
+ : endText
853
+ ? normalizeUtf16Boundary(endText.stringContent, rawYEnd, 'forward')
854
+ : rawYEnd;
855
+ let shouldRestoreNormalizedDomRange = yStart !== rawYStart || yEnd !== rawYEnd;
856
+ if (!isCollapsed &&
857
+ endText &&
858
+ startText.parent !== endText.parent &&
859
+ endText === endText.parent.firstText &&
860
+ yEnd === 0) {
861
+ // The shown block before: a closed toggle's header, not its hidden body.
862
+ const previousEndText = visibility.shown(endText.parent, 'blockBefore')?.lastText ?? null;
863
+ if (previousEndText?.node?.isConnected) {
864
+ endText = previousEndText;
865
+ yEnd = previousEndText.length;
866
+ shouldRestoreNormalizedDomRange = true;
167
867
  }
168
868
  }
169
- // Flatten the blocks into a single array of content parts
170
- let allContentParts = blocks.flatMap((block) => block.content);
171
- let contentParts = [];
172
- let currentPart = startText;
173
- let i = 0;
174
- while (currentPart && currentPart !== endText) {
175
- contentParts.push(currentPart);
176
- currentPart = allContentParts[i + 1] || null;
177
- i++;
178
- }
179
- if (endText) {
180
- contentParts.push(endText);
181
- }
182
- this.state = {
183
- selection,
184
- start,
185
- end,
186
- yStart,
187
- yEnd,
188
- length: content.length,
189
- content,
190
- isCollapsed,
191
- ranges,
192
- texts: contentParts.filter((part) => part instanceof Text),
193
- contentParts,
194
- blocks,
195
- isBlockSpanning,
196
- startText,
197
- endText,
198
- isAtStartOfText,
199
- isAtEndOfText,
200
- isAtEndOfBlock,
201
- isAtStartOfBlock,
202
- isTextSpanning,
203
- startNode,
204
- endNode,
205
- startBlock,
206
- endBlock,
207
- isVoid,
208
- isIsland,
209
- islandRoot,
210
- voidRoot,
211
- relativePosition: startText
212
- ? Y.createRelativePositionFromTypeIndex(startText.yText, yStart, -1)
213
- : null,
214
- // TOREMOVE
215
- yTextContent: startText?.yText.toJSON()
216
- };
217
- this.edytorOnSelectionChange?.(this);
218
- this.edytor.plugins.forEach((plugin) => {
219
- plugin.onSelectionChange?.(this);
220
- });
221
- };
222
- restoreRelativePosition = (text) => {
223
- if (text !== this.state.startText || !this.state.relativePosition) {
224
- return;
869
+ if (!isCollapsed && startText === endText && yStart === yEnd) {
870
+ // An empty native range reads as a caret; the DOM is normalized to one.
871
+ shouldRestoreNormalizedDomRange = true;
225
872
  }
226
- const absolutePosition = Y.createAbsolutePositionFromRelativePosition(this.state.relativePosition, this.edytor.doc);
227
- if (!absolutePosition) {
873
+ if (startText?.parent.isEmpty && yStart > startText.length) {
874
+ // In case the placeholder or suggestion is not absolutely positioned, we need to set the selection to the start of the text because caret may be placed after the placeholder which is deceptive
875
+ return this.setAtTextOffset(startText, startText.length);
876
+ }
877
+ const inlineBlockSelectedByBoundaryRange = getInlineBlockBetweenBoundaryTexts(startText, endText, yStart, yEnd);
878
+ if (inlineBlockSelectedByBoundaryRange) {
879
+ clearDomSelection(this.edytor.node);
880
+ this.selectInlineBlock(inlineBlockSelectedByBoundaryRange, isReversed ? 'after' : 'before');
228
881
  return;
229
882
  }
230
- this.setAtTextOffset(text, absolutePosition?.index);
231
- };
232
- selectBlocks = (...blocks) => {
233
- const difference = this.selectedBlocks.difference(new Set(blocks));
234
- difference.forEach((block) => {
235
- this.selectedBlocks.delete(block);
236
- block.definition.onBlur?.({ block });
237
- block.node?.removeAttribute('data-edytor-selected');
883
+ this.select(this.textValue(startText, yStart, endText ?? startText, yEnd, isReversed), 'dom', {
884
+ edge: isCollapsed ? getMarkEdgeSide(startText, startNode, yStart) : undefined
238
885
  });
239
- blocks.forEach((block) => {
240
- this.selectedBlocks.add(block);
241
- block.definition.onSelect?.({ block });
242
- block.node?.setAttribute('data-edytor-selected', 'true');
243
- });
244
- if (blocks.length === 1) {
245
- this.focusBlocks();
886
+ this.edytor.projector.observe();
887
+ if (shouldRestoreNormalizedDomRange && options.restoreNormalizedDomRange !== false && endText) {
888
+ // Writing the DOM under a pointer drag would reset its anchor: after it.
889
+ if (this.dragging)
890
+ this.#held = true;
891
+ else
892
+ this.setAtRange(startText, yStart, endText, yEnd, { isReversed });
246
893
  }
247
- };
248
- focusBlocks = (...blocks) => {
249
- const difference = this.focusedBlocks.difference(new Set(blocks));
250
- difference.forEach((block) => {
251
- this.focusedBlocks.delete(block);
252
- block.definition.onBlur?.({ block });
253
- block.node?.removeAttribute('data-edytor-focused');
254
- });
255
- blocks.forEach((block) => {
256
- this.focusedBlocks.add(block);
257
- block.definition.onFocus?.({ block });
258
- block.node?.setAttribute('data-edytor-focused', 'true');
259
- });
260
- };
261
- findTextNode = (node, offset = 0) => {
262
- let nodeOffset = 0;
263
- const treeWalker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, (node) => {
264
- if (node.nodeType === Node.TEXT_NODE) {
265
- return NodeFilter.FILTER_ACCEPT;
266
- }
267
- return NodeFilter.FILTER_SKIP;
268
- });
269
- let currentNode = treeWalker.nextNode();
270
- let textNode = null;
271
- let currentOffset = 0;
272
- while (currentNode) {
273
- const endOffset = currentOffset + currentNode.length;
274
- if (offset >= currentOffset && offset <= endOffset) {
275
- textNode = currentNode;
276
- nodeOffset = offset - currentOffset;
277
- break;
278
- }
279
- currentOffset = endOffset;
280
- currentNode = treeWalker.nextNode();
894
+ // Remember when the derived selection came from a node-bound
895
+ // native range (endpoints outside text elements — e.g. a block
896
+ // node selection or a stray boundary node). Engines asynchronously
897
+ // normalize such DOM selections into text points, so by the time a
898
+ // keydown arrives the live DOM selection may no longer look
899
+ // node-bound — the signature pins the flag to the derived
900
+ // selection it describes and any differently-shaped derive clears
901
+ // it.
902
+ const derivedKey = `${startText.id}:${yStart}:${endText?.id}:${yEnd}:${isReversed ? 1 : 0}`;
903
+ if (!isTextBoundSelectionPoint(selection.anchorNode) ||
904
+ !isTextBoundSelectionPoint(selection.focusNode)) {
905
+ this.#nodeBoundSelectionKey = derivedKey;
281
906
  }
282
- return [textNode, nodeOffset];
283
- };
284
- setAtTextOffset = async (textOrId, textOffset = this.state.yStart) => {
285
- if (!textOrId || typeof textOffset !== 'number') {
286
- return;
907
+ else if (this.#nodeBoundSelectionKey !== derivedKey) {
908
+ this.#nodeBoundSelectionKey = null;
287
909
  }
288
- const node = await this.edytor.getTextNode(textOrId);
289
- const [textNode, nodeOffset] = this.findTextNode(node, textOffset);
290
- if (textNode) {
291
- this.setAtNodeOffset(textNode, nodeOffset);
910
+ };
911
+ #nodeBoundSelectionKey = null;
912
+ /**
913
+ * Whether the live model selection was derived from a node-bound
914
+ * native range — the shape whose Shift+Arrow extension is
915
+ * engine-defined and therefore intercepted by the hotkey layer.
916
+ */
917
+ hasNativeNodeSelection = () => {
918
+ const { startText, endText, yStart, yEnd, isReversed } = this.state;
919
+ if (!startText || !endText) {
920
+ return false;
292
921
  }
922
+ return (this.#nodeBoundSelectionKey ===
923
+ `${startText.id}:${yStart}:${endText.id}:${yEnd}:${isReversed ? 1 : 0}`);
293
924
  };
294
- setAtTextsRange = async (startText, endText) => {
295
- if (!startText.node || !endText.node) {
296
- return;
925
+ /**
926
+ * Anchor a text position into the BACKING text that owns the atom at
927
+ * that display position — the v14 replacement for
928
+ * `Y.createRelativePositionFromTypeIndex(yText, offset, -1)`.
929
+ *
930
+ * `affinity` decides which side of the position the anchor binds to
931
+ * (`'left'` = bound to the preceding atom, inserts at the position
932
+ * land on the anchor's right — the caret baseline `-1`; `'right'` =
933
+ * bound to the following atom, inserts land outside a range starting
934
+ * here). `offset` is an offset inside `text`'s display segment;
935
+ * `text.segStart + offset` is the block-level display offset the
936
+ * facade maps to the owning backing text — content routed from other
937
+ * blocks' backing texts (merges/splits) anchors correctly.
938
+ *
939
+ * `null` while the text isn't bound to a live block (mirrors the
940
+ * baseline where a non-integrated Y.Text couldn't produce an absolute
941
+ * position).
942
+ */
943
+ createTextAnchor = (text, offset, affinity = 'left') => {
944
+ if (text.parent.isRoot || !text.isInDocument)
945
+ return null;
946
+ return this.edytor.facade.anchorAt(text.blockId, text.segStart + offset, affinity);
947
+ };
948
+ /**
949
+ * Resolve a `TextAnchor` → `{text, offset}` (offset within the text
950
+ * segment). The anchor is resolved through ownership — when the bound
951
+ * atoms moved to another block (merge/split), the returned text lives
952
+ * in that block; deleted atoms resolve to the gap where they lived;
953
+ * fully-unowned backing resolves to the owner seam. `null` when the
954
+ * anchor is unresolvable (deleted/hidden target or not-yet-integrated
955
+ * bound item — callers fall back to id/path restore).
956
+ */
957
+ resolveTextAnchor = (anchor) => {
958
+ const facade = this.edytor.facade;
959
+ if (!facade) {
960
+ return null;
297
961
  }
298
- const [startTextNode, startNodeOffset] = this.findTextNode(startText.node, 0);
299
- const [endTextNode, endNodeOffset] = this.findTextNode(endText.node, endText.yText.length);
300
- if (startTextNode && endTextNode) {
301
- const selection = window.getSelection();
302
- const range = document.createRange();
303
- range.setStart(startTextNode, startNodeOffset);
304
- range.setEnd(endTextNode, endNodeOffset);
305
- selection?.removeAllRanges();
306
- selection?.addRange(range);
307
- startTextNode.parentElement?.focus();
962
+ const resolved = facade.resolveAnchor(anchor);
963
+ if (!resolved) {
964
+ return null;
308
965
  }
966
+ return this.edytor.idToBlock.get(resolved.blockId)?.textAtOffset(resolved.offset) ?? null;
967
+ };
968
+ /** The text and block the selection's start last resolved to (the seam origin once they die). */
969
+ #lastText = null;
970
+ #lastBlock = null;
971
+ /**
972
+ * Displayable (§2.4, a Surface fact): the block's own content is mounted
973
+ * and not hidden by view state — a collapsed toggle's body, a `hidden`
974
+ * subtree. A phantom content slot (a snippet that renders no `content()`)
975
+ * never mounts.
976
+ */
977
+ displayable = (id) => {
978
+ const node = this.edytor.idToBlock
979
+ .get(id)
980
+ ?.content.find((part) => part instanceof Text && part.node != null)?.node;
981
+ return !!node && !node.closest(visibility.HIDDEN);
309
982
  };
310
- setAtBlockRange = async (block, startOffset, endOffset) => {
311
- if (!block) {
983
+ /** The view's visibility rule (`selection/visibility.ts`), for extensions: hidden by view state, and the shown neighbour. */
984
+ hidden = visibility.hidden;
985
+ shown = visibility.shown;
986
+ /** The text's computed direction (a Surface fact): arrow and word keys are visual. */
987
+ rtl = (text) => !!text.node?.isConnected && getComputedStyle(text.node).direction === 'rtl';
988
+ /** A caret at the seam `dead` vacated (`doc/anchors`); `null` when nothing displays. */
989
+ #seamValue = (dead) => {
990
+ const { facade } = this.edytor;
991
+ const at = seam(facade, dead, this.displayable);
992
+ const anchor = at && facade.anchorAt(at.block, at.offset, 'left');
993
+ return anchor ? textSelection(anchor) : null;
994
+ };
995
+ /**
996
+ * Post-mirror-flush repair of the endpoints this view did not author
997
+ * (R9: a command that declared its result selection authors its own), through
998
+ * `select()`. Text endpoints follow their anchors by projection; what is
999
+ * repaired is a range whose content died (a caret at that point) and a
1000
+ * value that no longer resolves: a block set keeps its live
1001
+ * members, an atom that vanished leaves a caret at its block's start, and
1002
+ * otherwise the selection lands at the seam of the block it last resolved in
1003
+ * (`doc/anchors`: the replicated slot, displayable stops only). The model is
1004
+ * written at once; the projector displays it (also when the text the caret
1005
+ * was displayed in died while its anchor moved on).
1006
+ */
1007
+ restoreDeadSelectionEndpoints = () => {
1008
+ if (this.expectHistoryRestore) {
312
1009
  return;
313
1010
  }
314
- if (!startOffset) {
315
- startOffset = 0;
316
- }
317
- if (!endOffset) {
318
- endOffset = block.lastText.length;
319
- }
320
- const startNode = await this.edytor.getTextNode(block.firstText);
321
- const [startTextNode, startNodeOffset] = this.findTextNode(startNode, startOffset);
322
- const endNode = await this.edytor.getTextNode(block.lastText);
323
- const [endTextNode, endNodeOffset] = this.findTextNode(endNode, endOffset);
324
- if (startTextNode && endTextNode) {
325
- const selection = window.getSelection();
326
- const range = document.createRange();
327
- range.setStart(startTextNode, startNodeOffset);
328
- range.setEnd(endTextNode, endNodeOffset);
329
- selection?.removeAllRanges();
330
- selection?.addRange(range);
331
- // this.edytor.node!.focus();
332
- startTextNode.parentElement?.focus();
333
- }
334
- };
335
- setAtTextRange = async (text, start, end) => {
336
- if (!text || typeof start !== 'number' || typeof end !== 'number') {
1011
+ // Mid pointer-drag the user's in-progress range owns the selection; a
1012
+ // command that declared its result selection owns this view's endpoints.
1013
+ if (this.pointerDragStart !== null || this.edytor.dispatcher.authoring) {
337
1014
  return;
338
1015
  }
339
- const node = await this.edytor.getTextNode(text);
340
- let startNode = null;
341
- let startOffset = 0;
342
- let endNode = null;
343
- let endOffset = 0;
344
- const treeWalker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, (node) => {
345
- if (node.nodeType === Node.TEXT_NODE) {
346
- return NodeFilter.FILTER_ACCEPT;
1016
+ const { value } = this;
1017
+ const facade = this.edytor.facade;
1018
+ let dead;
1019
+ if (value.kind === 'blocks') {
1020
+ const live = value.ids.filter((id) => facade.isVisibleBlock(id));
1021
+ if (live.length === value.ids.length)
1022
+ return;
1023
+ if (live.length) {
1024
+ this.select(blockSelection(live), 'repair');
1025
+ return;
347
1026
  }
348
- return NodeFilter.FILTER_SKIP;
349
- });
350
- let currentNode = treeWalker.nextNode();
351
- let currentOffset = 0;
352
- while (currentNode) {
353
- const nodeLength = currentNode.textContent?.length || 0;
354
- const endPosition = currentOffset + nodeLength;
355
- // Find start position
356
- if (!startNode && start >= currentOffset && start <= endPosition) {
357
- startNode = currentNode;
358
- startOffset = start - currentOffset;
1027
+ dead = value.ids[0] ?? null;
1028
+ }
1029
+ else if (value.kind === 'atom') {
1030
+ if (this.state.startText)
1031
+ return;
1032
+ const anchor = facade.isVisibleBlock(value.blockId)
1033
+ ? facade.anchorAt(value.blockId, 0, 'left')
1034
+ : null;
1035
+ if (anchor) {
1036
+ this.#land(textSelection(anchor));
1037
+ return;
359
1038
  }
360
- // Find end position
361
- if (!endNode && end >= currentOffset && end <= endPosition) {
362
- endNode = currentNode;
363
- endOffset = end - currentOffset;
1039
+ dead = value.blockId;
1040
+ }
1041
+ else if (value.kind === 'text') {
1042
+ // A peer or host code turned the block it rests in into one that shows
1043
+ // no content (an empty line into a divider): the nearest shown line.
1044
+ const shown = this.#shown(value);
1045
+ if (shown !== value) {
1046
+ this.#land(shown);
1047
+ return;
1048
+ }
1049
+ const state = this.state;
1050
+ const { start, isCollapsed } = this.projection;
1051
+ if (state.startText && start && isCollapsed && value.anchor !== value.focus) {
1052
+ // A range whose content died is a caret at the seam (`sel.seam.covered-atom`):
1053
+ // its two anchors would re-open around text re-inserted there.
1054
+ const caret = facade.anchorAt(start.block, start.offset, 'left');
1055
+ if (caret) {
1056
+ this.#land(textSelection(caret));
1057
+ return;
1058
+ }
364
1059
  }
365
- if (startNode && endNode) {
366
- break;
1060
+ if (state.startText) {
1061
+ // The text it was displayed in died while its anchor moved on: display again.
1062
+ if (this.#lastText !== null && !this.#lastText.isInDocument)
1063
+ this.display();
1064
+ this.#lastText = state.startText;
1065
+ this.#lastBlock = state.startBlock?.id ?? null;
1066
+ return;
367
1067
  }
368
- currentOffset = endPosition;
369
- currentNode = treeWalker.nextNode();
370
- }
371
- if (startNode && endNode) {
372
- const selection = window.getSelection();
373
- const range = document.createRange();
374
- range.setStart(startNode, startOffset);
375
- range.setEnd(endNode, endOffset);
376
- selection?.removeAllRanges();
377
- selection?.addRange(range);
378
- // this.edytor.node!.focus();
379
- startNode.parentElement?.focus();
380
- }
381
- };
382
- setAtNodeOffset = (node, offset) => {
383
- const selection = window.getSelection();
384
- const range = document.createRange();
385
- range.setStart(node, offset);
386
- range.collapse(true);
387
- selection?.removeAllRanges();
388
- selection?.addRange(range);
389
- this.onSelectionChange();
1068
+ dead = this.#lastBlock;
1069
+ }
1070
+ else {
1071
+ // An emptied document rests the caret in its virtual paragraph (`doc.empty.virtual`).
1072
+ const virtual = facade.virtual();
1073
+ if (virtual !== null)
1074
+ this.#land(textSelection(facade.anchorAt(virtual, 0, 'left')));
1075
+ return;
1076
+ }
1077
+ // A live origin means the anchors are not integrated yet: they converge.
1078
+ if (dead !== null && facade.isVisibleBlock(dead))
1079
+ return;
1080
+ // With no displayable stop yet (a whole-document remote delete runs this
1081
+ // pass before the replacement paragraph mounts), the value stays: the
1082
+ // projector's pass after the flush that mounts it runs this repair again.
1083
+ const target = this.#seamValue(dead);
1084
+ if (target)
1085
+ this.#land(target);
1086
+ };
1087
+ /** Select a repaired caret (the projector displays it). */
1088
+ #land = (target) => this.select(target, 'repair');
1089
+ /**
1090
+ * A text range from `first`'s first shown line to `last`'s last (the
1091
+ * triple-click shape; `rangeEdge`); the start binds left. None when no
1092
+ * line is shown.
1093
+ */
1094
+ #contentRange = (first, last = first) => {
1095
+ const [from, to] = [rangeEdge(first, 'first', true), rangeEdge(last, 'last', true)];
1096
+ return from && to
1097
+ ? this.textValue(from.text, from.offset, to.text, to.offset, false, 'left')
1098
+ : null;
1099
+ };
1100
+ /** Select `block`'s content range (the triple-click shape), when it shows a line. */
1101
+ setStateFromBlockContentRange = (block) => {
1102
+ const range = this.#contentRange(block);
1103
+ if (range)
1104
+ this.select(range);
1105
+ };
1106
+ /**
1107
+ * Select a set of whole blocks. With no block, leave block selection:
1108
+ * the value becomes the text range the set showed, from its first shown
1109
+ * line to its last (a list's items; a divider at an edge stays covered
1110
+ * unless no line lies beyond it, `rangeEdge`), and stays as it is
1111
+ * when no line is shown anywhere.
1112
+ */
1113
+ selectBlocks = (...blocks) => {
1114
+ if (blocks.length) {
1115
+ this.select(blockSelection(blocks.map((block) => block.id)));
1116
+ return;
1117
+ }
1118
+ const { value } = this;
1119
+ if (value.kind !== 'blocks')
1120
+ return;
1121
+ const [first, last] = [this.state.blocks[0], this.state.blocks.at(-1)];
1122
+ const range = first && last ? this.#contentRange(first, last) : noSelection;
1123
+ if (range)
1124
+ this.select(range);
1125
+ };
1126
+ addBlockToSelection = (block) => {
1127
+ const ids = this.value.kind === 'blocks' ? this.value.ids : [];
1128
+ if (!ids.includes(block.id))
1129
+ this.select(blockSelection([...ids, block.id]));
1130
+ };
1131
+ removeBlockFromSelection = (block) => {
1132
+ const ids = this.value.kind === 'blocks' ? this.value.ids : [];
1133
+ if (ids.includes(block.id))
1134
+ this.select(blockSelection(ids.filter((id) => id !== block.id)));
1135
+ };
1136
+ /**
1137
+ * Select a caret at `offset` of `text`; the projector displays it after
1138
+ * the flush (R10). The value is minted now (R4): a text that dies before the
1139
+ * display is followed through its atoms, else the seam of its block.
1140
+ */
1141
+ setAtTextOffset = (text, textOffset = this.state.yStart) => {
1142
+ if (!text || typeof textOffset !== 'number')
1143
+ return;
1144
+ this.#admit(this.#intent(text, Math.min(Math.max(textOffset, 0), text.length)), text);
1145
+ };
1146
+ /**
1147
+ * A write's model-side fallback: the value minted when the write was
1148
+ * requested (R4: anchors are captured at call time, before any await, so
1149
+ * a target merged away meanwhile is followed through its atoms), or the
1150
+ * seam of `origin`'s block when nothing of it resolves any more.
1151
+ */
1152
+ #admit = (intended, origin) => {
1153
+ this.clearModelSelectionPreservation();
1154
+ if (project(intended, this.edytor.facade).start) {
1155
+ this.select(intended);
1156
+ return;
1157
+ }
1158
+ const target = origin && this.#seamValue(origin.parent?.id ?? null);
1159
+ if (target)
1160
+ this.select(target);
1161
+ };
1162
+ /** Select the whole content from `startText` to `endText` (the code block's select-all). */
1163
+ setAtTextsRange = (startText, endText) => {
1164
+ this.setAtRange(startText, 0, endText, endText.length);
1165
+ };
1166
+ /** Select a text range; the projector displays it after the flush (R10). */
1167
+ setAtRange = (startText, startOffset, endText, endOffset, options = {}) => {
1168
+ if (!startText ||
1169
+ !endText ||
1170
+ typeof startOffset !== 'number' ||
1171
+ typeof endOffset !== 'number') {
1172
+ return;
1173
+ }
1174
+ const state = this.state;
1175
+ const range = this.normalizeTextRangePoints(startText, startOffset, endText, endOffset, options.isReversed ??
1176
+ (!state.isCollapsed &&
1177
+ state.isReversed &&
1178
+ state.startText === startText &&
1179
+ state.endText === endText &&
1180
+ state.yStart === startOffset &&
1181
+ state.yEnd === endOffset));
1182
+ this.#admit(this.#intent(range.startText, range.startOffset, range.endText, range.endOffset, range.isReversed), range.startText);
1183
+ };
1184
+ /**
1185
+ * Select `block`'s content (the whole of it by default): from its first
1186
+ * shown line to its last (a list's items, a code block's lines, GX-03), the
1187
+ * offsets in those lines. A block that shows no line (a divider) keeps the
1188
+ * current value. Displayed after the flush.
1189
+ */
1190
+ setAtBlockRange = (block, startOffset = 0, endOffset) => {
1191
+ const [first, last] = block ? [shownText(block, 'first'), shownText(block, 'last')] : [];
1192
+ if (!first || !last)
1193
+ return;
1194
+ const end = endOffset || last.length;
1195
+ if (!startOffset && end === last.length)
1196
+ this.select(this.textValue(first, 0, last, last.length, false, 'left'));
1197
+ else
1198
+ this.setAtRange(first, startOffset, last, end);
1199
+ };
1200
+ /**
1201
+ * Anchors from a DOM selection, without selecting them: the projector mints
1202
+ * an unobserved native move before a transaction it did not issue (BI-3).
1203
+ */
1204
+ mint = (snapshot) => {
1205
+ const container = this.edytor.node;
1206
+ const { anchorNode, focusNode } = snapshot;
1207
+ if (!container || !anchorNode || !focusNode)
1208
+ return null;
1209
+ for (const node of [anchorNode, focusNode])
1210
+ if (!container.contains(node) || isNestedForeignEditableTarget(container, node))
1211
+ return null;
1212
+ const reversed = isBackward(snapshot);
1213
+ const [startNode, start, endNode, end] = reversed
1214
+ ? [focusNode, snapshot.focusOffset, anchorNode, snapshot.anchorOffset]
1215
+ : [anchorNode, snapshot.anchorOffset, focusNode, snapshot.focusOffset];
1216
+ const { startText, endText } = this.getTextsInSelection(startNode, endNode, start, end);
1217
+ if (!startText || !endText)
1218
+ return null;
1219
+ const at = (text, node, offset) => Math.min(Math.max(getYIndex(text, node, offset), 0), text.length);
1220
+ return this.textValue(startText, at(startText, startNode, start), endText, at(endText, endNode, end), reversed);
390
1221
  };
391
1222
  }